Skip to main content

Status Field Conventions

Post split-booking-status-axes (2026-04), every booking carries TWO orthogonal status axes (lifecycle + payment). The payment_transaction.status is a third, independent enum with its own conventions.

Canonical forms​

FieldCaseSpellingExample values
booking.lifecycle_statuslowercaseUShold, confirmed, arrived, completed, canceled, no_show, expired
booking.payment_statuslowercasen/apending, paid, part_paid, failed, refunded
payment_transaction.statuslowercaseUSpending, processing, completed, failed, canceled, refunded

Note the US spelling canceled (one L) used system-wide. The UK spelling cancelled (two Ls) is considered a bug.

The two axes are independent. A booking can be lifecycleStatus='completed' + paymentStatus='pending' (service delivered, customer still owes — surfaced in the admin "Unpaid completed" filter). Pre-split this combined state was unrepresentable.

Why these conventions?​

  • booking.lifecycle_status is a Postgres enum (lifecycle_status_enum). The DB enforces the 7 lowercase canonical values.
  • booking.payment_status is a Postgres enum (payment_status_enum). The DB enforces the 5 lowercase canonical values. (Pre-split this was a free-form String column with UPPERCASE writes — the split normalised it to a strict enum.)
  • payment_transaction.status is its own domain enum (PAYMENT_TRANSACTION_STATUS) with values for the payment-processor lifecycle, distinct from booking's split axes.

Import canonical constants, don't hardcode strings​

Backend (API, worker)​

import {
LIFECYCLE_STATUS,
PAYMENT_STATUS,
PAYMENT_TRANSACTION_STATUS,
type LifecycleStatus,
type PaymentStatus,
type PaymentTransactionStatus,
} from '@booking/contracts';

// ✓ Good — two-axis assertions
if (booking.lifecycleStatus === LIFECYCLE_STATUS.CANCELED) { ... }
if (booking.paymentStatus === PAYMENT_STATUS.PAID) { ... }

tx.paymentTransaction.create({
data: { status: PAYMENT_TRANSACTION_STATUS.COMPLETED, ... },
});

// ✗ Bad — hardcoded literal, case-sensitive typo-prone
if (booking.lifecycleStatus === 'CANCELLED') { ... } // wrong case + wrong spelling

Frontend (web)​

The web app has its own local constants file at apps/web/lib/constants/booking-status.ts. It re-exports LIFECYCLE_STATUS and PAYMENT_STATUS from @booking/contracts:

import { LIFECYCLE_STATUS } from '@/lib/constants/booking-status';

if (booking.lifecycleStatus === LIFECYCLE_STATUS.CANCELED) { ... }

For combined-state UI badges (e.g. "Deposit paid", "Unpaid completed"), use the derived DISPLAY_STATUS enum + deriveDisplayStatus(lifecycle, payment) helper from @booking/contracts. Don't try to recompute combined display states from raw axes in components.

Why does my comparison always return false?​

Most common cause: comparing against a pre-split value that no longer exists. The pre-split single-axis enum had 'tentative', 'paid', 'part_paid', 'compensation_failed' — none of which are valid lifecycle values post-split. Use the two-axis equivalents:

Pre-split (no longer valid)Post-split equivalent
lifecycleStatus === 'tentative'lifecycleStatus === 'confirmed' && paymentStatus === 'pending' (PoA)
lifecycleStatus === 'paid'lifecycleStatus === 'confirmed' && paymentStatus === 'paid'
lifecycleStatus === 'part_paid'lifecycleStatus === 'confirmed' && paymentStatus === 'part_paid'
lifecycleStatus === 'compensation_failed'lifecycleStatus === 'canceled' && paymentStatus === 'failed' && compensationStatus === 'exhausted'

Second most common cause: case mismatch. Check what the data source actually returns:

Data sourceWhat it returns
Prisma booking.findManylowercase booking.lifecycleStatus + booking.paymentStatus
GET /api/v1/customers/me/bookingslowercase lifecycleStatus + paymentStatus
GET /api/v1/admin/bookings/searchlowercase lifecycleStatus + paymentStatus
Prisma paymentTransaction.findManylowercase status

Then compare against the matching canonical constant — never a hardcoded string literal.

Historical context​

Three status-related bugs were fixed in quick succession in 2026-04, all case-mismatch between writer and reader. The constants pattern above prevents this class of bug at the type level — typos and wrong-case literals become compile errors when the comparison target is typed as LifecycleStatus/PaymentStatus etc.

The split itself (Section 11 of the split-booking-status-axes change, 2026-04) made one more class of bug impossible: conflating lifecycle and payment axes. Pre-split, "did the customer pay?" required reading the lifecycle enum (where 'paid' was a value); post-split, the payment axis is its own column with its own enum. Cancellation can no longer accidentally clear payment state, and "completed + unpaid" becomes a representable state.