Skip to main content

housekeeping-service — API_CONTRACTS

Base URL: https://housekeeping.<env>.melmastoon.com/api/v1/housekeeping · Versioning: URL-prefixed v1 · Errors: RFC 7807 with MELMASTOON.HOUSEKEEPING.* codes · AuthN: tenant-scoped JWT issued by iam-service.

This document is the binding REST contract. Every endpoint here MUST be present in openapi.yaml and exercised in tests/integration/api-contract.spec.ts. Direct callers are bff-backoffice-service (Electron desktop) and bff-tenant-booking-service (mid-stay request). Other services use events, not these endpoints.


1. Common headers

HeaderDirectionRequiredNotes
AuthorizationrequestyesBearer <jwt> issued by iam-service; carries tenant_id, staff_id, roles.
X-Tenant-Idrequestyes (defence-in-depth)Must match JWT claim or → 403.
X-Request-IdrequestrecommendedEchoed in response and logs; ULID.
Idempotency-Keyrequestrequired on POST and PATCH mutating endpointsUUID; replay window 24 h.
Accept-LanguagerequestrecommendedPersisted as locale_hint on tasks.
If-MatchrequestoptionalETag for optimistic concurrency on aggregate updates.
ETagresponseyes on aggregate reads"v=<version>".
X-RateLimit-*responseyesStandard limits.

2. Error envelope (RFC 7807)

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
{
"type": "https://errors.melmastoon.com/MELMASTOON.HOUSEKEEPING.ROOM_STATE_CONFLICT",
"title": "Room cannot transition from 'ready' to 'cleaning'",
"status": 422,
"code": "MELMASTOON.HOUSEKEEPING.ROOM_STATE_CONFLICT",
"detail": "Room rom_01HZX… is currently 'ready' and has no active task to start cleaning.",
"instance": "/api/v1/housekeeping/tasks/hkt_01HZY…/start",
"i18nKey": "errors.housekeeping.room_state_conflict",
"context": { "currentStatus": "ready", "attemptedTransition": "cleaning" },
"requestId": "req_01HZZ..."
}

The full code catalogue lives in docs/standards/ERROR_CODES.md.

3. Pagination, filtering, sorting

  • Pagination: cursor-based — ?cursor=<opaque>&limit=<n> (default 50, max 200). Response includes nextCursor (null if last page).
  • Filtering: RHS-colon style ?filter[status]=in:assigned,in_progress&filter[priority]=urgent.
  • Sorting: ?sort=-priority,scheduled_for (- prefix = desc).

4. Endpoints

4.1 Tasks

Create task

POST /tasks

{
"kind": "turnover",
"propertyId": "prp_01HZ…",
"roomId": "rom_01HZ…",
"reservationId": "rsv_01HZ…",
"priority": "high",
"scheduledFor": "2026-04-22T11:30:00Z",
"checklistKind": "turnover",
"localeHint": "ps-AF",
"note": "Late checkout; arriving guest at 13:00"
}

Response 201 Created:

{ "id": "hkt_01J…", "status": "pending", "version": 1,
"checklistId": "chl_01HZ…", "checklistVersion": 7,
"createdAt": "2026-04-22T11:05:11Z" }

Errors: 400 INVALID_INPUT, 409 TASK_ALREADY_EXISTS_FOR_ROOM, 404 ROOM_NOT_FOUND, 422 ROOM_BLOCKED.

List tasks (board)

GET /tasks?propertyId=prp_…&filter[status]=in:pending,assigned,in_progress&sort=-priority,scheduled_for

{
"items": [ { "id": "hkt_…", "status": "in_progress", "priority": "urgent",
"roomId": "rom_…", "assigneeStaffId": "stf_…",
"scheduledFor": "2026-04-22T11:30:00Z", "version": 4 } ],
"nextCursor": null
}

Read one task

GET /tasks/{id} — full aggregate with checklist binding, linen, durations, last 20 audit entries.

Assign / reassign

POST /tasks/{id}/assign

