Events
:::info Source
Sourced from services/marketplace-service/EVENT_SCHEMAS.md in the documentation repo.
:::
Companion: 04 Event-Driven Architecture · APPLICATION_LOGIC
1. Envelope (F01, frozen)
All events conform to the platform EventEnvelope:
{
"eventId": "evt_01J...",
"type": "marketplace.order.placed.v1",
"specversion": "1.0",
"source": "marketplace-service",
"time": "2026-04-15T12:34:56.789Z",
"tenantId": "ten_01J...",
"correlationId": "sga_01J...",
"causationId": "evt_01J...",
"idempotencyKey": "ord_01J...",
"version": 1,
"data": { ... },
"trace": { "traceparent": "...", "tracestate": "..." }
}
All events are JSON-encoded, UTF-8, compressed snappy on the NATS stream. Schema versioning is additive only within a major version; breaking changes require a new .vN suffix and 90-day dual-publish.
2. Streams & Subjects
NATS JetStream stream: MARKETPLACE_EVENTS.
| Subject | Retention | Max bytes | Consumers |
|---|---|---|---|
marketplace.listing.* | 30d | 10 GiB | search-service, catalog-service, analytics-service |
marketplace.order.* | 365d (legal) | 50 GiB | notification-service, enrollment-service, analytics-service, billing-service |
marketplace.license.* | 7y (finance) | 100 GiB | enrollment-service, sync-service, analytics-service |
marketplace.coupon.* | 90d | 5 GiB | analytics-service |
marketplace.payout.* | 7y (finance) | 10 GiB | billing-service, analytics-service |
marketplace.saga.* | 30d | 5 GiB | support / observability only |
3. Published Events
3.1 marketplace.listing.submitted.v1
Producer: UC-03. Consumer: analytics, admin-notification.
Ghasi-EdTech implementation (2026-04): payload includes listingId, courseId, state (submitted), and aiReview (stub or AI-gateway snapshot JSON).
{
"listingId": "lst_01J...",
"providerTenantId": "ten_01J...",
"courseId": "crs_01J...",
"courseVersionId": "crv_01J...",
"submittedAt": "2026-04-15T12:34:56Z",
"pricingPlanCount": 2
}
3.1a marketplace.plan.created.v1 (EP-6 US-28)
Ghasi-EdTech implementation: emitted when POST /marketplace/listings/{id}/plans appends a validated plan on a draft listing.
{
"listingId": "lst_...",
"plan": {
"currency": "USD",
"amountMicros": 4900000,
"kind": "one_time",
"interval": "month",
"seatCount": 10,
"active": true
}
}
(interval / seatCount only when applicable per plan kind.)
3.2 marketplace.listing.approved.v1
Producer: UC-04. Consumer: search-service (index), catalog-service.
{
"listingId": "lst_...",
"providerTenantId": "ten_...",
"courseId": "crs_...",
"courseVersionId": "crv_...",
"approvedAt": "...",
"approvedBy": "usr_admin_...",
"marketing": { "tagline": "...", "hero": "...", "screenshots": [] },
"pricingPlans": [ { "id": "pln_...", "kind": "one_time", "price": { "amount": 4900, "currency": "USD" } } ]
}
Ghasi-EdTech implementation note: human approve transitions listing to state live (not a separate approved state); payload includes listingId, courseId, state: "live".
3.2a marketplace.listing.rejected.v1 (EP-6 US-27)
Ghasi-EdTech implementation: platform approver POST …/reject with rationale.
{
"listingId": "lst_...",
"courseId": "crs_...",
"state": "rejected",
"rationale": "Policy violation: …"
}
3.3 marketplace.listing.suspended.v1
{ "listingId": "lst_...", "reason": "tos_violation", "suspendedAt": "...", "suspendedBy": "usr_admin_..." }
3.4 marketplace.listing.retired.v1
{ "listingId": "lst_...", "retiredAt": "...", "retiredBy": "usr_..." }
3.5 marketplace.order.placed.v1
Producer: UC-09. Consumer: billing, analytics.
Ghasi-EdTech EP-6 slice: first emission is often from billing-service outbox on POST /commerce/orders (payload is a slim aggregate: orderId, listingId, courseId, buyerTenantId, providerTenantId, totalAmountMicros, currency). Full multi-line cart shape below remains the target canonical schema.
{
"orderId": "ord_...",
"sagaId": "sga_...",
"buyerTenantId": "ten_...",
"buyerUserId": "usr_...",
"currency": "USD",
"lines": [
{ "lineId": "oln_...", "listingId": "lst_...", "pricingPlanId": "pln_...", "courseId": "crs_...", "courseVersionId": "crv_...", "quantity": 1, "unitPrice": { "amount": 4900, "currency": "USD" } }
],
"subtotal": { "amount": 4900, "currency": "USD" },
"discountTotal": { "amount": 0, "currency": "USD" },
"appliedCoupons": [],
"placedAt": "..."
}
3.6 marketplace.order.fulfilled.v1
Producer: UC-13. Consumer: notification-service (receipts), analytics.
{
"orderId": "ord_...",
"sagaId": "sga_...",
"buyerTenantId": "ten_...",
"buyerUserId": "usr_...",
"licenseIds": ["lic_...", "lic_..."],
"totals": { "amount": 4900, "currency": "USD" },
"fulfilledAt": "..."
}
3.7 marketplace.order.failed.v1
Producer: UC-11, UC-25. Consumer: notification-service, analytics.
{
"orderId": "ord_...",
"sagaId": "sga_...",
"reason": "payment_failed" ,
"failureCode": "card_declined",
"failureMessage": "Your card was declined.",
"failedAt": "..."
}
Allowed reason values: payment_failed, payment_timeout, licensing_failed, enrollment_failed, compensation_complete.
3.8 marketplace.order.refunded.v1
Producer: UC-15. Consumer: notification, analytics, billing reconciliation.
{
"orderId": "ord_...",
"refundedAmount": { "amount": 4900, "currency": "USD" },
"reason": "duplicate_purchase",
"initiatedBy": "usr_...",
"refundedAt": "..."
}
3.9 marketplace.license.granted.v1
Producer: UC-12. Consumer: enrollment-service, sync-service, analytics.
Ghasi-EdTech EP-6: enrollment-service publishes this to outbox after from-paid-order (individual / subscription) or after license row creation for seat-pack / site-license; payload includes licenseId, orderId, courseId, listingId, buyerUserId, optional seatTotal.
{
"licenseId": "lic_...",
"orderId": "ord_...",
"tenantId": "ten_...",
"providerTenantId": "ten_...",
"listingId": "lst_...",
"courseId": "crs_...",
"courseVersionId": "crv_...",
"pricingPlanKind": "one_time",
"scope": "individual",
"seats": 1,
"validFrom": "...",
"validUntil": null,
"perpetualOfflineAccess": true,
"source": "purchase"
}
3.10 marketplace.license.seat_assigned.v1
Producer: UC-16. Consumer: enrollment-service.
Ghasi-EdTech EP-6 wire name: marketplace.license.assigned.v1 (seat assignment from org admin).
{
"licenseId": "lcn_...",
"assigneeUserId": "usr_...",
"seatAssignmentId": "ssa_...",
"orderId": "ord_..."
}
3.11 marketplace.license.seat_released.v1
Producer: UC-17. Consumer: enrollment-service.
3.12 marketplace.license.revoked.v1
Producer: UC-15, UC-18. Consumer: enrollment-service, sync-service.
{ "licenseId": "lic_...", "reason": "refund", "revokedAt": "...", "revokedBy": "usr_..." }
3.13 marketplace.license.expired.v1
Producer: scheduler (license-expiry-sweep).
3.14 marketplace.payout.initiated.v1
{ "providerTenantId": "ten_...", "periodMonth": "2026-03", "amount": { "amount": 1015000, "currency": "USD" }, "initiatedAt": "..." }
3.15 marketplace.payout.completed.v1
Producer: on consume of billing.payout.completed.v1; re-emitted in marketplace namespace.
3.16 marketplace.coupon.redeemed.v1
{ "couponId": "cpn_...", "code": "LAUNCH25", "orderId": "ord_...", "discount": { "amount": 1225, "currency": "USD" } }
Ghasi-EdTech EP-6: billing-service outbox emits marketplace.coupon.redeemed.v1 with payload { "orderId", "couponId", "discountMicros" } (integer micro-units; no separate code field on wire).
4. Consumed Events
| Event | Source | Handler | Idempotency |
|---|---|---|---|
billing.payment.succeeded.v1 | billing | UC-10 | eventId + processed_events |
billing.payment.failed.v1 | billing | UC-11 | same |
billing.payment.refunded.v1 | billing | reconcile refund state | same |
enrollment.created.v1 | enrollment | UC-13 | eventId |
content.play_package.built.v1 | content | update cached shippable flag | eventId |
catalog.course_version.published.v1 | catalog | update cached published flag | eventId |
gdpr.subject_request.received.v1 | cross-cutting | UC-24 | subjectId |
5. Schema Registry
- Schemas stored at
schemas/marketplace/*.json(JSON Schema Draft 2020-12). - CI validates every emitted payload against its schema in integration tests.
- Backward-compat check: new additive fields are OK; removing/renaming requires a version bump.
- Generated TS types in
@ghasi/eventspackage for all services.
6. Ordering & Delivery Guarantees
| Guarantee | Strategy |
|---|---|
| At-least-once delivery | NATS JetStream AckExplicit, consumer processed_events dedupe |
| Per-order ordering | Subject-key partitioning by orderId; consumer uses OrderedConsumer |
| Saga correlation | correlationId = sagaId across all marketplace + billing + enrollment events in a saga |
| Causation tracking | causationId = triggering eventId |
7. Retry & DLQ
- Transient handler failures: retry with backoff 1s → 2s → 4s → … up to 7 attempts (max ~2min).
- Permanent failures: push to subject
dlq.marketplace.{original_subject}with error metadata; page on-call. - DLQ replay tool:
pnpm marketplace:dlq:replay --subject <x> --filter '...'.
8. PII & Retention
| Event | PII | Retention |
|---|---|---|
| order.* | buyerUserId, billing name/email | 7y (finance) |
| license.* | userId in seat allocations | Until license expiry + 7y |
| coupon.* | none | 90d |
| listing.* | none (marketing only) | 30d |
After retention, events are archived to encrypted cold storage. PII fields are hashed with tenant-scoped salt before archival.
9. Contract Tests
Every consumer MUST have a Pact or contract test that:
- Asserts the schema shape it expects.
- Asserts the fields it relies on (subset).
- Is re-run on every producer PR in marketplace-service.