housekeeping-service — EVENT_SCHEMAS
All events follow the canonical envelope from
docs/04-event-driven-architecture.md. Subjects:melmastoon.<service>.<aggregate>.<verb-past-tense>.v<n>. Payloads JSON, encoded UTF-8, max 256 KiB. Schemas validated againstcontracts/events/*.schema.jsonin CI.
1. Common envelope
Every published message wraps the payload below in the standard envelope:
{
"specVersion": "1.0",
"id": "evt_01J0…",
"subject": "melmastoon.housekeeping.task.created.v1",
"tenantId": "tnt_01HZ…",
"occurredAt": "2026-04-22T11:05:11.451Z",
"producer": "housekeeping-service@1.4.2",
"traceparent": "00-…-…-01",
"actor": { "type": "system|user|integration", "id": "stf_… | sys_…" },
"correlationId":"req_01HZ…",
"causationId": "evt_01J0…",
"payload": { /* per-subject payload below */ }
}
Idempotency key for consumers = (subject, id). Retention = 7 days at Pub/Sub; 90 days in event_archive GCS bucket.
2. Published events
2.1 melmastoon.housekeeping.task.created.v1
interface TaskCreatedV1 {
taskId: string; // hkt_…
tenantId: string;
propertyId: string;
roomId: string;
reservationId?: string;
kind: "turnover"|"mid_stay_clean"|"deep_clean"|"post_maintenance"|"post_renovation"|"inspection";
priority: "low"|"normal"|"high"|"urgent";
scheduledFor?: string; // ISO-8601 UTC
checklistId: string;
checklistVersion: number;
localeHint: string; // "ps-AF" | "fa-AF" | …
source: "event"|"manual"|"scheduler";
sourceEventId?: string; // e.g., the reservation.checked_out.v1 event id
}
Example payload:
{ "taskId":"hkt_01J…","tenantId":"tnt_01HZ…","propertyId":"prp_01HZ…",
"roomId":"rom_01HZ…","reservationId":"rsv_01HZ…","kind":"turnover",
"priority":"high","scheduledFor":"2026-04-22T11:30:00Z",
"checklistId":"chl_01HZ…","checklistVersion":7,"localeHint":"ps-AF",
"source":"event","sourceEventId":"evt_01J…" }
2.2 .assigned.v1 / .reassigned.v1
interface TaskAssignedV1 { taskId:string; tenantId:string; staffId:string; }
interface TaskReassignedV1 { taskId:string; tenantId:string; previousStaffId:string;
staffId:string; reason?: "router_suggestion"|"manual"|"sickness"|"capacity"; }
2.3 .started.v1 / .paused.v1 / .resumed.v1
interface TaskStartedV1 { taskId:string; tenantId:string; staffId:string; startedAt:string; }
interface TaskPausedV1 { taskId:string; tenantId:string; staffId:string; pausedAt:string;
reason:"break"|"awaiting_linen"|"awaiting_maintenance"|"staff_swap"|"guest_present"|"other";
note?:string; }
interface TaskResumedV1 { taskId:string; tenantId:string; staffId:string; resumedAt:string; }
2.4 .completed.v1
interface TaskCompletedV1 {
taskId:string; tenantId:string; staffId:string;
completedAt:string; durationMinutes:number;
checklistResults: Array<{ itemKey:string; checked:boolean; note?:string; photoMediaId?:string }>;
linen?: { issued:number; returned:number };
noMaintenanceFound:boolean;
}
2.5 .failed.v1 / .cancelled.v1 / .escalated.v1
interface TaskFailedV1 { taskId:string; tenantId:string; staffId?:string;
reason:"room_not_vacated"|"access_denied"|"sickness"|"equipment_missing"|"other"; note?:string; }
interface TaskCancelledV1 { taskId:string; tenantId:string; reason:string; cancelledBy:{type:string;id:string}; }
interface TaskEscalatedV1 { taskId:string; tenantId:string; to:{ staffId?:string; role?:"supervisor"|"property_manager" };
reason:string; escalatedBy:{type:string;id:string}; }
2.6 melmastoon.housekeeping.room.status_changed.v1
interface RoomStatusChangedV1 {
tenantId:string; propertyId:string; roomId:string;
previousStatus:"clean"|"dirty"|"cleaning"|"cleaned"|"inspected"|"ready"|"out_of_order"|"out_of_service";
status: "clean"|"dirty"|"cleaning"|"cleaned"|"inspected"|"ready"|"out_of_order"|"out_of_service";
flippedAt:string; cause:"task_started"|"task_completed"|"inspection_passed"|"inspection_failed"|
"maintenance_required"|"maintenance_completed"|"manual_override"|"reservation_checked_out";
taskId?:string; actor:{type:string;id:string};
}
Example:
{ "tenantId":"tnt_01HZ…","propertyId":"prp_01HZ…","roomId":"rom_01HZ…",
"previousStatus":"cleaned","status":"ready","flippedAt":"2026-04-22T12:30:11Z",
"cause":"inspection_passed","taskId":"hkt_01J…",
"actor":{"type":"user","id":"stf_01HZ…"} }
2.7 melmastoon.housekeeping.room.maintenance_required.v1
interface RoomMaintenanceRequiredV1 {
tenantId:string; propertyId:string; roomId:string;
taskId:string;
issue: { category:string; severity:"info"|"minor"|"blocking"; description:string; photoMediaIds?:string[] };
reportedAt:string; reportedBy:{ type:"user"; staffId:string };
outcome:"convert_terminal"|"pause";
}
Consumed by maintenance-service to open a work order.
2.8 melmastoon.housekeeping.inspection.passed.v1 / .failed.v1
interface InspectionPassedV1 { inspectionId:string; tenantId:string; taskId:string; roomId:string;
inspectorStaffId:string; performedAt:string; checklistId:string; checklistVersion:number; }
interface InspectionFailedV1 extends InspectionPassedV1 { reason:string; failedItemKeys:string[]; }
2.9 melmastoon.housekeeping.checklist.template_updated.v1
interface ChecklistTemplateUpdatedV1 {
checklistId:string; tenantId:string;
kind:"turnover"|"mid_stay_clean"|"deep_clean"|"post_maintenance"|"post_renovation"|"inspection";
version:number; previousVersion:number;
publishedBy:{type:string;id:string}; publishedAt:string;
diff:{ added:string[]; removed:string[]; modified:string[] }; // item keys
}
2.10 melmastoon.housekeeping.lost_item.recorded.v1 / .matched.v1 / .disposed.v1
interface LostItemRecordedV1 { lostItemId:string; tenantId:string; propertyId:string; roomId:string;
reservationId?:string; description:string; photoMediaIds?:string[]; recordedAt:string; finderStaffId:string; }
interface LostItemMatchedV1 { lostItemId:string; tenantId:string;
claimantName:string; claimantPhone?:string; matchedAt:string; matchedByStaffId:string; }
interface LostItemDisposedV1 { lostItemId:string; tenantId:string; method:"donated"|"trashed"|"kept_storage";
disposedAt:string; note?:string; }
2.11 melmastoon.housekeeping.linen.low_stock_alert.v1
interface LinenLowStockAlertV1 { tenantId:string; propertyId:string;
line:string; onHand:number; lowWatermark:number; detectedAt:string; }
Debounced: at most once per (tenant, property, line) per 60 minutes.
2.12 melmastoon.housekeeping.shift.staffing_gap_detected.v1
interface ShiftStaffingGapDetectedV1 { tenantId:string; propertyId:string; shiftId:string;
detectedAt:string; pendingMinutes:number; capacityMinutes:number;
suggestedAction:"call_relief"|"reroute"|"defer_low_priority";
affectedTaskIds:string[]; }
3. Consumed events
| Subject | Producer | Effect | Idempotency |
|---|---|---|---|
melmastoon.reservation.checked_out.v1 | reservation-service | CreateTaskUseCase(kind=Turnover) + RoomStatus → dirty | (subject, id) inbox |
melmastoon.reservation.early_checkout.v1 | reservation-service | CreateTaskUseCase if absent; BumpPriorityUseCase | inbox |
melmastoon.reservation.modification.requested.v1 | reservation-service | If mods includes mid_stay_clean → CreateTaskUseCase(kind=MidStayClean) | inbox |
melmastoon.reservation.cancelled.v1 | reservation-service | Cancel pending tasks for the reservation | inbox |
melmastoon.maintenance.work_order.completed.v1 | maintenance-service | Unblock room; CreateTaskUseCase(kind=PostMaintenance) | inbox |
melmastoon.staff.shift.started.v1 | staff-service | Project StaffShiftAssignment | inbox |
melmastoon.staff.shift.ended.v1 | staff-service | Tear down assignment; reroute open tasks | inbox |
melmastoon.ai_orchestrator.suggestion.housekeeping_routing.v1 | ai-orchestrator-service | Apply via HITL gate | inbox |
melmastoon.property.room.archived.v1 | property-service | Cancel pending tasks; clear RoomStatus row | inbox |
melmastoon.tenant.settings.changed.v1 | tenant-service | Refresh in-memory tenant cache | not strict (cache reload) |
3.1 Expected consumed shape (subset)
interface ReservationCheckedOutV1 {
reservationId:string; tenantId:string; propertyId:string;
checkedOutAt:string; earlyCheckout:boolean; overstayedNights:number;
rooms: Array<{ itemId:string; roomId:string }>;
actor:{type:string;id:string};
}
interface RoutingSuggestionV1 {
suggestionId:string; tenantId:string; propertyId:string; shiftId:string;
rows: Array<{ taskId:string; staffId:string; score:number; reasoning:string }>;
generatedAt:string; modelVersion:string;
}
4. Topic / subscription topology (GCP Pub/Sub)
| Topic | Mode | Subscribers (this service) |
|---|---|---|
melmastoon.housekeeping.task.* | publish | external: notification-service, analytics-service, audit-service, search-aggregation-service |
melmastoon.housekeeping.room.* | publish | external: search-aggregation-service, analytics-service, audit-service, bff-backoffice-service (board) |
melmastoon.housekeeping.room.maintenance_required.v1 | publish | maintenance-service (push), notification-service |
melmastoon.housekeeping.linen.low_stock_alert.v1 | publish | notification-service |
melmastoon.housekeeping.shift.staffing_gap_detected.v1 | publish | notification-service, bff-backoffice-service |
melmastoon.reservation.checked_out.v1 | consume | push → /internal/events/reservation.checked-out |
melmastoon.reservation.early_checkout.v1 | consume | push |
melmastoon.reservation.modification.requested.v1 | consume | push |
melmastoon.maintenance.work_order.completed.v1 | consume | push |
melmastoon.staff.shift.* | consume | push |
melmastoon.ai_orchestrator.suggestion.housekeeping_routing.v1 | consume | push |
melmastoon.property.room.archived.v1 | consume | push |
melmastoon.tenant.settings.changed.v1 | consume | push |
Push subscriptions deliver with OIDC auth (service-account housekeeping-events@<project>.iam.gserviceaccount.com); ACK deadline 60 s; max deliveries 10; retry policy exponential 10s → 600s; DLQ topic melmastoon.dlq.housekeeping.
5. Schema evolution
- Additive only within
vN. Removing or renaming a field requiresvN+1. - Both versions run in parallel for ≥ 30 days. Producers may emit only
vN+1once 95% of consumers have migrated. - Schema files live in
contracts/events/<subject>.schema.json. CI rejects breaking changes against the previous tag.
6. Outbox table (DDL excerpt — full in DATA_MODEL)
CREATE TABLE housekeeping.outbox (
id BIGSERIAL PRIMARY KEY,
event_id TEXT NOT NULL UNIQUE, -- evt_<ulid>
tenant_id TEXT NOT NULL,
subject TEXT NOT NULL,
payload JSONB NOT NULL,
envelope_meta JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
published_at TIMESTAMPTZ
);
CREATE INDEX outbox_unpublished_idx ON housekeeping.outbox (id) WHERE published_at IS NULL;
The outbox relay (sidecar) reads WHERE published_at IS NULL ORDER BY id LIMIT 100, publishes each, then UPDATE … SET published_at = now() on success.
7. Cross-link
- Use-case → emitted event mapping:
APPLICATION_LOGIC.md§3. - Topic taxonomy and global rules:
docs/04-event-driven-architecture.md. - DLQ playbook:
FAILURE_MODES.md.