Payment Workflow — Option B
Parent pays outside the system (cash, MoMo, bank, agent), then submits a payment claim in the portal. The bursar verifies against real-world records before a receipt is issued. Both sides always see the same status.
School policy (non-negotiable)
- A fee is officially paid only when status is APPROVED and a receipt exists in the system.
- Parents must submit a claim within 7 calendar days of payment (configurable per school).
- Claims older than the policy window move to EXPIRED unless bursar extends.
- Balance on the parent portal shows outstanding until approval — not when MoMo is sent.
End-to-end flow
PARENT SYSTEM (fee-service) BURSAR
| | |
|-- Pays cash/MoMo/bank ------------>| |
| (outside system) | |
| | |
|-- Opens portal "I have paid" ----->| |
|-- Fills claim form -------------->| status: PENDING_VERIFICATION |
| |-- SMS/email alert ---------->| New claim in queue
| | |
| |<-- Opens claim --------------|
| | status: UNDER_REVIEW |
| | |
| |<-- Approves + links invoice -|
| | status: APPROVED |
| |-- PaymentReceived event ---->|
| |-- Receipt PDF + SMS -------->| Parent
|<-- Receipt in document library ----| |
Direct in-office recording (bursar enters while parent waits) remains supported and skips the claim queue — it creates an APPROVED payment immediately with source BURSAR_DIRECT.
Payment claim statuses
| Status | Who sees it | Meaning | Next actions |
|---|---|---|---|
| DRAFT | Parent only | Form started, not submitted | Parent completes and submits |
| PENDING_VERIFICATION | Parent + Bursar | Claim submitted, awaiting bursar | Bursar opens → UNDER_REVIEW |
| UNDER_REVIEW | Parent + Bursar | Bursar is actively checking | Approve, reject, or request info |
| NEEDS_INFO | Parent + Bursar | Bursar needs more details | Parent updates claim → back to PENDING_VERIFICATION |
| APPROVED | Parent + Bursar + Director | Verified; receipt issued; balance reduced | Terminal (unless void by admin) |
| REJECTED | Parent + Bursar | Payment not accepted (with reason) | Parent may resubmit new claim if error |
| DUPLICATE_SUSPECTED | Bursar | Same transaction ID exists | Bursar merges or rejects duplicate |
| CANCELLED | Parent | Parent withdrew before review | Terminal |
| EXPIRED | Parent + Bursar | Not actioned within policy window | Bursar may reopen with override |
Payment record status (after approval)
Separate from claim status — the financial payments table:
CONFIRMED— approved claim or direct bursar entryVOIDED— director/bursar void with mandatory reason (audit logged)REFUNDED— partial or full refund linked to original payment
Parent claim form — fields
| Field | Required | Validation |
|---|---|---|
| Student | Yes | Must be child linked to logged-in guardian |
| Invoice / term | Yes | Open invoice for selected student; partial pay allowed |
| Amount (UGX) | Yes | > 0; warn if > outstanding balance |
| Payment method | Yes | CASH, MTN_MOMO, AIRTEL_MOMO, BANK_TRANSFER, BANK_AGENT, OTHER |
| Transaction ID | Conditional | Required for MoMo/bank; optional for cash (use office slip ref) |
| Phone used / payer phone | Conditional | Required for MoMo; validates E.164 Uganda |
| Agent number | Conditional | Required when method = BANK_AGENT |
| Bank name | Conditional | Required for BANK_TRANSFER |
| Payment date | Yes | Not in future; not older than school max (default 90 days) |
| Notes / reason | No | Free text — e.g. "Term 2 boarding balance" |
| Proof attachment | Recommended | PNG/JPG/PDF max 5 MB → file-service |
UI sample — Parent payment claim
Open full screen →
Bursar verification queue
Bursar dashboard shows filters: Pending, Under review, Needs info, Duplicates, All today.
On approve, bursar must:
- Confirm amount matches MoMo/bank/cash record
- Select invoice line(s) to apply payment (auto-suggest oldest open invoice)
- Optionally adjust approved amount (with reason if different from claim)
- Click Approve & issue receipt — atomic transaction
System generates receipt number, publishes PaymentReceived, posts to finance-service ledger (Phase 2 basic), notifies parent.
UI sample — Bursar verification queue
Open full screen →
Edge cases & business rules
| Scenario | System behaviour |
|---|---|
| Partial payment | Reduces invoice balance; student stays on defaulter list until fully paid |
| Overpayment | Warn parent on submit; bursar can approve excess → credit on fee account for next term |
| One MoMo for two children | Parent submits two claims with same transaction ID + note; bursar splits amounts; second flagged DUPLICATE_SUSPECTED until linked |
| Duplicate transaction ID | Second claim auto-flagged DUPLICATE_SUSPECTED; bursar rejects or confirms split payment |
| Wrong student selected | Bursar rejects with reason; parent resubmits |
| Cash — no transaction ID | Parent enters office_reference or bursar-generated slip number |
| Payment before invoice exists | Block claim — show "No open invoice for this term" |
| Guardian not linked to student | 403 — cannot claim for unlinked child |
| Two bursars open same claim | Optimistic lock on version column; second gets conflict message |
| Bursar approves wrong amount | Void payment (director approval if > threshold); re-open claim |
| Parent uploads fake screenshot | Bursar rejects; repeated abuse → admin flag on account |
| Claim not reviewed in 7 days | Auto EXPIRED; SMS reminder to bursar at day 3 and 6 |
| Sibling discount / bursary | Applied on invoice before claim; parent pays net amount |
| Multi-campus | Claim scoped to student's campus; bursar role limited by campus |
Domain events & notifications
| Event | Trigger | Consumers |
|---|---|---|
PaymentClaimSubmitted | Parent submits | notification-service → bursar SMS/email; report-service metrics |
PaymentClaimNeedsInfo | Bursar requests info | notification-service → parent |
PaymentReceived | Claim approved or direct entry | file-service (receipt PDF), notice-service (document library), notification-service (parent SMS), finance-service (ledger) |
PaymentClaimRejected | Bursar rejects | notification-service → parent with reason |
PaymentVoided | Admin void | finance-service reversal, parent notice |
API endpoints (fee-service via gateway)
# Parent portal
GET /api/v1/portal/fee-accounts/{studentId} # balance, open invoices
POST /api/v1/portal/payment-claims # submit claim
GET /api/v1/portal/payment-claims # list own claims
GET /api/v1/portal/payment-claims/{id} # detail + status history
PUT /api/v1/portal/payment-claims/{id} # update when NEEDS_INFO
DELETE /api/v1/portal/payment-claims/{id} # cancel if PENDING only
POST /api/v1/portal/payment-claims/{id}/attachments # proof upload
# Bursar / admin
GET /api/v1/admin/payment-claims?status=&campusId= # verification queue
GET /api/v1/admin/payment-claims/{id} # full detail + audit
POST /api/v1/admin/payment-claims/{id}/start-review # → UNDER_REVIEW
POST /api/v1/admin/payment-claims/{id}/needs-info # body: { message }
POST /api/v1/admin/payment-claims/{id}/approve # body: { amount, invoiceIds[] }
POST /api/v1/admin/payment-claims/{id}/reject # body: { reason }
POST /api/v1/admin/payments # direct entry (BURSAR_DIRECT)
POST /api/v1/admin/payments/{id}/void # void with reason
Data model (fee-service DB)
payment_claims
id UUID PK
organization_id, campus_id
student_id, guardian_id
invoice_id (nullable if multi-invoice apply)
amount_claimed DECIMAL(14,2)
amount_approved DECIMAL(14,2) NULL
currency CHAR(3) DEFAULT 'UGX'
payment_method ENUM
transaction_id VARCHAR(128) NULL
payer_phone VARCHAR(20) NULL
agent_number VARCHAR(32) NULL
bank_name VARCHAR(64) NULL
payment_date DATE
notes TEXT
proof_file_id UUID NULL → file-service
status ENUM (see statuses above)
duplicate_of_claim_id UUID NULL
reviewed_by UUID NULL
reviewed_at TIMESTAMPTZ NULL
rejection_reason TEXT NULL
version INT DEFAULT 1 -- optimistic lock
created_at, updated_at
payment_claim_status_history
id, claim_id, from_status, to_status, actor_id, note, created_at
payments
id, organization_id, campus_id, student_id
claim_id UUID NULL -- null if BURSAR_DIRECT
source ENUM: PARENT_CLAIM | BURSAR_DIRECT
amount, payment_method, transaction_id
receipt_number VARCHAR UNIQUE
status CONFIRMED | VOIDED | REFUNDED
void_reason, voided_by, voided_at
created_at
payment_allocations
payment_id, invoice_id, amount_applied
fee_invoices / fee_invoice_lines -- existing invoice model
fee_accounts -- per-student running balance
Index requirements: unique partial index on
(organization_id, transaction_id) where status NOT IN (REJECTED, CANCELLED, EXPIRED) to catch duplicates at insert time.