{ "staffId": "stf_01HZ…", "reason": "router_suggestion", "suggestionId": "sug_01HZ…" }

Response 200 OK with new version. Reassignment auto-detected if assignee already set.

Errors: 409 INVALID_TRANSITION, 422 STAFF_UNAVAILABLE.

Start

POST /tasks/{id}/start

{ "at": "2026-04-22T11:42:01Z", "actor": { "staffId": "stf_…" } }

Errors: 409 INVALID_TRANSITION, 422 STAFF_UNAVAILABLE, 422 ROOM_BLOCKED.

Pause / Resume

POST /tasks/{id}/pause

{ "reason": "awaiting_linen", "note": "Need 4 large towels", "at": "2026-04-22T11:51:00Z" }

POST /tasks/{id}/resume

{ "at": "2026-04-22T11:58:21Z" }

Complete

POST /tasks/{id}/complete

{
"completedAt": "2026-04-22T12:14:55Z",
"checklistResults": [
{ "itemKey": "bath_clean", "checked": true, "photoMediaId": "med_01J…" },
{ "itemKey": "linen_change","checked": true },
{ "itemKey": "tv_remote_present","checked": true }
],
"linen": { "issued": 4, "returned": 4 },
"noMaintenanceFound": true,
"note": ""
}

Errors: 409 INVALID_TRANSITION, 422 CHECKLIST_INCOMPLETE.

Fail

POST /tasks/{id}/fail{ reason, note }. Routes the task back to pending unless reason is terminal (sickness reroutes; room_not_vacated escalates).

Cancel

POST /tasks/{id}/cancel{ reason }. Idempotent.

Escalate

POST /tasks/{id}/escalate{ to: { staffId? | role: "supervisor" }, note }.

Maintenance required

POST /tasks/{id}/maintenance-required

{
"issue": {
"category": "plumbing",
"severity": "blocking",
"description": "Bathroom sink leak under cabinet"
},
"photoMediaIds": ["med_01J…"],
"outcome": "convert_terminal" // or "pause"
}

Side effects: emits room.maintenance_required.v1; flips room → out_of_order; if outcome=convert_terminal task moves to requires_maintenance, otherwise paused(awaiting_maintenance).

Bump priority

PATCH /tasks/{id}/priority{ priority: "urgent", reason: "front_desk_needs_now" }.

4.2 Inspections

POST /tasks/{id}/inspections

{
"inspectorStaffId": "stf_01HZ…",
"checklistId": "chl_01HZ…",
"checklistVersion": 7,
"results": [ { "itemKey": "bath_clean", "checked": true } ],
"outcome": "pass",
"note": ""
}

outcome=fail resets the room to dirty and creates a follow-up turnover task (priority bumped one level).

GET /tasks/{id}/inspections — list all inspections for the task (typically 0 or 1, more if re-inspected).

4.3 Rooms

GET /rooms/{roomId}/status

{ "roomId": "rom_…", "status": "ready", "lastFlippedAt": "2026-04-22T12:30:11Z",
"lastTaskId": "hkt_…", "blocks": [], "version": 12 }

POST /rooms/{roomId}/status (manual flip)

{ "to": "clean", "reason": "post_renovation_walkthrough_ok", "actor": { "role": "property_manager" } }

POST /rooms/{roomId}/block

{ "reason": "inspection", "until": "2026-04-22T13:00:00Z", "note": "Awaiting supervisor walkthrough" }

DELETE /rooms/{roomId}/block/{blockId} — clears the block.

4.4 Checklists

POST /checklists

{
"kind": "turnover",
"items": [
{ "key": "bath_clean", "labelI18n": { "en":"Bathroom cleaned",
"ps-AF":"تشناب پاک شو","fa-AF":"حمام تمیز شد" }, "mandatory": true, "requiresPhoto": true },
{ "key": "linen_change","labelI18n": {"en":"Linen changed","ps-AF":"څادر بدل شو"},
"mandatory": true, "requiresPhoto": false },
{ "key": "tv_remote_present", "labelI18n":{"en":"TV remote present"},
"mandatory": false, "requiresPhoto": false }
]
}

