Customer pass
A specific Company customer’s instance of a Pass. It holds a snapshot of the template fields at purchase time (priceName, price, currency, paymentMethod) plus per-instance state (status, activatedAt, validUntil, pausedAt). Each booking that uses the pass consumes one Customer entitlement — a child row that tracks remaining sessions per activity. Template edits do not propagate here; the snapshot is what the customer keeps.
Identity & key fields
- Primary key:
id(uuid, defaultgen_random_uuid()). customerId(uuid, NOT NULL, FK →companies.company_customer.id, on-delete restrict).passId(uuid, NOT NULL, FK →passes.pass.id, on-delete restrict).priceName(nullable text),price(decimal 10,2, default'0'),currency(text, default'UAH') — price tier snapshot at purchase time.paymentMethod(enumpayment_method:WALLET,LIQPAY,MANUAL).activatedAt(nullable timestamp) — null until first use.validUntil(nullable timestamp) —activatedAt + validityDaysper pass template, adjusted on resume.pausedAt(nullable timestamp) — set on freeze; on resume the schema comment saysvalidUntil += (now - pausedAt).status(enumcustomer_pass_status:AWAITING_PAYMENT,PENDING,ACTIVE,PAUSED,EXPIRED,CANCELLED, defaultPENDING).lowSessionsNotifiedAt,expiryNotifiedAt(nullable timestamps) — one-shot notification flags.
activatedAt IS NULL means the customer hasn’t started using the pass yet — validity timer hasn’t begun. pausedAt IS NOT NULL means the validity timer is currently frozen. The (status, activatedAt, validUntil, pausedAt) quartet together describes the pass state.
Invariants
customerId,passIdNOT NULL with ON DELETE RESTRICT (enforced in tktspace-backend/libs/shared/data-access-db/src/lib/schema/passes.schema.ts).statusdefaults toPENDING;pricedefaults to'0';currencydefaults to'UAH'(enforced in tktspace-backend/libs/shared/data-access-db/src/lib/schema/passes.schema.ts).paymentMethodis NOT NULL — every purchase records the channel (enforced in tktspace-backend/libs/shared/data-access-db/src/lib/schema/passes.schema.ts).
Business invariants:
customerIdandpassIdare cross-schema FKs withON DELETE RESTRICT— you cannot hard-delete a customer or a pass template while any customer-pass row references them.- Snapshot semantics:
priceName,price,currency,paymentMethodare written at purchase time and never updated to reflect later template changes. The customer keeps the terms they bought under. - Activation rule: at first use, the service sets
activatedAt = nowandvalidUntil = activatedAt + pass.validityDays. Before activation (activatedAt IS NULL), status isPENDING. - Pause/resume: on pause,
pausedAt = now. On resume,validUntil += (now - pausedAt)andpausedAt = NULL. The validity timer is effectively frozen while paused. lowSessionsNotifiedAtandexpiryNotifiedAtare one-shot flags — once set they are not cleared automatically; resetting requires app logic (e.g. on resume).
Lifecycle
Status enum values, per schema comments:
AWAITING_PAYMENT— LiqPay flow: row created, waiting for gateway webhook.PENDING— paid, not yet activated (first use missing).ACTIVE— activated; validity timer is running.PAUSED— frozen;validUntilis not consumed while paused.EXPIRED— pastvalidUntil.CANCELLED— cancelled, optionally with wallet refund per pass template’scancelRefundPolicy.
created (LIQPAY) → AWAITING_PAYMENTcreated (WALLET / MANUAL) → PENDING (paid synchronously)AWAITING_PAYMENT → PENDING (LiqPay webhook: success)AWAITING_PAYMENT → CANCELLED (LiqPay webhook: failure / timeout)PENDING → ACTIVE (first use → activatedAt, validUntil set)ACTIVE ↔ PAUSED (admin or customer: pause / resume)ACTIVE → EXPIRED (cron daily 1AM, validUntil passed)ACTIVE | PAUSED | PENDING → CANCELLED (explicit cancel; refund per cancelRefundPolicy if WALLET)Mutation sources (verified against code)
| Source | What it does |
|---|---|
CustomerPassesService.issue / purchase | Creates the row. paymentMethod=LIQPAY → AWAITING_PAYMENT; otherwise → PENDING. |
| LiqPay webhook handler | AWAITING_PAYMENT → PENDING on success, → CANCELLED on failure. |
| First-use activation path | PENDING → ACTIVE, sets activatedAt, validUntil (customer-passes.service.ts:400, :680). |
CustomerPassesService.pause / resume | ACTIVE ↔ PAUSED. Resume bumps validUntil by paused duration (customer-passes.service.ts:196). |
CustomerPassesService.cancel | * → CANCELLED. Refund computed per cancelRefundPolicy when WALLET-paid. |
PassesScheduler.handlePassExpiry (cron EVERY_DAY_AT_1AM) | ACTIVE → EXPIRED for rows past validUntil. |
PassesScheduler.handleLowSessionsNotifications (cron 0 9 * * *) | Sends “low sessions left” push. Does not change status — sets lowSessionsNotifiedAt. |
PassesScheduler.handleExpiryNotifications (cron 0 10 * * *) | Sends “expiring soon” push. Does not change status — sets expiryNotifiedAt. |
Files: libs/features/passes/src/lib/services/customer-passes.service.ts, libs/features/passes/src/lib/services/passes-scheduler.service.ts.
Relationships
- Company customer (ENT-017) —
customerId→companies.company_customer.id, on-delete restrict. N:1. - Pass (ENT-029) —
passId→passes.pass.id, on-delete restrict. N:1. - Customer entitlement (ENT-027) — 1:N per-activity allotment rows.
API surfaces
| Surface | Exposed | Notes |
|---|---|---|
| client | yes — /companies/{companyId}/passes/mine, /companies/{companyId}/passes/purchase, /companies/{companyId}/passes/{passId}/cancel (MyPassDto, PurchasePassResponse, PurchasePassPaymentMethod) | Swagger UI |
| business | yes — /customers/{customerId}/passes, .../pause, .../resume, .../adjust (CustomerPassResponseDto, PaginatedCustomerPassResponseDto, IssuePassDto, IssuePassPaymentMethod, AdjustCustomerPassDto, CustomerPassStatus) | Swagger UI |
| super-admin | no | — |
Known gotchas / open questions
- Pause/resume logic compensates
validUntilby elapsed paused duration (per schema comment) — DB does not enforce this; service layer must do it. - ON DELETE RESTRICT on both FKs — cancelling a customer or archiving a pass template requires explicit handling.
lowSessionsNotifiedAt/expiryNotifiedAtare once-only triggers; resetting them is application-side.AWAITING_PAYMENTis LIQPAY-specific. WALLET / MANUAL purchases skip straight toPENDING. The lifecycle differs bypaymentMethodat the moment of purchase.- Snapshot semantics are surprising. Customers keep the price and terms they bought under, even if the template was raised, lowered, or restructured later. Refunds aren’t issued automatically when an admin lowers a price.
- There is no DB-enforced cap on pause duration. A pass with repeated long pauses can effectively become indefinite.
Recommendations
Forward-looking improvements suggested while filling this doc — not currently in place.
- Cap on total paused time (or on a single pause duration) — without it, pause-and-resume is a workaround that defeats
validityDays. - Audit log for status transitions — especially for disputes (“when was my pass cancelled? when did it expire?”).
- DB CHECK
validUntil > activatedAt(when both set) — promote the implicit rule. - Reset
lowSessionsNotifiedAt/expiryNotifiedAton resume — otherwise customers who pause then resume never get warned again. - Document payment-method-specific lifecycles separately — LIQPAY (
AWAITING_PAYMENT→ webhook →PENDING) vs WALLET / MANUAL (instantPENDING). Today the schema enum suggests one flow; reality has two. - Idempotency guard on the LiqPay webhook — repeated callbacks must not duplicate-issue passes. Pairs with the Payment
externalIdUNIQUE recommendation.