# DSRC isolated mock API contract

This is an educational, supported-subset mock. It makes no TM Forum conformance or real interoperability claim. `demoProfile` is an explicit DSRC extension; the IODSP routine profile is proposed v0.3, not a TM Forum API. All money in `demoProfile`, financial records and state is integer USD cents. Standard TMF Money values are decimal USD display values only.

## Session and common behavior

`POST /mock-api/session` JSON `{ "scenario":"success", "quantityMbps":20, "durationHours":2, "locationId":"maintenance-area-1", "budgetLimitCents":2000, "creditLimitCents":2000, "capacityMbps":100 }` (all optional) returns 201 `{sessionId,expiresAt,state}`. Scenario enum: `success`, `backhaul_failure`, `buyer_cancel`, `activation_unknown`. Quantity integer 1..100, duration integer 1..24, limits integer 0..1000000, capacity integer 0..1000, location bounded identifier. The server seeds synthetic operator A / operator B and framework records. Session expires after 24 actual hours. The simulated clock remains frozen between explicit time controls/fulfilment; presentation pauses do not consume quote validity. State schema version is 2; a prior version returns `409 session_version_changed` and requires a fresh session.

Every following endpoint requires `X-Demo-Session: <sessionId>`; missing/malformed is 400, nonexistent/expired is 404. Every mutation except session creation requires `Idempotency-Key` (1..100 characters, safe identifier). Same key+method+path+JSON body returns original status/body even after quote expiry. Reuse with different content returns 409. This includes phase-dependent rejection outcomes: retrying the same operation key always returns its original result; after resolving a prerequisite, use a fresh key for the new operation. Bodies are limited to 16 KiB, collections to 100; unknown input fields are rejected. JSON errors: `{ "@type":"Error", "code":"...", "reason":"...", "status":"409" }`. Unsupported routes/methods return 404/405. Session isolation is a Durable Object per random UUID; read/modify/write is serialized and persisted.

`GET /mock-api/state` returns the state projection described below. Collection GETs return arrays; `GET collection/{id}` returns one resource or 404. All resources include `id`, `href`, `@type` and `demoProfile` marker where relevant.

## Supported TMF-shaped subset

* `GET /tmf-api/productCatalogManagement/v5/productOffering[/id]`: one `ran-backhaul-bundle` product offering. RAN rate 10 cents / Mbps-hour; backhaul 5 cents / Mbps-hour. Default total 600 cents. Maximum liability is max(service total, 100-cent cancellation schedule).
* `POST /tmf-api/quoteManagement/v5/quote` body `{ "@type":"Quote", "@baseType":"Entity", "quoteItem":[{"@type":"QuoteItem","id":"1","action":"add","productOffering":{"@type":"ProductOfferingRef","id":"ran-backhaul-bundle"}}], "demoProfile": { "quantityMbps":20, "durationHours":2, "locationId":"maintenance-area-1" } }` (profile fields optional, default session configuration). Returns 201 approved Quote, two quoteItems, server prices, validity 5 simulated minutes, extension `demoProfile:{quantityMbps,durationHours,locationId,totalCents,maxLiabilityCents,termsDigest,offerVersion}`. Quote is not a reservation. One quote and one purchase per session; multiple purchases and additive cross-transaction holds are intentionally unsupported.
* `PATCH /tmf-api/quoteManagement/v5/quote/{id}` body `{ "state":"accepted" }`: records buyer agreement to the quoted terms, still no reservations. Expired quote rejects new assent. Assent already recorded is a no-op, including fresh-key duplicates after operational acceptance, without extra events. GET `quote[/id]` supported.
* `POST /tmf-api/productOrderingManagement/v4/productOrder` body `{ "quote":[{"id":"quote-1"}], "productOrderItem":[{"id":"1","action":"add","productOffering":{"id":"ran-backhaul-bundle"}}] }`: checks accepted quote, authoritative expiry, single-use offer, budget, credit and physical capacity atomically. Returns 201 acknowledged ProductOrder with item refs and immutable acceptance receipt/terms. A different key cannot order the same offer twice. GET `productOrder[/id]` supported.
* `POST /tmf-api/serviceOrdering/v4/serviceOrder` body `{ "demoProfile": { "productOrderId":"product-order-1" }, "serviceOrderItem":[{"id":"1","action":"add","service":{"name":"RAN/backhaul bundle","serviceSpecification":{"id":"ran-backhaul-bundle-spec"}}}] }`: explicitly creates ServiceOrder after accepted ProductOrder, reserves both service lines and enters `activating`. IODSP does not provision services. GET `serviceOrder[/id]` supported.
* `GET /tmf-api/serviceInventory/v4/service[/id]`: external fulfilment's RAN/backhaul Service resources, created with the ServiceOrder and changed only by mock control evidence.
* `GET /tmf-api/agreementManagement/v4/agreement[/id]`: synthetic routine framework.

