Skip to content

Payments, Finance & GST Invoicing

This domain covers every flow where money moves through the MAS / Mr. Mentor backend: a student paying for tokens via Razorpay, the Finance Portal that mirrors all Razorpay payments into a local store and enriches each into a GST-compliant tax invoice (HSN/SAC, state-code routing, CGST/SGST/IGST/UTGST), the sales discount request → admin approval workflow that sets the price a student actually pays, an append-only GST audit trail, password-protected finance exports, and the internal pipeline cost ledger that tracks the AI/LLM/telephony spend per recruitment job. It is the system of record for revenue recognition, tax reporting (GSTR-1 style summaries) and reconciliation against Razorpay.

Status: documented from source on this branch.


Overview

The domain has several distinct surfaces, each with its own actors:

Surface Actor / role What they do
Token purchase USER (student) Buy mentorship tokens with Razorpay, credited on signature verification
Finance Portal FINANCE, ADMIN Reconcile Razorpay payments, generate & issue GST invoices, run reports, export protected sheets, manage finance-team users
Discount requests SALES (create), ADMIN (approve) Negotiate a per-student enrollment price below the batch fee
GST masters FINANCE, ADMIN Maintain HSN/SAC rate master and state-code master
Pipeline cost ledger recruitment HR users + ADMIN View AI/telephony spend per job / candidate

Two payment "engines" coexist:

  1. Live token purchasesTokenService talks to the Razorpay SDK directly (orders / invoices API), verifies the HMAC signature on the client callback, and credits tokens. This is the only place the system initiates a charge.
  2. Reconciliation + invoicingGlobalPaymentSyncService pulls all Razorpay payments (regardless of how they originated) into superadmin.global_payments, then turns each into a legal tax invoice. This is read-mostly: Razorpay source data is authoritative, only the invoice-side fields are editable.

Mount-path note: the finance routes are mounted at /api/finance (src/routes/index.ts:383). Many JSDoc comments in src/controllers/finance.controller.ts still say /api/superadmin/... — those are stale; the real prefix is /api/finance.

Scope note: the task brief referenced FinanceAuditService, financeAudit middleware and a FinanceAuditLog entity. Those do not exist in the codebase. The only immutable audit trail in this domain is GstAuditLog (superadmin.gst_audit_log), written by GlobalPaymentSyncService.writeAudit. Where the brief says "finance audit logging", read "GST audit logging".


Key concepts & entities

Glossary

  • Token purchase — a student buys mentorship tokens; one TokenPurchase row, PENDING → COMPLETED after Razorpay signature verification.
  • Global payment — one canonical mirrored row per Razorpay payment in superadmin.global_payments, enriched into a GST invoice.
  • Invoice series — which product an invoice belongs to (MAS101, MAS102, PAP, AERO, DRO). Drives the number prefix; numbering resets per financial year, per series.
  • HSN/SAC — tax classification code for the line item (courses use SAC 999293, MIT technical services use 999223).
  • State code — 2-digit GST state code (supplier is Delhi = 07). Decides intra-state (CGST+SGST), inter-state (IGST) or union-territory (CGST+UTGST).
  • GST-inclusive — amounts paid are treated as tax-inclusive; the engine back-computes the taxable value as total / (1 + rate/100).
  • B2C unregistered — default buyer when there is no GSTIN; place of supply defaults to the supplier state → intra-state CGST+SGST.
  • Discount request — sales-proposed, admin-approved per-student price below Batch.enrollmentFee; on approval sets Application.finalAmount.
  • Pipeline cost — internal USD spend (LLM tokens, voice-call minutes) per recruitment job/candidate. Not customer-facing money.

Entities

Entity File Table Notes
TokenPurchase src/entities/TokenPurchase.ts token_purchases PaymentStatus enum; Razorpay order/payment/signature/invoice ids
GlobalPayment src/entities/GlobalPayment.ts superadmin.global_payments Razorpay mirror + full GST invoice layout (~70 columns)
GstAuditLog src/entities/GstAuditLog.ts superadmin.gst_audit_log Append-only; one row per changed field
GstStateCode src/entities/GstStateCode.ts superadmin.gst_state_codes code PK, is_ut flag
HsnSacMaster src/entities/HsnSacMaster.ts superadmin.hsn_sac_master code PK, gst_rate, is_exempt, active
DiscountRequest src/entities/DiscountRequest.ts discount_requests DiscountRequestStatus enum; price snapshot
PipelineCostLedger src/entities/PipelineCostLedger.entity.ts pipeline_cost_ledger CostStage + CostProvider enums; USD + token counts

