staff-service
Catalog entry. Full bundle:
services/staff-service/Cluster: Workforce & Operations · Pattern: Conformist to
iam-serviceandtenant-service; OHS publisher forhousekeeping-service,maintenance-service,reservation-service.
1. Purpose
Owns operational HR for hospitality: who is employed, what they do, when they work, and whether they are on the floor right now. Provides the capacity signal that downstream task assignment (housekeeping, maintenance, front desk) depends on. Explicitly lightweight — payroll, performance reviews, complex compensation, and full HRIS are out of scope and will not be added without an ADR amendment.
It does not own:
- User authentication, sessions, or password/MFA — that is
iam-service. - User-tenant membership or platform role — that is
tenant-service. - Property structure, room inventory, or operating hours — that is
property-service. - Task assignment logic itself —
housekeeping-serviceandmaintenance-serviceconsume our capacity events and decide. - Money. No payroll, no rates, no payslips. Period.
2. Bounded Context
| Property | Value |
|---|---|
| Bounded context | Staff & Shifts |
| Domain class | Supporting |
| Strategic pattern | Conformist to iam-service (User) and tenant-service (Membership); OHS publisher for assignment services |
| Ubiquitous language | Staff · Position · Department · Shift · Pattern · Assignment · Clock · Break · Leave · Certification · On-Call · Hand-off |
3. Aggregates Owned
| Aggregate | Prefix | Notes |
|---|---|---|
Staff | stf_ | Hospitality-specific extension of iam.User; multi-property capable |
Position | pos_ | Job role (front desk, housekeeper, GM, maintenance, F&B, security, …) |
Department | dpt_ | Org grouping per property; many positions per department |
ShiftPattern | shp_ | Recurring weekly/bi-weekly template (e.g., "Front Desk Morning") |
Shift | shf_ | Concrete time window for a property/date with a position requirement |
ShiftAssignment | sha_ | Many-to-many between Shift and Staff; supports primary/standby |
ClockEntry | clk_ | Append-only punch (in / out / break_start / break_end) with source attribution |
LeaveRequest | lvr_ | Sick / vacation / unpaid; light approval flow |
StaffSkill | skl_ | Language, equipment, soft skill — used by AI suggestion + housekeeping matching |
StaffCertification | crt_ | Food handling, first aid, fire safety; carries expiry → .expired.v1 on TTL |
HandoffNote | hno_ | Append-only between-shift notes scoped to a position/department |
Holds + outbox + inbox are persistence-only and not aggregates.
4. Top Responsibilities
- Staff CRUD with hospitality attributes (position, languages, certifications, emergency contact, employment dates, status).
- Allow staff records without personal email (PIN-only clock-in, manager email as fallback).
- Define recurring shift patterns and generate concrete shifts for a date window per property.
- Manual + AI-suggested shift assignment with conflict detection (double-shift, leave collision, multi-property at-once).
- Clock in/out + break tracking from the front-desk Electron desktop (PIN-based) or via manager override on mobile.
- Light leave request + approval workflow.
- Publish capacity signals (
housekeepers active now,technicians on duty) for downstream assigners. - On-call/standby roster with promotion-to-active when a no-show is detected.
- Multi-property staff (same tenant) with at-most-one active shift across all properties.
- Hand-off notes between shifts (read by next position holder via Electron).
- Basic attendance reports (per staff, per period, per position).
- Emit
staffing_gap_detectedproactively when a shift starts within N minutes with no clocked-in primary.
5. Top REST Endpoints
| Verb | Path | Purpose |
|---|---|---|
POST | /api/v1/staff | Create a staff record (with or without email) |
GET | /api/v1/staff?propertyId=…&position=…&status=active | List + filter staff for a property |
POST | /api/v1/shifts/generate | Generate concrete shifts from a pattern + window |
POST | /api/v1/shifts/{id}/assignments | Assign staff to a shift (primary or standby) |
POST | /api/v1/clock/punch | Record clock in/out/break (PIN or JWT) |
POST | /api/v1/leave-requests | Submit a leave request |
POST | /api/v1/leave-requests/{id}:approve | Approve / reject (manager+) |
GET | /api/v1/capacity?propertyId=…&position=…&at=2026-04-22T15:00 | Active staff snapshot for downstream assigners |
Full surface in API_CONTRACTS.md.
6. Top Events Published
| Subject | When |
|---|---|
melmastoon.staff.created.v1 | New staff record |
melmastoon.staff.updated.v1 | Profile attributes mutate |
melmastoon.staff.position_changed.v1 | Position re-assignment (audited) |
melmastoon.staff.terminated.v1 | Employment ends; triggers IAM session revocation |
melmastoon.staff.reactivated.v1 | Re-hire of terminated staff |
melmastoon.staff.shift.scheduled.v1 | Concrete shift created from a pattern |
melmastoon.staff.shift.assigned.v1 | Staff added to a shift |
melmastoon.staff.shift.swapped.v1 | Two assignments swap staff atomically |
melmastoon.staff.shift.cancelled.v1 | Shift removed before start |
melmastoon.staff.shift.started.v1 | First primary clocks in |
melmastoon.staff.shift.ended.v1 | Last primary clocks out (or auto-close at +N min) |
melmastoon.staff.clock.in.v1 / .out.v1 | Punch persisted |
melmastoon.staff.clock.break_started.v1 / .break_ended.v1 | Break punch persisted |
melmastoon.staff.leave.requested.v1 / .approved.v1 / .rejected.v1 / .cancelled.v1 | Leave lifecycle |
melmastoon.staff.certification.added.v1 / .expired.v1 | Cert added or TTL elapsed |
melmastoon.staff.shift.staffing_gap_detected.v1 | Pre-shift gap warning |
melmastoon.staff.handoff.note_added.v1 | New hand-off note |
Full payloads in EVENT_SCHEMAS.md.
7. Top Events Consumed
| Subject | Producer | Reaction |
|---|---|---|
melmastoon.iam.user.registered.v1 | iam-service | Auto-link to a pending Staff record if invitation accepted |
melmastoon.tenant.membership.created.v1 | tenant-service | Provision a default Staff shell if user_type=staff |
melmastoon.tenant.membership.removed.v1 | tenant-service | Cascade-terminate any active staff for that user |
melmastoon.ai.suggestion.shift_optimization.v1 | ai-orchestrator | Materialize advisory schedule; never auto-applied |
melmastoon.property.deactivated.v1 | property-service | Cancel future shifts at the property |
8. Storage
| Layer | Engine | Notes |
|---|---|---|
| Primary DB | Cloud SQL PostgreSQL 16 (regional HA) | Schema staff; tenant_id RLS on every table |
| Cache | Memorystore Redis 7 | PIN attempt counter, current-capacity snapshot, idempotency index |
| Sync state | Firestore | Per-device cursors (read by sync-service) |
| Secrets | Secret Manager | PIN HMAC pepper |
| Crypto | Cloud KMS (regional) | PIN HMAC, emergency-contact PII envelope key |
| Events | Pub/Sub | Topics melmastoon.staff.*.v1, retention 7 d |
| Cold export | BigQuery | Attendance facts, certification expiry funnel |
9. Multi-Tenancy
- Shared schema + RLS (per ADR-0002). Every table carries
tenant_idand aUSING (tenant_id = current_setting('app.tenant_id'))policy. - Multi-property within a tenant is a first-class concept:
Staffhashome_property_idplus a many-to-manystaff_property_accesstable. - Staff at multiple properties simultaneously is forbidden by the
clock.ininvariant — see DOMAIN_MODEL §4 I-Clock-1.
10. Hotel-Specific Concerns
- Informal staffing. A
Staffrecord can be markedemployment_type = 'temporary' | 'seasonal' | 'family_help'; onboarding reduces to ID + position + property + (optional) PIN. - No personal email.
emailis nullable; if absent,manager_email_for_notificationsis required and astaff_code(human-readable, e.g.,GM-DOH-FD-014) is auto-generated. - PIN clock-in via Electron. Front-desk desktop accepts a 6-digit PIN bound to
staff_id+property_id; PIN is HMAC'd at rest, lockout after 5 failures, see SECURITY_MODEL §4. - Multilingual UI. All response titles are localized via
Accept-Language; canonical labels forPositionandDepartmentship withen,ps,fa,artranslations. - Offline clock-in. Electron queues punches locally and ships them via the sync push; conflict policy is
append_only— see SYNC_CONTRACT §2.
11. Edge Cases (representative)
| # | Scenario | Resolution |
|---|---|---|
| 1 | Clock-in without a scheduled shift | Allowed with manager_override flag; emits both .clock.in.v1 and .shift.staffing_gap_detected.v1 for the period |
| 2 | Double-shift conflict on same staff/property | Reject with MELMASTOON.STAFF.SHIFT_CONFLICT (HTTP 409) |
| 3 | Leave request collides with already-assigned shift | Approval requires explicit forceUnassign=true; otherwise 409 |
| 4 | Two properties try to clock-in same staff at once | Second punch rejected MELMASTOON.STAFF.MULTI_PROPERTY_ACTIVE |
| 5 | Termination of an actively-clocked-in staff | Auto-emit .clock.out.v1, then .terminated.v1; IAM session revoke fires |
| 6 | Staff has no personal device | PIN clock-in only; manager email receives notifications |
| 7 | Offline clock-in via Electron after >24 h offline | Punch accepted with source=offline_replay; out-of-window punches flagged |
| 8 | Certification expires mid-shift | .certification.expired.v1 published; shift continues; gap warning issued |
Full deviation register in SERVICE_RISK_REGISTER.md.