## Mock-only external systems control

`POST /mock-api/control` body `{ "action":"..." }` returns the complete state. Actions:

* `fulfil`: after ServiceOrder, execute selected scenario. Success emits activation, verification, measured usable service and release acknowledgements for both components; phase `fulfilled`. Backhaul failure emits sellerFailed bundle/cancellation and separate physical release evidence; phase `failed`, fee zero. Buyer cancel emits buyerCancelled and physical release evidence; phase `cancelled`, fee 100 cents. Unknown emits uncertain activation evidence only, retains all holds/capacity and phase `unknown`.
* `resolve` with `outcome:"success"|"backhaul_failure"|"buyer_cancel"`: only while unknown; emits definitive external evidence.
* `advance_time` with `seconds` integer 1..172800: advance authoritative simulated clock (does not shorten real session lifetime). No automatic inference from deadlines.
* `expire_quote`: advance to quote expiry; retries of earlier committed requests still return original result.
* `confirm_payment` with `statementId`: mock external payment confirmation; reduces receivable once, increases paid, never restores spent budget. Statement remains required. The server derives amount.

## Proposed IODSP routine records

Base `/tmf-api/interOperatorSettlement/v0.3`. GET list/id supported for `acceptedTerms`, `preAuthorisationPool`, `commitmentRecord`, `chargeRecord`, `papCapture`, `bilateralStatement`, `nettingRegister`, `disputeResolution`. `chargeRecord` and `papCapture` are explicit proposed mock profile resources. PAP represents the seller credit account; its `captureEntries` expose each unique `holdToAccruedReceivable` capture. Each charge has `papDrawRef` and resolvable `papDrawHref`, linking matching amount, charge and exposure-reservation references. Zero-rated consequences have zero-valued capture entries. disputeResolution is initially empty.

`POST .../commitmentRecord` body `{ "transactionId":"transaction-1", "evidenceRefs":["evidence-..."] }`. References must include all definitive outcome evidence IDs displayed in `state.evidence` with `billable:true` (success: two service-usage records; cancellation/failure: two component cause records). Evidence must exist in this session and match transaction. Server derives quantities, liability and immutable charge records; supplied amounts/charges rejected. Unknown/incomplete outcome returns 409. Once posted, all retries with the same evidence return the original record; changed evidence rejected. Posting captures incurred service/cancel fee, releases residual budget/credit hold; physical capacity only releases on external release evidence.

`POST .../bilateralStatement` body `{ "transactionId":"transaction-1" }`: only after commitment finalizes; derives charge refs, totals and debtor A / creditor B. The `period` is the accepted service window and `demoProfile.periodBasis` labels this whole-window aggregation; this mock does not split periods, while fixed-fee trigger times remain on charge records. Positive amounts do not settle exposure. Zero amounts settle immediately (`paymentStatus:"settled"`, state phase `settled`, netting paymentStatus `notRequired`) with no fictional payment reference; `confirm_payment` then rejects `409 payment_not_required`. Repeated requests return same statement; no duplicate charge inclusion. `nettingRegister` is emitted with statement (gross exposure held until mock payment confirmation).

