BF-009 — Housekeeping Lifecycle
Business flow (cross-EPIC user journey). Wave 2 of the RTM Scale-Out (BF-002…BF-013) following the BF-001 template. Reconciled 2026-05-25.
Narrates the daily heartbeat of an Indian SME hotel's housekeeping floor: a guest checks out → the room flips DIRTY → the next available cleaner gets a push on their phone → photo evidence on completion → supervisor signs off → the room becomes a bed for the next arrival in time for the 14:00 standard Indian check-in window.
Each step names (a) the user story that owns the requirement, (b) the primary code path that runs it, (c) the closest automated test that exercises it, and (d) pre/post-conditions and notes. Where the spec contains no matching catalog story, the step is tagged
[SME-PROPOSED]as a NEW story for the SME's intake.Single-EPIC scope: EP-006 Housekeeping Management (with documented read-only seams into EP-002 Property — room ownership, room status — and EP-005A Front Desk — checkout event, folio post on minibar; see "Cross-EPIC seams" at the bottom for the explicit boundaries).
Personas
| Persona | Role | Source |
|---|---|---|
| Front Desk Agent | Posts checkout, releases the room to housekeeping | EP-005A; emits RESERVATION_CHECKED_OUT |
| Housekeeping Supervisor | Triages the dirty-rooms queue, assigns cleaners, runs inspection | EP-006 (US-006-001 board, US-006-007 inspection) |
| Room Attendant ("Cleaner") | Mobile-app user; receives tasks, uploads before/after photos, marks done | EP-006 (US-006-004 mobile queue, US-006-005 task status, US-006-006 photos) |
| Maintenance Technician | Engineering staff that takes over a MaintenanceRequest when the cleaner finds a fault | EP-006 (US-006-008) + EP-017 (asset-service Work Orders) |
| Lost & Found Custodian | Logs items left behind, runs the 90-day disposal clock | EP-006 (US-006-012) |
| Front Office Manager | Reads the discrepancy report and shift KPIs at handover | EP-006 (US-006-014, US-006-015) |
Entry points
| Entry | URL / interface | Backed by |
|---|---|---|
| Front-desk checkout button | https://dashboard.<env>.keynova.ai/front-desk/checkout | hmp-frontdesk-service (cross-EPIC trigger — fires RESERVATION_CHECKED_OUT consumed in Step 1) |
| HK supervisor board | https://dashboard.<env>.keynova.ai/housekeeping (Rooms tab + Tasks tab) | hmp-housekeeping-service rooms/tasks controllers + WebSocket /housekeeping namespace |
| Cleaner mobile app | Expo / React Native install on Android — primary device in Indian SME hotels (cleaners share a property phone, not personal) | hmp-mobile-apps/apps/housekeeping/ + WebSocket /ws/housekeeping/socket.io/ |
| Maintenance request | dashboard /housekeeping/maintenance OR cleaner mobile maintenance.tsx | hmp-housekeeping-service/src/maintenance/maintenance.controller.ts |
| Lost & Found register | dashboard /housekeeping/lost-found | hmp-housekeeping-service/src/lost-found/lost-found.controller.ts |
The 10-step journey
Step 1: Front desk posts checkout → room flips OCCUPIED → DIRTY → CHECKOUT_CLEANING task auto-created
- Story: US-006-016 — Auto-Task Generation on Checkout (catalog "Not Started", EPIC body claims same — but catalog is stale, the consumer is shipped; see EP-006 RTM D-006 numbering drift).
- Code:
hmp-frontdesk-service/src/checkout/checkout.service.tspublishesRESERVATION_CHECKED_OUTto Kafka on a successful checkout. Consumer inhmp-housekeeping-service/src/events/events.service.ts:163subscribes, callsTasksService.create({ taskType: 'CHECKOUT_CLEANING', priority: 'NORMAL', roomId, propertyId, notes: 'Auto-created on checkout' }). Dedup (W1.B.9 / AP-T05-001) viahousekeepingTask.findFirst({ where: { roomId, reservationId, taskType: 'CHECKOUT_CLEANING', createdAt: { gte: startOfDay } } })— same checkout never spawns a second task. As a side-effect ofTasksService.create(),Room.housekeepingStatusis set toDIRTY. - Test:
hmp-housekeeping-service/src/events/events.service.spec.ts:: 26 specs incl. 3 W1.B.9 dedup tests covering "dedup within same UTC day" + "re-checkout same room next day creates fresh task" + "two CHECKOUT_CLEANING events for different reservations both create" — PASS. Flow:hmp-infrastructure/testing/flow-tests/src/flows/housekeeping-lifecycle.flow.spec.ts— boots the fixture chain and exercises a DAILY_CLEANING create variant (CHECKOUT_CLEANING auto-path is covered indirectly viaflow-5-fd-hk-updates.spec.ts11/12 PASS). - Pre-conditions: Reservation in
CHECKED_INstate. Front desk POSTs/api/checkout(frontdesk-service). The destination room'shousekeepingStatusisINSPECTEDorCLEAN(occupied side). - Post-conditions:
HousekeepingTaskrow withstatus=PENDING,taskType=CHECKOUT_CLEANING,priority=NORMAL,propertyId,roomId,reservationIdpopulated.Room.housekeepingStatus=DIRTY.HOUSEKEEPING_TASK_CREATEDKafka event emitted. WebSockettask:createdfanned out to all/housekeepingsubscribers on the property. - Notes: Dedup uses same UTC day boundary — across the IST midnight handover (00:00 IST = 18:30 UTC) the dedup window resets ~5.5 h before the local day ends. Indian shift practice (06:00–14:00 / 14:00–22:00 / 22:00–06:00) lives entirely on one side of this boundary so it's typically fine, but late-night double-checkouts of the same room could in theory de-dup unexpectedly if they straddle the UTC date line.
[SME-PROPOSED]Add AC: "Dedup window SHOULD use propertytimezonefromProperty.timezoneand the property's business date, NOT UTC. Indian properties:Asia/Kolkata." See D-009 below.
Step 2: HK queue picks up the DIRTY room
- Story: US-006-001 — Real-Time Room Status Board.
- Code:
hmp-housekeeping-service/src/rooms/rooms.controller.ts:18-44—@Get('/rooms/status')returns the board grouped by floor + status, scoped totenantIdviagetRequestContext().rooms.service.ts::getRoomStatusBoard()joins the local Room read-model (mirror from property-service) with thehousekeepingStatuscolumn and active task counts. Dashboard:hmp-web-dashboard/src/app/(dashboard)/housekeeping/page.tsx. Real-time push:hmp-housekeeping-service/src/gateway/housekeeping.gateway.tsemitstask:created,task:updated,task:assigned,task:inspected,room:status-changedon the/housekeepingnamespace. - Test:
rooms.service.spec.ts:: 13 specs (board grouping, status update + Kafka, inspect-success/fail, discrepancy classification) — PASS.housekeeping.gateway.spec.ts:: 24 WebSocket emission tests — PASS. E2E:hmp-web-dashboard/e2e/tests/housekeeping-board.spec.ts. - Pre-conditions: Step 1 completed; supervisor's dashboard tab is open OR their
/housekeepingWebSocket is connected. - Post-conditions: The DIRTY room appears in the "Dirty Rooms" column within < 2 s of the checkout (qa-PMS-7 routing fix gated on the WebSocket emit, not the HTTP poll). Supervisor sees badge counts update on the floor card.
- Notes: 7-status board UI joins HK-side enum (4 values: CLEAN/DIRTY/INSPECTED/PICKUP) with property-side
RoomStatus/frontOfficeStatus(3 more: OUT_OF_ORDER/OUT_OF_SERVICE/DO_NOT_DISTURB) — see EP-006 D-002. Two enums, two services, one composite view.
Step 3: Supervisor assigns the task to a cleaner
- Story: US-006-003 — Assign Task to Staff.
- Code:
hmp-housekeeping-service/src/tasks/tasks.controller.ts:144-158—@Patch(':id/assign')(role-gated SUPER_ADMIN/ADMIN/MANAGER/HOUSEKEEPING;@Audit;x-property-idfrom JWT).tasks.service.ts::assign()validates assignee, auto-transitions PENDING → IN_PROGRESS (per the exportedTASK_STATUS_TRANSITIONSmachine), publishesHOUSEKEEPING_TASK_ASSIGNED, emits WebSockettask:assigned. Workload guard —SafeguardService.HK_STAFF_OVERLOADEDenforced inassign(): blocks if assignee has>= maxTasksPerStaffactive tasks (persrc/tasks/CLAUDE.mdandstaff.service.ts). - Test:
tasks.service.spec.ts:: 15 specs incl. assignment + state transition + safeguard cases — PASS.staff.service.spec.ts:: 10 specs — PASS. Flow:housekeeping-lifecycle.flow.spec.ts:: "assigns the task to a housekeeper (auto-transitions PENDING → IN_PROGRESS)" — exercises the auto-transition + read-back. E2E:hmp-web-dashboard/e2e/tests/housekeeping-assign.spec.ts. - Pre-conditions: Step 2 completed; task is
PENDING; supervisor knows which cleaner is on shift on that floor (Indian shift practice: 1 cleaner per 14 rooms is the standard ratio). - Post-conditions:
HousekeepingTask.status=IN_PROGRESS,assignedTopopulated,assignedAttimestamped.HOUSEKEEPING_TASK_ASSIGNEDKafka event. WebSockettask:assignedpush to the cleaner's app and the supervisor's board. - Notes: Auto-assign endpoint
POST /tasks/auto-assignis NOT shipped (EP-006 D-005). Supervisors do this manually today, one tap per task. For a 60-room property at 11:00 IST checkout-peak that's 60 taps.[SME-PROPOSED]see D-005 in EP-006 RTM for round-robin + nearest-floor proposal that requires a newStaffShiftclock-in table.
Step 4: Mobile app push the assignment to the cleaner's phone
- Story: US-006-004 — Mobile Task Queue.
- Code:
hmp-mobile-apps/apps/housekeeping/app/(tabs)/my-tasks.tsxrenders the assigned-to-me queue, sorted by priority + scheduledFor. Realtime:hmp-mobile-apps/packages/shared-realtime/src/notifications-store.ts+useNotifications.tslistens onwss://<host>/ws/housekeeping/socket.io/per AP-T05-003 Nginx pattern; auth viaauth.tokenfrom store on every reconnect. Foreground FCM suppression:fcm-foreground-suppressor.ts(so the user doesn't get a duplicate toast + push). The WebSocket emit is the in-app notification; the Kafka eventHOUSEKEEPING_TASK_ASSIGNEDis consumed byhmp-notification-servicewhich fires the out-of-band FCM for backgrounded apps. - Test:
notifications-store.test.ts+useNotifications.test.ts+deep-link-handler.test.ts(shared-realtime package) — PASS. No mobile E2E — no Detox / Maestro test of the swipe-start / mark-complete UI loop (EP-006 D-010 SME-PROPOSED Detox coverage). - Pre-conditions: Step 3 completed; cleaner's phone is logged in (RN auth store has a valid JWT); WebSocket is connected OR FCM token is registered.
- Post-conditions: Cleaner sees the task at the top of their queue with a vibration / toast / push notification. Tap-through navigates to the per-room detail screen (
app/room/[id].tsx). - Notes: Indian SME hotel reality — the housekeeping phone is a single shared Android device (typically a low-end model, 2–4 GB RAM, Android 9–12) handed off at every shift change. Personal mobile-data plans are not provided. The hotel WiFi covers public + corridors but basement service areas + back-of-house staircases are dead zones in most Tier-2 / Tier-3 properties. The offline-sync surface (Step 7) carries the weight of this.
[SME-PROPOSED]Add AC: "On every shift change the supervisor SHALL force-logout the previous shift's account on the shared phone via a one-tapSign out of shiftbutton; the next shift signs in fresh. This prevents the next-shift cleaner from seeing the previous shift's task history (audit + privacy)."
Step 5: Cleaner accepts and starts the task (status → IN_PROGRESS via auto-assign, otherwise via explicit start)
- Story: US-006-005 — Update Task Status (In-Progress / Complete).
- Code: Auto-transitioned to IN_PROGRESS by
assign()in Step 3 (no separate user action needed). An explicit start path also exists:hmp-housekeeping-service/src/tasks/tasks.controller.ts:160-174—@Patch(':id/status')body{ status: 'IN_PROGRESS' }validated byUpdateTaskStatusDtovia@IsIn. State machineTASK_STATUS_TRANSITIONSexported attasks.service.ts:39-45enforces PENDING→IN_PROGRESS (and rejects IN_PROGRESS→IN_PROGRESS — see the flow-spec comment about the guard). - Test:
tasks.service.spec.ts:: 15 specs incl. state-machine transitions — PASS. Flow:housekeeping-lifecycle.flow.spec.ts:: "transitions task to COMPLETED" viaclient.patch('/api/tasks/:id/status', { status: 'COMPLETED' })— passes after the IN_PROGRESS auto-transition from Step 3 lands. - Pre-conditions: Step 4 completed; task is in
IN_PROGRESS(auto-set by assign) OR wasPENDINGand the cleaner POSTs the explicit start. - Post-conditions:
HousekeepingTask.status=IN_PROGRESS,startedAttimestamped (if not already from assign).HOUSEKEEPING_TASK_UPDATEDKafka event. WebSockettask:updatedpush. - Notes: The redundant explicit-start path is held for properties that auto-assign tasks at shift-start (US-006-003 AC-2, not shipped — see D-005) and need the cleaner to consciously "claim" the task. With manual assign (today) the auto-transition is the canonical path.
Step 6: Cleaning + photo capture (before / after)
- Story: US-006-006 — Photo Upload for Task Completion (catalog: Partial; verified: Partial — presigned URL endpoint shipped, BEFORE/AFTER discriminator + sharp compression NOT).
- Code: Mobile camera capture → POST
/api/housekeeping/photos/presigned-url→ S3 PUT → POST/api/housekeeping/photosrecords theHousekeepingPhotorow.hmp-housekeeping-service/src/photos/photos.controller.tsexposes presigned-URL + listing.photos.service.tsgenerates the URL via the shared StorageService. HK_INSPECTION_PHOTO_REQUIRED safeguard — whenrequireInspectionPhotois true and no photos attached, COMPLETED is blocked (tasks.service.ts). - Test:
photos.service.spec.ts:: 9 specs — PASS. - Pre-conditions: Step 5 completed; cleaner is in the room; the per-property
requireInspectionPhotoflag is set per regulatory + brand-standard policy. - Post-conditions:
HousekeepingPhotorows linked to the task viareferenceId;photoUrlis an S3 URL (not base64-in-DB per the pre-AP-T16-001 anti-pattern). - Notes: No
photoType: BEFORE | AFTERenum onHousekeepingPhoto(EP-006 D-010). The mobile UI today shows an unordered photo grid — supervisor cannot reliably split BEFORE vs AFTER for the comparison view the EPIC body AC asks for.[SME-PROPOSED]see D-010 SME refinements: addphotoType, enforce S3content-length-range ≤ 1 MB, mobile-side Sharp /expo-image-manipulator1080px / 70% JPEG pre-compression. 2G/3G basement fallback: upload SHALL be background-queued with WiFi-only resume; task completion SHALL NOT be blocked on photo upload (status flips to COMPLETED immediately;HousekeepingPhoto.uploadStatustracks the upload separately). Indian SME reality: every basement service corridor in Tier-2/3 properties is a Faraday cage for 4G — the cleaner has to walk back to the lobby to get signal, and the task list has to keep working through that.
Step 7: Cleaner marks the task COMPLETED
- Story: US-006-005 (continued) — Update Task Status.
- Code: PATCH
/api/tasks/:id/statusbody{ status: 'COMPLETED' }.tasks.service.ts::updateStatus()runs the state-machine check (IN_PROGRESS → COMPLETED allowed; COMPLETED → COMPLETED rejected), validates the photo safeguard, setscompletedAt, computescleanTimeMinutes = Math.round((completedAt - startedAt) / 60000), publishesHOUSEKEEPING_TASK_COMPLETEDcarryingassignedTo+cleanTimeMinutes+completedAt+targetHousekeepingStatus(the FU-55 producer contract, locked by a regression test intasks.service.spec.tsbecausemv_hk_staff_productivityin analytics-service silently drops the row ifassignedTois absent). The completed task does not auto-flipRoom.housekeepingStatus; the room staysDIRTYuntil supervisor inspection (Step 9). - Test:
tasks.service.spec.ts:: 15 specs incl. FU-55 producer-contract regression "publishes assignedTo + cleanTimeMinutes + completedAt" — PASS. Flow:housekeeping-lifecycle.flow.spec.ts:: "transitions task to COMPLETED" + "reads back the COMPLETED task by id" with polling — PASS in the harness. - Pre-conditions: Step 5+6 completed; task
IN_PROGRESS; photos uploaded ifrequireInspectionPhoto. - Post-conditions:
HousekeepingTask.status=COMPLETED,completedAtset,cleanTimeMinutescomputed.HOUSEKEEPING_TASK_COMPLETEDKafka event (consumed by analytics-service formv_hk_staff_productivity). WebSockettask:updatedpush. Room status unchanged (still DIRTY) — awaits inspection. - Notes: Indian SME hotel productivity benchmark: 25–30 min per checkout-clean for a standard double room.
cleanTimeMinutesover 45 min is a soft flag for the supervisor's KPI board (US-006-015).
Step 8: Supervisor inspection queue
- Story: US-006-007 — Supervisor Inspection with Checklist Scoring (catalog: Partial; verified: Partial — only binary pass / fail today, no structured checklist).
- Code:
hmp-housekeeping-service/src/tasks/inspection.service.spec.ts:: 6 specs referenceinspection.service.ts(CITED-BUT-NOT-FOUND in this pass; CLAUDE.md doesn't enumerate). Dashboard:hmp-web-dashboard/src/app/(dashboard)/housekeeping/inspection tab — supervisor sees a queue of COMPLETED tasks awaiting inspection on their assigned floors. The board uses the sametask:updatedWebSocket signal Step 7 emitted. - Test:
inspection.service.spec.ts:: 6 specs — PASS. - Pre-conditions: Step 7 completed; supervisor on shift; task
COMPLETED. - Post-conditions: Task is visible in the supervisor's "Awaiting inspection" queue. No state change yet.
- Notes: Indian SME hotel reality — supervisor inspects 15–25 rooms per shift, ~3–5 min per inspection. Without a structured checklist (D-011), inspection is "supervisor walks in, eyeballs the room, taps Pass" — there's no audit trail of WHICH items were checked. This is fine for owner-managed Tier-3 properties; it's a gap for brand-standard Tier-1 hotels claiming HACCP / DLF / IHCL audits.
Step 9: Supervisor passes (or fails) the inspection
- Story: US-006-007 (continued).
- Code:
hmp-housekeeping-service/src/rooms/rooms.controller.ts—@Post('/rooms/:id/inspection')(binary pass;rooms.service.ts::inspectRoom()finds the latest COMPLETED task for the room, marksinspectedBy/inspectedAt, publishesROOM_INSPECTED, and flipsRoom.housekeepingStatustoINSPECTEDon pass).@Post('/rooms/:id/request-inspection')queues an INSPECTION task type if the supervisor wants a structured re-inspection record. The EPIC-body fail-path ("Failed inspection triggers re-clean task with specific failed items") is NOT shipped — auto re-clean on fail does not happen today (EP-006 D-011). - Test:
rooms.service.spec.ts:: inspectRoom specs within the 13-spec suite — PASS. Flow:flow-5-fd-hk-updates.spec.tscovers the happy-path pass (11/12 PASS). - Pre-conditions: Step 8 completed; supervisor reviews the room (in person or via the BEFORE/AFTER photos, gated on D-010 landing).
- Post-conditions: On pass:
Room.housekeepingStatus=INSPECTED,inspectedBy/inspectedAtset;ROOM_INSPECTEDKafka event. On fail (manual today): supervisor manually creates a follow-up CLEANING task — no atomic state-revert + re-task flow. - Notes:
[SME-PROPOSED]see EP-006 D-011 for the structuredInspectionTemplate+ per-item scoring proposal — 4 / 5 / 6-section templates by roomType (Standard / Deluxe / Suite) with weighted-score pass thresholds (default 85%). Failed inspection SHALL atomically (a) mark current task INSPECTED-FAIL, (b) auto-create a new CLEANING task withpriority=HIGHand notes auto-populated with failed item labels, (c) reverthousekeepingStatusto DIRTY, (d) publishHOUSEKEEPING_INSPECTION_FAILEDcarrying the original housekeeper UUID for analytics.
Step 10: Room flips to VACANT_CLEAN → bed for next arrival
- Story: US-006-001 (room status board) + cross-EPIC into EP-005A (front office status). Also covered indirectly by US-006-014 (Discrepancy Report, FD vs HK).
- Code: After
Room.housekeepingStatus=INSPECTED(Step 9), the front-office side of the room status (frontOfficeStatuson the property-service Room table) is what determines "saleable" for the next reservation. If the reservation was already checked out (Step 1 emittedRESERVATION_CHECKED_OUTwhich the property-service Room mirror consumed to setfrontOfficeStatus=VACANT), then the composite status(VACANT, INSPECTED)is what the front-desk arrivals tab shows as "Ready for arrival". The board UI joins both enums and shows the room with the green "READY" tag. The next arrival'sassignRoomstep (in EP-005A / EP-003) is now unblocked. - Test:
rooms.service.spec.tsdiscrepancy classification specs cover the (FRONT_OFFICE=VACANT, HK=INSPECTED) happy state.housekeeping-lifecycle.flow.spec.tsends at the COMPLETED read-back — the explicit room status flip in the optional final step is documented in the flow's preamble. - Pre-conditions: Step 9 completed (
housekeepingStatus=INSPECTED); Step 1's checkout has propagatedfrontOfficeStatus=VACANTto the property-service Room mirror. - Post-conditions: Room is sellable. Next reservation's
assignRoomcan pick it. Discrepancy report (US-006-014) shows no discrepancy. - Notes: Indian SME hotel target SLA — for a 14:00 IST standard arrival window the room MUST be
(VACANT, INSPECTED)by 13:30 IST at the latest. The 30-min buffer accounts for room-walk + key-card encode at the front desk. If checkout was at 11:00 IST (standard checkout time), the housekeeping window is 2.5 hours for the full Step 1 → Step 10 loop. The double-cycle expectation (06:00 IST stayover clean + 17:00 IST turndown —[SME-PROPOSED]US-006-NEW-A in the EP-006 RTM) is layered on top of this checkout cycle.
Auxiliary paths (run in parallel with steps 1-10)
Step 11: Discrepancy path — FD says VACANT, HK says DIRTY (or vice versa)
- Story: US-006-014 — Discrepancy Report (FD vs HK). Catalog: Partial; verified: Partial.
- Code:
hmp-housekeeping-service/src/rooms/rooms.controller.ts—@Get('/rooms/discrepancy')callsrooms.service.ts::getDiscrepancies()which queries the local Room read-model forOR: [{ frontOfficeStatus: 'OCCUPIED', housekeepingStatus: 'CLEAN' }, { frontOfficeStatus: 'VACANT', housekeepingStatus: 'DIRTY' }]and classifies asPOSSIBLE_SKIP(occupied + clean — guest may have left without checking out) orNEEDS_ATTENTION(vacant + dirty — checked out but cleaning slipped).reporting-feeds-hk.controller.tsexposes the analytics live-proxy atGET /reporting-feeds/housekeeping/discrepancies. - Test:
rooms.service.spec.tsdiscrepancy classification specs (within 13-spec suite) — PASS.reporting-feeds-hk.controller.spec.ts:: 16 specs — PASS. - Pre-conditions: A room appears in the OR-classified set (Step 1 checkout flipped FO to VACANT but the auto-task hasn't fired yet — race condition; OR housekeeping marked CLEAN before front office processed the late checkout).
- Post-conditions: Room is on the discrepancy list. No resolution endpoint today — supervisor manually reconciles by issuing the corrective PATCH (either
/api/rooms/:id/statusto HK side or/api/properties/.../rooms/:id/statusto property-service for FO side). - Notes: EP-006 D-007 — the report is read-only; no
POST /rooms/:id/discrepancy/resolveendpoint, noDiscrepancyResolutionaudit log, no duration tracking, no daily summary email at night-audit time.[SME-PROPOSED]see EP-006 RTM D-007 SME refinements for the full resolution-with-logging surface.
Step 12: Lost & Found path — cleaner finds an item
- Story: US-006-012 — Lost & Found Register. Catalog: Built; verified: Built-Untested (16 service specs but no flow / smoke / E2E despite
lost-and-found.flow.spec.tsfile existing — exists with TBD pass count). - Code: Cleaner taps "Log lost & found" on the mobile per-room screen → POST
/api/housekeeping/lost-foundbody{ propertyId, itemDescription, foundLocation, foundDate, foundBy, category, roomNumber, notes, estimatedValue }.hmp-housekeeping-service/src/lost-found/lost-found.controller.ts+lost-found.service.tswith full CRUD + filters. Claim path: PATCH/:id/claimbody{ claimedBy, verificationRef, contactPhone, notes }flips status toCLAIMED. Disposal: PATCH/:id/dispose. Reporting-feed:/reporting-feeds/housekeeping/lost-and-foundfor analytics-service. Note: a legacyInventoryControlleralso mounts at/api/lost-foundper CLAUDE.md gotcha 1 — two paths, dashboard should target the newer/api/housekeeping/lost-found. - Test:
lost-found.service.spec.ts:: 16 specs — PASS. Flow:lost-and-found.flow.spec.ts:: probes both routes, exercises Scenario 1 (log) / Scenario 2 (list) / Scenario 3 (claim). - Pre-conditions: Cleaner is in a checked-out room (Step 1-10 path) and finds an item the guest left behind.
- Post-conditions:
LostAndFoundrow withstatus=FOUND(orPENDING), categorized (PERSONAL / CLOTHING / ELECTRONICS / JEWELRY / DOCUMENT / OTHER), 90-day disposal clock starts ticking. - Notes: EP-006 D-009 —
roomIdis NOT validated against the requesting tenant inlost-found.create(). ForgedroomIdin the POST body would auto-link the item to the wrong tenant's last-checked-out guest, leaking tenant-B's guest into a tenant-A lost-found list.[SME-PROPOSED]see D-009 SME refinements. Auto-link to last guest (EPIC AC-1) requires a guest-service cross-service read; not verified in source. High-value approval (JEWELRY/ELECTRONICS Manager approval before logging) is not implemented — no role-gated branch. 90-day auto-disposal cron is NOT shipped — manual review only. Indian regulatory context: items with monetary value over ₹10,000 SHOULD be entered in the hotel's Goods Register and (if unclaimed beyond 90 days) reported to the local police station per the Hotel Industry Best Practices guidelines.
Step 13: Maintenance request blocks cleaning (cleaner finds a fault mid-clean)
- Story: US-006-008 — Maintenance Request Creation. Catalog: Built; verified: Built.
- Code: Cleaner taps "Report maintenance" on the per-room mobile screen → POST
/api/maintenancebody{ propertyId, location, category: 'PLUMBING'|'ELECTRICAL'|'HVAC'|'CARPENTRY'|'PAINTING'|'GAS'|'OTHER', description, priority: 'LOW'|'NORMAL'|'HIGH'|'URGENT', reportedBy }.hmp-housekeeping-service/src/maintenance/maintenance.controller.ts+maintenance.service.tsruns the full lifecycle (OPEN/IN_PROGRESS/WAITING_PARTS/COMPLETED/CANCELLED) + Kafka events. Escalation to engineering Work Order:hmp-housekeeping-service/src/tasks/escalation.service.ts— POST/tasks/:id/escalatecreates a Work Order in asset-service (port 3016) and atomically writes backHousekeepingTask.workOrderId(idempotent; 400 if already linked). - Test:
maintenance.service.spec.ts:: 6 specs — PASS.escalation.service.spec.ts:: 6 specs (double-escalation safety, atomic WO write-back) — PASS. Flow:maintenance-requests.flow.spec.ts:: 4 scenarios (create / list / patch IN_PROGRESS / resolve COMPLETED). - Pre-conditions: Cleaner is in a room (Step 5-7) and finds a leaking faucet / broken AC / blown fuse.
- Post-conditions:
MaintenanceRequestrow withstatus=OPEN. If URGENT + (ELECTRICAL|GAS|PLUMBING),[SME-PROPOSED]EP-006 D-008 wants atomicfrontOfficeStatus=OUT_OF_ORDERflip — today this requires a SEPARATE second write by the dashboard, and if the supervisor forgets, the room can be sold with live exposed wiring.MAINTENANCE_REQUEST_CREATEDKafka event published. - Notes: Two paths to engineering: (a) free-standing maintenance via
POST /maintenance(this step); (b) escalate an in-progress cleaning task viaPOST /tasks/:id/escalate. Path (b) is the typical "I started cleaning, found a problem, now Engineering owns the room" flow. Engineering ownership is inhmp-asset-serviceport 3016 (cross-EPIC into EP-017) — HK just holds theworkOrderIdforeign key. Double-escalation guarded (BadRequestException ifworkOrderIdalready set) but race condition (two near-simultaneous clicks) is acknowledged as v1 limitation (slated for Phase 5 unique-constraint hardening on asset-service).
Failure modes worth testing
| # | Scenario | Expected behaviour | Coverage today | Owning story / drift |
|---|---|---|---|---|
| F1 | Cleaner phone goes offline mid-clean (basement Faraday cage) | Mutations queue locally via sync.service.ts batch endpoint; on reconnect, last-write-wins merge via mutationId idempotency | sync.service.spec.ts (10 specs) — PASS | US-006-004 |
| F2 | Photo upload fails (2G/3G drop, S3 5xx) | Task SHALL still complete; HousekeepingPhoto.uploadStatus=PENDING_RETRY; background queue retries on WiFi | [NOT IMPLEMENTED] — today the upload is blocking; failure surfaces as error toast | US-006-006 / EP-006 D-010 [SME-PROPOSED] |
| F3 | Supervisor backlog — 30+ COMPLETED tasks awaiting inspection at shift end | Tasks remain in queue; handed over to next shift's supervisor via the (proposed) shift-handover report | [NOT IMPLEMENTED] — no shift-handover surface today | EP-006 D-004 [SME-PROPOSED] US-006-NEW-SHIFT-HANDOVER |
| F4 | Inspection skipped (supervisor never opens the task → room stuck COMPLETED but never INSPECTED) | Night-audit job at 02:00 IST (night-audit-hk-reset.consumer.ts) flags rooms with COMPLETED task >12h old; emails Front Office Manager | night-audit-hk-reset.consumer.spec.ts (16 specs) — PASS | US-006-007 |
| F5 | Discrepancy unresolved at end-of-day | Surfaces on the /rooms/discrepancy list + analytics live-proxy; daily summary email at night-audit time | Read-only today — no resolution endpoint, no auto-email; surfaces in report only | US-006-014 / EP-006 D-007 [SME-PROPOSED] |
| F6 | Maintenance request blocks cleaning — cleaner can't complete because the AC is broken and the room is not "clean to brand standard" | Cleaner POSTs /tasks/:id/escalate → creates Work Order in asset-service → HK task auto-cancels (or stays IN_PROGRESS pending WO completion) | escalation.service.spec.ts (6 specs covers atomic WO link); the auto-cancel of the HK task is NOT shipped — task stays IN_PROGRESS forever until WO closes | US-006-008 |
| F7 | Lost item without owner — guest checked out, item logged, but no claim within 90 days | Auto-disposal cron flags + emails Front Office Manager; item moved to status=DISPOSED after manager approval | [NOT IMPLEMENTED] — no cron, manual review only | US-006-012 / EP-006 D-009 [SME-PROPOSED] |
| F8 | Cross-tenant forgery — attacker POSTs /tasks with a roomId from another tenant | 403 ROOM_NOT_OWNED_BY_TENANT from assertRoomOwnership() | task-cross-tenant-denial.spec.ts (12 specs) — ORPHANED for create-path #3 per EP-006 D-009 / D-006; create-time tenant validation gap is open | EP-006 D-009 [SME-PROPOSED] + Task #11 security gap |
Cross-EPIC seams (this BF is single-EPIC, but reads + writes cross 2 service boundaries)
| Seam | Direction | Surface | Risk |
|---|---|---|---|
| EP-005A → EP-006 | frontdesk-service emits RESERVATION_CHECKED_OUT, housekeeping-service consumes | Step 1 auto-task | Dedup window is UTC-based, not property-timezone (D-009 below) |
| EP-006 → EP-002 (property-service) | housekeeping reads local Room mirror for frontOfficeStatus + room ownership | Step 2 board, Step 11 discrepancy | Read-model freshness bounded by property-service Kafka sync cadence; under sync lag, supervisor sees stale FO state |
| EP-006 → EP-017 (asset-service) | HK escalates a task to a Work Order | Step 13 maintenance escalation | Engineering owns the WO; HK only carries the workOrderId FK; double-escalation race acknowledged |
| EP-006 → EP-005B (frontdesk folio) | Minibar consumption (US-006-011) posts a charge to the folio | NOT in this BF flow but adjacent | India GST slab math happens at folio-post time, not minibar-consume time |
| EP-006 → EP-012 (analytics-service) | HOUSEKEEPING_TASK_COMPLETED → mv_hk_staff_productivity | Step 7 KPI | FU-55 contract — assignedTo MUST be on the event or the MV silently drops the row |
Indian SME hotel reality applied throughout
- 3-shift handover: morning 06:00-14:00 / evening 14:00-22:00 / night 22:00-06:00.
HousekeepingTask.scheduledForis a freeDateTimetoday with no shift discriminator (EP-006 D-004); supervisors can't filter the queue by "tasks left over from previous shift" or "tasks I created for next shift".[SME-PROPOSED]US-006-NEW-SHIFT-HANDOVER + ashiftenum on the task. - Double-cycle 06:00 + 17:00 IST: 06:00 daily-clean for stayovers + 17:00 turndown service (chocolates / robes / lights). Today both are manual or bulk operations at shift-change.
[SME-PROPOSED]US-006-NEW-A (Turndown) + US-006-NEW-C (Stayover deep clean) in EP-006 RTM. - 7-stayover deep-clean cadence: per-property
deepCleanConfig.stayoverInterval=7. Cron at 06:00 IST per property timezone creates a DEEP_CLEANING task for each occupied room whose night-count is a multiple of 7. Long-stay (≥7 nights) checkouts SHALL upgrade auto-CHECKOUT_CLEANING to DEEP_CLEANING.[SME-PROPOSED]EP-006 US-006-NEW-C. - 2G/3G fallback in basements: photo upload SHALL background-queue; task completion SHALL NOT block.
[SME-PROPOSED]EP-006 D-010. - Shared housekeeping phone: one shared Android handed off at shift change.
[SME-PROPOSED]Step 4 — one-tap "Sign out of shift" + audit log. - ₹10,000+ Goods Register: Lost-and-found items above this threshold SHALL be logged in the hotel's Goods Register; unclaimed beyond 90 days SHOULD be reported to local police.
[SME-PROPOSED]US-006-012 surface. - 14:00 IST standard check-in / 11:00 IST standard check-out: 3-hour housekeeping window per room turn. Step 10 SLA target is
(VACANT, INSPECTED)by 13:30 IST.
Drift summary
D-NNN refs follow the EP-006 RTM numbering. BF-009 surfaces a few BF-only drifts not owned by a single story; these get fresh BF-009 D-NNN ids to keep traceability.
| ID | Type | Severity | Description | Surfaced by step |
|---|---|---|---|---|
| EP-006 D-001 | Catalog-vs-EPIC-body | P0 | Catalog and EPIC-body disagree on numbering from US-006-005 onward; 4 catalog-only stories have no EPIC counterpart. BF-009 follows EPIC body. | All steps |
| EP-006 D-002 | Status-enum-drift | P1 | EPIC body says room board has 7 statuses; Prisma HousekeepingStatus enum has 4. Board UI joins HK + property-side enums. | Step 2 |
| EP-006 D-003 | Story-vs-code-enum-drift | P0 | Three copies of task-type enum disagree (EPIC body / Prisma+service / dashboard filter). | Step 1 auto-CHECKOUT_CLEANING |
| EP-006 D-004 | Story-incomplete | P1 | No shift-handover model on HousekeepingTask (scheduledFor is a bare DateTime). | Step 3, F3 |
| EP-006 D-005 | Story-incomplete + integration-gap | P1 | Auto-assign endpoint POST /tasks/auto-assign not shipped; prerequisite StaffShift/clock-in model also missing. | Step 3 |
| EP-006 D-006 | Catalog-vs-code | P0 | Catalog row for auto-reorder claims "Not Started" but code, schema, 18-spec suite are shipped (inverse of usual drift direction). | (US-006-013 minibar/linen — adjacent to this BF, not in the 10-step path) |
| EP-006 D-007 | Story-incomplete + cross-service-handoff | P0 | Discrepancy report is read-only; no resolution endpoint, no audit log, no duration tracking. | Step 11 / F5 |
| EP-006 D-008 | Cross-service-handoff | P1 | URGENT + (ELECTRICAL/GAS/PLUMBING) maintenance does NOT auto-flip room to OUT_OF_ORDER; requires a second dashboard write that supervisors can forget. | Step 13 / F6 |
| EP-006 D-009 | Security + missing-validation | P0 | roomId not validated for tenant on lost-found.create(), maintenance.create(), task.create(). Forged roomId leaks data across tenants. Task #11 security gap. | Step 12, Step 13, Step 1, F8 |
| EP-006 D-010 | Story-incomplete + missing-AC | P1 | HousekeepingPhoto has no photoType: BEFORE | AFTER discriminator; no Sharp compression; no 2G/3G background-queue; mobile E2E missing. | Step 6, F2 |
| EP-006 D-011 | Story-incomplete + India-hospitality-critical | P1 | Inspection is binary pass/fail; no checklist, no per-item scoring, no auto re-clean on fail. | Step 8, Step 9 |
| BF-009 D-001 | Cross-service-handoff timing | P0 | Step 1 dedup window is UTC-based (startOfDay = 00:00 UTC = 05:30 IST). Late-night double-checkouts straddling the UTC date line could dedup unexpectedly. Indian properties run on Asia/Kolkata. | Step 1 |
| BF-009 D-002 | Test-vs-flow-gap | P1 | housekeeping-lifecycle.flow.spec.ts exercises DAILY_CLEANING happy path but does NOT exercise the auto-CHECKOUT_CLEANING path triggered by RESERVATION_CHECKED_OUT. The flow asserts cleanliness state but not the cross-service event chain. | Step 1 |
| BF-009 D-003 | Test-vs-flow-gap | P1 | housekeeping-lifecycle.flow.spec.ts ends at COMPLETED read-back; does NOT exercise Step 8-10 (inspection → INSPECTED → room sellable). The supervisor side of the loop has no flow coverage. | Steps 8-10 |
| BF-009 D-004 | Cross-EPIC-handoff missing-test | P1 | Step 10 cross-service join (frontOfficeStatus=VACANT + housekeepingStatus=INSPECTED) is the "room sellable" invariant for the next reservation — no flow asserts the next reservation's assignRoom is unblocked after the cycle. | Step 10 |
Cross-EPIC narrative summary
| Step | Spans | Why cross-EPIC |
|---|---|---|
| 1 | EP-005A → EP-006 | Front desk emits checkout event; HK consumer auto-creates task. |
| 2-9 | EP-006 (internal) | Single-EPIC steps inside housekeeping-service. |
| 4 | EP-006 → EP-013 (notification) | WebSocket + FCM push to cleaner phone via notification-service. |
| 6 | EP-006 → EP-002 (storage/S3) | Photo presigned URL flow via shared StorageService. |
| 7 | EP-006 → EP-012 (analytics) | HOUSEKEEPING_TASK_COMPLETED populates mv_hk_staff_productivity (FU-55 contract). |
| 10 | EP-006 → EP-002 (property) + EP-005A (FO state) | Composite (VACANT, INSPECTED) status that unblocks next-arrival room-assign. |
| 11 | EP-006 ↔ EP-002 | Discrepancy joins HK + property-side enums; resolution writes to both. |
| 12 | EP-006 → EP-004 (guest) | Auto-link to last-checked-out guest requires guest-service cross-service read. |
| 13 | EP-006 → EP-017 (asset) | Maintenance escalation creates a Work Order in asset-service. |
The dominant cross-EPIC seam is Step 1 (checkout → auto-task); without that handoff the entire flow doesn't start. The dominant in-EPIC weakness is Steps 8-9 (inspection) — binary pass/fail with no audit trail, the gap most likely to bite Indian SME hotels claiming brand-standard inspection regimes.
See also
./BF-001-tenant-property-onboarding.md— template../reconciliation/PLAYBOOK.md— RTM reconciliation pattern../reconciliation/EP-006-rtm.md— per-EPIC RTM (16 stories, 12 D-NNN findings)hmp-infrastructure/testing/flow-tests/src/flows/housekeeping-lifecycle.flow.spec.ts— flow test (5 specs, daily-clean variant)hmp-infrastructure/testing/flow-tests/src/flows/maintenance-requests.flow.spec.ts— flow test (4 scenarios)hmp-infrastructure/testing/flow-tests/src/flows/lost-and-found.flow.spec.ts— flow test (3 scenarios)hmp-housekeeping-service/CLAUDE.md+src/tasks/CLAUDE.md+src/rooms/CLAUDE.md+src/maintenance/CLAUDE.md+src/sync/CLAUDE.md+src/photos/CLAUDE.md— code ground-truth