Skip to main content

property-service — Event Schemas

Companion: DOMAIN_MODEL · APPLICATION_LOGIC · 04 Event-Driven Architecture · Naming

All events follow melmastoon.<service>.<aggregate>.<verb_past>.v<n> and ship over GCP Pub/Sub with the transactional outbox pattern. Ordering key is tenant_id|aggregate_id. Schemas live in services/property-service/contracts/events/*.json (JSON Schema draft 2020-12) and are CI-validated against the example payloads in this document.

1. Envelope (shared across every Melmastoon event)

{
"eventId": "evt_01H...",
"eventType": "melmastoon.property.published.v1",
"schemaVersion": 1,
"occurredAt": "2026-04-22T08:30:11.123Z",
"tenantId": "tnt_01H...",
"aggregate": { "type": "property", "id": "ppt_01H..." },
"version": 4,
"producer": { "service": "property-service", "version": "1.7.3" },
"trace": {
"traceId": "00f067aa0ba902b7",
"spanId": "00f067aa0ba902b8",
"causationId": "evt_01H...",
"correlationId":"req_01H..."
},
"data": { /* payload, schema-version-specific */ }
}

Headers (Pub/Sub attributes):

AttributeValue
eventTypesame as JSON eventType
tenantIdsame as JSON tenantId
schemaVersionstring '1'
traceparentW3C-formatted trace context
idempotencyKeysame as eventId

2. Versioning rules

  • Additive changes (new optional field) → bump schemaVersion minor in JSON Schema, same topic version (.v1).
  • Breaking changes (removed field, type change, semantic change) → publish a new version (.v2) on a new topic for ≥ 90 days alongside .v1. Mark .v1 deprecated in contracts/events/DEPRECATIONS.md.
  • All consumers must tolerate unknown additional fields (forward-compatible parsing).

3. Topics & subscriptions

TopicProducersSubscribers (initial)
melmastoon.property.lifecycle.v1property-servicesearch-aggregation, inventory, bff-tenant-booking, bff-backoffice, analytics
melmastoon.property.rooms.v1property-serviceinventory, housekeeping, lock-integration, search-aggregation, bff-backoffice
melmastoon.property.media.v1property-servicesearch-aggregation, bff-tenant-booking
melmastoon.property.policy.v1property-servicepricing, reservation, bff-tenant-booking
melmastoon.property.amenity.v1property-servicesearch-aggregation, bff-consumer
melmastoon.property.room_groups.v1property-servicehousekeeping, bff-backoffice

DLQ per subscription with 5 delivery attempts, exponential backoff (1s..60s).

4. Property lifecycle events

4.1 melmastoon.property.created.v1

{
"data": {
"id": "ppt_01H...",
"slug": "ghasi-house-kabul",
"name": { "default": "en", "values": { "en": "Ghasi House Kabul", "ps": "غاسي ښارگوټی کابل" } },
"description": { "default": "en", "values": { "en": "Boutique guesthouse." } },
"address": { "line1": "House 12, Wazir Akbar Khan", "city": "Kabul", "countryIso2": "AF" },
"geo": { "lat": 34.5328, "lng": 69.1727, "source": "manual" },
"timezone": "Asia/Kabul",
"starRating": 3,
"status": "draft",
"enabledLocales": ["en", "ps", "fa"],
"defaultLocale": "en"
}
}

4.2 melmastoon.property.updated.v1

{
"data": {
"id": "ppt_01H...",
"changedFields": ["name", "starRating"],
"current": {
"name": { "default": "en", "values": { "en": "Ghasi House Kabul (Renovated)" } },
"starRating": 4
}
}
}

4.3 melmastoon.property.published.v1

{
"data": {
"id": "ppt_01H...",
"publishedAt": "2026-04-22T08:30:11Z",
"geo": { "lat": 34.5328, "lng": 69.1727 },
"defaultLocale": "en",
"amenities": ["wifi", "halal_kitchen", "prayer_room", "generator_backup"]
}
}

4.4 melmastoon.property.unpublished.v1

{
"data": {
"id": "ppt_01H...",
"unpublishedAt": "2026-04-22T09:00:00Z",
"reason": "tenant_request",
"note": "Closed for renovation"
}
}

4.5 melmastoon.property.deleted.v1 (soft delete / archive)

{
"data": {
"id": "ppt_01H...",
"archivedAt": "2026-04-22T10:00:00Z",
"cascade": { "rooms": 30, "roomTypes": 5, "photos": 12, "policies": 1, "roomGroups": 4 }
}
}

5. Room type events (melmastoon.property.rooms.v1)

5.1 melmastoon.property.room_type.created.v1

{
"data": {
"id": "rmt_01H...",
"propertyId": "ppt_01H...",
"code": "FAMILY_4",
"name": { "default": "en", "values": { "en": "Family Room (4)" } },
"bedConfig": { "primary": "king", "extras": ["single", "single"] },
"maxOccupancy": 4,
"amenities": ["wifi", "air_conditioning", "halal_kitchen", "family_room"]
}
}

5.2 melmastoon.property.room_type.updated.v1

{
"data": {
"id": "rmt_01H...",
"propertyId": "ppt_01H...",
"changedFields": ["maxOccupancy", "amenities"],
"current": { "maxOccupancy": 5, "amenities": ["wifi", "air_conditioning", "halal_kitchen", "family_room", "non_smoking"] }
}
}

5.3 melmastoon.property.room_type.archived.v1

{
"data": { "id": "rmt_01H...", "propertyId": "ppt_01H...", "archivedAt": "2026-04-22T11:00:00Z" }
}

6. Room events (melmastoon.property.rooms.v1)

6.1 melmastoon.property.room.created.v1

