Konsep Inti

Pahami tenant, layanan, ketersediaan, slot, booking & status, customer, dan deposit di Termilo dengan nama field aslinya. Setiap konsep dipetakan langsung ke kolom yang benar-benar tersimpan di database tenant.

Lihat .md
Semua field di halaman ini nyata
Nama kolom di bawah diambil langsung dari skema database tenant (db/migrations/tenant). Yang ditandai Tidak tersedia tidak bisa dipakai lewat API atau dashboard hari ini.

Konvensi data#

Sebelum membaca field mana pun, kenali tiga aturan penyimpanan yang berlaku di seluruh Termilo:

  • Instant disimpan sebagai UTC epoch milidetik di kolom INTEGER bersufiks _at_ms — mis. created_at_ms, starts_at_ms, hold_expires_at_ms. Bukan epoch detik.
  • Waktu-dalam-hari adalah menit sejak tengah malam: start_minute dan end_minute (mis. 09:00 = 540).
  • Hari diwakili weekday bernilai 0–6 (0 = Minggu … 6 = Sabtu).
Zona waktu
Setiap tenant punya timezone (bawaan Asia/Jakarta). Epoch ms selalu UTC; zona waktu hanya dipakai untuk menampilkan dan menghitung jam operasional.

Tenant (bisnis)#

Satu tenant adalah satu bisnis booking. Setiap baris data diisolasi berdasarkan tenant_id dan berada di shard D1 yang dipilih router control-plane (tenant_shard_routes / tenant_registry). Tabel tenants:

PARAMETERTIPEWAJIBKETERANGAN
tenant_idstringwajibID unik bisnis. Setiap baris data diisolasi per tenant_id.
slugstringwajibPengenal di URL booking page, mis. /book/:slug.
namestringwajibNama bisnis yang tampil ke pelanggan.
timezonestringopsionalZona waktu operasional. Bawaan Asia/Jakarta.
localestringopsionalBahasa & format. Bawaan id-ID.
statusactive | disabled | suspendedopsionalStatus bisnis.

Layanan (event type)#

Sebuah layanan adalah hal yang bisa dibooking — sesi 1:1, kelas, meeting, atau konsultasi. Di standar penamaan Termilo istilahnya event type, tetapi di skema dan admin API resource-nya bernama services. Tidak ada tabel event_type terpisah. Tabel services:

PARAMETERTIPEWAJIBKETERANGAN
service_idstringwajibID unik layanan.
slugstringwajibPengenal layanan di URL & availability.
namestringwajibNama layanan yang dipesan.
descriptionstring | nullopsionalDeskripsi layanan.
duration_minnumberwajibDurasi sesi dalam menit.
buffer_before_minnumberopsionalJeda sebelum sesi (menit). Bawaan 0.
buffer_after_minnumberopsionalJeda setelah sesi (menit). Bawaan 0.
price_amountnumber | nullopsionalHarga dalam minor unit (mis. rupiah), boleh kosong.
price_currencystringopsionalKode mata uang harga, mis. IDR.
category_idstring | nullopsionalKategori untuk pengelompokan katalog.
statusactive | hidden | disabledopsionalStatus layanan. Bawaan active.

Layanan bisa dikaitkan ke staf dan resource lewat tabel join. Detail kolomnya:

Ketersediaan & jam operasional#

Ketersediaan dibentuk dari tiga sumber. Aturan ketersediaan (availability_rules) adalah jendela mingguan berulang — pemiliknya bisa tenant, staff, atau resource:

PARAMETERTIPEWAJIBKETERANGAN
rule_idstringwajibID aturan ketersediaan.
owner_typetenant | staff | resourcewajibPemilik jam ini.
owner_idstringwajibID pemilik sesuai owner_type.
weekdaynumber 0–6wajibHari dalam minggu (0 = Minggu … 6 = Sabtu).
start_minutenumberwajibMenit buka sejak 00:00. Wajib < end_minute.
end_minutenumberwajibMenit tutup sejak 00:00.
timezonestringopsionalZona waktu aturan.
effective_from_at_msnumber | nullopsionalMulai berlaku (epoch ms).
effective_until_at_msnumber | nullopsionalBerhenti berlaku (epoch ms).
statusactive | disabledopsionalStatus aturan.

Dua sumber lain memotong jam itu:

  • Blokir waktu (blocked_times) — cuti satu kali: block_id, owner_type, owner_id, starts_at_ms, ends_at_ms, reason, status (active | cancelled).
  • Hari libur (holidays) — penutupan se-tenant: holiday_id, name, starts_at_ms, ends_at_ms, timezone, status.

Slot#

Slot bukan tabel — ia dihitung. Mesin ketersediaan menghasilkan kandidat slot dari aturan dikurangi hari libur, blokir waktu, dan booking yang sudah ada. Setiap kandidat berbentuk { startsAtMs, endsAtMs, staffId|null, resourceId|null, capacityRemaining }.

