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):
| Attribute | Value |
|---|---|
eventType | same as JSON eventType |
tenantId | same as JSON tenantId |
schemaVersion | string '1' |
traceparent | W3C-formatted trace context |
idempotencyKey | same as eventId |
2. Versioning rules
- Additive changes (new optional field) → bump
schemaVersionminor 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.v1deprecated incontracts/events/DEPRECATIONS.md. - All consumers must tolerate unknown additional fields (forward-compatible parsing).
3. Topics & subscriptions
| Topic | Producers | Subscribers (initial) |
|---|---|---|
melmastoon.property.lifecycle.v1 | property-service | search-aggregation, inventory, bff-tenant-booking, bff-backoffice, analytics |
melmastoon.property.rooms.v1 | property-service | inventory, housekeeping, lock-integration, search-aggregation, bff-backoffice |
melmastoon.property.media.v1 | property-service | search-aggregation, bff-tenant-booking |
melmastoon.property.policy.v1 | property-service | pricing, reservation, bff-tenant-booking |
melmastoon.property.amenity.v1 | property-service | search-aggregation, bff-consumer |
melmastoon.property.room_groups.v1 | property-service | housekeeping, 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)
| Event | Handler | Side effect |
|---|---|---|
melmastoon.tenant.created.v1 | TenantContextWarmer | none persistent; allow property creation for tenant |
melmastoon.tenant.deleted.v1 | TenantArchiver | unpublish + soft-archive every property/room/photo of tenant |
melmastoon.housekeeping.room.maintenance_required.v1 | RoomAutoOOO | OOO with reason housekeeping |
melmastoon.maintenance.work_order.completed.v1 | RoomAutoRTS | RTS if OOO with reason maintenance |
melmastoon.file_storage.media.scanned.v1 | PhotoReadinessFlipper | uploaded → ready (or archive on infected) |
melmastoon.lock_integration.device.paired.v1 | RoomLockBinder | bind lockDeviceId |
melmastoon.lock_integration.device.unpaired.v1 | RoomLockBinder | clear 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 topicmelmastoon.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 withreplay=truePub/Sub attribute. Consumers idempotent via inbox.