Skip to main content

API_CONTRACTS — reporting-service

Sibling: APPLICATION_LOGIC · DOMAIN_MODEL · EVENT_SCHEMAS · SECURITY_MODEL

Strategic anchors: 05 API Design · standards/ERROR_CODES · standards/NAMING §10 API paths

All endpoints follow the platform conventions in 05 API Design: REST under /api/v1/reports, JSON content type, ULID + prefix identifiers, RFC 7807 Problem-Details errors with the canonical MELMASTOON.<DOMAIN>.<CODE> codes, optimistic concurrency via If-Match: "v<n>" on mutating endpoints, RFC 5988 link headers for pagination, Idempotency-Key accepted on every POST that creates server state.


1. Common headers (request)

HeaderRequiredPurpose
Authorization: Bearer <jwt>yesCaller identity (staff, system, partner)
X-Tenant-Id: tnt_<ulid>yes (except platform-admin endpoints)Tenant context; cross-checked vs JWT
X-Property-Id: ppt_<ulid>recommended for ops-scoped readsProperty scoping
X-Request-Id: <ULID>recommendedCorrelation; service generates one if absent
Idempotency-Key: <ULID>required on POSTs that create server stateReplay-safe
If-Match: "v<n>"required on PATCH / DELETEOCC
Accept-Language: <locale>optionalLocalized error userMessageKey

2. Common headers (response)

X-Request-Id, X-Trace-Id, ETag: "v<n>" on every aggregate read, Retry-After on 429/503, Cache-Control per endpoint, Content-Disposition only for direct artifact downloads (we typically redirect to a V4 signed URL instead).

3. Error response shape

Per standards/ERROR_CODES. Reporting-specific codes are added in the same PR that introduces them under a new REPORTING section:

CodeHTTPRetriable
MELMASTOON.REPORTING.TEMPLATE_LOCKED409no
MELMASTOON.REPORTING.TEMPLATE_VERSION_LOCKED409no
MELMASTOON.REPORTING.TEMPLATE_NOT_FOUND404no
MELMASTOON.REPORTING.FILTER_INVALID422no
MELMASTOON.REPORTING.FORMAT_NOT_SUPPORTED422no
MELMASTOON.REPORTING.RUN_TERMINAL409no
MELMASTOON.REPORTING.RUN_NOT_FOUND404no
MELMASTOON.REPORTING.RENDER_BUDGET_EXCEEDED422no
MELMASTOON.REPORTING.ARTIFACT_LOCKED409no
MELMASTOON.REPORTING.ARTIFACT_CHECKSUM_MISMATCH500no
MELMASTOON.REPORTING.SCHEDULE_INVALID_CRON422no
MELMASTOON.REPORTING.SCHEDULE_INVALID_TZ422no
MELMASTOON.REPORTING.SCHEDULE_NO_RECIPIENTS422no
MELMASTOON.REPORTING.SUBSCRIPTION_LIMIT429no
MELMASTOON.REPORTING.REGULATORY_INCONSISTENT422no
MELMASTOON.REPORTING.REGULATORY_ADAPTER_MISSING409no
MELMASTOON.REPORTING.SUBMISSION_RETRY_EXHAUSTED409no
MELMASTOON.REPORTING.PROOF_OF_DELIVERY_REQUIRED500no

Universal codes (MELMASTOON.GENERAL.*, MELMASTOON.IDENTITY.*, MELMASTOON.TENANT.*, MELMASTOON.SYNC.*) apply.


4. Templates

4.1 POST /api/v1/reports/templates

Create a new logical template with its first version. Subsequent versions go through POST /:id/versions.

Auth: platform_admin (when tenantId omitted ⇒ shared) or tenant_admin/gm/owner.

Request:

