Token Economy & Token Governance¶
The token economy is the internal currency that gates premium, mentor-time-consuming actions on the
MAS / Mr. Mentor platform. A student spends 1 token to book a 1:1 mentor meeting and 1 resume
token per resume review. Tokens enter a user's wallet through Razorpay purchases, promotional grants,
or Batch-Lead-approved allotment requests; they leave it as meeting bookings and resume-review spends;
and they return as automatic refunds when a meeting is cancelled or expires. Every balance change is
mirrored into an append-only token_usage audit ledger. "Token Governance" is the Phase-1, feature-flagged
overlay that adds a student-request → Batch-Lead-approval workflow on top of the wallet primitives.
Status: documented from source on this branch.
Overview¶
The domain answers four questions:
- How many tokens do I have? —
GET /api/tokens/balance, computed from the persistent wallet plus a still-valid promotional grant. - How do I get more? — buy them via Razorpay (
/api/tokens/purchase+/api/tokens/verify-payment), receive a promotional grant, or raise a governance allotment request that a Batch Lead approves. - How are they spent? — booking a mentor meeting (
MentorService) or requesting a resume review (ResumeReviewService) atomically deducts a token and writes a ledger row. - What happens on cancellation? — the meeting flows (
MentorService,SlotCleanupService) credit aREFUNDtoken back.
Personas / roles (User.role enum: USER, ADMIN, SUPERADMIN, EXPERT, SALES; Batch Lead is a
sales-side role resolved through batch assignment):
| Persona | What they do in this domain |
|---|---|
| Student / USER | View balance, buy tokens, spend tokens on meetings & resume reviews, raise allotment requests, withdraw their own requests. |
| Mentor / EXPERT | Indirectly trigger refunds by approving meeting cancellations. |
| Batch Lead | Review (approve / reject) student token-allotment requests for batches they own. |
| Admin / Superadmin | Issue manual refunds / adjustments (via AdminDashboardService), monitor purchases. |
| System (BullMQ) | Auto-refund expired pending meetings (SlotCleanupService, cleanup queue). |
Where it sits: the token economy is a horizontal service consumed by the mentorship meeting flow,
the resume-review flow, and the finance/payments stack. It is owned entirely by mr-mentor-backend
(TypeORM entities in the mas PostgreSQL database). Purchases bridge to Razorpay (see
payments); invoice notifications bridge to WhatsApp.
Key concepts & entities¶
| Concept | Meaning |
|---|---|
| Wallet token | Persistent, paid balance held on tokens.token (mock/meeting) and tokens.resumeToken (resume). Never auto-expires in practice (1-year expiresAt is set but not enforced on read). |
| Promotional token | Time-boxed grant on promotional_tokens split into meetToken and resumeToken, with a legacy aggregate tokenValue. Counts toward balance only while expiredAt is in the future. Spent before wallet tokens. |
| Token hold | The platform does not use a separate "hold" state. Booking debits immediately and sets the slot to UNDER_REVIEW; cancellation/expiry credits a refund. The "hold" is effectively the deducted-token-plus-pending-slot. |
| Token usage / ledger | Append-only audit row in token_usage capturing usageType, tokensUsed, balanceBefore, balanceAfter, optional slotId / referenceId. The source of truth for reconciliation. |
| Allotment request | Governance row in token_allotment_requests — a student asks for N mock + M resume tokens; a Batch Lead approves (credits promotional tokens) or rejects. |
| Resume-review request | Governance row in resume_review_requests — spending 1 resume token creates a review record. |
| TOKEN_VALUE | Rupee conversion constant (process.env.TOKEN_VALUE or default 350), used for mentor-earning valuation, not for purchase pricing. |
Main TypeORM entities (all in src/entities/):
| Entity | Table | File | Role |
|---|---|---|---|
Token |
tokens |
src/entities/Tokens.ts |
One-to-one wallet per user (token, resumeToken, expiresAt). |
TokenUsage |
token_usage |
src/entities/TokenUsage.ts |
Append-only ledger; enum TokenUsageType. |
TokenPurchase |
token_purchases |
src/entities/TokenPurchase.ts |
Razorpay order/invoice + payment status. Enum PaymentStatus. |
PromotionalToken |
promotional_tokens |
src/entities/PromotionalToken.ts |
Time-boxed promo grant (meetToken, resumeToken, tokenValue, expiredAt). |
TokenAllotmentRequest |
token_allotment_requests |
src/entities/TokenAllotmentRequest.ts |
Governance request + approval state. Enum TokenAllotmentRequestStatus. |
ResumeReviewRequest |
resume_review_requests |
src/entities/ResumeReviewRequest.ts |
Resume-review spend record. Enum ResumeReviewRequestStatus. |
Supporting services & utils:
src/services/TokenService.ts— wallet primitives (balance, purchase, verify, credit, debit, history).src/services/TokenAllotmentRequestService.ts— governance request lifecycle + transactional approval.src/services/ResumeReviewService.ts— transactional resume-token spend.src/services/StudentTokenLedgerService.ts— derived per-student allotted/used snapshot.src/utils/tokenValue.ts—TOKEN_VALUEconstant.src/utils/featureFlags.ts— governance kill-switches.
Note:
TokenUsageType.RESUME_REVIEWand resume tokens are also used by the Mr. Hire / resume-screening domain. This doc covers the wallet + governance mechanics; see recruitment for what a resume review does.
Architecture¶
flowchart TD
subgraph FE["Frontends"]
WEB["mas-website-live student portal"]
ADMIN["mr-mentor-frontend admin"]
end
subgraph RT["Routes (mounted at /api)"]
TR["token.routes.ts"]
GR["tokenGovernance.routes.ts (feature-flagged)"]
end
subgraph CT["Controllers"]
TC["TokenController"]
AC["TokenAllotmentRequestController"]
RC["ResumeReviewController"]
end
subgraph SV["Services"]
TS["TokenService"]
AS["TokenAllotmentRequestService"]
RS["ResumeReviewService"]
LS["StudentTokenLedgerService"]
MS["MentorService (booking / refund)"]
SC["SlotCleanupService (auto-refund)"]
end
subgraph DB["PostgreSQL (mas)"]
T["tokens"]
TU["token_usage"]
TP["token_purchases"]
PT["promotional_tokens"]
TAR["token_allotment_requests"]
RR["resume_review_requests"]
end
subgraph EXT["External"]
RZP["Razorpay (orders / invoices)"]
WA["WhatsApp invoice notify"]
Q["BullMQ cleanup queue"]
end
WEB --> TR
WEB --> GR
ADMIN --> TR
TR --> TC
GR --> AC
GR --> RC
TC --> TS
AC --> AS
RC --> RS
TS --> RZP
TS --> WA
TS --> T
TS --> TU
TS --> TP
TS --> TP
TS --> TP
AS --> TAR
AS --> PT
AS --> TU
RS --> RR
RS --> PT
RS --> T
RS --> TU
LS --> T
LS --> PT
LS --> TU
MS --> T
MS --> PT
MS --> TU
MS --> TS
SC --> TS
Q --> SC
TS --> TP
TP -.stores order/invoice ids.-> RZP
Data model¶
erDiagram
USER ||--o| TOKEN : "has wallet"
USER ||--o| PROMOTIONAL_TOKEN : "has promo grant"
USER ||--o{ TOKEN_USAGE : "ledger rows"
USER ||--o{ TOKEN_PURCHASE : "purchases"
USER ||--o{ TOKEN_ALLOTMENT_REQUEST : "raises"
USER ||--o{ RESUME_REVIEW_REQUEST : "raises"
SLOTS ||--o{ TOKEN_USAGE : "referenced by"
BATCH ||--o{ TOKEN_ALLOTMENT_REQUEST : "scoped to"
TOKEN {
uuid id PK
float token
float resumeToken
uuid userId FK
timestamp expiresAt
}
PROMOTIONAL_TOKEN {
bigint id PK
int tokenValue
int meetToken
int resumeToken
string userId FK
timestamp expiredAt
}
TOKEN_USAGE {
uuid id PK
uuid userId FK
uuid slotId FK
enum usageType
int tokensUsed
int balanceBefore
int balanceAfter
string referenceId
text description
}
TOKEN_PURCHASE {
uuid id PK
uuid userId FK
int tokenQuantity
decimal amount
string currency
enum paymentStatus
string razorpayOrderId
string razorpayPaymentId
string razorpayInvoiceId
timestamp completedAt
}
TOKEN_ALLOTMENT_REQUEST {
uuid id PK
uuid studentId FK
uuid batchId FK
uuid requestedByUserId FK
int mockTokens
int resumeTokens
text reason
enum status
uuid reviewedByBatchLeadId FK
timestamp reviewedAt
text reviewNotes
}
RESUME_REVIEW_REQUEST {
uuid id PK
uuid studentId FK
string resumeUrl
text notes
enum status
}
Notable enums / status fields:
PaymentStatus(token_purchases.paymentStatus):pending|completed|failed|cancelled.TokenUsageType(token_usage.usageType):meeting_booking|resume_review|penalty|refund|bonus.TokenAllotmentRequestStatus:pending|approved|rejected|withdrawn.ResumeReviewRequestStatus:pending|under_review|completed|rejected.
Audit convention:
token_usage.tokensUsedis stored as a positive magnitude for all types, includingrefundandbonus(credits). The direction is implied byusageTypeand bybalanceBefore/balanceAfter.recordTokenUsagecolumns are typedinteven though the wallettoken/resumeTokencolumns arefloat(a gotcha — see below).
API surface¶
All token routes are mounted at /api (src/routes/index.ts → this.router.use('/api', this.tokenRoutes.router)
and this.router.use('/api', this.tokenGovernanceRoutes.router)). Every endpoint requires a valid JWT via
authMiddleware (req.user.id). Governance endpoints additionally short-circuit to HTTP 503 when their
feature flag is OFF.
Core wallet — src/routes/token.routes.ts¶
| Method | Path | Auth/role | Purpose |
|---|---|---|---|
| GET | /api/tokens/balance |
JWT | Current spendable balance (wallet + valid promo). |
| POST | /api/tokens/purchase |
JWT | Create a TokenPurchase + Razorpay order/invoice. |
| POST | /api/tokens/verify-payment |
JWT | Verify Razorpay signature, mark completed, credit wallet. |
| GET | /api/tokens/history |
JWT | This user's purchase history (TokenPurchase[]). |
| GET | /api/tokens/usage-history |
JWT | This user's ledger (TokenUsage[], with slot relation). |
| DELETE | /api/tokens/purchase/:purchaseId |
JWT | Cancel a still-pending purchase. |
| POST | /api/tokens/deduct |
JWT | Direct wallet debit (no usage row recorded — see gotchas). |
Governance — src/routes/tokenGovernance.routes.ts¶
| Method | Path | Auth/role | Flag | Purpose |
|---|---|---|---|---|
| POST | /api/student/token-requests |
JWT | ENABLE_TOKEN_REQUEST_FLOW |
Student raises a token-allotment request. |
| GET | /api/student/token-requests |
JWT | ENABLE_TOKEN_REQUEST_FLOW |
List own requests. |
| DELETE | /api/student/token-requests/:id |
JWT | ENABLE_TOKEN_REQUEST_FLOW |
Withdraw own pending request. |
| GET | /api/batchlead/token-requests |
JWT + batchLeadMiddleware |
ENABLE_TOKEN_REQUEST_FLOW |
List requests for the BL's batches (?status=pending\|approved\|rejected\|withdrawn\|all). |
| PATCH | /api/batchlead/token-requests/:id |
JWT + batchLeadMiddleware |
ENABLE_TOKEN_REQUEST_FLOW |
Approve / reject a request. |
| POST | /api/resume-reviews |
JWT | ENABLE_RESUME_REVIEW_SPEND |
Spend 1 resume token, create a review. |
| GET | /api/resume-reviews |
JWT | ENABLE_RESUME_REVIEW_SPEND |
List own resume-review requests. |
| GET | /api/student/batch-meetings |
JWT | (no token flag) | Read-only batch-meeting visibility (co-located in this router). |
Booking and refunds have no dedicated token endpoint — they are side effects of the meeting flow (
MentorService.requestMeeting,approveMeetingCancellation) and the cleanup worker. ThePOST /api/tokens/deductendpoint exists but is a thin wrapper arounddeductTokensFromUserwith no options, so it does not write a ledger row; the real booking path bypasses it.
User journeys¶
1. View token balance¶
getUserTokenBalance sums the wallet token and, if a promo grant is still valid, its meeting tokens.
Expired promotional tokens contribute zero.
sequenceDiagram
participant FE as Frontend
participant API as TokenController
participant TS as TokenService
participant DB as PostgreSQL
FE->>API: GET /api/tokens/balance with JWT
API->>TS: getUserTokenBalance for userId
TS->>DB: find tokens row for user
TS->>DB: find promotional_tokens row for user
DB-->>TS: wallet and promo rows
Note over TS: promo counts only if expiredAt is in the future
TS-->>API: wallet.token plus valid promo meetToken
API-->>FE: 200 with tokenBalance and userId
2. Purchase tokens (Razorpay)¶
A two-step flow: create order then verify. The service prefers the Razorpay Invoices API so the student gets an official emailed tax invoice, and falls back to orders.create when invoicing is unavailable (missing email, feature disabled). Detailed payment mechanics live in payments; here is the token-side view.
sequenceDiagram
participant FE as Frontend
participant API as TokenController
participant TS as TokenService
participant RZP as Razorpay
participant DB as PostgreSQL
FE->>API: POST /api/tokens/purchase with quantity and amount
API->>TS: createTokenPurchase
TS->>DB: insert token_purchases status pending
TS->>RZP: invoices.create with customer and line item
alt invoice succeeds
RZP-->>TS: invoice with order_id and short_url
TS->>DB: save razorpayOrderId invoiceId invoiceUrl
else invoice fails or no email
TS->>RZP: orders.create fallback
RZP-->>TS: order id
TS->>DB: save razorpayOrderId
end
TS-->>API: tokenPurchase and razorpayOrder
API-->>FE: 201 with order and razorpayKeyId
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 token_purchases by order or invoice or latest pending
alt already completed
TS-->>API: error already completed
API-->>FE: 400 payment already completed
else signature mismatch
Note over TS: tries order and invoice HMAC formulas
TS-->>API: error invalid signature
API-->>FE: 400 invalid signature
else verified
TS->>DB: set paymentStatus completed and completedAt
TS->>TS: addTokensToUser credits wallet
TS->>DB: upsert tokens row new balance
TS->>RZP: send invoice WhatsApp best effort
TS-->>API: success with tokenPurchase
API-->>FE: 200 with new balance
end
Key alternates: duplicate verify is rejected with "Payment already completed"; an invalid HMAC across all
candidate formulas returns "Invalid signature"; the WhatsApp invoice notify is wrapped in try/catch and
never blocks the credit. addTokensToUser here is called without an options object, so a successful
purchase credit does not write a token_usage row (only refunds/bonuses do).
3. Spend a token to book a mentor meeting¶
Booking is the canonical spend. It runs inside a single DB transaction in MentorService.requestMeeting,
preferring promotional tokens, then wallet tokens. There is no separate "hold" record — the token is
debited immediately and the slot moves to UNDER_REVIEW.
sequenceDiagram
participant FE as Frontend
participant MS as MentorService
participant TXN as DB Transaction
participant MAIL as Email queue
FE->>MS: requestMeeting with mentorId and slot time
MS->>TXN: start transaction
TXN->>TXN: lock promotional_tokens for student
alt valid promo meetToken at least 1
TXN->>TXN: set slot UNDER_REVIEW and assign student
TXN->>TXN: decrement promo meetToken and sync tokenValue
TXN->>TXN: insert token_usage meeting_booking magnitude 1
TXN->>TXN: decrement mentor slotsLeft
TXN-->>MS: commit
else fall back to wallet
TXN->>TXN: read tokens row for student
alt wallet token below 1
TXN-->>MS: rollback Insufficient tokens
MS-->>FE: error Insufficient tokens to book this slot
else wallet token at least 1
TXN->>TXN: set slot UNDER_REVIEW and assign student
TXN->>TXN: decrement wallet token
TXN->>TXN: insert token_usage meeting_booking magnitude 1
TXN->>TXN: decrement mentor slotsLeft
TXN-->>MS: commit
end
end
MS->>MAIL: queue meeting approval email to mentor
MS-->>FE: saved slot UNDER_REVIEW
4. Refund a token on cancellation or expiry¶
Three paths credit a REFUND token, all via TokenService.addTokensToUser(..., { usageType: REFUND }),
which both bumps the wallet and writes a ledger row:
- Mentor approves a student's cancellation request —
MentorService.approveMeetingCancellation. - Admin-driven cancellation —
MentorServicecancellation path /AdminDashboardService. - A pending meeting request expires unattended —
SlotCleanupService(cleanup queue, every 24h).
sequenceDiagram
participant ACT as Mentor or Cleanup worker
participant MS as MentorService or SlotCleanupService
participant TS as TokenService
participant DB as PostgreSQL
ACT->>MS: approve cancellation or detect expired pending slot
MS->>MS: verify slot state cancellable or expired
MS->>TS: addTokensToUser student plus 1 usageType refund
TS->>DB: upsert tokens row balance plus 1
TS->>DB: insert token_usage refund magnitude 1 with slotId
MS->>DB: set slot CANCELLED and recreate available slot
MS-->>ACT: refund issued and slot freed
Note: the cleanup-worker refund calls TokenService outside the slot transaction, which the source
flags as not fully transaction-safe (best-effort).
5. Promotional token grant¶
Promotional tokens are written directly to promotional_tokens (by seeding, admin tooling, or governance
approval). They are split into meetToken and resumeToken, with tokenValue kept as the legacy
aggregate. They only count toward balance while expiredAt is in the future and are always spent before
wallet tokens.
sequenceDiagram
participant GRANT as Admin or governance or seed
participant PT as promotional_tokens
participant TS as TokenService balance read
GRANT->>PT: upsert meetToken and resumeToken and expiredAt
Note over PT: tokenValue equals meetToken plus resumeToken
TS->>PT: read promo on next balance check
alt expiredAt in future
TS-->>TS: add meetToken to spendable balance
else expired
TS-->>TS: ignore promo contributes zero
end
6. Governance — allotment request, approval, credit¶
The headline governance journey. A student raises a request for mock + resume tokens with a reason; the Batch Lead for the student's batch approves or rejects. Approval is fully transactional with pessimistic row locks and gated behind onboarding completion.
sequenceDiagram
participant ST as Student
participant AC as TokenAllotmentRequestController
participant AS as TokenAllotmentRequestService
participant BL as Batch Lead
participant TXN as DB Transaction
Note over AC: every handler returns 503 if ENABLE_TOKEN_REQUEST_FLOW is off
ST->>AC: POST student token-requests with mock resume and reason
AC->>AS: createStudentRequest
AS->>AS: validate amounts positive and reason present
AS->>TXN: reject if a pending request already exists
AS->>TXN: resolve batchId from student Application
AS->>TXN: insert token_allotment_requests status pending
AS-->>AC: created request
AC-->>ST: 201 request submitted
BL->>AC: GET batchlead token-requests filtered by batch
AC->>AS: listRequestsForBatchLead via BatchLeadAssignment and Batch
AS-->>BL: scoped request list
BL->>AC: PATCH batchlead token-requests id action approve
AC->>AS: reviewRequest
AS->>TXN: start transaction and lock the request row
alt request not pending
TXN-->>AS: error already approved or rejected
AS-->>BL: 400 already reviewed
else BL not authorized for batch
TXN-->>AS: error not authorized
AS-->>BL: 400 not authorized
else onboarding incomplete
Note over TXN: requires diagnosticCompleted and meetingCompleted
TXN-->>AS: error complete onboarding first
AS-->>BL: 400 onboarding pending
else approve
TXN->>TXN: lock or create promotional_tokens row
TXN->>TXN: add mockTokens to meetToken and resumeTokens to resumeToken
TXN->>TXN: extend expiredAt and sync tokenValue
TXN->>TXN: insert token_usage bonus magnitude total granted
TXN->>TXN: set request approved with reviewer and timestamp
TXN-->>AS: commit
AS-->>BL: 200 request approved
end
Reject path: reviewNotes is required, the row is locked and re-checked as pending, then set to
rejected with reviewer + notes — no token movement. Withdraw path: a student can only withdraw their own
pending request (withdrawStudentRequest).
7. Governance — resume-review spend¶
Spending a resume token mirrors meeting booking: promo resumeToken first, then wallet resumeToken, all
in one locked transaction, writing a RESUME_REVIEW ledger row.
sequenceDiagram
participant ST as Student
participant RC as ResumeReviewController
participant RS as ResumeReviewService
participant TXN as DB Transaction
Note over RC: returns 503 if ENABLE_RESUME_REVIEW_SPEND is off
ST->>RC: POST resume-reviews with resumeUrl
RC->>RS: createRequest
RS->>TXN: start transaction and lock promotional_tokens
alt valid promo resumeToken at least 1
TXN->>TXN: decrement promo resumeToken and sync tokenValue
TXN->>TXN: source is promotional
else fall back to wallet
TXN->>TXN: lock tokens row and read resumeToken
alt wallet resumeToken below 1
TXN-->>RS: rollback Insufficient resume tokens
RS-->>ST: 400 request more from Batch Lead
else wallet resumeToken at least 1
TXN->>TXN: decrement wallet resumeToken
TXN->>TXN: source is wallet
end
end
TXN->>TXN: insert resume_review_requests status pending
TXN->>TXN: insert token_usage resume_review magnitude 1
TXN-->>RS: commit
RS-->>ST: 201 review created
8. Ledger reconciliation (derived snapshot)¶
StudentTokenLedgerService.getSnapshot reconstructs an allotted-vs-used picture without storing it: it
sums token_usage per type and adds current balances. updateAllottedTokens lets an operator set a target
allotted number; the service back-solves the wallet balance as allotted - used - validPromo, clamped
at zero.
sequenceDiagram
participant OP as Admin or dashboard
participant LS as StudentTokenLedgerService
participant DB as PostgreSQL
OP->>LS: getSnapshot studentId
LS->>DB: find tokens row
LS->>DB: find promotional_tokens row
LS->>DB: sum token_usage meeting_booking as mockUsed
LS->>DB: sum token_usage resume_review as resumeUsed
Note over LS: allotted equals used plus walletBalance plus validPromo
LS-->>OP: snapshot allotted used per bucket and totals
OP->>LS: updateAllottedTokens with mockAllotted target
LS->>DB: set wallet token to max of zero and target minus used minus promo
LS-->>OP: refreshed snapshot
Background jobs & async¶
- Cleanup queue (BullMQ).
SlotCleanupServiceruns on a 24h schedule (seesrc/workers/cleanup.worker.tsand the cleanup job scheduled at startup). It finds expired pending meeting requests, sets themCANCELLED, recreates the available slot, and refunds 1 token viaTokenService.addTokensToUser(usageType: REFUND). This is the only async token mutation. - Email queue. Booking success queues a mentor approval email; cancellations queue cancellation-requested emails. These are notification side effects, not token mutations.
- WhatsApp invoice notify. After a verified purchase,
sendTokenPurchaseInvoiceWhatsAppfires best-effort (tries themas_invoicetemplate, falls back to plain text). It is awaited inside a try/catch and never blocks the token credit. - No webhooks for tokens. Payment confirmation is client-driven via
POST /api/tokens/verify-payment(HMAC signature verification), not a Razorpay server webhook in this domain. - No Socket.IO events are specific to tokens; balances are fetched on demand.
External integrations¶
| Integration | Used for | Env vars | Failure / fallback |
|---|---|---|---|
| Razorpay | Token purchase orders, invoices, payment verification | RAZORPAY_KEY_ID, RAZORPAY_KEY_SECRET |
If keys are unset, razorpay is null; createTokenPurchase throws "Razorpay is not configured" and verifyAndCompletePayment returns a failure. Invoices API failure falls back to orders.create. |
WhatsApp (WhatsAppService) |
Emailing/sending the token-purchase invoice | provider creds resolved by WhatsAppService |
Skipped if no phone or no configured provider; template failure falls back to plain text; entire block is best-effort. |
| MAS website URL | Invoice portal deep link in WhatsApp body | MAS_WEBSITE_URL |
Defaults to https://www.myanalyticsschool.com. |
| TOKEN_VALUE | Mentor-earning rupee valuation | TOKEN_VALUE |
Defaults to 350. |
Feature flags (src/utils/featureFlags.ts)¶
Flags are default-ON: a flag is OFF only if its env var is exactly "false" or "0". They are
kill-switches, not opt-ins — set the var and pm2 reload to disable without a redeploy.
| Flag | Env var | Gates |
|---|---|---|
tokenRequestFlow |
ENABLE_TOKEN_REQUEST_FLOW |
All student/BL token-request endpoints (503 when off). |
resumeReviewSpend |
ENABLE_RESUME_REVIEW_SPEND |
Resume-review spend endpoints (503 when off). |
starterBundle |
ENABLE_STARTER_BUNDLE |
Reserved — auto-grant starter bundle. Not yet consumed. |
grantAudit |
ENABLE_GRANT_AUDIT |
Reserved — audit rows on legacy direct grant. Not yet consumed. |
applicationMeetings |
ENABLE_APPLICATION_MEETINGS |
Free applicant meetings (adjacent flow). |
Rollout context: Token Governance Phase 1 (see docs/TOKEN_GOVERNANCE_PHASE1_DEPLOY.md in the backend repo,
dated 2026-04-18) shipped the two new tables (token_allotment_requests, resume_review_requests) and
their routes additively. TypeORM synchronize: true creates the tables + indexes on first boot; no ALTER
on existing tables. The deploy is safe with flags either ON or OFF — OFF means endpoints 503 with zero DB work.
Status lifecycles¶
TokenPurchase (PaymentStatus)¶
stateDiagram-v2
[*] --> pending : createTokenPurchase
pending --> completed : verifyAndCompletePayment signature valid
pending --> cancelled : DELETE tokens purchase id
pending --> failed : payment failure (inferred)
completed --> [*]
cancelled --> [*]
failed --> [*]
note right of completed : wallet credited here, terminal
failedis defined in the enum but not set by the current code paths shown (cancellation setscancelled); marked (inferred) as the intended terminal for declined payments.
TokenAllotmentRequest (TokenAllotmentRequestStatus)¶
stateDiagram-v2
[*] --> pending : student createStudentRequest
pending --> approved : BL approve, promo credited, bonus ledger row
pending --> rejected : BL reject with required notes
pending --> withdrawn : student withdraws own request
approved --> [*]
rejected --> [*]
withdrawn --> [*]
note right of pending : only one pending request per student allowed
ResumeReviewRequest (ResumeReviewRequestStatus)¶
stateDiagram-v2
[*] --> pending : createRequest token spent
pending --> under_review : reviewer picks it up
under_review --> completed : review delivered
pending --> rejected : declined
under_review --> rejected : declined
completed --> [*]
rejected --> [*]
Slot spend/refund interplay (token side)¶
stateDiagram-v2
[*] --> booked : requestMeeting debits 1 token, slot UNDER_REVIEW
booked --> confirmed : mentor confirms, no token change
confirmed --> cancellation_requested : student requests cancel
cancellation_requested --> refunded : mentor approves, plus 1 REFUND
booked --> refunded : pending request expires via cleanup worker
refunded --> [*]
Edge cases, limits & gotchas¶
- No true hold state. Booking debits immediately; there is no escrow/hold row. The refund path is the compensating action. If a refund call fails after the slot transaction commits (cleanup worker case), the source itself notes it is "not fully transaction-safe."
- Promo-before-wallet ordering. Every spend path (meeting booking, resume review) consumes valid promotional tokens before wallet tokens. Expired promos silently contribute zero — they are never cleaned up, just ignored on read.
POST /api/tokens/deductwrites no ledger row. The controller callsdeductTokensFromUserwith no options, so it mutates the wallet but skipsrecordTokenUsage. The real booking flow does not use this endpoint; treat it as a low-level/legacy utility and preferMentorServicefor auditable spends.- Purchase credit writes no ledger row.
verifyAndCompletePaymentcallsaddTokensToUserwithout options, so successful purchases are auditable only viatoken_purchases, nottoken_usage. Onlyrefund/bonuscredits land in the ledger. - int vs float mismatch.
tokens.token/resumeTokenarefloat;token_usagebalance columns areint. Fractional balances would be truncated in the ledger snapshot. - Idempotency on verify. A second
verify-paymentfor acompletedpurchase returns "Payment already completed" (no double credit). The verifier tries multiple HMAC formulas (order-flow and invoice-flow) to tolerate Checkout callbacks that omitorder_id, and can fall back to the most recent pending purchase for the user when neither order nor invoice id is supplied. - Governance auth nuance.
reviewRequestre-validates BL authorization againstBatchLeadAssignment(active) or the denormalisedBatch.batchLeadId, inside the locked transaction. Approval also requires the student to havediagnosticCompletedandmeetingCompletedon theirApplication— otherwise it errors with "Complete onboarding first." - One pending request per student.
createStudentRequestrejects a new request while anypendingone exists (anti-spam). - Flags default ON. Forgetting that the kill-switch is
false/0(not "unset = off") is a common trap: an empty ortruevalue leaves governance enabled. - Multi-platform. Token logic is platform-agnostic; the
x-platformheader does not branch token behavior in these services. - Reserved flags.
ENABLE_STARTER_BUNDLEandENABLE_GRANT_AUDITexist but no code path consumes them yet (Phase 2+ placeholders).
Related docs¶
- Payments & Razorpay — purchase order/invoice mechanics and signature verification.
- Mentor meetings & slots — booking, cancellation, and slot lifecycle that drive spends/refunds.
- Mentor earnings & payouts — how
TOKEN_VALUEconverts tokens to mentor rupees. - Recruitment / Mr. Hire — what a resume review does downstream.
- Finance & invoicing — GST invoices and finance reconciliation around purchases.
- Background jobs & workers — BullMQ cleanup queue that auto-refunds.
- Data model overview — full entity catalogue.