Slot hanya kandidat tampilan, bukan otoritas final
Respons ketersediaan membawa penanda harfiah finalAuthority: "booking_create_recheck_under_lock". Ketersediaan sebenarnya diuji ulang di bawah lock saat booking dibuat, sehingga dua orang tidak bisa mengambil waktu yang sama.

Bentuk respons dari GET /book/:slug/availability (jendela dibatasi 31 hari):

200 OKapplication/json
{
"service": { "serviceId": "svc_01", "durationMin": 30 },
"finalAuthority": "booking_create_recheck_under_lock",
"slots": [
{ "startsAtMs": 1750742400000, "endsAtMs": 1750744200000,
"staffId": null, "resourceId": null, "capacityRemaining": 1 }
]
}

Booking & status#

Sebuah booking (tabel bookings) menautkan layanan, customer, waktu, dan opsional staf/resource. Booking baru selalu dibuat dengan status held dan hold_expires_at_ms = now + 10 menit; jika tidak dikonfirmasi sebelum batas itu, hold kedaluwarsa dan slot dilepas.

PARAMETERTIPEWAJIBKETERANGAN
booking_idstringwajibID unik booking.
service_idstringwajibLayanan yang dipesan.
customer_idstringwajibPemesan.
primary_staff_idstring | nullopsionalStaf yang ditugaskan, bila ada.
primary_resource_idstring | nullopsionalResource yang dipakai, bila ada.
statusheld | confirmed | … wajibheld | confirmed | cancelled | completed | no_show.
sourcestringopsionalAsal booking, mis. public_page | embed.
starts_at_msnumberwajibWaktu mulai (epoch ms, UTC).
ends_at_msnumberwajibWaktu selesai (epoch ms, UTC).
timezonestringopsionalZona waktu booking saat ditampilkan.
hold_expires_at_msnumber | nullopsionalBatas hold; diisi now + 10 menit saat dibuat.
confirmed_at_msnumber | nullopsionalSaat dikonfirmasi (epoch ms).
cancelled_at_msnumber | nullopsionalSaat dibatalkan (epoch ms).
idempotency_keystringopsionalKunci agar create yang sama tidak menggandakan booking.

Status & transisi

Lima status:heldconfirmedcompletedcancelledno_show

Status terminal — tidak bisa berubah lagi — adalah cancelled, completed, dan no_show. Transisi yang diizinkan:

DariKeAksi
heldconfirmedKonfirmasi booking
held · confirmedcancelledBatalkan booking
confirmedcompletedTandai selesai
confirmedno_showTandai tidak hadir
held · confirmed(status tetap)Reschedule — pindahkan waktu
Transisi tidak sah ditolak
Memanggil transisi yang tidak ada di tabel di atas mengembalikan kode error BOOKING_POLICY_BLOCKED. Reschedule mempertahankan status dan hanya memindahkan waktu.

Membuat booking publik (header Idempotency-Key wajib; 201 untuk booking baru, 200 untuk replay idempoten):

curl -X POST "https://termilo.id/book/{tenantSlug}/bookings" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 5f3c1b9e-..." \
-d '{
"serviceSlug": "konsultasi",
"startsAtMs": 1750742400000,
"source": "public_page",
"customer": { "name": "Sari", "phone": "+628120000000" }
}'

Deposit & pembayaran#

AktifPembayaran deposit diproses lewat gateway bisnis (BYOK).

Deposit adalah kebijakan per layanan yang dibayar lewat gateway pembayaran milik bisnis sendiri. Yang tidak tersedia: event webhook untuk pembayaran.
Deposit sebagai kebijakan per layanan
Bisnis mengatur price_amount + price_currency pada services, lalu memilih tanpa deposit, nominal tetap, atau persentase. Pembayarannya masuk langsung ke rekening bisnis lewat gateway yang dihubungkan bisnis, bukan ke Termilo.

Pengaturannya ada di editor layanan: persen “Minta deposit” per layanan (20% / 30% / 50%), toggle bawaan di Settings, dan halaman /checkout yang menurunkan default 30% saat layanan tidak menetapkan persentase eksplisit.

Customer#

Customer adalah pemesan, dan tidak perlu login. Saat membuat booking, wajib mengisi email ATAU phone (validasi createBookingSchema). Tabel customers:

PARAMETERTIPEWAJIBKETERANGAN
customer_idstringwajibID unik pemesan.
namestringwajibNama pemesan.
primary_emailstring | nullopsionalEmail. Wajib bila phone kosong.
phonestring | nullopsionalNomor telepon. Wajib bila email kosong.
localestringopsionalBahasa pemesan.
timezonestringopsionalZona waktu pemesan.
Customer dibuat saat booking atau lewat API
Baris customers dibuat implisit saat booking pertama, atau langsung lewat POST /api/customers dari dashboard. Daftar dan baca customer tersedia lewat GET /api/customers dan GET /api/customers/:customerId.