Lifecycle booking

Status booking Termilo: held, confirmed, completed, cancelled, dan no_show — beserta transisi yang diizinkan, reschedule, kunci slot + idempotency saat membuat booking publik, dan manage token untuk customer.

Lihat .md
Alur utama (happy path)
  1. held — slot ditahan

    hold 10 menit

  2. confirmed — booking sah

  3. completed — sesi selesai

Status booking#

Field bookings.status hanya bisa bernilai salah satu dari lima status berikut. Tiga di antaranya terminal — sekali tercapai, booking tidak berpindah status lagi.

StatusLabelTerminalKeterangan
heldMenunggutidakSlot ditahan sementara saat booking baru dibuat. Kedaluwarsa otomatis jika tidak dikonfirmasi.
confirmedTerkonfirmasitidakBooking sah dan slot terkunci. Dari sini bisa diselesaikan, ditandai tidak hadir, dibatalkan, atau dijadwal ulang.
completedSelesaiyaSesi selesai dijalankan. Status terminal.
cancelledDibatalkanyaBooking dibatalkan oleh tenant atau customer. Status terminal.
no_showTidak hadiryaCustomer tidak hadir di sesi yang sudah terkonfirmasi. Status terminal.
Konvensi waktu
Semua instan disimpan sebagai UTC epoch milliseconds di kolom INTEGER dengan akhiran _at_ms — misalnya starts_at_ms, hold_expires_at_ms, confirmed_at_ms. Bukan epoch detik.

Transisi yang diizinkan#

Perpindahan status diatur di modules/booking-lifecycle/policy.ts. Hanya transisi di bawah ini yang valid; transisi lain ditolak dengan kode error BOOKING_POLICY_BLOCKED.

POST /api/bookings/:bookingId/confirm. Mengonfirmasi hold menjadi booking sah; mengisi confirmed_at_ms dan menghentikan kedaluwarsa hold.

Reschedule tidak mengubah status
Menjadwal ulang hanya memindahkan starts_at_ms / ends_at_ms. Booking yang confirmed tetap confirmed setelah dijadwal ulang — slot baru tetap dikunci ulang dan diperiksa tumpang-tindih.

Hold, slot-lock & idempotency#

Booking baru selalu lahir sebagai held dengan hold_expires_at_ms = now + 10 menit. Selama jendela itu, slot ditahan supaya dua orang tidak mengambil waktu yang sama. Saat create, slot dicek ulang di bawah Durable Object lock — jika sudah tidak tersedia, request gagal dengan BOOKING_SLOT_UNAVAILABLE (409).

Slot adalah perkiraan; dicek ulang saat booking dibuat
Respons availability membawa marker finalAuthority: "booking_create_recheck_under_lock". Slot yang ditampilkan adalah kandidat, bukan otoritas final — kebenaran ditentukan saat create di bawah lock.

Endpoint create publik mewajibkan header Idempotency-Key. Mengulang request dengan kunci yang sama mengembalikan booking yang sama (idempotentReplay: true, status 200) alih-alih membuat booking ganda — aman untuk retry jaringan.

PARAMETERTIPEWAJIBKETERANGAN
Idempotency-KeyheaderwajibWajib di header. Mengulang request dengan kunci sama mengembalikan booking yang sama (replay), bukan booking baru.
serviceSlugstringwajibSlug layanan yang dibooking.
startsAtMsepoch mswajibAwal slot dalam UTC epoch milliseconds. Dicek ulang di bawah lock.
source"public_page" | "embed"wajibPermukaan asal booking. Tercatat di bookings.source.
customerobjectwajib{ name, primaryEmail?, phone?, locale?, timezone? } — email ATAU phone wajib ada.
curl -X POST https://termilo.id/book/akun-anda/bookings \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 1f2c9a7e-…" \ -d '{ "serviceSlug": "konsultasi-30", "startsAtMs": 1750848000000, "source": "public_page", "customer": { "name": "Budi", "phone": "+628120000000" } }'

Booking baru mengembalikan 201; replay idempoten mengembalikan 200 dengan booking yang sama plus manageToken dan manageUrl untuk self-service customer.

201 Createdapplication/json
{
"booking": {
"bookingId": "bkg_3aF9…",
"status": "held",
"source": "public_page",
"startsAtMs": 1750848000000,
"endsAtMs": 1750849800000,
"holdExpiresAtMs": 1750846800000,
"timezone": "Asia/Jakarta"
},
"idempotentReplay": false,
"manageToken": "tm_m1.…",
"manageUrl": "https://…/booking/manage/tm_m1.…"
}

Endpoint lifecycle#

Create bersifat publik (tanpa auth, dilindungi kebijakan). Aksi lifecycle lainnya berjalan di dashboard dengan sesi login Google + header CSRF X-Termilo-CSRF dan tunduk pada RBAC.

MethodPathCatatan
POST/book/:tenantSlug/bookingsBuat booking publik (header Idempotency-Key wajib). Status awal: held.
GET/api/bookingsDaftar booking. Query: status, fromMs, toMs, limit (bawaan 50, maks 100).
GET/api/bookings/:bookingIdDetail satu booking.
POST/api/bookings/:bookingId/confirmheld → confirmed.
POST/api/bookings/:bookingId/cancelheld | confirmed → cancelled (body: reason).
POST/api/bookings/:bookingId/completeconfirmed → completed.
POST/api/bookings/:bookingId/no-showconfirmed → no_show (body: reason).
POST/api/bookings/:bookingId/reschedulePindahkan waktu (body: startsAtMs). Status tetap.

Manage token customer#

Customer tidak punya akun. Setiap booking create mengembalikan manageToken bertanda tangan dan manageUrl yang membatasi self-service pada satu booking. Format tm_m1.<payload>.<hmac> (HMAC-SHA256, TTL 30 hari), terikat ke satu { tenantId, bookingId, customerId } dengan kumpulan aksi read | cancel | reschedule.

MethodPathCatatan
GET/booking/manage/:tokenDetail booking + actions: { canCancel, canReschedule }.
POST/booking/manage/:token/cancelCustomer membatalkan bookingnya sendiri.
POST/booking/manage/:token/reschedulePindahkan waktu (body: startsAtMs). Kunci ulang + cek tumpang-tindih.
Batas akses token
Manage token tidak bisa menjangkau /api/* dan tidak membuat sesi. Responsnya selalu dikirim dengan cache-control: private, no-store.
Semua endpoint di halaman ini sudah liveNotifikasi & webhook booking: Live

Mutasi lifecycle menulis baris ke tabel integration_outbox / notification_events, tetapi pengiriman webhook keluar (mis. booking.created, booking.cancelled) dikirim secara at-least-once dengan signature dan retry. Lihat halaman Webhooks untuk verifikasi signature serta panduan deduplikasi.