Response 201 with { id, version, publishedAt }. Publishing is immediate; no draft state.

GET /checklists?kind=turnover&active=true → list of latest active versions per kind.

4.5 Lost & found

POST /lost-and-found

{
"roomId": "rom_…",
"reservationId": "rsv_…",
"foundAt": "2026-04-22T12:20:00Z",
"description": "Black umbrella, brand 'Knirps'",
"photoMediaIds": ["med_…"],
"storageLocation": "front-desk-cabinet-3",
"finderStaffId": "stf_…"
}

POST /lost-and-found/{id}/match

{ "claimantName": "Ali R.", "claimantPhone": "+93 70 …",
"matchedAt": "2026-04-23T10:11:00Z", "matchedByStaffId": "stf_…" }

POST /lost-and-found/{id}/dispose

{ "method": "donated", "note": "Donated to staff lounge" }

4.6 Linen

GET /linen?propertyId=prp_…

{ "items":[
{"id":"lin_…","line":"towel-large","onHand":42,"lowWatermark":20},
{"id":"lin_…","line":"sheet-double","onHand":11,"lowWatermark":15} ] }

POST /linen/{lineId}/issue{ taskId, count }.

POST /linen/{lineId}/return{ taskId, count }. May be < issued.

4.7 Board (composite read)

GET /board?propertyId=prp_…&shiftDate=2026-04-22

{
"rooms": [ { "roomId":"rom_…","number":"203","status":"cleaning",
"activeTaskId":"hkt_…","priority":"high" } ],
"tasks": [ { "id":"hkt_…","status":"in_progress","assigneeStaffId":"stf_…",
"checklistProgress":{"checked":3,"total":7} } ],
"shifts":[ { "staffId":"stf_…","name":"Mariam","languages":["ps-AF","fa-AF"],
"capacityMinutes":420,"loadMinutes":280,"activeTaskCount":2 } ],
"stats": { "pendingNext90Min":4, "linenRunwayMinutes":120 }
}

Designed to be a single round-trip for the desktop board on first load. Subsequent updates flow through the sync stream.

4.8 Stats

GET /stats/turnover?propertyId=prp_…&from=2026-04-15&to=2026-04-21

{ "windows":[
{"date":"2026-04-15","tasks":18,"avgTimeToReadyMinutes":42,"failedCount":1,"escalatedCount":0}
] }

5. WebSocket / SSE (board live updates)

Live board updates are not served from this service; they go through bff-backoffice-service's WebSocket which subscribes to relevant Pub/Sub topics and fans out to connected sessions. This service only exposes REST.

6. OpenAPI source of truth

openapi.yaml lives in the service repo at contracts/openapi.yaml and is published to the central API registry on merge to main. CI fails if the file is out of sync with controllers (verified by tests/integration/api-contract.spec.ts).

7. Rate limits

Per tenant:

BucketLimitWindow
Board reads (GET /board)60060 s
Task mutations120060 s
Lost & found writes6060 s
Linen ops24060 s
Stats3060 s

Limits enforced by iam-service gateway upstream and by an in-process token bucket for defence-in-depth.

8. Internal endpoints (Pub/Sub push, OIDC-authenticated)

POST /internal/events/reservation.checked-out etc. — one route per consumed topic. All require Authorization: Bearer <gcp-oidc> from the configured Pub/Sub service account; rejected otherwise. Not exposed via the public load balancer.

9. Sync endpoints

Documented in SYNC_CONTRACT.md. Mounted under /sync/v1/*; they share auth with the REST endpoints but use separate request/response DTOs and are versioned independently.

You want…Read
Domain semantics behind these endpointsDOMAIN_MODEL.md
Use-case flow per endpointAPPLICATION_LOGIC.md
Event payloads emitted as side effectsEVENT_SCHEMAS.md
AuthZ matrixSECURITY_MODEL.md
Failure surfaces by endpointFAILURE_MODES.md