{
"key": "reservation.daily_arrivals",
"category": "operational",
"tenantId": "tnt_01H...", // optional; omit ⇒ platform-shared
"spec": {
"columns": [
{ "key": "reservationCode", "header": { "default": "Code", "variants": { "fa-AF": "کد" } }, "type": "string" },
{ "key": "guest.fullName.latin", "header": { "default": "Guest" }, "type": "string" },
{ "key": "stayWindow.start", "header": { "default": "Arrival" }, "type": "date", "format": { "pattern": "yyyy-MM-dd" } },
{ "key": "totals.grandTotalMicro", "header": { "default": "Total" }, "type": "currency", "format": { "currency": "TENANT", "decimals": 2 }, "align": "end" }
],
"filters": [
{ "key": "propertyId", "type": "tenant_property_picker", "required": true, "multi": false },
{ "key": "stayDate", "type": "date_range", "required": true }
],
"layout": [
{ "kind": "header", "logo": "tenant", "title": { "default": "Daily Arrivals" } },
{ "kind": "kpi_band", "metrics": [
{ "label": { "default": "Arriving" }, "sourceKey": "kpi.arriving" },
{ "label": { "default": "Pre-Paid" }, "sourceKey": "kpi.prepaid" }
]},
{ "kind": "table", "columns": ["reservationCode","guest.fullName.latin","stayWindow.start","totals.grandTotalMicro"], "sourceKey": "rows" },
{ "kind": "footer", "signed": true, "pageNumbers": true }
],
"supportedFormats": ["pdf","xlsx","csv"],
"defaultLocale": "fa-AF",
"localeVariants": ["fa-AF","ps-AF","en-US"],
"regulatory": false,
"retentionClass": "operational_2y",
"dataSourceSpec": {
"primary": { "kind": "service_read", "service": "reservation", "queryName": "arrivalsForDate" }
}
}
}

Response: 201 Created

{
"templateId": "tpl_rep_01H...",
"versionId": "tpv_01H...",
"versionNumber": 1,
"publishedAt": "2026-04-22T10:00:00Z"
}

4.2 POST /api/v1/reports/templates/:id/versions

Publish a new version of an existing template. Body matches spec in §4.1.

Errors: MELMASTOON.REPORTING.TEMPLATE_VERSION_OUT_OF_ORDER, MELMASTOON.REPORTING.LAYOUT_REFERENCE_INVALID, MELMASTOON.REPORTING.LOCALE_VARIANT_MISSING.

4.3 GET /api/v1/reports/templates

List templates available to the caller. Query: category=operational|financial|compliance|regulatory|manager_dashboard, regulatory=true|false, jurisdictionCode=AF, cursor=, limit<=100.

Response:

{
"items": [
{ "templateId": "tpl_rep_01H...", "key": "reservation.daily_arrivals", "category": "operational",
"latestVersion": 4, "regulatory": false, "supportedFormats": ["pdf","xlsx","csv"], "archived": false }
],
"next": null
}

4.4 GET /api/v1/reports/templates/:id/versions

Returns version history (immutable). Useful for auditors; supports ?at=<RFC3339> to resolve "what was the latest version at this date".

4.5 DELETE /api/v1/reports/templates/:id

Archives a template. Existing scheduled runs continue to use the last published version until the schedule is updated.


5. Reports (logical instances)

5.1 POST /api/v1/reports

{
"templateId": "tpl_rep_01H...",
"templateVersionPin": null,
"displayName": { "default": "Daily Arrivals - Kabul Branch" },
"defaultFilters": { "propertyId": "ppt_01H...", "stayDate": { "preset": "today" } }
}

Response: 201 with reportId.

5.2 PATCH /api/v1/reports/:id

Updates displayName, templateVersionPin, defaultFilters. OCC via If-Match.

5.3 GET /api/v1/reports/:id

Returns Report plus recentRunIds (last 30).


6. Runs

6.1 POST /api/v1/reports/runs

Request a render. Idempotency-Key required.

Request:

{
"reportId": "rep_01H...",
"templateVersionPin": null,
"filters": { "propertyId": "ppt_01H...", "stayDate": { "from": "2026-04-22", "to": "2026-04-22" } },
"formats": ["pdf","csv"],
"locale": "fa-AF",
"correlationId": "01H..."
}

Response: 202 Accepted

{
"runId": "run_01H...",
"status": "queued",
"queuedAt": "2026-04-22T10:00:00Z",
"expectedReadyBy": "2026-04-22T10:00:30Z",
"subscribePollUrl": "/api/v1/reports/runs/run_01H...",
"subscribeStreamUrl": "/api/v1/reports/runs/run_01H.../events"
}

Errors: FILTER_INVALID, FORMAT_NOT_SUPPORTED, TEMPLATE_LOCKED, RATE_LIMITED, IDEMPOTENCY_KEY_REUSED.

6.2 GET /api/v1/reports/runs/:id

Returns the full run.

{
"runId": "run_01H...",
"reportId": "rep_01H...",
"templateId": "tpl_rep_01H...",
"templateVersionId": "tpv_01H...",
"templateVersionNumber": 4,
"status": "completed",
"queuedAt": "2026-04-22T10:00:00Z",
"startedAt": "2026-04-22T10:00:01Z",
"renderedAt": "2026-04-22T10:00:10Z",
"completedAt": "2026-04-22T10:00:11Z",
"artifacts": [
{ "artifactId": "art_01H...", "format": "pdf", "locale": "fa-AF", "sizeBytes": 187432, "sha256": "…" },
{ "artifactId": "art_01H...", "format": "csv", "locale": "fa-AF", "sizeBytes": 12031, "sha256": "…" }
],
"deliveries": [
{ "subscriptionId": "sub_01H...", "channel": "email", "status": "delivered", "deliveredAt": "2026-04-22T10:00:13Z" }
],
"version": 5
}

6.3 GET /api/v1/reports/runs/:id/events (SSE)

Server-Sent Events stream of status transitions; closes on completed/failed/cancelled. Used by the desktop and BFFs to avoid polling.

6.4 POST /api/v1/reports/runs/:id/cancel

Cancels a queued/running run. 409 RUN_TERMINAL if already terminal.

6.5 GET /api/v1/reports/runs/:id/artifacts/:artId/download

Returns 302 Found with Location: <V4 signed URL> and Cache-Control: private, max-age=60. The signed URL TTL is 15 min (SIGNED_URL_TTL_SECONDS=900).

For partner integrations that cannot follow redirects, ?inline=true returns the raw bytes streamed directly (heavier, audited at higher severity).

6.6 GET /api/v1/reports/runs

List runs. Query: reportId, status, from, to, cursor, limit<=100.


7. Schedules

7.1 POST /api/v1/reports/schedules

{
"reportId": "rep_01H...",
"cronExpr": "0 6 * * *",
"timezone": "Asia/Kabul",
"templateVersionPin": null,
"filters": { "propertyId": "ppt_01H...", "stayDate": { "preset": "today" } },
"subscriptionIds": ["sub_01H..."]
}

Errors: SCHEDULE_INVALID_CRON, SCHEDULE_INVALID_TZ, SCHEDULE_NO_RECIPIENTS, SCHEDULE_COLLISION.

7.2 PATCH /api/v1/reports/schedules/:id

Update cron, recipients, status (active/paused). To re-enable a disabled_after_failures schedule, set status="active" (audit-recorded).

7.3 DELETE /api/v1/reports/schedules/:id

Disables.

7.4 POST /internal/jobs/schedule-fire (system, GCP-Scheduler)

Cloud Scheduler endpoint. Body: { scheduleId: "sch_01H...", firedAt: "..." }. Auth: GCP service-account JWT, principal must be scheduler@<project>.iam.gserviceaccount.com. Maps to FireScheduleUseCase.


8. Subscriptions

8.1 POST /api/v1/reports/subscriptions

