Skip to main content

EVENT_SCHEMAS — reservation-service

Sibling: DOMAIN_MODEL · APPLICATION_LOGIC · API_CONTRACTS · DATA_MODEL

Strategic anchors: 04 Event-Driven Architecture · standards/NAMING · standards/ERROR_CODES

All events follow the platform event subject grammar: melmastoon.<service>.<aggregate>.<verb-past-tense>.v<n>. Schemas are committed to event-schemas/melmastoon/reservation/<aggregate>/<verb>.v<n>.json and validated in CI on every PR. Any breaking change requires a new vN.


1. Common envelope

Every event is wrapped in the canonical EventEnvelope<T> from 04 §4. Reservation-specific defaults:

FieldValue
producedBy.servicereservation-service
metadata.dataResidency<tenant.region> (typically gcp-me-central1 or gcp-asia-south1)
metadata.orderingKey<tenantId>:<reservationId> (per-aggregate ordering)
causationIdthe consumed event id for saga reactions; absent for command-driven emissions

2. Subject taxonomy + retention + partition

All reservation.* events partition on <tenantId>:<reservationId> so consumers see strict per-aggregate ordering.

SubjectRetention classPartition key
melmastoon.reservation.quote.requested.v1operational (90d)tenantId:guestId|deviceId
melmastoon.reservation.quote.created.v1operational (90d)tenantId:quoteId
melmastoon.reservation.held.v1regulated (7y)tenantId:reservationId
melmastoon.reservation.hold_expired.v1operational (1y)tenantId:reservationId
melmastoon.reservation.confirmed.v1regulated (7y)tenantId:reservationId
melmastoon.reservation.cancelled.v1regulated (7y)tenantId:reservationId
melmastoon.reservation.modified.v1regulated (7y)tenantId:reservationId
melmastoon.reservation.dates_changed.v1regulated (7y)tenantId:reservationId
melmastoon.reservation.check_in_started.v1operational (1y)tenantId:reservationId
melmastoon.reservation.checked_in.v1regulated (7y)tenantId:reservationId
melmastoon.reservation.checkout_started.v1operational (1y)tenantId:reservationId
melmastoon.reservation.checked_out.v1regulated (7y)tenantId:reservationId
melmastoon.reservation.no_show.v1regulated (7y)tenantId:reservationId
melmastoon.reservation.early_checkout.v1regulated (7y)tenantId:reservationId
melmastoon.reservation.overstayed.v1operational (1y)tenantId:reservationId
melmastoon.reservation.special_request.added.v1operational (1y)tenantId:reservationId

Retention classes follow 04 §5: regulated lands in BigQuery events_regulated.* with 7-year hot retention; operational in events_operational.*. PII fields are tokenized via audit-service envelope encryption before BigQuery sink (see SECURITY_MODEL §6).


3. Published events — payload + JSON schema + example

3.1 melmastoon.reservation.held.v1

interface ReservationHeldV1 {
reservationId: string; // rsv_…
tenantId: string; // tnt_…
propertyId: string; // ppt_…
reservationCode: string; // GM-XXXXXX
channel: 'direct' | 'meta' | 'walk_in' | 'phone_by_staff' | 'ota';
partnerId?: string; // for ota / partnered meta
primaryGuest: {
guestId: string;
locale: string; // BCP-47
contactHash: { email?: string; phone?: string }; // sha256(tenantSalt + value)
};
stayWindow: { start: string; end: string; nights: number };
items: Array<{
itemId: string;
roomTypeId: string;
occupants: { adults: number; children: number; infants: number };
nightlyRateMicro: string; // bigint as string; per-night list omitted from event
currency: string;
}>;
totals: { grandTotalMicro: string; currency: string };
hold: { placedAt: string; expiresAt: string; ttlSeconds: number };
paymentIntentId?: string;
fxSnapshot?: {
base: string; quote: string; rate: number; source: string; capturedAt: string;
};
createdBy: { type: 'guest' | 'staff' | 'system'; id: string };
occurredAt: string;
}
{
"$id": "https://schemas.melmastoon.io/reservation/held/v1.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["reservationId","tenantId","propertyId","reservationCode","channel","primaryGuest","stayWindow","items","totals","hold","createdBy","occurredAt"],
"properties": {
"reservationId": { "type": "string", "pattern": "^rsv_[0-9A-HJKMNP-TV-Z]{26}$" },
"tenantId": { "type": "string", "pattern": "^tnt_[0-9A-HJKMNP-TV-Z]{26}$" },
"propertyId": { "type": "string", "pattern": "^ppt_[0-9A-HJKMNP-TV-Z]{26}$" },
"reservationCode": { "type": "string", "pattern": "^GM-[A-Z0-9]{6,8}$" },
"channel": { "enum": ["direct","meta","walk_in","phone_by_staff","ota"] },
"stayWindow": {
"type": "object",
"required": ["start","end","nights"],
"properties": {
"start": { "type": "string", "format": "date" },
"end": { "type": "string", "format": "date" },
"nights": { "type": "integer", "minimum": 1 }
}
},
"totals": {
"type": "object",
"required": ["grandTotalMicro","currency"],
"properties": {
"grandTotalMicro": { "type": "string", "pattern": "^[0-9]+$" },
"currency": { "type": "string", "minLength": 3, "maxLength": 3 }
}
},
"hold": {
"type": "object",
"required": ["placedAt","expiresAt","ttlSeconds"],
"properties": {
"ttlSeconds": { "type": "integer", "minimum": 120, "maximum": 1800 }
}
}
}
}
{
"reservationId": "rsv_01H3Z4WK7TS5K8B5RBQ8H6JQK7Z",
"tenantId": "tnt_01H3Z3GXSE7Y8B5RBQ8H6JQK7Z",
"propertyId": "ppt_01H3Z3GXSE7Y8B5RBQ8H6JQK7Z",
"reservationCode": "GM-9F4K2C",
"channel": "direct",
"primaryGuest": { "guestId": "gst_01H…", "locale": "ps-AF", "contactHash": { "email": "ab12…", "phone": "cd34…" } },
"stayWindow": { "start": "2026-04-25", "end": "2026-04-28", "nights": 3 },
"items": [
{ "itemId": "01H3Z…1", "roomTypeId": "rmt_…SUITE", "occupants": { "adults": 2, "children": 1, "infants": 0 }, "nightlyRateMicro": "120000000", "currency": "USD" }
],
"totals": { "grandTotalMicro": "559000000", "currency": "USD" },
"hold": { "placedAt": "2026-04-22T18:31:00Z", "expiresAt": "2026-04-22T18:41:00Z", "ttlSeconds": 600 },
"paymentIntentId": "pyi_01H…",
"fxSnapshot": { "base": "USD", "quote": "IRR", "rate": 42000, "source": "pricing_service", "capturedAt": "2026-04-22T18:31:00Z" },
"createdBy": { "type": "guest", "id": "gst_01H…" },
"occurredAt": "2026-04-22T18:31:00.123Z"
}

