Skip to main content

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)

SubjectRetention classDLQOrderingNotes
melmastoon.inventory.availability.queried.v1analytical (90 d)n/anoneSampled at 1%
melmastoon.inventory.allocation.requested.v1operational (1 y)yesper-aggregate
melmastoon.inventory.allocation.confirmed.v1transactional (forever in BigQuery)yesper-aggregateKey event for saga
melmastoon.inventory.allocation.failed.v1transactionalyesper-aggregateDrives saga compensation
melmastoon.inventory.allocation.released.v1transactionalyesper-aggregate
melmastoon.inventory.room.assigned.v1operationalyesper-aggregate
melmastoon.inventory.room.reassigned.v1transactionalyesper-aggregate
melmastoon.inventory.block.created.v1operationalyesper-aggregate
melmastoon.inventory.block.released.v1operationalyesper-aggregate
melmastoon.inventory.overbooking_alert.v1transactionalyesper-tenantPages on-call
melmastoon.inventory.reaccommodation_required.v1transactionalyesper-aggregateDrives 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

SubjectHandlerInbox dedupeEffect
melmastoon.reservation.held.v1PlaceHoldAllocationUseCaseyestentative allocation
melmastoon.reservation.confirmed.v1CommitAllocationUseCaseyesflip held→committed
melmastoon.reservation.cancelled.v1ReleaseAllocationUseCaseyesrelease
melmastoon.reservation.dates_changed.v1ReleaseAllocationUseCase (atomic two-phase)yesrelease old, hold new
melmastoon.reservation.no_show.v1ReleaseAllocationUseCase per policyyesconditional release
melmastoon.reservation.hold_expired.v1ReleaseAllocationUseCaseyesimmediate release
melmastoon.property.room.created.v1ExtendNewRoomCalendarUseCaseyescalendar lane add
melmastoon.property.room.taken_out_of_order.v1CreateInventoryBlockUseCase(source=system)yesblock + reaccommodation
melmastoon.property.room.returned_to_service.v1ReleaseInventoryBlockUseCaseyesrelease block
melmastoon.tenant.settings.changed.v1RefreshPolicyCacheUseCaseyeshot 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