Multi Staff
Endpoint untuk mengundang, menampilkan, dan mengelola staf merchant. Cocok untuk Badan Usaha yang memerlukan akses bersama (admin, kasir, viewer). Perseorangan dibatasi 1 user (owner) — lihat Quota & Batasan.
Base URL:
| Environment | Host |
|---|---|
| Production | https://api-merchant.kesles.com |
| Staging | https://api-merchant-staging.kesles.com |
1. Otorisasi
Seluruh endpoint butuh Bearer token JWT user.
RBAC di-enforce oleh middleware withMerchantRole. Lihat
merchant-rbac-permission-matrix.md
untuk matriks lengkap.
| Endpoint | Method | Role yang diizinkan |
|---|---|---|
/merchant/staff/invitations | POST | owner |
/merchant/staff/invitations | GET | owner |
/merchant/staff/invitations/{id} | DELETE | owner |
/merchant/staff/invitations/verify | POST | — (deprecated, return 410 Gone) |
/merchant/staff | GET | owner, admin, viewer |
/merchant/staff/{user_id} | PATCH | owner (PIN gate) |
/merchant/staff/{user_id} | DELETE | owner (PIN gate) |
/staff/invite/{id} | GET | publik (no JWT) |
/merchant/staff/invitations/{id}/accept | POST | authenticated user (Bearer) |
/merchant/staff/invitations/{id}/reject | POST | authenticated user (Bearer) |
2. Role yang Tersedia
5 role enum di merchant_users.membership_role:
| Role | Deskripsi singkat |
|---|---|
owner | Pemilik merchant. 1 per merchant, auto-assigned saat registrasi. Tidak bisa di-invite atau di-edit-ke. |
admin | Operasional penuh kecuali transfer ownership. |
staff | Kasir / operasional harian. |
viewer | Read-only (audit eksternal, reconciliation). |
member | Akses minimum (notif only). |
3. Quota & Batasan
Limit jumlah user per merchant berdasarkan business_entity_type:
| Bentuk Usaha | Default limit (owner + staf) |
|---|---|
| Perseorangan | 1 (owner only — tidak bisa invite) |
| Badan Usaha | 3 (owner + max 2 staf) |
Override limit tersedia via admin Kesles. Pre-check quota dilakukan saat
invite (cegah kelebihan undangan) dan saat verify (final guard
sebelum row dimasukkan ke merchant_users).
4. Endpoint
4.1 Kirim Undangan
POST /merchant/staff/invitations
Owner mengundang staf via WhatsApp atau Email. OTP 6 digit di-generate server dan dikirim ke channel target. TTL default 10 menit.
Request body
{
"phone_e164": "+628114169868",
"role": "admin",
"channel": "email",
"email": "head@example.com"
}
| Field | Tipe | Wajib | Catatan |
|---|---|---|---|
phone_e164 | string | ya | Format +62… (otomatis di-normalize server-side). |
role | string | ya | Salah satu: admin, staff, viewer, member. owner ditolak. |
channel | string | ya | whatsapp atau email. |
email | string | bersyarat | Wajib kalau channel = email. |
Response 202 Accepted
{
"invitation_id": "0c7d4d12-…",
"expires_at": "2026-05-27T18:28:00+07:00",
"channel": "email"
}
Error responses
| HTTP | Code | Penyebab |
|---|---|---|
| 400 | bad_request | phone_e164 kosong / format invalid. |
| 403 | forbidden | Caller bukan owner. |
| 422 | user_limit_reached | Slot quota habis. Hubungi admin Kesles untuk upgrade. |
| 409 | already_member | Phone tsb sudah jadi member merchant. |
| 409 | invitation_active | Sudah ada invitation aktif untuk phone tsb. |
| 400 | invalid_role | Role tidak ada di enum allowed. |
| 429 | rate_limited | Hourly (10) atau daily (50) limit lampaui. |
4.2 Daftar Undangan Tertunda
GET /merchant/staff/invitations
List undangan yang masih tertunda untuk merchant caller. Status
"tertunda" = consumed_at IS NULL AND revoked_at IS NULL AND expires_at > now().
Response 200 OK
{
"items": [
{
"id": "0c7d4d12-…",
"phone_e164": "+628114169868",
"email": "head@example.com",
"role": "admin",
"channel": "email",
"expires_at": "2026-05-27T18:28:00+07:00",
"created_at": "2026-05-26T18:28:00+07:00"
}
],
"summary": { "total": 1 }
}
Field email di-return untuk channel email. Untuk channel whatsapp field email umumnya kosong ("").
ORDER BY created_at DESC — undangan terbaru muncul duluan.
Error responses
| HTTP | Code | Penyebab |
|---|---|---|
| 403 | forbidden | Caller bukan owner (admin/viewer dst). |
4.3 Batalkan Undangan
DELETE /merchant/staff/invitations/{id}
Owner membatalkan undangan tertunda. Setelah revoke, OTP yang sudah
terlanjur dikirim ke channel target otomatis tidak valid. Owner bebas
mengirim undangan ulang via POST /merchant/staff/invitations.
Path parameter
id— UUID invitation.
Response 204 No Content — sukses revoke.
Error responses
| HTTP | Code | Penyebab |
|---|---|---|
| 400 | bad_request | Path param id kosong / format invalid. |
| 403 | forbidden | Caller bukan owner. |
| 404 | not_found | Invitation tidak ditemukan / sudah consumed / sudah revoked. |
4.4 Verifikasi OTP — Deprecated
POST /merchant/staff/invitations/verify
Endpoint ini mengembalikan 410 Gone untuk semua request.
Gunakan flow accept/reject di bawah (§5.1):
GET /staff/invite/{id}— info publik tanpa authPOST /merchant/staff/invitations/{id}/accept— terima undangan (Bearer JWT)POST /merchant/staff/invitations/{id}/reject— tolak undangan (Bearer JWT)
4.5 List Staf Aktif
GET /merchant/staff
Daftar member aktif merchant. Dapat diakses oleh role owner, admin, atau viewer.
Response 200 OK
{
"items": [
{
"user_id": "…",
"phone": "+628114169868",
"name": "Budi Suwandi",
"full_name": "Budi Suwandi",
"display_name": "Budi",
"membership_role": "owner",
"is_owner": true,
"joined_at": "2025-08-12T10:00:00+07:00"
}
],
"summary": {
"total": 1,
"by_role": { "owner": 1 }
},
"quota": {
"business_entity_type": "Badan Usaha",
"effective_limit": 3,
"active_member_count": 1,
"pending_unique_invitations": 0,
"can_invite_more": true
}
}
Field name adalah nama tampilan ter-resolve dengan urutan display_name → full_name → ''.
Field full_name dan display_name juga di-return apa adanya untuk kebutuhan label berbeda.
4.6 Ubah Role Staf
PATCH /merchant/staff/{user_id}
Owner ubah role staf non-owner. Memerlukan PIN gate.
Request body
{ "role": "admin" }
Response 204 No Content.
4.7 Hapus Staf
DELETE /merchant/staff/{user_id}
Owner remove staf non-owner. Memerlukan PIN gate. Tolak target owner
(409 cannot_remove_owner).
Response 204 No Content.
5. Lifecycle Undangan
[Owner] POST /invitations
│
▼
[server] generate OTP, kirim via channel
│
▼
status: pending
│
├─ [Owner cancel] DELETE /invitations/{id} ────► revoked
│
├─ [TTL 10 menit habis] ────► expired
│
└─ [Invitee] GET /staff/invite/{id}
│
▼
[accept/reject]
│
├─ accept ──► INSERT merchant_users
└─ reject ──► revoked
Endpoint GET /invitations hanya mengembalikan baris yang masih
status pending. Begitu transition ke revoked, expired, atau
consumed, baris keluar dari hasil list.
5.1 Endpoint Accept/Reject
GET /staff/invite/{id} — Info Publik (no auth)
Dipakai app untuk menampilkan detail undangan sebelum user login. Tidak mengembalikan data sensitif.
Response 200:
{
"invitation_id": "uuid",
"merchant_name": "Warung Bu Susi",
"inviter_name": "Bu Susi",
"role": "staff",
"role_label": "Staf",
"expires_at": "2026-05-28T10:00:00+07:00"
}
410 Gone — invitation expired atau sudah consumed/revoked.
POST /merchant/staff/invitations/{id}/accept — Terima Undangan
Auth: Bearer JWT (authenticated user, bukan merchant member context).
Otorisasi: target_user_id == authenticated_user.id ATAU phone_e164 == authenticated_user.primary_phone.
Response 200:
{
"merchant_id": "uuid",
"merchant_name": "Warung Bu Susi",
"role": "staff",
"role_label": "Staf",
"joined_at": "2026-05-27T10:30:00+07:00"
}
| Error | HTTP | Code |
|---|---|---|
| Invitation tidak ditemukan | 404 | invitation_not_found |
| Sudah expired | 410 | invitation_expired |
| Sudah consumed/revoked | 409 | invitation_already_used |
| Bukan untuk user ini | 403 | invitation_not_for_you |
| Quota merchant penuh | 422 | user_limit_reached |
Idempotent: accept ulang saat sudah member → return 200 existing data.
POST /merchant/staff/invitations/{id}/reject — Tolak Undangan
Auth: Bearer JWT. Otorisasi: sama dengan accept.
Request body (opsional): { "reason": "salah orang" }
Response 200:
{
"invitation_id": "uuid",
"rejected_at": "2026-05-27T10:30:00+07:00"
}
6. Audit Events
Setiap aksi staff/invitation menulis ke log audit.
| Event type | Pemicu |
|---|---|
staff_invitation_sent | POST /invitations sukses. |
staff_invitation_revoked | DELETE /invitations/{id} sukses. |
staff_joined | Invitee selesai accept dan bergabung. |
staff_role_changed | PATCH /staff/{user_id} sukses. |
staff_removed | DELETE /staff/{user_id} sukses. |
staff_invitation_rejected | POST /invitations/{id}/reject sukses. |