3.2 melmastoon.reservation.confirmed.v1

interface ReservationConfirmedV1 {
reservationId: string;
tenantId: string;
propertyId: string;
reservationCode: string;
stayWindow: { start: string; end: string; nights: number };
totals: { grandTotalMicro: string; currency: string; inTenantCurrencyMicro?: string; tenantCurrency?: string };
payment: {
method: 'paypal' | 'card' | 'cash_on_arrival' | 'mfs' | 'mixed';
status: 'captured' | 'pending_cash';
paymentIntentIds: string[];
capturedMicro: string;
};
fxSnapshot?: { base: string; quote: string; rate: number; source: string; capturedAt: string };
channel: { channel: string; partnerId?: string };
primaryGuest: { guestId: string; locale: string };
confirmedAt: string;
causationEventId: string; // payment.transaction.captured.v1 id
}

Consumers: billing-service (open folio), lock-integration-service (issue if same-day), notification-service (multi-channel confirmation), analytics-service, search-aggregation-service, audit-service, sync-service.

3.3 melmastoon.reservation.cancelled.v1

interface ReservationCancelledV1 {
reservationId: string;
tenantId: string;
propertyId: string;
cancelledAt: string;
reason: string; // free text, sanitized
reasonCode: 'guest_cancelled' | 'staff_cancelled' | 'no_show' | 'overbooked'
| 'payment_failed' | 'inventory_failed' | 'merged_into' | 'split_into';
policyApplied: { ref: string; refundEligibilityMicro: string; penaltyMicro: string; currency: string };
paymentRefundsRequested: Array<{ paymentIntentId: string; amountMicro: string }>;
itemsCancelled: string[]; // item ids; equal to all on full cancel
actor: { type: string; id: string };
causationEventId?: string;
}

Consumers: inventory-service (release), payment-gateway-service (issue refunds), lock-integration-service (revoke if issued), notification-service, billing-service (apply credit), analytics-service.

3.4 melmastoon.reservation.modified.v1 and dates_changed.v1

Common shape; dates_changed.v1 is a stronger-typed sibling carrying explicit pre/post stay windows for re-allocation consumers.

interface ReservationModifiedV1 {
reservationId: string;
tenantId: string;
modificationId: string; // ULID
type: 'date_change' | 'room_change' | 'room_added' | 'room_removed'
| 'guest_count_change' | 'rate_change' | 'guest_profile_update'
| 'channel_attribution_corrected' | 'manual_state_override';
occurredAt: string;
actor: { type: string; id: string };
before: Record<string, unknown>; // typed per sub-type; documented in schema registry
after: Record<string, unknown>;
policyAppliedRef?: string;
priceDeltaMicro?: string;
refundOrChargeRequest?: { paymentIntentId?: string; amountMicro: string; direction: 'charge' | 'refund' };
}

3.5 melmastoon.reservation.checked_in.v1

interface ReservationCheckedInV1 {
reservationId: string;
tenantId: string;
propertyId: string;
checkedInAt: string;
assignedRooms: Array<{ itemId: string; roomId: string }>;
keyCredentialIds: string[]; // populated when lock issued; empty if requiresManualKey
requiresManualKey: boolean;
actor: { type: 'staff' | 'guest_self_service'; id: string };
earlyCheckIn: boolean;
}

