Naalya Handbook
Admissions & Recruitment

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 its campusId.
  • 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

On this page