## State projection

`{ sessionId, expiresAt, now, config, phase, quote:null|Quote, productOrder:null|ProductOrder, serviceOrders:[], services:[], acceptedTerms:[], evidence:[], charges:[], commitments:[], statements:[], ledger:{budgetLimitCents,budgetHeldCents,budgetSpentCents,creditLimitCents,creditHeldCents,receivableCents,paidCents,refundCents,capacityReservedMbps}, events:[] }`.

Phases: `ready`, `quoted`, `quote_accepted`, `accepted`, `activating`, `unknown`, `fulfilled`, `failed`, `cancelled`, `rated`, `statement`, `settled`, `paid`. Events contain bounded deterministic `{id,at,type,detail}`; evidence `source` names mock external fulfilment/assurance. Term hashes identify content, not signatures or real consent. Synthetic authority is server-owned and explicitly labelled; no production auth assertion or interoperability guarantee is represented.

Quote `accepted` means buyer assent only; ProductOrder admission is the separate seller operational acceptance. TMF ServiceOrder uses standard `inProgress` state and extension phase `activating`. Required create-envelope fields are enforced for this subset. `demoProfile.termsContent` is the canonical SHA-256 digest input, including maximum liability, line rates/cancellation schedule, triggers, service window, all deadlines, framework, parties and rounding policy. AcceptedTerms copy every immutable commercial field from this same content. Resource wrapper metadata and the later acceptance receipt are excluded; the receipt binds the computed digest. Internal idempotency storage is omitted from projections. State also exposes `papCaptures` and derived `evidenceStatus` with deadlines, overdue/cutoff booleans, `liabilityUnresolved` and a readable status.

## Bounded synthetic time policy

Quote terms pin a service window starting ten simulated minutes after quote creation and ending after the requested whole hours. The activation deadline is the window start. `fulfil` with success simulates scheduled activation, verifies both components before emitting either usage record, then fast-forwards through this complete window. A new ServiceOrder or any new `fulfil` outcome (success, seller failure, buyer cancellation or unknown) requested after the activation deadline returns `409 activation_deadline_passed`; it does not invent historical delivery or release holds. Activation/verification events carry the start timestamp; usable-service completion carries the end timestamp. This demonstration is whole-window rating, not streaming telemetry or a correction engine.

`resolve` is permitted only for a previously unknown ServiceOrder. Success is explicit mock external reconciliation of that earlier activation, with `reconciledHistoricalEvidence:true`; event times retain the accepted window and observation time is at least the completion time and current reconciliation clock. Time advancement by itself supplies neither fulfilment proof nor a default fee. At the reconciliation deadline, unresolved records are shown as `latePendingExternalResolution`; elapsed time supplies no cause or charge. At or after the final evidence cutoff, new `resolve` and first `commitmentRecord` posting reject `409 evidence_cutoff_passed`; financial and unresolved physical holds stay retained (`cutoffPassedHoldsRetained`). A previously finalized commitment remains retrievable/retryable after the cutoff. Cutoff default determination, disputes and financial corrections remain outside this supported subset.

Verification: `node --test tests/cloudflare-mock-api.mjs` exercises the actual exported router and Durable Object class with clone-on-read durable-storage doubles, including actor reconstruction. `MOCK_API_TEST_ORIGIN=http://127.0.0.1:8879 node --test tests/cloudflare-mock-http.mjs` runs all scenario journeys and concurrent duplicate admission against a separately running Wrangler/workerd server and its real local Durable Object storage.

All new fulfilment outcomes also obey the final evidence cutoff. A rejected late outcome creates no evidence, releases no capacity, and incurs no fee. Elapsed time never determines seller failure, buyer cancellation or any other cause. Historical unknown resolution to cancellation/failure pins the synthetic cause event time to the original timely ServiceOrder request and its observation time to the explicit reconciliation control; these records carry `reconciledHistoricalEvidence:true`.