Consumers: housekeeping-service (mark room occupied), billing-service (post arrival folio events), notification-service (in-stay welcome), analytics-service, lock-integration-service (only if key issuance was deferred), sync-service.

3.6 melmastoon.reservation.checked_out.v1

interface ReservationCheckedOutV1 {
reservationId: string;
tenantId: string;
propertyId: string;
checkedOutAt: string;
earlyCheckout: boolean;
overstayedNights: number; // 0 if not overstayed
folio: { folioId: string; finalBalanceMicro: string; currency: string; settled: boolean };
rooms: Array<{ itemId: string; roomId: string }>;
actor: { type: string; id: string };
}

Consumers: housekeeping-service (queue turnover), lock-integration-service (revoke), billing-service (close), notification-service (post-stay survey), analytics-service, audit-service.

3.7 melmastoon.reservation.no_show.v1, early_checkout.v1, overstayed.v1

All three carry { reservationId, tenantId, propertyId, occurredAt, actor, policyApplied?, penaltyMicro?, currency? }. overstayed.v1 is observation-only — the reservation stays in checked_in and billing-service posts an overstay charge.

3.8 melmastoon.reservation.hold_expired.v1

Emitted by the hold-expiry worker.

interface ReservationHoldExpiredV1 {
reservationId: string;
tenantId: string;
propertyId: string;
holdId: string;
expiredAt: string;
inventoryAllocationIds: string[];
paymentIntentId?: string; // for cancellation by payment-gateway-service
}

Consumers: inventory-service (release allocation), payment-gateway-service (cancel pending intent), notification-service (optional "hold expired — re-book?" nudge).

3.9 melmastoon.reservation.special_request.added.v1

interface SpecialRequestAddedV1 {
reservationId: string;
tenantId: string;
requestId: string;
tags: string[]; // taxonomy in DOMAIN_MODEL §1
freeText?: string;
locale: string;
source: 'guest' | 'staff' | 'ai_parser';
occurredAt: string;
actor: { type: string; id: string };
aiProvenance?: AIProvenance; // present when source == ai_parser
}

3.10 melmastoon.reservation.quote.requested.v1 and quote.created.v1

quote.requested.v1 is operational telemetry (drives funnel analytics in analytics-service); quote.created.v1 carries the full priced quote and is consumed by pricing-service for price-history caching.


4. Consumed events (inbox)

The service subscribes to the following subjects via Pub/Sub push to /internal/events/<subject>. Each handler is idempotent on message id (inbox dedupe table) and commutative within an orderingKey.

SubjectEffect
melmastoon.inventory.allocation.committed.v1Mark pendingSagaStep cleared; advance saga to await_payment
melmastoon.inventory.allocation.failed.v1Compensation: cancel held reservation; emit cancelled.v1 with reasonCode inventory_failed; refund pending payment if any
melmastoon.payment.transaction.captured.v1Run ConfirmReservationUseCase (held → confirmed)
melmastoon.payment.transaction.failed.v1Compensation: cancel held reservation; release inventory hold
melmastoon.payment.transaction.refunded.v1Update payment.totalRefundedMicro; emit modified.v1 if mid-stay refund
melmastoon.lock_integration.key_credential.issued.v1Append keyCredentialIds; transition check_in_started → checked_in if pending
melmastoon.lock_integration.key_credential.failed.v1Set requiresManualKey=true; alert via notification-service; allow check-in to complete
melmastoon.tenant.settings.changed.v1Refresh in-memory cache of holdTtlSeconds, cancellationPolicyVersion, noShowGraceHours
melmastoon.property.room.taken_out_of_order.v1Find affected active reservations; trigger room_change sub-saga or alert if no alternative

4.1 Inbox dedupe

Every consumed message is checked against inbox_processed (event_id, processor_name) before its handler runs. Successful handler completion writes the row in the same transaction as the aggregate save. Replay safety: a re-delivered message hits the dedupe row and short-circuits.


5. Schema evolution rules

  • Additive non-breaking changes (new optional fields, new enum values where consumers handle unknowns) do not bump the version.
  • Required-field additions, type changes, removed enum values, or semantic-meaning changes ship a new vN and run side-by-side for ≥ 8 weeks.
  • Subject names are never reused with different semantics. …modified.v1 and …dates_changed.v1 exist as separate subjects precisely to keep semantics scoped.
  • Schema registry CI rejects PRs that drop fields without a major bump or that add required fields to v1.

6. Replay & dead-letter

  • DLQ: every subscription has a per-subject DLQ (melmastoon-reservation-<subject>-dlq). Messages move to DLQ after maxDeliveryAttempts=20 with backoff min=1s, max=600s.
  • Replay: Pub/Sub seek-to-timestamp is supported; the inbox dedupe makes replays safe. A replay runbook exists in OBSERVABILITY §6.
  • Outbox-to-Pub/Sub lag: alarmed if > 30 s p99 (OBSERVABILITY §3).

7. Cross-references