Contract API compatibility boundaries
This guide records the local behavior that is verified against the checked-in Transform OpenAPI snapshot. A successful wire response proves the local request and response contract; it does not by itself establish vendor semantic parity.
Primary sources:
-
frisbii-transform-swagger.jsondefines the request, query, response, and status contracts. -
contract-preview-semantics.mdrecords the official evidence and remaining unknowns for interim billing and pause previews. -
api-contract-final.mdrecords the independent bounded review of the local implementation and tests.
Contract changes
The bounded storage, query, and projection layers support the vendor-confirmed ContractChange read shape, exact expansion modes, and PostgreSQL-bounded filtering. See Contract history for parameters, raw-ID pagination, projection rules, limits, legacy behavior, and errors.
Key compatibility points:
-
list order is descending by
Timestampand change ID; -
fromis a raw 24-character change ID and repeats its anchor inclusively; -
no continuation header is emitted;
-
default list rows are core-only;
-
default detail behaves as
includeContract=trueand includesContractplusPricePhases; -
includeContract=falseomits bothContractandPricePhases, even when other families requestAll; -
None,Changed, andAllare projected only from immutable stored snapshots.
Legacy generic history is never reconstructed. If a selected page or detail has no complete typed snapshot, the API returns 422 contract_change_history_unsupported.
The mutation writer and independent lifecycle activation paths have bounded approval for atomic typed changes, foreground draining, scheduled phase/pause activation, durable failure handling, and import/read-only rejection. See the writer and activation review. This approval is local and does not establish complete Transform parity or reconstruct legacy history.
Pause preview
POST /api/v1/contracts/{contractId}/pausepreview validates the requested start/end interval and returns PauseDate, PauseStart, and nullable PauseEnd without mutating the contract. The local response does not calculate the optional financial Invoice preview. This is a date-only local preview and is not Transform financial-preview parity.
Next recurring preview
GET /api/v1/contracts/{id}/previewnextrecurring calculates the next recurring quote from the current local price, tax, discount, component, and billing-period state. It returns BillingDate, totals, AmountPayable, currency, and line items.
ignoreZeroTotals=false or omission returns the verified local quote and retains zero-total lines. Official material does not define the true behavior precisely, so ignoreZeroTotals=true fails closed with 422 next_recurring_option_unsupported. Unsupported billing alignment or incomplete pricing configuration returns the existing explicit domain error rather than a fabricated preview.
Interim bill endpoint
POST /api/v1/contracts/{contractId}/bill is documented by Transform as interim billing, distinct from normal recurring-cycle billing. Official guidance says it bills eligible unbilled usage immediately and can separately bill changed component fees without advancing NextBillingDate.
The local interim handler bills eligible unbilled metered/rated usage whose due time is at or before the request time, plus eligible frozen component-change lines. Metered usage and component changes require the component’s explicit IncludeInInterimBilling=true opt-in. During a proven current trial, BillInTrial=true applies only to frozen fees from completed component-change orders; metered usage is rejected. Scheduled-paused, ended, expired-trial and future-start contracts are rejected.
The supported pricing profile is intentionally narrow. Rated items use their frozen price. Metered usage is rejected with 422 InterimPricingUnavailable when the plan variant has a matching free quota, an active contract discount targets the component, or multiple due postings would require aggregate tier pricing. Frozen component fees retain their order-time discount lines only when the complete same-product group is due; ambiguous partial groups wait. Zero-valued and fully discounted selections do not produce invoices. These cases are explicit failures rather than approximate charges.
The handler excludes the base recurring fee and future or opted-out usage, marks only selected rows as billed, and leaves NextBillingDate and the recurring cadence unchanged. Selection happens under the merchant transaction and the invoice, immutable PDF, ledger, consumption markers and order settlement markers commit atomically. Candidate scans are filtered in PostgreSQL before their 5,000-record bound; one invoice is limited to 1,000 items, discount candidates to 5,000, and price-list inheritance to 32 levels. A breached bound returns 422 InterimBillingLimitExceeded without financial mutation. Empty selections return 422 nothing_to_bill. These local financial effects are tested independently; remaining vendor allocation and proration unknowns in contract-preview-semantics.md prevent a broad Transform-parity claim.
Deletion and provider-dependent operations
DELETE /api/v1/contracts/{id} uses a stricter local retention policy. Physical deletion is allowed only for a terminal contract with no retained resource carrying its ContractId. Usage, subscriptions, orders, contract-change audit, invoices, drafts, ledger entries, postings, payment records, and other retained resources therefore block deletion with 422 contract_not_deletable. Short-lived self-service capability rows are removed atomically when an otherwise clean terminal contract is deleted.
POST /api/v1/contracts/{contractId}/refund and POST /api/v1/contracts/{contractId}/changepaymentmethod return 422 PaymentProviderNotConfigured without ledger, payment, refund, or outbox effects when no PSP adapter is configured.
The separate Admin refund-log viewer is read-only. GET /admin/logs/refunds shows stored payment_refund rows for the current merchant, 50 per page, with page numbers 1 through 100. Its optional filters are a 200-character PaymentTransactionId search, Succeeded/Failed status, and inclusive from/to merchant-local calendar dates. The viewer refuses a selected archive over 5,000 rows. GET /admin/logs/refunds/{id} projects only the stored timestamp, status, amount, currency, transaction ID, and up to 100 original assignments, plus an allowlisted linked payment summary. Detail JSON is capped at 1 MiB and displayed scalar values at 2 KiB. Missing or invalid financial fields remain explicitly unknown/invalid; the UI does not substitute zero or a merchant currency. Tenant-scoped ID-only links do not reconstruct current business labels, provider tokens and raw PSP/status payloads are omitted, and no refund/retry form is rendered.
This viewer matches the controls observed in gui-observations-2026-09-06.md and is covered with populated synthetic records, tenant/filter/limit cases, pinned public wire schemas, and no-side-effect assertions in admin_payment_logs_test.rb. It proves local stored-history display, not provider refund execution or source-account data access.
Write-off fails closed
The individual Admin route POST /admin/contracts/{contract_id}/finance/writeoff requires explicit confirmation and delegates to POST /api/v1/contracts/{contractId}/writeoff. The current domain handler returns 422 WriteOffSemanticsUnsupported before changing the contract or creating ledger entries, assignments, postings, typed history, schedules, idempotency results, or outbound events. Contracts already marked WrittenOff retain the existing 409 terminal guard.
This is a deliberate fail-closed boundary while document allocation, tax correction, configured write-off accounts, passive deferrals, and immutable pre-write-off escalation evidence remain unimplemented. The source evidence, missing semantics, and future implementation order are recorded in writeoff-semantics.md. The confirmed no-effect Admin regression is in admin_finance_actions_test.rb.
GET /api/v1/contracts/{id}/prepaidcards is feature-gated in this local clone and currently returns 422 with { "Message": "Epay feature is not active" } for disabled prepaid-card setups. No prepaid-card card list semantics are inferred from the published schema alone.