Skip to main content

Transactions

Transactions covers payment captures, refunds, payouts, and wallet adjustments. This page documents the Web/Admin views and actions that reflect backend payment and ledger rules with critical business logic for fee calculations, refund policies, and financial reconciliation.

Core Concepts

  • Payment Statuses: PENDING, SUCCESSFUL, FAILED, EXPIRED with automatic retry logic
  • Transaction Types: PAYMENT, REFUND, PAYOUT, ADJUSTMENT with specific business rules
  • Fee Structure: Consultation fees, platform fees, taxes with dynamic calculation
  • Refund Policies: Time-based refund rules, penalty calculations, and wallet adjustments
  • Reconciliation: Real-time sync with payment providers (Razorpay) and internal ledger
  • Audit Trail: Complete transaction history with compliance tracking

Web/Admin Capabilities

1) List & Filter

Business Rules:

  • Filter by transaction type, status, date range, and user/appointment
  • Show real-time payment status with provider reconciliation
  • Display fee breakdown and net amounts for each transaction
  • Highlight failed transactions requiring manual intervention

Implementation:

// Filter logic with payment status and fee calculations
const filtered = rows.filter((transaction) => {
// Basic filtering
const matchesType = !typeFilter || transaction.type === typeFilter;
const matchesStatus = !statusFilter || transaction.status === statusFilter;
const matchesDateRange =
!dateRange ||
(transaction.created_at >= dateRange.start &&
transaction.created_at <= dateRange.end);

// Calculate fee breakdown for display
const feeBreakdown = calculateFeeBreakdown(transaction);
transaction.feeBreakdown = feeBreakdown;
transaction.netAmount = calculateNetAmount(transaction);

// Check reconciliation status
const reconciliationStatus = checkReconciliationStatus(transaction);
transaction.reconciliationStatus = reconciliationStatus;

return matchesType && matchesStatus && matchesDateRange;
});

// Fee calculation formula
const calculateFeeBreakdown = (transaction) => {
const baseAmount = transaction.amount;
const consultationFee = transaction.consultation_fee || 0;
const platformFee = calculatePlatformFee(baseAmount);
const taxes = calculateTaxes(baseAmount + platformFee);

return {
consultation_fee: consultationFee,
platform_fee: platformFee,
taxes: taxes,
total_fees: platformFee + taxes,
net_amount: baseAmount - platformFee - taxes,
};
};

// Platform fee calculation (percentage-based)
const calculatePlatformFee = (amount) => {
const platformFeeRate = 0.05; // 5% platform fee
return Math.round(amount * platformFeeRate * 100) / 100;
};

// Tax calculation (GST)
const calculateTaxes = (amount) => {
const gstRate = 0.18; // 18% GST
return Math.round(amount * gstRate * 100) / 100;
};

2) Refund Processing (GraphQL)

Refund Business Rules:

  • Time-Based Policies: Different refund rules based on cancellation timing
  • Penalty Calculations: Automatic penalty deduction for late cancellations
  • Partial Refunds: Support for partial refunds with specific amounts
  • Provider Integration: Real-time refund processing through Razorpay
  • Wallet Adjustments: Automatic wallet credit for refunded amounts

Refund Calculation Logic:

// Calculate refund amount based on cancellation timing and policies
const calculateRefundAmount = (appointment, cancellationTime) => {
const appointmentTime = new Date(appointment.start_time);
const hoursUntilAppointment =
(appointmentTime - cancellationTime) / (1000 * 60 * 60);

let refundPercentage = 1.0; // Full refund by default
let penaltyAmount = 0;

// Time-based refund policies
if (hoursUntilAppointment < 24) {
// Less than 24 hours: 50% refund + ₹250 penalty
refundPercentage = 0.5;
penaltyAmount = 250;
} else if (hoursUntilAppointment < 48) {
// Less than 48 hours: 75% refund + ₹100 penalty
refundPercentage = 0.75;
penaltyAmount = 100;
}

const baseRefund = appointment.amount * refundPercentage;
const finalRefund = Math.max(0, baseRefund - penaltyAmount);

return {
refund_amount: finalRefund,
penalty_amount: penaltyAmount,
refund_percentage: refundPercentage,
reason: generateRefundReason(hoursUntilAppointment, penaltyAmount),
};
};

