EVENT_SCHEMAS — inventory-service
Sibling: APPLICATION_LOGIC · API_CONTRACTS · DATA_MODEL · DOMAIN_MODEL
Strategic anchors: 04 Event-Driven Architecture · standards/NAMING §events
Every event published by inventory-service is wrapped in the platform-canonical envelope (04 §4) and subjects follow melmastoon.<service>.<aggregate>.<verb-past-tense>.v<n>. Producers write to the transactional outbox in the same DB transaction as the aggregate write; the outbox relay publishes to Pub/Sub with at-least-once semantics and a <tenantId>:<aggregateId> ordering key. Consumers dedupe by event_id in the inbox.
1. Common envelope
interface EventEnvelope<TPayload> {
eventId: string; // ULID
subject: string; // e.g. 'melmastoon.inventory.allocation.confirmed.v1'
source: 'inventory-service';
tenantId: TenantId;
aggregateKind: 'RoomAllocation' | 'InventoryBlock' | 'AvailabilityCalendar' | 'OverbookingPolicy';
aggregateId: string;
occurredAt: string; // RFC3339
publishedAt: string;
schemaVersion: 1;
causationId?: string; // upstream event id (e.g. reservation.held.v1.eventId)
correlationId: string;
idempotencyKey?: string;
orderingKey: string; // `${tenantId}:${aggregateId}`
retentionClass: 'transactional' | 'operational' | 'analytical';
payload: TPayload;
}
2. Topic layout (per-subject topics)
| Subject | Retention class | DLQ | Ordering | Notes |
|---|---|---|---|---|
melmastoon.inventory.availability.queried.v1 | analytical (90 d) | n/a | none | Sampled at 1% |
melmastoon.inventory.allocation.requested.v1 | operational (1 y) | yes | per-aggregate | |
melmastoon.inventory.allocation.confirmed.v1 | transactional (forever in BigQuery) | yes | per-aggregate | Key event for saga |
melmastoon.inventory.allocation.failed.v1 | transactional | yes | per-aggregate | Drives saga compensation |
melmastoon.inventory.allocation.released.v1 | transactional | yes | per-aggregate | |
melmastoon.inventory.room.assigned.v1 | operational | yes | per-aggregate | |
melmastoon.inventory.room.reassigned.v1 | transactional | yes | per-aggregate | |
melmastoon.inventory.block.created.v1 | operational | yes | per-aggregate | |
melmastoon.inventory.block.released.v1 | operational | yes | per-aggregate | |
melmastoon.inventory.overbooking_alert.v1 | transactional | yes | per-tenant | Pages on-call |
melmastoon.inventory.reaccommodation_required.v1 | transactional | yes | per-aggregate | Drives reservation-service sub-saga |
DLQ subjects suffix -dlq per 04 §11. Producers never delete from BigQuery; analytical retention is enforced by partition expiration policy.
3. Produced events — payload schemas
3.1 melmastoon.inventory.allocation.requested.v1
Emitted at the start of a hold attempt (after lock acquired, before insert). Useful for funnel observability.
interface AllocationRequestedPayload {
allocationCandidateRef: string; // ULID; ephemeral
reservationId: ReservationId;
reservationItemId: ReservationItemId;
propertyId: PropertyId;
roomTypeId: RoomTypeId;
qty: number;
stayWindow: { checkIn: string; checkOut: string };
mode: 'auto_pick' | 'specific_room' | 'group_member' | 'walk_in';
specificRoomId?: RoomId;
ttlSeconds?: number;
}
3.2 melmastoon.inventory.allocation.confirmed.v1
Emitted when an allocation enters held (saga step 1 success) and when it later transitions to committed. The same allocationId will appear with two different status values.
interface AllocationConfirmedPayload {
allocationId: AllocationId;
reservationId: ReservationId;
reservationItemId: ReservationItemId;
propertyId: PropertyId;
roomTypeId: RoomTypeId;
roomId?: RoomId;
bedId?: BedId;
stayWindow: { checkIn: string; checkOut: string };
status: 'held' | 'committed';
heldUntil?: string;
committedAt?: string;
mode: 'auto_pick' | 'specific_room' | 'group_member' | 'walk_in';
groupHoldId?: string;
}
JSON Schema (excerpt)
{
"$id": "https://schemas.melmastoon.ghasi.io/inventory/allocation.confirmed.v1.json",
"type": "object",
"required": ["allocationId", "reservationId", "reservationItemId", "propertyId", "roomTypeId", "stayWindow", "status", "mode"],
"properties": {
"allocationId": { "type": "string", "pattern": "^inv_[0-9A-Z]{26}$" },
"reservationId": { "type": "string", "pattern": "^rsv_[0-9A-Z]{26}$" },
"status": { "enum": ["held", "committed"] },
"stayWindow": {
"type": "object",
"required": ["checkIn", "checkOut"],
"properties": {
"checkIn": { "type": "string", "format": "date" },
"checkOut": { "type": "string", "format": "date" }
}
}
}
}
3.3 melmastoon.inventory.allocation.failed.v1
interface AllocationFailedPayload {
reservationId: ReservationId;
reservationItemId: ReservationItemId;
propertyId: PropertyId;
roomTypeId: RoomTypeId;
stayWindow: { checkIn: string; checkOut: string };
reasonCode:
| 'insufficient_availability'
| 'lock_timeout'
| 'stop_sell_active'
| 'overbooking_cap_exceeded'
| 'horizon_exhausted'
| 'room_not_in_type';
perNightAvailability?: Array<{ stayDate: string; available: number }>;
detail?: string;
}
Drives compensation C1 in reservation-service (reservation TESTING_STRATEGY).
3.4 melmastoon.inventory.allocation.released.v1
interface AllocationReleasedPayload {
allocationId: AllocationId;
reservationId: ReservationId;
reservationItemId: ReservationItemId;
propertyId: PropertyId;
roomTypeId: RoomTypeId;
roomId?: RoomId;
stayWindow: { checkIn: string; checkOut: string };
releaseReasonCode:
| 'reservation_cancelled'
| 'reservation_dates_changed'
| 'reservation_no_show'
| 'hold_expired'
| 'saga_compensation'
| 'block_cascade_reaccommodation'
| 'staff_manual_release';
releasedAt: string;
}
3.5 melmastoon.inventory.room.assigned.v1
interface RoomAssignedPayload {
allocationId: AllocationId;
reservationId: ReservationId;
reservationItemId: ReservationItemId;
propertyId: PropertyId;
roomTypeId: RoomTypeId;
roomId: RoomId; // never null for this event
bedId?: BedId;
stayWindow: { checkIn: string; checkOut: string };
assignmentSource: 'system' | 'staff' | 'guest_request';
}
3.6 melmastoon.inventory.room.reassigned.v1
interface RoomReassignedPayload {
allocationId: AllocationId;
reservationId: ReservationId;
fromRoomId: RoomId;
toRoomId: RoomId;
stayWindow: { checkIn: string; checkOut: string };
reasonCode: 'block_cascade' | 'guest_request' | 'staff_initiated' | 'date_change';
causationEventId?: string;
}
3.7 melmastoon.inventory.block.created.v1
interface BlockCreatedPayload {
blockId: BlockId;
propertyId: PropertyId;
roomId?: RoomId;
roomTypeId?: RoomTypeId;
stayWindow: { checkIn: string; checkOut: string };
reason: 'ooo' | 'oos' | 'maintenance' | 'event' | 'other';
reasonText?: string;
source: { kind: 'staff' | 'system'; eventId?: string; actorId?: string };
}
3.8 melmastoon.inventory.block.released.v1
interface BlockReleasedPayload {
blockId: BlockId;
propertyId: PropertyId;
releasedAt: string;
}
3.9 melmastoon.inventory.overbooking_alert.v1
Pages on-call. Carries enough context for incident triage.
interface OverbookingAlertPayload {
policyId: OverbookingPolicyId;
propertyId: PropertyId;
roomTypeId: RoomTypeId;
stayDate: string;
total: number;
committed: number;
held: number;
cap: number;
overflow: number; // committed - total
triggeringAllocationId?: AllocationId;
alertRoutes: ReadonlyArray<string>;
}
3.10 melmastoon.inventory.reaccommodation_required.v1
Consumed by reservation-service to start room-change sub-saga.
interface ReaccommodationRequiredPayload {
blockId: BlockId;
propertyId: PropertyId;
affectedAllocations: ReadonlyArray<{
allocationId: AllocationId;
reservationId: ReservationId;
reservationItemId: ReservationItemId;
roomId: RoomId;
overlapNights: ReadonlyArray<string>;
}>;
recommendedAction: 'auto_pick_in_type' | 'staff_intervention';
}
3.11 melmastoon.inventory.availability.queried.v1 (sampled)
interface AvailabilityQueriedSamplePayload {
propertyIds: ReadonlyArray<PropertyId>;
stayWindow: { checkIn: string; checkOut: string };
occupancy: { adults: number; children: number; infants: number };
resultsCount: number;
cacheHit: boolean;
latencyMs: number;
callerType: 'meta' | 'funnel' | 'backoffice' | 'sync';
}
PII rule: never include guest identifiers, IP, or device ids in this payload. The sample is for capacity planning only.
4. Consumed events
| Subject | Handler | Inbox dedupe | Effect |
|---|---|---|---|
melmastoon.reservation.held.v1 | PlaceHoldAllocationUseCase | yes | tentative allocation |
melmastoon.reservation.confirmed.v1 | CommitAllocationUseCase | yes | flip held→committed |
melmastoon.reservation.cancelled.v1 | ReleaseAllocationUseCase | yes | release |
melmastoon.reservation.dates_changed.v1 | ReleaseAllocationUseCase (atomic two-phase) | yes | release old, hold new |
melmastoon.reservation.no_show.v1 | ReleaseAllocationUseCase per policy | yes | conditional release |
melmastoon.reservation.hold_expired.v1 | ReleaseAllocationUseCase | yes | immediate release |
melmastoon.property.room.created.v1 | ExtendNewRoomCalendarUseCase | yes | calendar lane add |
melmastoon.property.room.taken_out_of_order.v1 | CreateInventoryBlockUseCase(source=system) | yes | block + reaccommodation |
melmastoon.property.room.returned_to_service.v1 | ReleaseInventoryBlockUseCase | yes | release block |
melmastoon.tenant.settings.changed.v1 | RefreshPolicyCacheUseCase | yes | hot reload of overbooking policy + TTL defaults |
Consumers verify the envelope's tenantId matches the loaded aggregate's tenant; mismatch → dead-letter.
5. Schema evolution
- Additive changes ship under the same major version (
v1). Optional fields only. - Breaking changes ship a new major (
v2); the old version is emitted in parallel for ≥ 30 days. - The schema registry (
schema-registry.melmastoon-platform) is the source of truth; CI validates every emit against the registered schema.
6. Cross-references
- Envelope and topology: 04 §4 + §11
- Naming grammar: standards/NAMING
- Use cases that emit each event: APPLICATION_LOGIC §2
- Reservation counterparts: reservation-service EVENT_SCHEMAS