{
"data": {
"id": "rmu_01H...",
"propertyId": "ppt_01H...",
"roomTypeId": "rmt_01H...",
"number": "301",
"floor": 3,
"roomGroupId": "rgp_01H...",
"features": ["balcony", "city_view"],
"accessibility": { "wheelchair": false, "rollInShower": false, "hearingAids": false, "visualAids": false }
}
}

6.2 melmastoon.property.room.updated.v1

{
"data": {
"id": "rmu_01H...",
"propertyId": "ppt_01H...",
"changedFields": ["features", "roomGroupId"],
"current": { "features": ["balcony", "city_view", "kitchenette"], "roomGroupId": "rgp_01H..." }
}
}

6.3 melmastoon.property.room.taken_out_of_order.v1

{
"data": {
"id": "rmu_01H...",
"propertyId": "ppt_01H...",
"reason": "maintenance",
"until": "2026-05-01T00:00:00Z",
"originatedBy": { "kind": "event", "eventId": "evt_01H...", "eventType": "melmastoon.housekeeping.room.maintenance_required.v1" }
}
}

originatedBy.kind ∈ { 'user', 'event', 'system' }.

6.4 melmastoon.property.room.returned_to_service.v1

{
"data": {
"id": "rmu_01H...",
"propertyId": "ppt_01H...",
"at": "2026-04-30T08:00:00Z",
"originatedBy": { "kind": "event", "eventId": "evt_01H...", "eventType": "melmastoon.maintenance.work_order.completed.v1" }
}
}

6.5 melmastoon.property.room.archived.v1

{
"data": { "id": "rmu_01H...", "propertyId": "ppt_01H...", "archivedAt": "2026-04-22T13:00:00Z" }
}

7. Amenity events (melmastoon.property.amenity.v1)

7.1 melmastoon.property.amenity_set.updated.v1

{
"data": {
"propertyId": "ppt_01H...",
"added": ["prayer_room", "hot_water_24h"],
"removed": ["non_smoking"],
"current": ["wifi", "halal_kitchen", "prayer_room", "generator_backup", "hot_water_24h"]
}
}

8. Policy events (melmastoon.property.policy.v1)

8.1 melmastoon.property.policy.updated.v1

{
"data": {
"propertyId": "ppt_01H...",
"changedKinds": ["check_in_time", "cancellation"],
"current": [
{ "kind": "check_in_time", "value": "14:00" },
{ "kind": "cancellation", "value": { "freeUntilHoursBeforeArrival": 24, "lateFeeMicro": "5000000", "currency": "AFN" } }
]
}
}

9. Photo events (melmastoon.property.media.v1)

9.1 melmastoon.property.photo.added.v1

{
"data": {
"photoId": "pht_01H...",
"scope": { "kind": "property", "propertyId": "ppt_01H..." },
"mediaKey": "tenants/tnt_.../properties/ppt_.../photos/2026/04/22/01H...jpg",
"order": 0,
"altText": { "default": "en", "values": { "en": "Lobby at sunset" } }
}
}

9.2 melmastoon.property.photo.removed.v1

{
"data": {
"photoId": "pht_01H...",
"scope": { "kind": "property", "propertyId": "ppt_01H..." },
"reason": "operator_delete"
}
}

reason ∈ { 'operator_delete', 'virus_scan_failed', 'cascade_archive' }.

9.3 melmastoon.property.photo.order_changed.v1

{
"data": {
"scope": { "kind": "property", "propertyId": "ppt_01H..." },
"order": [
{ "photoId": "pht_01H...", "position": 0 },
{ "photoId": "pht_01H...", "position": 1 },
{ "photoId": "pht_01H...", "position": 2 }
]
}
}

10. Room group events (melmastoon.property.room_groups.v1)

10.1 melmastoon.property.room_group.changed.v1

{
"data": {
"id": "rgp_01H...",
"propertyId": "ppt_01H...",
"kind": "floor",
"ordinal": 3,
"label": { "default": "en", "values": { "en": "Floor 3" } },
"archived": false
}
}

11. Consumed events (with handlers)

EventHandlerSide effect
melmastoon.tenant.created.v1TenantContextWarmernone persistent; allow property creation for tenant
melmastoon.tenant.deleted.v1TenantArchiverunpublish + soft-archive every property/room/photo of tenant
melmastoon.housekeeping.room.maintenance_required.v1RoomAutoOOOOOO with reason housekeeping
melmastoon.maintenance.work_order.completed.v1RoomAutoRTSRTS if OOO with reason maintenance
melmastoon.file_storage.media.scanned.v1PhotoReadinessFlipperuploaded → ready (or archive on infected)
melmastoon.lock_integration.device.paired.v1RoomLockBinderbind lockDeviceId
melmastoon.lock_integration.device.unpaired.v1RoomLockBinderclear lockDeviceId

12. Validation example (CI test)

Each event has at least:

  • valid.json (passes schema),
  • invalid-missing-field.json,
  • invalid-type.json,
  • valid-edge-rtl.json (Pashto/Dari multi-language string fixture).

CI runs pnpm contracts:validate, which loads every JSON file in contracts/events/fixtures/**/*.json and validates against the matching schema; the build fails on any mismatch.

13. Replay & DLQ runbook

  • Subscriptions use ack-deadline=120s, min-backoff=1s, max-backoff=60s, max-delivery-attempts=5 → DLQ topic melmastoon.dlq.<topic>.
  • Replay tool pnpm tools:replay --topic melmastoon.property.rooms.v1 --since '2026-04-22T00:00:00Z' --tenant tnt_… reads from the replay store (BigQuery archive) and republishes with replay=true Pub/Sub attribute. Consumers idempotent via inbox.