Supporting config & helpers:

  • src/config/invoiceConfig.ts — supplier identity, series config, GST slabs, computeGst, formatInvoiceNumber, stateCodeFromGstin, amountToWords, bank/terms blocks, S3 + email recipient config.
  • src/utils/invoiceBillingAddress.ts — derives Razorpay billing-address lines for a user.

Architecture

flowchart TD
  subgraph FE["Frontends"]
    Student["Student portal (token buy)"]
    Finance["Finance Portal UI"]
    SalesUI["Sales CRM UI"]
    HRUI["Recruitment HR UI"]
  end

  subgraph Routes["Express routes"]
    TokenR["/api/tokens/* (token.routes)"]
    FinR["/api/finance/* (finance.routes)"]
    SalesR["/api/sales/discount-requests/* (sales.routes)"]
    CostR["/api/hr/costs + /api/admin/costs (pipelineCost.routes)"]
  end

  subgraph Ctrls["Controllers"]
    TokenC["TokenController"]
    FinC["FinanceController"]
    SalesC["SalesController"]
    CostC["PipelineCostController"]
  end

  subgraph Svcs["Services"]
    TokenS["TokenService (Razorpay SDK)"]
    GPS["GlobalPaymentSyncService"]
    RAS["RazorpayAdminService (read-only)"]
    GMS["GstMasterService"]
    GPDF["GstInvoicePdfService (pdf-lib)"]
    FRS["FinanceReportsService"]
    FES["FinanceExportService (xlsx)"]
    DS["DiscountService"]
    PCS["PipelineCostService"]
  end

  subgraph DB["PostgreSQL"]
    TP["token_purchases"]
    GP["global_payments"]
    GAL["gst_audit_log"]
    SC["gst_state_codes"]
    HSN["hsn_sac_master"]
    DR["discount_requests"]
    PCL["pipeline_cost_ledger"]
  end

  subgraph Ext["External systems"]
    RZP["Razorpay API"]
    S3["AWS S3 (secured invoice bucket)"]
    SMTP["Gmail SMTP via EmailService"]
    Queue["BullMQ emailQueue"]
  end

  Student --> TokenR --> TokenC --> TokenS
  Finance --> FinR --> FinC
  SalesUI --> SalesR --> SalesC --> DS
  HRUI --> CostR --> CostC --> PCS

  TokenS --> RZP
  TokenS --> TP
  FinC --> GPS
  FinC --> RAS
  FinC --> FRS
  FinC --> FES
  GPS --> RAS --> RZP
  GPS --> GMS
  GPS --> GPDF
  GPS --> GP
  GPS --> GAL
  GMS --> SC
  GMS --> HSN
  GPS --> S3
  GPS --> SMTP
  DS --> DR
  DS --> Queue
  PCS --> PCL
  FRS --> GP

Data model

