Admission Payments
How an application resolves its campus's compulsory admission-fee item, pays it idempotently through a self-registering resolver, and gates submission on it.
Admission fees connect two modules that deliberately don't import each other: payments knows how to charge; admissions knows who owns an application. The bridge is a resource-bound payment with an inverted dependency — worth understanding because it's the template for charging any future resource.
Resolving the fee
findAdmissionFeeItem(campusId) looks for a PaymentItem with reason: ADMISSION_FEES, status: ACTIVE, isCompulsory: true, and an audience containing a RESOURCE/APPLICATION rule. A campus-scoped item beats a school-scoped fallback; no item at all means 400 "No admission fee is configured for this campus". Because compulsory items are single-instance per scope + reason + year, a campus has exactly one live admission fee to find.
Paying it
initiatePayment guards first — the application must be draft, owned by the calling guest (400 "pay for your own"), campus-assigned, and complete — then delegates to paymentItemService.pay(...) with resourceId: application.id. Two properties matter:
- Idempotent by design: an existing active or successful transaction for the resource is returned as-is. Families can retry the payment button freely; no duplicate charges.
- Ownership is checked by the payments side too, via the resolver below — the admissions-side guard is UX, the resolver is the wall.
Submission then reads the same summary: getResourcePaymentSummary().paid is true only when the resource's transaction reached SUCCESS — that boolean is both the Paid/Unpaid badge staff see and the gate submit enforces.
The inverted dependency
ApplicationAudienceResolver (in application/audience/) implements the payment module's ResourceAudienceResolver contract and registers itself with the ResourceAudienceRegistry in onModuleInit():
resolveOwnership(userId, resourceId)→ the application belongs to the payer's guest profile, plus itscampusId.listOwned(userId)→ the guest's applications as payable resources.
The payment-item module therefore has no compile-time dependency on admissions — it just asks the registry "who resolves APPLICATION resources?". To make any new resource payable, implement the same contract and self-register; if the resolver isn't instantiated, resource-bound payments for that type silently can't resolve ownership — the one sharp edge to remember.
Where to go next
Admissions
The admission application — a guest-owned draft through a strict state machine, with fee-gated submission, emailed decisions, and worker-rendered PDFs.
Recruitment
Job vacancies and applications — staff-published postings, guest applicants, and a map-driven review state machine that strips PII from its audit trail.