order-service - API Contract¶
| Field | Value |
|---|---|
| Port | 9004 |
| Mode | Domain Orchestration (saga) |
| Base path | /api/v1 |
| Owning sprint | Sprint 08 (built), Sprint 09 (saga) |
| Build status | TODO |
| Requirements | FR-09, FR-10, FR-11, FR-12 |
Bounded context: order orchestration. Coordinates customer -> catalog -> payment -> subscription via the onboarding saga with compensation.
Authentication and Authorization¶
All endpoints require a valid JWT.
Endpoints¶
| Method | Path | Auth | Idempotency | Summary |
|---|---|---|---|---|
| POST | /api/v1/orders |
JWT | mandatory | Place an order (PENDING_PAYMENT); validates customer + price snapshot. |
| GET | /api/v1/orders/{id} |
JWT | - | Fetch an order with saga/status. |
| GET | /api/v1/orders |
JWT | - | List a customer's orders (paged). |
| DELETE | /api/v1/orders/{id} |
JWT | - | Cancel an order (soft delete/status transition, not a physical delete); triggers compensation. |
Events¶
| Direction | Event |
|---|---|
| Publish | order.created.v1, order.confirmed.v1, order.cancelled.v1, addon.purchased.v1 (one per ADDON item at fulfillment, topic addon.events) |
| Consume | payment.completed.v1, payment.failed.v1, subscription.activated.v1, subscription.tariff-changed.v1 (PLAN_CHANGE fulfillment) |
Notes¶
Idempotency-Keyis mandatory on order creation; replays return the original result.- Order list pagination:
page(default 0),size(default 20) and optionalsort=field,asc|desc(direction optional,descassumed; defaultcreatedAt,desc). Sortable fields:createdAt,totalAmount,status. Any other field or a malformed value returns the standard 400 validation error shape. - Order capture validates the customer (ACTIVE/KYC) and snapshots catalog price synchronously.
- Saga state is persisted; activation failure compensates (refund + order CANCELLED).
- Optional campaign discount at order capture (Sprint 21 Feature 21.3.3, ADR-027 Decision Section
4): after the existing tariff price-snapshot call, order-service asks campaign-service (tokenless,
POST /internal/campaigns/validateonCampaignServiceClient, behind a fail-open Resilience4j circuit breaker) whether a campaign discount applies to each line item. Each item inPOST /api/v1/orders's request body may optionally carry acampaignCode; when omitted, campaign-service auto-resolves the best-matching ACTIVE campaign for the item's tariff. When eligible, theOrderItem'sunitPriceis discounted (PERCENTAGE:monthlyFee * (1 - discountValue/100);FIXED_AMOUNT:monthlyFee - discountValue, both floored at zero) and the nullablecampaignId/campaignCodesnapshot columns onorder_itemsare populated (campaignIdalways when a discount applied;campaignCodeonly when the caller explicitly requested that campaign - matching thetariff_id/tariff_code/tariff_versionsnapshot symmetry). A campaign-service outage, an OPEN circuit breaker, or a genuinely ineligible decision all leave the item priced at today's undiscountedmonthlyFee- a campaign outage never blocks order creation.OrderCreatedEvent.OrderItemPayloadcarries a matching nullablecampaignIdfield for Feature 21.4's redemption-confirmation flow.
Order kinds (Sprint 24 Feature 24.2, design-note D1/D2)¶
POST /api/v1/orders accepts three order kinds through one generalized item model. Each request
item may carry, in addition to the pre-24.2 tariffId/quantity/campaignCode:
itemType(optional,TARIFF|ADDON, defaults toTARIFFwhen omitted - every pre-24.2 request body keeps working unchanged),productCode(catalog addon code; required onADDONitems),targetSubscriptionId(an existing subscription; see the matrix).
The order kind is derived from the items (never sent by the caller) and persisted plus exposed
as orderType on all order responses: every item ADDON -> ADDON; exactly one TARIFF item
carrying a targetSubscriptionId -> PLAN_CHANGE; anything else -> NEW_LINE.
Validation matrix (400 VALIDATION_FAILED for malformed shapes, 422 BUSINESS_RULE_VIOLATION for
domain-rule violations):
| Kind | Items | Rules |
|---|---|---|
NEW_LINE |
exactly 1 TARIFF (+ 0..N bundled ADDON) |
tariffId required on the tariff item; productCode required on addon items; targetSubscriptionId forbidden on every item. |
ADDON |
1..N ADDON |
all items carry the SAME non-null targetSubscriptionId; the target subscription must exist (404 otherwise), be ACTIVE and belong to the ordering customer (fail-closed hop to subscription-service GET /internal/subscriptions/{id}). |
PLAN_CHANGE |
exactly 1 TARIFF |
tariffId AND targetSubscriptionId required; target subscription ACTIVE, owned, and its current tariff must differ from the requested one. The order charges the new tariff's monthly fee through the unchanged payment saga (design-note D2). |
| any | - | campaignCode on a non-TARIFF item is a validation error (campaign eligibility is tariff-scoped, ADR-027). |
ADDON items are priced fail-closed from product-catalog's
GET /internal/addons/{code}/snapshot (Feature 24.1): the addon's price becomes the item
unitPrice and its name/allowances are snapshotted onto the item
(allowance_data_mb/allowance_minutes/allowance_sms columns), so downstream consumers never
need a runtime catalog hop. Item responses expose itemType, productCode and
targetSubscriptionId alongside the existing tariff/campaign snapshot fields (the tariff snapshot
fields are null on ADDON items; tariffName doubles as the generic product-name snapshot).
order.created.v1 carries matching additive item fields itemType (default TARIFF),
productCode and targetSubscriptionId (see event-catalog Section 5).
Saga fulfillment per order kind (Sprint 24 Features 24.3/24.4, design-note D1/D2/D3)¶
All kinds confirm on payment.completed.v1 (PENDING -> CONFIRMED). Fulfillment then differs:
NEW_LINE- fulfills onsubscription.activated.v1(unchanged). If the order bundledADDONitems, fulfillment additionally publishes oneaddon.purchased.v1PER addon item through the outbox in the same transaction (aggregate_typeaddon-> topicaddon.events; aggregate_id = the order-item id, so each item is an independently deduplicable message), carrying the item's creation-time catalog snapshot (name, type, unit price, currency, per-unit allowance deltas) and the just-activated subscription id.ADDON(standalone) - no activation leg: thepayment.completed.v1reaction confirms AND fulfills in one transaction (saga stepADDON_FULFILLED) and publishes oneaddon.purchased.v1per item against each item'stargetSubscriptionId. subscription-service deliberately ignores these orders' payment events.PLAN_CHANGE- fulfills onsubscription.tariff-changed.v1(published by subscription-service after it applies the change), correlated by the event'sorderId; the fulfillment command is deduped on"plan-change-fulfill:" + orderIdbecause the record key (subscriptionId) is not unique across successive plan changes. A failed change reusessubscription.activation-failed.v1, so the existing refund/cancel compensation applies to plan-change orders unchanged.
Reference: service-catalog, event-catalog, ADR-015, ADR-027.