// Generate refund reason based on timing
const generateRefundReason = (hoursUntil, penalty) => {
if (hoursUntil < 24) {
return `Late cancellation (${Math.round(hoursUntil)}h notice): 50% refund + ₹${penalty} penalty`;
} else if (hoursUntil < 48) {
return `Short notice cancellation (${Math.round(hoursUntil)}h notice): 75% refund + ₹${penalty} penalty`;
}
return 'Full refund - adequate notice provided';
};

GraphQL Implementation:

import { gql, ApolloClient } from '@apollo/client';

const REFUND = gql`
mutation Refund($input: RefundInput!) {
refund(input: $input) {
success
message
data {
id
refund_amount
penalty_amount
provider_refund_id
wallet_credit_amount
status
processing_time
}
}
}
`;

async function refund(client: ApolloClient<unknown>, input: RefundInput) {
// Pre-calculate refund amount and validate
const appointment = await getAppointmentDetails(input.appointment_id);
const refundCalculation = calculateRefundAmount(appointment, new Date());

// Validate refund amount
if (input.amount > refundCalculation.refund_amount) {
throw new Error(
`Refund amount cannot exceed calculated amount: ₹${refundCalculation.refund_amount}`,
);
}

const processedInput = {
...input,
calculated_refund: refundCalculation.refund_amount,
penalty_amount: refundCalculation.penalty_amount,
refund_reason: refundCalculation.reason,
};

const { data } = await client.mutate({
mutation: REFUND,
variables: { input: processedInput },
});

if (!data?.refund?.success) {
throw new Error(data?.refund?.message || 'Refund failed');
}

return data.refund.data;
}

3) Therapist Payouts (GraphQL)

Payout Business Rules:

  • Earning Calculation: Based on completed appointments with platform fee deduction
  • Payout Schedule: Weekly/monthly payout cycles with automatic processing
  • Tax Deduction: TDS calculation and deduction for tax compliance
  • Bank Account Validation: Verify therapist bank account details before payout

Payout Calculation Logic:

// Calculate therapist earnings and payout amount
const calculateTherapistPayout = (therapist, dateRange) => {
const completedAppointments = getCompletedAppointments(
therapist.id,
dateRange,
);

let totalEarnings = 0;
let totalPlatformFees = 0;
let totalTaxes = 0;

completedAppointments.forEach((appointment) => {
const consultationFee = appointment.consultation_fee;
const platformFee = calculatePlatformFee(consultationFee);
const taxes = calculateTaxes(consultationFee - platformFee);

totalEarnings += consultationFee;
totalPlatformFees += platformFee;
totalTaxes += taxes;
});

const netEarnings = totalEarnings - totalPlatformFees - totalTaxes;
const minimumPayout = 500; // Minimum payout threshold

return {
total_earnings: totalEarnings,
platform_fees: totalPlatformFees,
taxes_deducted: totalTaxes,
net_payout: Math.max(0, netEarnings),
eligible_for_payout: netEarnings >= minimumPayout,
minimum_threshold: minimumPayout,
};
};

// TDS calculation for tax compliance
const calculateTDS = (payoutAmount) => {
const tdsRate = 0.1; // 10% TDS for professional services
const tdsThreshold = 40000; // Annual threshold for TDS

if (payoutAmount > tdsThreshold) {
return Math.round(payoutAmount * tdsRate * 100) / 100;
}
return 0;
};

GraphQL Implementation:

const PAYOUT = gql`
mutation Payout($input: PayoutInput!) {
payout(input: $input) {
success
message
data {
id
payout_amount
tds_deducted
net_amount
bank_account
processing_time
status
transaction_id
}
}
}
`;