erDiagram
  USER ||--o{ TOKEN_PURCHASE : "buys"
  USER ||--o{ DISCOUNT_REQUEST : "requests"
  APPLICATION ||--o{ DISCOUNT_REQUEST : "for"
  BATCH ||--o{ DISCOUNT_REQUEST : "priced from"
  GLOBAL_PAYMENT ||--o{ GST_AUDIT_LOG : "audited by"
  HSN_SAC_MASTER ||--o{ GLOBAL_PAYMENT : "rate source"
  GST_STATE_CODE ||--o{ GLOBAL_PAYMENT : "place of supply"
  JOB_POST ||--o{ PIPELINE_COST_LEDGER : "incurs"
  APPLICATION ||--o{ PIPELINE_COST_LEDGER : "candidate cost"

  TOKEN_PURCHASE {
    uuid id PK
    uuid userId FK
    int tokenQuantity
    decimal amount
    string paymentStatus "pending|completed|failed|cancelled"
    string razorpayOrderId
    string razorpayPaymentId
    string razorpaySignature
    string razorpayInvoiceId
    timestamp completedAt
  }

  GLOBAL_PAYMENT {
    uuid id PK
    string razorpayPaymentId UK
    bigint amountPaise
    bigint amountRefundedPaise
    string status
    string method
    boolean captured
    string invoiceSeries "MAS101|MAS102|PAP|AERO|DRO"
    string taxInvoiceNumber UK
    int invoiceSequence
    string financialYear
    string hsnSacCode
    numeric taxableValue
    numeric gstRate
    numeric cgstAmount
    numeric sgstAmount
    numeric igstAmount
    numeric utgstAmount
    numeric totalTaxAmount
    string recipientStateCode
    boolean isInterState
    boolean taxExempt
    boolean invoiceGenerated
    boolean isManual
    string invoicePdfS3Key
    timestamp invoiceEmailedAt
  }

  GST_AUDIT_LOG {
    uuid id PK
    string paymentId FK
    string invoiceNumber
    string action "create|update|issue"
    string field
    text oldValue
    text newValue
    string changedBy
    string changedByEmail
    timestamp createdAt
  }

  GST_STATE_CODE {
    string code PK
    string name
    boolean isUt
  }

  HSN_SAC_MASTER {
    string code PK
    string description
    numeric gstRate
    boolean isExempt
    boolean active
  }

  DISCOUNT_REQUEST {
    uuid id PK
    uuid applicationId FK
    uuid batchId FK
    uuid requestedBy FK
    decimal originalBatchPrice
    decimal requestedPrice
    decimal discountAmount
    decimal approvedPrice
    string status "pending|approved|rejected"
    uuid reviewedBy FK
    timestamp reviewedAt
  }

  PIPELINE_COST_LEDGER {
    uuid id PK
    uuid jobPostId FK
    uuid applicationId FK
    string stage
    string provider
    string model
    decimal costUsd
    int inputTokens
    int outputTokens
    timestamp recordedAt
  }

Enums

  • PaymentStatus (TokenPurchase): pending, completed, failed, cancelled.
  • DiscountRequestStatus: pending, approved, rejected.
  • CostStage (PipelineCostLedger): jd_generation, salary_benchmark, quiz_generation, resume_parsing, resume_embedding, resume_scoring, ai_voice_call, transcript_analysis, other.
  • CostProvider: openai, groq, vapi, elevenlabs, twilio, nvidia, internal.
  • InvoiceSeries (string in invoiceConfig.ts): MAS101, MAS102, PAP, AERO, DRO.

API surface

Token purchases — mounted at /api (src/routes/token.routes.ts)

Method Path Auth/role Purpose
GET /api/tokens/balance auth Current token balance
POST /api/tokens/purchase auth Create Razorpay order/invoice for a token purchase
POST /api/tokens/verify-payment auth Verify HMAC signature, credit tokens
GET /api/tokens/history auth Purchase history
GET /api/tokens/usage-history auth Token usage history
DELETE /api/tokens/purchase/:purchaseId auth Cancel a pending purchase
POST /api/tokens/deduct auth Deduct tokens (spend)

Finance Portal — mounted at /api/finance (src/routes/finance.routes.ts)

All routes require authMiddleware + financeOnlyMiddleware (FINANCE or ADMIN).

Method Path Purpose
GET /api/finance/razorpay/payments Live Razorpay payments (read-through)
GET /api/finance/razorpay/invoices Live Razorpay invoices
GET /api/finance/settlements Live Razorpay settlements
GET /api/finance/overview Dashboard KPIs + charts
GET /api/finance/gst GSTR-1 style tax summary
GET /api/finance/customers Payer directory
GET /api/finance/ledger One payer's full ledger (by email/phone)
GET /api/finance/refunds Refund register
GET /api/finance/gst-masters State codes + HSN/SAC master
POST /api/finance/gst-masters/hsn Add/update an HSN/SAC code
GET /api/finance/team List finance-role users
POST /api/finance/team Create finance-role user (emails creds)
PATCH /api/finance/team/:id Activate/deactivate or reset password
GET /api/finance/profile Current finance user's details
PUT /api/finance/profile Update name/phone/DOB
POST /api/finance/export Build password-protected .xlsx and stream
GET /api/finance/global-payments List mirrored payments (filters)
GET /api/finance/global-payments/unimported Razorpay rows not yet in DB
POST /api/finance/global-payments/sync Pull all Razorpay payments into DB
POST /api/finance/global-payments/generate-invoices Bulk assign numbers + GST (optional S3 store)
POST /api/finance/global-payments/manual Create off-system / manual invoice
POST /api/finance/global-payments/:id/issue Render PDF → S3 → email customer + finance + admin
GET /api/finance/global-payments/:id/pdf Stream a fresh PDF (download/preview)
GET /api/finance/global-payments/:id/audit GST modification history
GET /api/finance/global-payments/:id Full record
PATCH /api/finance/global-payments/:id Edit invoice / GST fields

Static sub-paths (/unimported, /sync, /generate-invoices, /manual) are registered before the dynamic /:id route so they are not swallowed as an id.

Discount requests — mounted at /api/sales (src/routes/sales.routes.ts)

All routes require authMiddleware (applied at router level).

Method Path Auth/role Purpose
POST /api/sales/discount-requests auth (sales) Create a discount request
GET /api/sales/discount-requests auth List requests (admin = all, sales = own)
GET /api/sales/discount-requests/pending-count auth Count of pending requests
GET /api/sales/discount-requests/application/:id auth Active request for an application
PATCH /api/sales/discount-requests/:id/review auth + adminMiddleware Approve / reject

Pipeline cost — mounted at /api (src/routes/pipelineCost.routes.ts)

Method Path Auth/role Purpose
GET /api/hr/costs/dashboard auth HR cost dashboard (own jobs)
GET /api/hr/costs/jobs auth All-jobs summary for the HR user
GET /api/hr/costs/jobs/:jobPostId/summary auth Per-job cost summary
GET /api/hr/costs/jobs/:jobPostId/candidates auth Per-candidate costs in a job
GET /api/hr/costs/candidates/:applicationId/summary auth One candidate's cost summary
GET /api/hr/costs/ledger auth Raw ledger rows
GET /api/admin/costs/dashboard auth + admin System-wide cost dashboard
GET /api/admin/costs/by-hr auth + admin Cost breakdown by HR user
GET /api/admin/costs/hr/:userId/jobs auth + admin One HR user's per-job detail

User journeys

1. Buy tokens with Razorpay and get credited

A student initiates a token purchase. The backend prefers Razorpay's Invoices API (so Razorpay emails an official invoice) and falls back to orders.create if that fails. On the client callback, TokenService tries several HMAC signature formulas to cover both the order flow and the invoice flow.

sequenceDiagram
  participant FE as Student frontend
  participant API as TokenController
  participant TS as TokenService
  participant DB as token_purchases
  participant RZP as Razorpay

  FE->>API: POST /api/tokens/purchase with quantity
  API->>TS: createTokenPurchase dto
  TS->>DB: save TokenPurchase PENDING
  TS->>RZP: invoices.create preferred
  alt invoice creation fails
    TS->>RZP: orders.create fallback
  end
  TS->>DB: store razorpayOrderId and invoice id and url
  TS-->>API: razorpayOrder plus key id
  API-->>FE: order details for Checkout
  Note over FE,RZP: Student completes Razorpay Checkout
  FE->>API: POST /api/tokens/verify-payment with paymentId and signature
  API->>TS: verifyAndCompletePayment
  TS->>DB: find purchase by order or invoice or latest pending
  TS->>TS: HMAC sha256 over candidate bodies vs signature
  alt no candidate matches
    TS-->>API: error Invalid signature
    API-->>FE: 400 verification failed
  else signature matches
    TS->>DB: set status COMPLETED and store paymentId and completedAt
    TS->>DB: credit tokens to user account
    TS->>RZP: send invoice WhatsApp best effort
    TS-->>API: success with tokenPurchase
    API-->>FE: 200 tokens credited
  end

Key guards (src/services/TokenService.ts):

  • If Razorpay keys are unset, createTokenPurchase throws "Razorpay is not configured"; verifyAndCompletePayment returns { success: false }.
  • A purchase already COMPLETED returns "Payment already completed" (idempotent re-verification guard).
  • Signature candidates include order|payment (from DB and from callback), invoice|receipt|status|payment, payment|invoice and invoice|payment.

2. Reconcile Razorpay → generate a GST tax invoice

Finance staff pull all Razorpay payments into the local mirror, then enrich each into a numbered GST invoice. The amount is treated as GST-inclusive; the taxable value and tax heads are back-computed and routed by state code.

sequenceDiagram
  participant FE as Finance Portal
  participant API as FinanceController
  participant GPS as GlobalPaymentSyncService
  participant RAS as RazorpayAdminService
  participant RZP as Razorpay
  participant GMS as GstMasterService
  participant DB as global_payments

  FE->>API: POST /api/finance/global-payments/sync
  API->>GPS: syncAllPayments from and to
  loop pages of 100 up to 20k
    GPS->>RAS: listPayments count skip
    RAS->>RZP: payments.all
    RZP-->>RAS: page of payments
    GPS->>DB: upsert by razorpayPaymentId idempotent
  end
  GPS-->>API: fetched created updated counts
  API-->>FE: sync result

  FE->>API: POST /api/finance/global-payments/generate-invoices storePdf true
  API->>GPS: generateInvoices ids optional
  loop each captured pending row oldest first
    GPS->>GPS: assignInvoiceFields
    GPS->>GMS: resolveRate for hsn sac code
    GMS->>DB: read hsn_sac_master cached
    GPS->>GMS: isUt for recipient state
    GPS->>GPS: computeGst total rate stateCode
    GPS->>DB: save row with number series GST split invoiceGenerated true
  end
  GPS-->>API: generated stored counts
  API-->>FE: backfill result

Number assignment (assignInvoiceFields + nextSequence): the financial year is Apr–Mar; the next sequence is MAX(invoice_sequence)+1 scoped to (financial_year, invoice_series), so numbering resets each FY and is independent per series. Re-running keeps the existing number (idempotent).

3. Issue one invoice end-to-end (PDF → S3 → email)

sequenceDiagram
  participant FE as Finance Portal
  participant API as FinanceController
  participant GPS as GlobalPaymentSyncService
  participant PDF as GstInvoicePdfService
  participant S3 as AWS S3 secured bucket
  participant MAIL as EmailService

  FE->>API: POST /api/finance/global-payments/:id/issue
  API->>GPS: issueInvoice id email true
  GPS->>GPS: assignInvoiceFields if not numbered
  GPS->>PDF: generateGstInvoicePdf invoice data
  PDF-->>GPS: pdf buffer
  alt invoice bucket configured
    GPS->>S3: uploadPrivateObject tax invoice key
    GPS->>GPS: set invoicePdfS3Key
  else bucket not set
    GPS->>GPS: warn skip S3 store
  end
  alt email enabled and recipient resolvable
    GPS->>MAIL: send to customer cc finance and admin with pdf attached
    GPS->>GPS: set invoiceEmailedAt
  else no recipient
    GPS-->>API: emailError no recipients
  end
  GPS->>GPS: writeAudit is NOT called on issue path
  GPS-->>API: row pdfStored emailed
  API-->>FE: 200 issue result

Notes:

  • getInvoicePdfBuffer (the GET /:id/pdf endpoint) renders a fresh PDF on demand and does not require the invoice to be issued; it back-fills derived fields first if the row has never been numbered.
  • Email recipients: to = recipient/customer email; cc = GLOBAL_INVOICE_FINANCE_EMAIL + GLOBAL_INVOICE_ADMIN_EMAIL. If there is no customer email, the first finance/admin address becomes the primary to.

4. Manual / off-system invoice with GST audit

For payments collected outside Razorpay, finance creates a manual entry. A synthetic MANUAL-<uuid> payment id is stored, GST is computed immediately, and a create audit row is written.

sequenceDiagram
  participant FE as Finance Portal
  participant API as FinanceController
  participant GPS as GlobalPaymentSyncService
  participant GMS as GstMasterService
  participant DB as global_payments
  participant AUD as gst_audit_log

  FE->>API: POST /api/finance/global-payments/manual with amount and recipient
  API->>GPS: createManual input actor
  GPS->>GPS: validate amount positive
  GPS->>GPS: validateGstinState GSTIN first two digits vs state
  alt GSTIN state mismatch
    GPS-->>API: 400 GSTIN does not match state
  else valid
    GPS->>GPS: assignInvoiceFields assigns number and GST
    GPS->>GMS: resolveRate and isUt
    GPS->>DB: save manual row invoiceGenerated true
    GPS->>AUD: writeAudit action create one row per field
    GPS-->>API: saved row
    API-->>FE: 201 created
  end

5. Edit an issued invoice and recompute tax (audited)

sequenceDiagram
  participant FE as Finance Portal
  participant API as FinanceController
  participant GPS as GlobalPaymentSyncService
  participant DB as global_payments
  participant AUD as gst_audit_log

  FE->>API: PATCH /api/finance/global-payments/:id with edited fields
  API->>GPS: update id patch actor
  GPS->>DB: load row
  GPS->>GPS: snapshot AUDIT_FIELDS before change
  GPS->>GPS: apply only whitelisted editable fields
  GPS->>GPS: validateGstinState
  alt tax inputs changed and amounts not hand edited
    GPS->>GPS: recompute computeGst and overwrite tax heads
  else tax amounts hand edited
    GPS->>GPS: respect manual override
  end
  GPS->>DB: save row
  GPS->>AUD: writeAudit action update for each changed field
  GPS-->>API: saved row
  API-->>FE: 200 updated

Razorpay source columns (amountPaise, status, razorpayPaymentId, rawPayload) are not in the editable whitelist — they stay authoritative.

6. Sales discount request → admin approval

sequenceDiagram
  participant SalesFE as Sales CRM
  participant API as SalesController
  participant DS as DiscountService
  participant DB as discount_requests
  participant AppDB as applications
  participant Q as BullMQ emailQueue
  participant AdminFE as Admin

  SalesFE->>API: POST /api/sales/discount-requests appId price reason
  API->>DS: createRequest
  DS->>DB: check no active pending or approved request
  alt requested price not below enrollment fee
    DS-->>API: error price must be less than current price
    API-->>SalesFE: 400
  else valid
    DS->>DB: save PENDING with price snapshot
    DS-->>API: request
    API-->>SalesFE: created
  end
  Note over AdminFE,API: Admin reviews
  AdminFE->>API: PATCH /api/sales/discount-requests/:id/review approve
  API->>DS: reviewRequest admin approve approvedPrice
  alt request not pending
    DS-->>API: error only pending can be reviewed
  else approve
    DS->>DB: set APPROVED approvedPrice reviewedBy reviewedAt
    DS->>AppDB: set Application.finalAmount and discountApplied
    DS->>Q: queue discount-approved-student email
    DS->>Q: queue discount-approved-sales email
    DS-->>API: updated request
    API-->>AdminFE: 200 approved
  end

The admin may approve at a price different from what sales requested (approvedPrice), but it must still be > 0 and < originalBatchPrice. Rejection just flips the status and records the reviewer/note; sales may retry with a new request.

7. Finance reports + password-protected export

sequenceDiagram
  participant FE as Finance Portal
  participant API as FinanceController
  participant FRS as FinanceReportsService
  participant FES as FinanceExportService
  participant DB as global_payments

  FE->>API: GET /api/finance/overview from to
  API->>FRS: overview
  FRS->>DB: aggregate KPIs trend method series status splits
  FRS-->>API: dashboard data
  API-->>FE: 200 charts

  FE->>API: POST /api/finance/export filename sheets
  API->>API: load user then derivePassword from dob and phone
  alt profile incomplete no dob or phone
    API-->>FE: 400 PROFILE_INCOMPLETE
  else password derived
    API->>FES: buildEncrypted sheets password
    FES-->>API: encrypted xlsx buffer
    API-->>FE: 200 attachment download
  end

The export password is derived server-side as DD-MM-YYYY-last4 from the finance user's date of birth and phone (FinanceExportService.derivePassword); the client never sends it. Without a DOB + phone on the user row, export returns PROFILE_INCOMPLETE.

8. Pipeline cost recording (internal spend)

Recruitment services record AI/telephony spend as fire-and-forget ledger rows; HR and admin then read aggregated dashboards.

sequenceDiagram
  participant Pipe as Recruitment pipeline stage
  participant PCS as PipelineCostService
  participant DB as pipeline_cost_ledger
  participant HRFE as HR dashboard
  participant API as PipelineCostController

  Pipe->>PCS: recordLlmCost or recordCallCost
  PCS->>DB: insert ledger row best effort
  Note over PCS: failures are swallowed and logged non-blocking
  HRFE->>API: GET /api/hr/costs/dashboard
  API->>PCS: getHrDashboard userId
  PCS->>DB: aggregate by stage provider job
  PCS-->>API: summary
  API-->>HRFE: 200 cost breakdown

Background jobs & async

  • Email queue (BullMQ emailQueue) — discount approval emails to the student and the sales person are enqueued via QueueService.addEmailJob with types discount-approved-student and discount-approved-sales (src/services/DiscountService.ts). Failures are caught and never block the approval.
  • WhatsApp invoice (async, non-blocking) — after a token purchase completes, TokenService.sendTokenPurchaseInvoiceWhatsApp attempts a WhatsApp message (mas_invoice template), gated on a configured messaging provider.
  • Invoice email (synchronous)issueInvoice sends the GST-invoice email directly through EmailService (not the queue), attaching the rendered PDF.
  • GST master seedingGstMasterService.ensureSeeded lazily seeds the 39-row state-code master and a 2-row HSN/SAC master on first use, then caches lookups in-process (utCache, hsnCache), invalidated on upsertHsn.
  • No cron — there is no scheduled reconciliation; sync and generate-invoices are operator-triggered from the Finance Portal.

Webhooks: token payment verification is a client callback (POST /api/tokens/verify-payment), not a Razorpay server webhook. There is no Razorpay webhook endpoint in this domain; signature verification happens on the client-initiated verify call.


External integrations

Integration Used by Env vars Failure / fallback
Razorpay SDK (charge) TokenService RAZORPAY_KEY_ID, RAZORPAY_KEY_SECRET If unset, purchase/verify return "not configured"; invoice flow falls back to orders.create
Razorpay SDK (read) RazorpayAdminService same keys isConfigured() false → finance reads return 503
AWS S3 (secured invoice bucket) GlobalPaymentSyncService GLOBAL_INVOICE_S3_BUCKET, GLOBAL_INVOICE_S3_PREFIX If bucket unset, PDF store is skipped with a warning; issue still succeeds
Gmail SMTP EmailService (invoice + finance creds) EMAIL_USER, EMAIL_PASS Errors captured into emailError; row still saved
Supplier identity / numbering invoiceConfig.ts GLOBAL_INVOICE_SUPPLIER_*, GLOBAL_INVOICE_*_PREFIX, GLOBAL_INVOICE_GST_RATE, GLOBAL_INVOICE_HSN_SAC Hardcoded real defaults (GSTIN 07AAECU1161F1ZM, Delhi)
Bank details / terms invoiceConfig.ts GLOBAL_INVOICE_BANK_*, GLOBAL_INVOICE_TERMS Ship with explicit PENDING — set ... placeholders
Invoice email recipients invoiceConfig.ts GLOBAL_INVOICE_FINANCE_EMAIL, GLOBAL_INVOICE_ADMIN_EMAIL Admin defaults to admin@myanalyticsschool.com
WhatsApp WhatsAppService provider creds Gated on hasConfiguredMessagingProvider; silently skipped otherwise
PDF rendering GstInvoicePdfService none Uses pdf-lib + bundled MAS logo
Excel export FinanceExportService none Uses xlsx-populate; password from user profile

Razorpay MCP note: in this environment the Razorpay MCP tool is OAuth-gated and cannot be used to inspect live Razorpay data; all reads here go through the server-side RazorpayAdminService using the configured API keys.

GST routing logic (computeGst): the supplier is Delhi (07). Buyer state is recipientStateCode if known, else defaults to the supplier state (B2C → place of supply = supplier). Same state → CGST + SGST (each rate/2); different state → IGST (full rate); union territory without legislature (is_ut) → CGST + UTGST. Exempt or 0% → no tax, taxable value = full amount. Tax heads are split so they sum back to the total tax with no 1-paisa drift.


Status lifecycles

TokenPurchase.paymentStatus

stateDiagram-v2
  [*] --> PENDING : createTokenPurchase
  PENDING --> COMPLETED : verifyAndCompletePayment signature valid
  PENDING --> CANCELLED : cancelTokenPurchase
  PENDING --> FAILED : payment abandoned or failed
  COMPLETED --> COMPLETED : re-verify rejected already completed
  CANCELLED --> [*]
  FAILED --> [*]
  COMPLETED --> [*]

DiscountRequest.status

stateDiagram-v2
  [*] --> PENDING : sales createRequest
  PENDING --> APPROVED : admin approve sets Application finalAmount
  PENDING --> REJECTED : admin reject
  REJECTED --> PENDING : sales retries new request
  APPROVED --> [*]

GlobalPayment invoice lifecycle

stateDiagram-v2
  [*] --> MIRRORED : syncAllPayments upsert from Razorpay
  [*] --> MANUAL : createManual off system entry
  MIRRORED --> NUMBERED : generateInvoices assigns number and GST
  MANUAL --> NUMBERED : assigned at creation
  NUMBERED --> ISSUED : issueInvoice PDF to S3 and emailed
  ISSUED --> ISSUED : re-issue keeps same number idempotent
  NUMBERED --> NUMBERED : update recomputes GST audited
  ISSUED --> NUMBERED : update recomputes GST audited

Edge cases, limits & gotchas

  • Mount path vs JSDoc. Real prefix is /api/finance; controller JSDoc says /api/superadmin/.... Trust src/routes/index.ts and finance.routes.ts.
  • FINANCE role is portal-scoped. financeOnlyMiddleware allows FINANCE and ADMIN only, and finance users do not bypass any other role guard.
  • Money units. Razorpay amounts are stored in paise (amountPaise, bigint as string); GST/invoice values are rupees numeric(14,2). The invoice engine divides paise by 100 and treats the result as GST-inclusive.
  • Idempotency.
  • Token verify rejects an already-COMPLETED purchase.
  • syncAllPayments upserts by razorpayPaymentId (unique).
  • Invoice numbering is preserved across re-issue/re-generate; a number is only assigned when missing or when the financial year changed.
  • Signature multi-formula matching. Because invoice-flow Checkout may omit order_id, TokenService tries several HMAC bodies and a last-resort lookup of the user's most recent pending purchase. The DB-stored ids are the primary source of truth, not the callback payload.
  • GSTIN ↔ state validation (Rule 1). validateGstinState throws 400 if a GSTIN's first two digits do not match the chosen recipientStateCode (enforced on both manual create and update).
  • Rate resolution precedence. An explicit non-zero gstRate (or taxExempt) wins; otherwise the HSN/SAC master rate; otherwise DEFAULT_GST_RATE (18).
  • Hand-edited tax overrides. On PATCH, if the caller edits tax amounts directly (cgstAmount, taxableValue, …) the engine does not recompute and respects the override; recompute only fires when tax inputs change.
  • Audit is GST-only and append-only. GstAuditLog records create/update per changed field with actor id+email. The issue path does not write an audit row (only create and update do). There is no general finance audit log entity.
  • Bank details / terms are PENDING. invoiceConfig.ts ships PENDING — set ... placeholders for bank name/account/IFSC and default terms; these must be set via env before go-live (see the add-env-var skill).
  • No superadmin schema auto-create caveat. GlobalPayment, GstAuditLog, GstStateCode, HsnSacMaster live in the Postgres superadmin schema (not public); TypeORM auto-sync manages them but the schema must exist.
  • Sync safety caps. syncAllPayments pages up to 200×100 = 20k payments; previewUnimported scans up to 50 pages. Larger histories need a date window.
  • Export gating. Protected .xlsx export requires the finance user to have both DOB and phone set; otherwise PROFILE_INCOMPLETE (400).
  • Pipeline cost is best-effort. PipelineCostService.record swallows DB errors and returns null so a costing failure never blocks the recruitment pipeline. Costs are in USD, unrelated to customer GST/INR money.
  • Multi-platform (x-platform). This domain does not branch on the platform header; supplier identity and invoice series are global, driven by env/config.