{
"reportId": "rep_01H...",
"recipient": { "kind": "user", "userId": "usr_01H..." },
"channel": "email",
"format": "pdf",
"locale": "fa-AF"
}

For desktop_device recipients the recipient.deviceId must be a dev_* bound to a current member; the subscription delivers via the desktop sync engine (artifact pulled into SQLite-backed local cache).

8.2 DELETE /api/v1/reports/subscriptions/:id

Cancels. Idempotent.

8.3 GET /api/v1/reports/subscriptions?reportId=…

Lists active subscriptions. Returns last delivery state per subscription.


9. Filters (saved)

9.1 POST /api/v1/reports/filters

{
"name": "Kabul, Today, Direct only",
"scope": "tenant", // 'user' or 'tenant'
"filters": { "propertyId": "ppt_01H...", "channel": ["direct"], "stayDate": { "preset": "today" } }
}

9.2 GET /api/v1/reports/filters

Lists. Query: scope, cursor, limit.

9.3 DELETE /api/v1/reports/filters/:id


10. Regulatory submissions

10.1 GET /api/v1/reports/regulatory/submissions

Lists submissions. Query: status, jurisdictionCode, from, to. Sample item:

{
"submissionId": "reg_01H...",
"runId": "run_01H...",
"jurisdictionCode": "AF",
"adapterRef": "af.tourism.daily_guests.v1",
"status": "succeeded",
"attempts": 1,
"submittedAt": "2026-04-22T22:00:01Z",
"succeededAt": "2026-04-22T22:00:09Z",
"proofOfDelivery": {
"receiptKind": "signed_xml_receipt",
"registerReference": "AF-TOUR-2026-04-22-038271",
"storedAtObjectPath": "tnt_01H.../regulatory/.../receipt.xml"
}
}

10.2 POST /api/v1/reports/regulatory/submissions/:id/retry

Manually retry. Auth: compliance_officer, gm, owner. 409 SUBMISSION_RETRY_EXHAUSTED if attempts >= maxAttempts and not manually escalated.

10.3 POST /api/v1/reports/regulatory/submissions/:id/manual-resolve

Mark as manually_resolved with operator-supplied resolutionNote. Audit-recorded; emits regulatory.submission_succeeded.v1 with proofOfDelivery.receiptKind='paper_print_signature' if the operator printed and hand-delivered.


11. Internal endpoints

PathCallerPurpose
POST /internal/events/notification-deliveredPub/Sub pushInbox handler for notification.delivery.recorded.v1
POST /internal/events/tenant-deletedPub/Sub pushCascade purge
POST /internal/events/report-completedPub/Sub push (self-loop)Trigger subscription dispatch + regulatory dispatch
POST /internal/jobs/schedule-fireCloud SchedulerSee §7.4
POST /internal/jobs/regulatory-retryCloud SchedulerTriggers RetryRegulatorySubmissionUseCase
GET /internal/healthVPCDB + Pub/Sub + GCS readiness
GET /internal/readyVPCAll inbox subscriptions attached

All /internal/events/* validate the GCP-issued OIDC token and refuse non-Pub/Sub principals. All /internal/jobs/* validate the Cloud Scheduler service account.


12. Pagination, sorting, rate limits

  • Default limit=50, max limit=100. Response envelope includes next cursor (opaque).
  • Sort: deterministic (updated_at DESC, id DESC) for list endpoints.
  • Rate limits per 05 §11: 600 req/min per tenant on read endpoints; 60 req/min on POST /runs per tenant; 5 concurrent in-flight regulatory retries per tenant.

13. OpenAPI

Generated by NestJS Swagger from controller decorators into openapi.json and committed; CI's OpenAPI diff gate fails any breaking change without a /api/v1/api/v2 jump. Pact consumer tests live in bff-backoffice-service and notification-service and run on every PR via the Pact broker.


14. Cross-references