product-catalog-service - API Contract¶
| Field | Value |
|---|---|
| Port | 9003 |
| Mode | CQRS + Mediator |
| Primary store | PostgreSQL + Redis (cache-aside); MongoDB read-projection post-MVP |
| Base path | /api/v1 |
| Owning sprint | Sprint 07 |
| Build status | TODO |
| Requirements | FR-05, FR-06, FR-07, FR-08 |
Bounded context: tariffs, addons, VAS. Read-heavy with Redis cache-aside. Versioned tariff changes preserve existing subscribers' tariff.
Authentication and Authorization¶
Reads require a valid JWT. Authoring (create/update/version) requires an admin role.
Endpoints¶
| Method | Path | Auth | Idempotency | Summary |
|---|---|---|---|---|
| GET | /api/v1/tariffs |
JWT | - | List tariffs (paged, cache-served). |
| GET | /api/v1/tariffs/{code} |
JWT | - | Fetch a tariff by its code (current version). |
| GET | /api/v1/tariffs/by-id/{id} |
JWT | - | Fetch a tariff by its UUID primary key. Used by cross-service callers (e.g. order-service) that hold the tariff's id rather than its code. |
| GET | /api/v1/tariffs/{code}/price |
JWT | - | Price snapshot for order capture. |
| POST | /api/v1/tariffs |
RBAC admin | optional | Create a tariff. |
| PUT | /api/v1/tariffs/{code} |
RBAC admin | - | New version on price/attribute change. |
| GET | /api/v1/addons?tariffCode=... |
JWT | - | List addons (paged); optional filter to addons linked to a tariff. |
| POST | /api/v1/addons |
RBAC admin | optional | Create an addon, optionally linked to tariffs by code. |
| GET | /internal/addons/{code}/snapshot |
internal (no JWT) | - | Addon snapshot for order-service pricing. Network-perimeter trust like the tariff internal routes: the gateway blocks /internal/** (internal-deny-route), reachable in-network only. |
Addons (Sprint 24, feature 24.1)¶
Addons carry nullable, type-dependent allowance fields: dataMb (DATA), voiceMinutes
(MINUTES), smsCount (SMS); VAS addons carry none. All addon responses include these fields.
POST /api/v1/addons request body:
{
"code": "DATA_5GB",
"name": "Data 5 GB",
"price": 49.90,
"currency": "TRY",
"type": "DATA",
"validityDays": 30,
"dataMb": 5120,
"voiceMinutes": null,
"smsCount": null,
"applicableTariffCodes": ["POSTPAID-BASIC"]
}
Constraints: code <= 50 chars, name <= 200, price >= 0.00, currency 3-letter uppercase,
type in DATA | SMS | MINUTES | VAS, validityDays > 0, allowances >= 0 when present.
applicableTariffCodes is optional; each code must reference an existing tariff (unknown code
-> 422), and a duplicate addon code -> 422. Returns 201 with the created addon wrapped in
ApiResult:
{
"success": true,
"data": {
"id": "…",
"code": "DATA_5GB",
"name": "Data 5 GB",
"price": 49.90,
"currency": "TRY",
"type": "DATA",
"validityDays": 30,
"dataMb": 5120,
"voiceMinutes": null,
"smsCount": null,
"status": "ACTIVE",
"createdAt": "…"
}
}
GET /internal/addons/{code}/snapshot (internal) returns ApiResult of
{id, code, name, type, price, currency, validityDays, dataMb, voiceMinutes, smsCount};
404 when the addon is absent or not ACTIVE.
A standard addon catalog (DATA_5GB, DATA_10GB, SMS_500, VOICE_300, VAS_CALLERTUNE) is seeded by
migration V2__addon_management.sql. Tariff links are runtime-managed (no tariffs are seeded),
so seeded addons appear under ?tariffCode= filters only after being linked via
POST /api/v1/addons or future admin tooling.
Events¶
| Direction | Event |
|---|---|
| Publish | tariff.created.v1, tariff.price-changed.v1, tariff.changed.v1 |
Notes¶
- A price change creates a new version; prior versions are preserved for existing subscribers.
- Reads are cache-aside (Redis); writes invalidate the affected cache keys.
- Tariff list pagination:
page(default 0),size(default 20) and optionalsort=field,asc|desc(direction optional,descassumed; defaultcreatedAt,desc). Sortable fields:createdAt,name,monthlyFee,effectiveFrom. Any other field or a malformed value returns the standard 400 validation error shape.
Reference: service-catalog, ADR-015.