async function payout(client: ApolloClient<unknown>, input: PayoutInput) {
// Calculate payout amount and validate
const therapist = await getTherapistDetails(input.therapist_id);
const payoutCalculation = calculateTherapistPayout(
therapist,
input.date_range,
);

// Validate minimum payout threshold
if (!payoutCalculation.eligible_for_payout) {
throw new Error(
`Minimum payout threshold not met. Required: ₹${payoutCalculation.minimum_threshold}, Available: ₹${payoutCalculation.net_payout}`,
);
}

// Calculate TDS
const tdsAmount = calculateTDS(payoutCalculation.net_payout);
const netPayoutAmount = payoutCalculation.net_payout - tdsAmount;

const processedInput = {
...input,
calculated_amount: payoutCalculation.net_payout,
tds_amount: tdsAmount,
net_payout: netPayoutAmount,
earnings_breakdown: payoutCalculation,
};

const { data } = await client.mutate({
mutation: PAYOUT,
variables: { input: processedInput },
});

if (!data?.payout?.success) {
throw new Error(data?.payout?.message || 'Payout failed');
}

return data.payout.data;
}

4) Manual Adjustments (GraphQL)

Adjustment Business Rules:

  • Admin-Only: Only admins can create manual adjustments
  • Audit Trail: All adjustments require detailed notes and approval
  • Amount Validation: Positive/negative adjustments with balance verification
  • Wallet Integration: Automatic wallet balance updates
  • Compliance: All adjustments logged for financial audit

Adjustment Validation Logic:

// Validate adjustment request
const validateAdjustment = (adjustment, user) => {
const errors = [];

// Check admin permissions
if (user.role !== 'ADMIN') {
errors.push('Only administrators can create manual adjustments');
}

// Validate amount
if (Math.abs(adjustment.amount) > 10000) {
// Large adjustment threshold
errors.push('Large adjustments require additional approval');
}

// Check wallet balance for negative adjustments
if (adjustment.amount < 0) {
const currentBalance = getUserWalletBalance(adjustment.user_id);
if (Math.abs(adjustment.amount) > currentBalance) {
errors.push('Insufficient wallet balance for negative adjustment');
}
}

// Validate note requirement
if (!adjustment.note || adjustment.note.trim().length < 10) {
errors.push('Detailed note required for audit trail');
}

return errors;
};

// Calculate new wallet balance after adjustment
const calculateNewBalance = (userId, adjustmentAmount) => {
const currentBalance = getUserWalletBalance(userId);
return currentBalance + adjustmentAmount;
};

GraphQL Implementation:

const ADJUST = gql`
mutation Adjust($input: AdjustmentInput!) {
adjust(input: $input) {
success
message
data {
id
amount
note
previous_balance
new_balance
created_by
created_at
approval_status
}
}
}
`;

async function adjust(
client: ApolloClient<unknown>,
input: AdjustmentInput,
currentUser: User,
) {
// Validate adjustment request
const validationErrors = validateAdjustment(input, currentUser);
if (validationErrors.length > 0) {
throw new Error(`Validation failed: ${validationErrors.join(', ')}`);
}

// Calculate new balance
const previousBalance = getUserWalletBalance(input.user_id);
const newBalance = calculateNewBalance(input.user_id, input.amount);

const processedInput = {
...input,
previous_balance: previousBalance,
new_balance: newBalance,
created_by: currentUser.id,
approval_status:
Math.abs(input.amount) > 10000 ? 'PENDING_APPROVAL' : 'APPROVED',
};

const { data } = await client.mutate({
mutation: ADJUST,
variables: { input: processedInput },
});

if (!data?.adjust?.success) {
throw new Error(data?.adjust?.message || 'Adjustment failed');
}

return data.adjust.data;
}

5) Export & Financial Reports

Export Business Rules:

  • Transaction Reports: Include fee breakdown, refunds, and adjustments
  • Reconciliation Reports: Compare internal records with payment provider data
  • Tax Reports: Generate TDS and GST reports for compliance
  • Audit Reports: Complete transaction history with approval trails

Export Implementation:

// Generate comprehensive transaction report
const generateTransactionReport = (transactions, dateRange) => {
const report = {
summary: {
total_transactions: transactions.length,
total_amount: transactions.reduce((sum, t) => sum + t.amount, 0),
total_fees: transactions.reduce(
(sum, t) => sum + t.feeBreakdown.total_fees,
0,
),
total_refunds: transactions
.filter((t) => t.type === 'REFUND')
.reduce((sum, t) => sum + t.amount, 0),
net_revenue: 0,
},
transactions: transactions.map((transaction) => ({
id: transaction.id,
type: transaction.type,
amount: transaction.amount,
fee_breakdown: transaction.feeBreakdown,
status: transaction.status,
created_at: transaction.created_at,
provider_reference: transaction.provider_reference,
reconciliation_status: transaction.reconciliationStatus,
})),
reconciliation_summary: {
matched_transactions: transactions.filter(
(t) => t.reconciliationStatus === 'MATCHED',
).length,
pending_reconciliation: transactions.filter(
(t) => t.reconciliationStatus === 'PENDING',
).length,
discrepancies: transactions.filter(
(t) => t.reconciliationStatus === 'DISCREPANCY',
).length,
},
};

// Calculate net revenue
report.summary.net_revenue =
report.summary.total_amount -
report.summary.total_fees -
report.summary.total_refunds;

return report;
};

const csv = toCsv(generateTransactionReport(rows, dateRange));
downloadCsv('financial-transaction-report.csv', csv);

Data Flow (Web ↔ Backend)

Critical Business Logic Flow:

  1. Payment Processing: Payment capture → Fee calculation → Provider reconciliation → Status update
  2. Refund Processing: Refund request → Policy validation → Amount calculation → Provider refund → Wallet credit
  3. Payout Processing: Earnings calculation → TDS deduction → Bank transfer → Status tracking
  4. Adjustment Processing: Admin request → Validation → Balance update → Audit logging

GraphQL Operations with Business Rules:

ActionOperationBusiness Rules
Listtransactions(filter)Fee breakdown, reconciliation status, real-time updates
Refundrefund(input)Time-based policies, penalty calculation, provider integration
Payoutpayout(input)Earning calculation, TDS deduction, minimum thresholds
Adjustmentadjust(input)Admin-only, audit trail, balance validation

Error Handling & Validation:

  • Refund Exceeded: "Refund amount cannot exceed calculated amount"
  • Minimum Payout: "Minimum payout threshold not met"
  • Insufficient Balance: "Insufficient wallet balance for negative adjustment"
  • Provider Error: "Payment provider reconciliation failed"
  • Large Adjustment: "Large adjustments require additional approval"

Security & Access Control

  • Admin-Only Operations: Manual adjustments and large refunds require admin access
  • Audit Trail: All financial operations logged with user information and timestamps
  • Provider Integration: Secure API keys and webhook validation for payment providers
  • Data Privacy: Financial data protected by role-based permissions and encryption
  • Compliance: TDS and GST calculations for tax compliance and reporting

Price Breakup Tooltip Component

import * as Tooltip from '@radix-ui/react-tooltip';

export function PriceBreakup({
consultation,
platform,
taxes,
other = 0,
}: {
consultation: number;
platform: number;
taxes: number;
other?: number;
}) {
const total = consultation + platform + taxes + other;
return (
<Tooltip.Provider delayDuration={150}>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<span className="cursor-help underline">{total.toFixed(2)}</span>
</Tooltip.Trigger>
<Tooltip.Content
className="rounded border bg-white p-3 shadow text-xs space-y-1"
sideOffset={6}
>
<div className="flex justify-between">
<span>Consultation</span>
<span>{consultation.toFixed(2)}</span>
</div>
<div className="flex justify-between">
<span>Platform fee (5%)</span>
<span>{platform.toFixed(2)}</span>
</div>
<div className="flex justify-between">
<span>GST (18%)</span>
<span>{taxes.toFixed(2)}</span>
</div>
{other !== 0 && (
<div className="flex justify-between">
<span>Other</span>
<span>{other.toFixed(2)}</span>
</div>
)}
<div className="my-1 border-t" />
<div className="flex justify-between font-medium">
<span>Total</span>
<span>{total.toFixed(2)}</span>
</div>
<Tooltip.Arrow className="fill-white" />
</Tooltip.Content>
</Tooltip.Root>
</Tooltip.Provider>
);
}