Skip to content

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, default gen_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 (enum payment_method: WALLET, LIQPAY, MANUAL).
  • activatedAt (nullable timestamp) — null until first use.
  • validUntil (nullable timestamp) — activatedAt + validityDays per pass template, adjusted on resume.
  • pausedAt (nullable timestamp) — set on freeze; on resume the schema comment says validUntil += (now - pausedAt).
  • status (enum customer_pass_status: AWAITING_PAYMENT, PENDING, ACTIVE, PAUSED, EXPIRED, CANCELLED, default PENDING).
  • 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, passId NOT NULL with ON DELETE RESTRICT (enforced in tktspace-backend/libs/shared/data-access-db/src/lib/schema/passes.schema.ts).
  • status defaults to PENDING; price defaults to '0'; currency defaults to 'UAH' (enforced in tktspace-backend/libs/shared/data-access-db/src/lib/schema/passes.schema.ts).
  • paymentMethod is NOT NULL — every purchase records the channel (enforced in tktspace-backend/libs/shared/data-access-db/src/lib/schema/passes.schema.ts).

Business invariants:

  • customerId and passId are cross-schema FKs with ON DELETE RESTRICT — you cannot hard-delete a customer or a pass template while any customer-pass row references them.
  • Snapshot semantics: priceName, price, currency, paymentMethod are 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 = now and validUntil = activatedAt + pass.validityDays. Before activation (activatedAt IS NULL), status is PENDING.
  • Pause/resume: on pause, pausedAt = now. On resume, validUntil += (now - pausedAt) and pausedAt = NULL. The validity timer is effectively frozen while paused.
  • lowSessionsNotifiedAt and expiryNotifiedAt are 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; validUntil is not consumed while paused.
  • EXPIRED — past validUntil.
  • CANCELLED — cancelled, optionally with wallet refund per pass template’s cancelRefundPolicy.
created (LIQPAY) → AWAITING_PAYMENT
created (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)

SourceWhat it does
CustomerPassesService.issue / purchaseCreates the row. paymentMethod=LIQPAYAWAITING_PAYMENT; otherwise → PENDING.
LiqPay webhook handlerAWAITING_PAYMENT → PENDING on success, → CANCELLED on failure.
First-use activation pathPENDING → ACTIVE, sets activatedAt, validUntil (customer-passes.service.ts:400, :680).
CustomerPassesService.pause / resumeACTIVE ↔ 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) — customerIdcompanies.company_customer.id, on-delete restrict. N:1.
  • Pass (ENT-029) — passIdpasses.pass.id, on-delete restrict. N:1.
  • Customer entitlement (ENT-027) — 1:N per-activity allotment rows.

API surfaces

SurfaceExposedNotes
clientyes — /companies/{companyId}/passes/mine, /companies/{companyId}/passes/purchase, /companies/{companyId}/passes/{passId}/cancel (MyPassDto, PurchasePassResponse, PurchasePassPaymentMethod)Swagger UI
businessyes — /customers/{customerId}/passes, .../pause, .../resume, .../adjust (CustomerPassResponseDto, PaginatedCustomerPassResponseDto, IssuePassDto, IssuePassPaymentMethod, AdjustCustomerPassDto, CustomerPassStatus)Swagger UI
super-adminno

Known gotchas / open questions

  • Pause/resume logic compensates validUntil by 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/expiryNotifiedAt are once-only triggers; resetting them is application-side.
  • AWAITING_PAYMENT is LIQPAY-specific. WALLET / MANUAL purchases skip straight to PENDING. The lifecycle differs by paymentMethod at 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 / expiryNotifiedAt on 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 (instant PENDING). 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 externalId UNIQUE recommendation.