Event Catalog¶
Telco CRM Platform¶
| Field | Value |
|---|---|
| Document | Domain Event Catalog |
| Version | 1.0 |
| Parent | ../product/BRD.md |
| Technical authority | ADR-009 (event-driven architecture), ADR-019 (event contract and schema governance) |
| Last updated | 2026-07-18 |
All events are immutable, versioned (domain.event.v1), Avro-encoded, and registered in the
Schema Registry. Events are published through the transactional outbox and consumed
idempotently via the inbox pattern (ADR-005). Topic names follow domain.event with the
version carried by the schema. Debezium routes on the outbox aggregate_type column via
${routedByValue}.events; therefore aggregate_type MUST be the lowercase domain (e.g.
subscription), producing the subscription.events topic consumers subscribe to. A PascalCase
aggregate_type routes to the wrong topic and is silently never delivered.
1. Event Naming and Versioning Rules¶
- Format:
domain.event.v1(for examplecustomer.registered.v1). - Events MUST be immutable and backward compatible (ADR-019).
- A breaking change requires a new version (
.v2); old versions remain until consumers migrate. - Producers MUST publish via outbox; consumers MUST deduplicate via inbox.
2. Event Registry¶
| Event | Producer | Consumers | Purpose |
|---|---|---|---|
customer.registered.v1 |
customer-service | notification | New customer created. |
user.created.v1 |
identity-service | - | New identity user provisioned (Keycloak-backed). No current consumer; registered for future audit/notification integration. |
user.deleted.v1 |
identity-service | - | Identity user deleted/deactivated. No current consumer; registered for future audit/notification integration. |
customer.kyc-approved.v1 |
customer-service | notification, order | KYC approved; customer becomes ACTIVE. |
customer.kyc-rejected.v1 |
customer-service | notification | KYC rejected. |
customer.updated.v1 |
customer-service | notification | Customer profile changed. |
tariff.created.v1 |
product-catalog-service | notification, campaign | New tariff published. campaign-service consumes defensively (Feature 21.4.3, ADR-027 Section 4) to log when a tariff code referenced by an ACTIVE campaign is (re)created. |
tariff.price-changed.v1 |
product-catalog-service | billing, notification, campaign | Tariff price updated (versioned). campaign-service consumes defensively (Feature 21.4.3, ADR-027 Section 4) to flag an ACTIVE campaign whose applicable_tariff_codes references the repriced tariff - never to mirror pricing data. |
order.created.v1 |
order-service | payment, notification, campaign, subscription | Order placed; starts the saga. campaign-service consumes it (Feature 21.4.3) to create a RESERVED CampaignRedemption row per campaign-priced order item, keyed by (campaignId, customerId, orderId). Carries nullable orderType/subscriptionId plus item-level addonCode/addonType/tariffCode/currency (added 2026-07-20, FR-09): payment charges only NEW_LINE; subscription-service provisions PLAN_CHANGE/ADDON orders directly from this event. |
order.confirmed.v1 |
order-service | subscription, notification | Order confirmed for fulfillment. (deferred; not produced in the MVP - subscription activates on payment.completed.v1) |
order.cancelled.v1 |
order-service | payment, subscription, notification, campaign | Order cancelled; triggers compensation. campaign-service consumes it (Feature 21.4.2) to release a RESERVED CampaignRedemption back to available. |
payment.completed.v1 |
payment-service | order, subscription, billing, notification, campaign | Payment succeeded. campaign-service consumes it (Feature 21.4.2, ADR-027 Section 4 ratification) as the "order is real" trigger, transitioning the matching CampaignRedemption RESERVED -> CONFIRMED. |
payment.failed.v1 |
payment-service | order, subscription, notification | Payment failed; may trigger retry. |
payment.refunded.v1 |
payment-service | order, notification | Refund issued (compensation). |
msisdn.allocated.v1 |
subscription-service | notification | MSISDN allocated to a subscription. |
msisdn.released.v1 |
subscription-service | fraud | MSISDN returned to the pool. Carries a nullable customerId (added 2026-07-17, ADR-029 Amendment 1) so fraud-service's MSISDN_CHURN_VELOCITY rule can key release events on the owning customer. fraud-service consumer wiring lands in Feature 23.2. |
subscription.activated.v1 |
subscription-service | order, billing, notification | Subscription activated. |
subscription.suspended.v1 |
subscription-service | billing, notification | Subscription suspended (non-payment). |
subscription.terminated.v1 |
subscription-service | billing, notification | Subscription terminated. |
subscription.activation-failed.v1 |
subscription-service | payment, order, notification | Subscription activation failed; triggers saga compensation. |
subscription.tariff-changed.v1 |
subscription-service | billing, order | Tariff changed via a PLAN_CHANGE order (FR-09, added 2026-07-20). billing reprices the next cycle; order fulfils the plan-change order. |
subscription.addon-attached.v1 |
subscription-service | billing, order | Addon attached via an ADDON order (FR-09, added 2026-07-20). billing invoices the fee as an ADDON/VAS line on the next bill-run (FR-22); order fulfils the addon order. |
usage.recorded.v1 |
usage-service | - | Usage applied to quota. |
quota.threshold-reached.v1 |
usage-service | notification | 80% usage threshold reached. |
quota.exceeded.v1 |
usage-service | billing, notification | 100% usage reached; overage begins. |
usage.aggregated.v1 |
usage-service | billing | Period usage aggregated for billing. |
invoice.generated.v1 |
billing-service | notification | Invoice created and PDF rendered. Not consumed by payment-service in the MVP - paying an invoice is a customer/admin-initiated POST /api/v1/payments call (Section 14.2), not an auto-pay reaction to this event. |
invoice.paid.v1 |
billing-service | notification | Invoice settled. |
invoice.overdue.v1 |
billing-service | notification, ticket | Invoice overdue. |
ticket.opened.v1 |
ticket-service | notification | Ticket created. |
ticket.assigned.v1 |
ticket-service | notification | Ticket assigned to a team/agent. |
ticket.resolved.v1 |
ticket-service | notification | Ticket resolved. |
ticket.sla-breached.v1 |
ticket-service | notification | SLA breached for a ticket. |
notification.dispatched.v1 |
notification-service | - | Notification sent on a channel. |
campaign.created.v1 |
campaign-service | - | Campaign created in DRAFT status (Feature 21.4.1). No current consumer; registered for future notification/reporting integration. |
campaign.activated.v1 |
campaign-service | - | Campaign transitioned DRAFT/PAUSED -> ACTIVE (Feature 21.4.1). No current consumer; registered for future notification/reporting integration. |
campaign.paused.v1 |
campaign-service | - | Campaign transitioned ACTIVE -> PAUSED (Feature 21.4.1). No current consumer; registered for future notification/reporting integration. |
campaign.expired.v1 |
campaign-service | - | Campaign transitioned ACTIVE/PAUSED -> EXPIRED, explicitly or defensively when validTo has passed (Feature 21.4.1). No current consumer; registered for future notification/reporting integration. |
campaign.cancelled.v1 |
campaign-service | - | Campaign transitioned any non-terminal status -> CANCELLED (Feature 21.4.1). No current consumer; registered for future notification/reporting integration. |
dispute.opened.v1 |
dispute-service | billing, payment, ticket, notification | Dispute opened; moves straight to UNDER_REVIEW. The only signal a provisional hold ever travels on (ADR-028 Section 5) - never a financial instruction. |
dispute.evidence-submitted.v1 |
dispute-service | - | Evidence attached to a dispute under review. No current consumer; registered for future audit/notification integration. |
dispute.resolved-customer.v1 |
dispute-service | billing, payment, notification | Dispute resolved in the customer's favor - the only dispute event a consumer may treat as authorization for a real credit/refund (ADR-028 Section 5). |
dispute.resolved-merchant.v1 |
dispute-service | billing, payment, notification | Dispute resolved in the merchant's favor - always a no-financial-change hold release, by contract. |
dispute.withdrawn.v1 |
dispute-service | notification | Customer withdrew the dispute. |
dispute.closed.v1 |
dispute-service | notification | Dispute reached its terminal CLOSED state. |
fraud.signal-raised.v1 |
fraud-service | - | Raised on every fraud-rule hit (RAPID_SIM_SWAP / MSISDN_CHURN_VELOCITY / SUSPEND_REACTIVATE_VELOCITY), informational (ADR-029 Section 5). Producer publish sites land in Feature 23.2; no consumer in this phase. |
fraud.case-opened.v1 |
fraud-service | ticket, notification | Emitted when related signals escalate into an actionable FraudCase (ADR-029 Section 5). ticket-service auto-opens a linked review ticket and notification-service raises an internal ops/security alert - both consumers are informational/agent-facing only (no automated suspension), wired in Feature 23.4. |
fraud.case-resolved.v1 |
fraud-service | - | Emitted when a FraudCase reaches a terminal CONFIRMED/DISMISSED outcome (ADR-029 Section 5). Producer publish site lands in Feature 23.3; no consumer in this phase. |
3. Saga: New-Line Order (event sequence)¶
order.created.v1
-> payment.completed.v1
-> subscription.activated.v1
-> order (FULFILLED), notification (welcome SMS)
Compensation on activation failure:
subscription.activation-failed.v1
-> payment.refunded.v1
-> order.cancelled.v1
4. Saga: Dispute Resolution (event sequence)¶
dispute.opened.v1
-> billing (Invoice.disputeStatus = ON_HOLD, excluded from dunning)
-> payment (Payment.disputed = true, excluded from retry/expiry)
-> ticket (auto-opens a DISPUTE-category ticket, existing SLA machinery)
dispute.evidence-submitted.v1 (zero or more, loops back to UNDER_REVIEW)
Resolution branch A - upheld:
dispute.resolved-customer.v1
-> billing (unpaid invoice: real ADJUSTMENT InvoiceLine, grandTotal reduced), OR
-> payment (already-paid: real refund via the existing RefundPaymentCommand)
[exactly one of the two acts, never both - selected by invoice/payment paid state]
-> dispute.closed.v1
Resolution branch B - rejected:
dispute.resolved-merchant.v1
-> billing (hold cleared, no financial change)
-> payment (disputed flag cleared, no financial change)
-> dispute.closed.v1
Withdrawal (either OPENED or UNDER_REVIEW):
dispute.withdrawn.v1
-> dispute.closed.v1
5. Schema Governance¶
- Each event has an Avro schema registered before first publish (ADR-019).
- Compatibility mode: backward (consumers can read older producer schemas).
- Schemas are versioned alongside the producing service.
- Consumers MUST tolerate unknown optional fields.
6. Schema Evolution Log¶
Additive, backward-compatible field changes (ADR-019). Each entry is a new nullable/optional field only; no field was renamed, removed, or retyped.
| Date | Event | Field added | Reason |
|---|---|---|---|
| 2026-07-04 | payment.completed.v1, payment.failed.v1 |
invoiceId (nullable string) |
Carries the invoice being settled so billing-service's PaymentCompletedBillingConsumer can mark the invoice paid when a customer pays via POST /api/v1/payments with an invoiceId (Section 14.2). Null for order-only charges. |
| 2026-07-04 | quota.threshold-reached.v1, quota.exceeded.v1 |
customerId (nullable string) |
Lets notification-service route the 80%/100% quota SMS to the real customer instead of falling back to the literal unknown. Resolved by usage-service from the Quota aggregate's locally stored customer_id (set at provisioning time from subscription.activated.v1). Null only for events emitted before this field existed (rolling-upgrade compatibility). |
| 2026-07-07 | customer.registered.v1 |
registeredByUserId (nullable string) |
Carries the Keycloak subject of the caller for genuine self-service registration, so identity-service's new inbox consumer can upsert users.customer_id and close the identity-to-customer linkage gap (Section 14.1.1 ruling). Null for agent/dealer-assisted registration - those customers stay unlinked until a future "claim my account" flow. |
| 2026-07-17 | msisdn.released.v1 |
customerId (nullable string) |
Lets fraud-service's MSISDN_CHURN_VELOCITY rule (ADR-029 Amendment 1) key allocate/release cycles on the owning customer. Populated by subscription-service's TerminateSubscriptionCommandHandler from subscription.getCustomerId() (already in scope). Null only for events emitted before this field existed; fraud-service falls back to the most recent prior MSISDN_ALLOCATED signal for the same MSISDN (ADR-029 Section 4). |
7. Schema Governance Reconciliation Log (Feature 14.5)¶
Tracking reference: docs/tasks/sprint-14-testing-and-hardening/14.5-avro-schema-governance-ruling.md
(tech-lead ruling, ADR-019 Amendment (2026-07-07)). Unlike Section 5 above (additive field changes to
already-registered schemas), this log records the one-time reconciliation of the canonical schema
directory (platform/platform-event-contracts/src/main/avro/) against the real, currently-shipping
event payloads, closing a gap where the .avsc files had never been cross-checked against the code
that actually publishes each event.
| Date | Change type | Detail |
|---|---|---|
| 2026-07-07 | Reconciled (type/shape fix, no behavior change) | 7 pre-existing canonical schemas corrected to match the real, already-shipping payload: order-created.avsc (added items array of the nested OrderItemPayload record, added idempotencyKey, removed currency, renamed/retyped createdAt(long) to occurredAt(string)); payment-completed.avsc (added customerId, removed currency, renamed/retyped completedAt(long) to occurredAt(string)); cdr-recorded.avsc (retyped occurredAt from long/timestamp-millis to string); usage-aggregated.avsc (retyped periodStart, periodEnd, aggregatedAt from long to string); usage-recorded.avsc (retyped recordedAt from long to string); quota-exceeded.avsc (retyped exceededAt from long to string); quota-threshold-reached.avsc (retyped reachedAt from long to string). The nested OrderItemPayload type required for order-created.avsc's items field is defined inline within order-created.avsc itself (not an independent Schema Registry subject - see tracking doc's tech-lead ruling on the registry-parsing conflict). |
| 2026-07-07 | Added (new canonical schema, closing a previously-unregistered gap) | 14 event types that were already being published in production code but had no canonical .avsc file at all: order.cancelled.v1, payment.failed.v1, payment.refunded.v1, tariff.created.v1, tariff.price-changed.v1, ticket.opened.v1, ticket.assigned.v1, ticket.resolved.v1, ticket.sla-breached.v1, invoice.paid.v1, invoice.overdue.v1, notification.dispatched.v1, user.created.v1, user.deleted.v1. All 14 are now registered in the Schema Registry under the standard backward-compatibility mode. See Section 2 above for the two identity-service rows this addition required in the event registry table. |
| 2026-07-18 | Added (new canonical schema, Sprint 22 Feature 22.6.1, ADR-028) | 6 new event types for the new dispute-service: dispute.opened.v1, dispute.evidence-submitted.v1, dispute.resolved-customer.v1, dispute.resolved-merchant.v1, dispute.withdrawn.v1, dispute.closed.v1. aggregate_id = disputeId for all six (load-bearing for per-dispute Kafka ordering, ADR-028 Section 6). A new DisputeEventSchemaCompatTest in dispute-service verifies all six field-for-field against these schemas (Docker-free, live-run this session, all 6 green). See Section 2 above for the new event-registry rows and Section 4 for the new Dispute Resolution saga sequence. |
| 2026-07-07 | Renamed (naming-convention fix, no shape change) | EventEnvelope.avsc -> event-envelope.avsc, closing a kebab-case-filename convention violation (ADR-019 Amendment, point A5). The Avro record's own name field is unchanged (EventEnvelope, PascalCase, drives Java class generation) - only the filename changed, along with the corresponding avro-maven-plugin property in platform/platform-event-contracts/pom.xml. |
| 2026-07-07 | Tooling (process change, going forward) | Added AvroContractAssertions, a shared, type-and-nullability-aware compatibility checker (platform/platform-event-contracts/src/test/java/com/telco/platform/events/testsupport/AvroContractAssertions.java, shipped as this module's test-jar). Every one of the 32 canonical schemas (18 reconciled/unchanged + 14 newly added above) now has a per-service *EventSchemaCompatTest/*EventContractTest that loads the schema directly from the canonical platform-event-contracts module (not a hand-maintained local copy) and asserts field name, type, and nullability against the real Java payload class or captured runtime payload. Going forward, any producing service whose payload class drifts from its canonical schema (field removed/renamed/retyped, or a nullability mismatch) fails that service's build - this is now a required, standing quality gate for every service that publishes a domain event, not a one-time manual reconciliation. |
Document end.