K1: card purchases on the server (schema 4, Enable Banking adapter, E-36 to E-44)
Build Kvitt images / build (push) Failing after 8m17s
Build Kvitt images / build (push) Failing after 8m17s
Contract (SPEC §14.8): migration 0004_card_purchases.sql (server schema 4, byte-identical contract and server copies), E-36 to E-44 in contracts/openapi.yaml with the four new error codes and a 502 response, examples for every card body and error code, the match_card_purchase vectors (17 cases) with their Python reference in tools/contracts-check.py (157 checks), web/src/api/schema.d.ts regenerated; web/src/lib/vectors.test.ts lists match_card_purchase as not ported until K3 ports it (orchestrator's allowance). Server: src/cards/ with the provider registry (CP-1), the CardSource trait and its Enable Banking adapter (JWT RS256 kid = application id, /aspsps, /auth, /sessions, transactions with PSU headers for user-present fetches, CP-14 error mapping, CP-7 mapping), the windowed fetch (30-day windows newest first, split at the 100-row cap down to a day, de-duplication by provider_ref, continuation keys, WRONG_TRANSACTIONS_PERIOD ends the walk), CP-8 matching with the secondary rule, the background poller (06:45, 11:45, 16:45, 21:45, 4 per 24 hours, 2-hour skip, no backlog), the bilingual callback page (E-39), E-36 to E-38 and E-40 to E-44, link clearing in sync push step 7, attempt pruning at 04:00, the card_fetch log line with aggregates only, KVITT_EB_* configuration. Tests against a local stub (tests/cards_api/, 34 tests); the real API is never called. Infra: Pi compose mounts secrets/cards read-only and passes the two application ids; .env.example keys; dev stack stages infra/secrets/cards/; NPM block with the /cards/callback gate line and unlogged location (kvitt.conf and README step 4); README "Card purchases". Pre-commit hook bypassed (--no-verify, orchestrator's decision): the Android fast tests fail in :domain VectorFilesTest (and the full check in :data ExampleRoundTripTest) because they pin the contract's vector and example sets; matchCardPurchase and the card DTOs are K2's by design (contract-first landing; K2 lands next). Server, contracts-check and web gates are green. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
Vendored
+698
-1
@@ -559,6 +559,180 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/cards/providers": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/**
|
||||
* E-36 Card catalogue
|
||||
* @description The supported cards in registry order (SPEC §9.12 CP-1). `available` is false when the server has no card application for the caller (CP-13).
|
||||
*/
|
||||
get: operations["listCardProviders"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/cards/auth": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/**
|
||||
* E-37 Start connecting or renewing a card
|
||||
* @description Creates an authorisation attempt (SPEC §9.12 CP-2) and returns the bank URL the client opens. A pending older
|
||||
* attempt of the caller for the same provider expires.
|
||||
*/
|
||||
post: operations["createCardAuth"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/cards/auth/{attempt_id}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/**
|
||||
* E-38 Poll an authorisation attempt
|
||||
* @description A pending attempt past `expires_at` is answered and stored as `expired` (SPEC §9.12 CP-2). `auth_url` is null.
|
||||
*/
|
||||
get: operations["getCardAuth"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/cards/callback": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/**
|
||||
* E-39 Card connection callback page (HTML)
|
||||
* @description The bank's redirect after BankID (SPEC §9.12 CP-2). Always 200 HTML, success and failure alike;
|
||||
* `Cache-Control: no-store`, `Referrer-Policy: no-referrer`. Swedish when `Accept-Language` prefers `sv`, English
|
||||
* otherwise. A malformed query answers the failure page.
|
||||
*/
|
||||
get: operations["getCardCallback"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/cards/sync": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/**
|
||||
* E-40 Pull the caller's card rows since a cursor
|
||||
* @description The caller's connections and purchases with `card_version > since`, tombstones included, counted together and
|
||||
* ordered by `card_version` (SPEC §14.3.4, like §14.4.2).
|
||||
*/
|
||||
get: operations["cardSync"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/cards/connections/{connection_id}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post?: never;
|
||||
/**
|
||||
* E-43 Disconnect a card
|
||||
* @description Revokes the session at the bank (best effort), sets `disconnected`, deletes the purchases neither linked nor
|
||||
* ignored and, with `erase_handled=true`, the linked and ignored ones too (SPEC §9.12 CP-12). Expenses stay.
|
||||
* Idempotent on a disconnected connection.
|
||||
*/
|
||||
delete: operations["deleteCardConnection"];
|
||||
options?: never;
|
||||
head?: never;
|
||||
/**
|
||||
* E-41 Shorten the history setting
|
||||
* @description Only a value of 7, 30, 90 or 180 at most the current one (SPEC §9.12 CP-3); a longer one is a renewal through
|
||||
* E-37. Stored purchases stay.
|
||||
*/
|
||||
patch: operations["patchCardConnection"];
|
||||
trace?: never;
|
||||
};
|
||||
"/cards/connections/{connection_id}/refresh": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/**
|
||||
* E-42 Fetch purchases with the user present
|
||||
* @description Contacts the bank at most once per connection per 60 seconds and waits up to 25 seconds (SPEC §9.12 CP-5).
|
||||
* `fetched` is false when the 60-second rule answered without contacting the bank or the fetch is still running
|
||||
* (then `connection.fetching` is true). A failed fetch is recorded in `last_fetch_error` and still answers 200.
|
||||
*/
|
||||
post: operations["refreshCardConnection"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/cards/purchases/{purchase_id}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
/**
|
||||
* E-44 Link, unlink or ignore a card purchase
|
||||
* @description Sets the link and the ignore flag (SPEC §9.12 CP-9, §9.6). The unchanged row with its existing `card_version`
|
||||
* when nothing changes.
|
||||
*/
|
||||
put: operations["putCardPurchase"];
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
}
|
||||
export type webhooks = Record<string, never>;
|
||||
export interface components {
|
||||
@@ -610,7 +784,7 @@ export interface components {
|
||||
* @description Exhaustive (SPEC §14.3.2). The app maps `code` (and `field`) to localised strings.
|
||||
* @enum {string}
|
||||
*/
|
||||
ErrorCode: "invalid_request" | "invite_invalid" | "unauthorized" | "device_revoked" | "forbidden" | "not_found" | "household_full" | "household_incomplete" | "name_taken" | "account_not_empty" | "category_in_use" | "category_is_default" | "cannot_revoke_current_device" | "no_accounts" | "cursor_ahead" | "payload_too_large" | "unsupported_media_type" | "validation_failed" | "image_invalid" | "batch_too_large" | "protocol_unsupported" | "rate_limited" | "internal" | "not_implemented" | "unavailable";
|
||||
ErrorCode: "invalid_request" | "invite_invalid" | "unauthorized" | "device_revoked" | "forbidden" | "not_found" | "household_full" | "household_incomplete" | "name_taken" | "account_not_empty" | "category_in_use" | "category_is_default" | "cannot_revoke_current_device" | "no_accounts" | "cursor_ahead" | "cards_unavailable" | "card_consent_expired" | "expense_already_linked" | "payload_too_large" | "unsupported_media_type" | "validation_failed" | "image_invalid" | "batch_too_large" | "protocol_unsupported" | "rate_limited" | "internal" | "card_bank_unavailable" | "not_implemented" | "unavailable";
|
||||
/**
|
||||
* @description Exhaustive (SPEC §14.4.6).
|
||||
* @enum {string}
|
||||
@@ -1074,6 +1248,121 @@ export interface components {
|
||||
/** @description One result per mutation, in request order. */
|
||||
results: components["schemas"]["MutationResult"][];
|
||||
};
|
||||
/** @enum {string} */
|
||||
CardConnectionStatus: "active" | "expired" | "disconnected";
|
||||
/**
|
||||
* @description CP-14.
|
||||
* @enum {string}
|
||||
*/
|
||||
CardFetchError: "bank_unavailable" | "rate_limited" | "consent_expired" | "failed";
|
||||
/** @enum {string} */
|
||||
CardPurchaseStatus: "pending" | "booked";
|
||||
/** @enum {string} */
|
||||
CardDirection: "debit" | "credit";
|
||||
/** @enum {string} */
|
||||
CardAttemptStatus: "pending" | "connected" | "failed" | "expired";
|
||||
/** @enum {string} */
|
||||
CardClient: "android" | "web";
|
||||
/** @description An entry of the card catalogue (CP-1). `available`, the server can connect it for the caller (CP-13). */
|
||||
CardProvider: {
|
||||
/** @description Open string, never an enum. */
|
||||
id: string;
|
||||
name: string;
|
||||
issuer: string;
|
||||
image_key: string;
|
||||
available: boolean;
|
||||
};
|
||||
CardProviderList: {
|
||||
providers: components["schemas"]["CardProvider"][];
|
||||
};
|
||||
/** @description Only ever sent to its own user's devices. No session id, account uid or provider reference. */
|
||||
CardConnection: {
|
||||
id: components["schemas"]["Uuid"];
|
||||
provider_id: string;
|
||||
status: components["schemas"]["CardConnectionStatus"];
|
||||
history_days: number;
|
||||
valid_until: components["schemas"]["Timestamp"] | null;
|
||||
last_fetch_at: components["schemas"]["Timestamp"] | null;
|
||||
last_fetch_error: components["schemas"]["CardFetchError"] | null;
|
||||
/** @description A fetch for it is running at the time of the response. */
|
||||
fetching: boolean;
|
||||
created_at: components["schemas"]["Timestamp"];
|
||||
updated_at: components["schemas"]["Timestamp"];
|
||||
/**
|
||||
* Format: int64
|
||||
* @description From `card_sequence` (SPEC §14.1.3), not the household's `server_version`.
|
||||
*/
|
||||
card_version: number;
|
||||
};
|
||||
/** @description A tombstone has `deleted_at` set and its content erased (SPEC §14.1.3). */
|
||||
CardPurchase: {
|
||||
id: components["schemas"]["Uuid"];
|
||||
connection_id: components["schemas"]["Uuid"];
|
||||
status: components["schemas"]["CardPurchaseStatus"];
|
||||
direction: components["schemas"]["CardDirection"];
|
||||
purchase_date: components["schemas"]["Date"];
|
||||
booking_date: components["schemas"]["Date"] | null;
|
||||
/**
|
||||
* Format: int64
|
||||
* @description Null for a purchase in another currency than SEK and for a tombstone.
|
||||
*/
|
||||
amount_ore: number | null;
|
||||
/** @description The bank's decimal amount, for a purchase in another currency than SEK. */
|
||||
original_amount: string | null;
|
||||
/** @description ISO 4217, for a purchase in another currency than SEK. */
|
||||
original_currency: string | null;
|
||||
merchant: string;
|
||||
expense_id: components["schemas"]["Uuid"] | null;
|
||||
ignored: boolean;
|
||||
created_at: components["schemas"]["Timestamp"];
|
||||
updated_at: components["schemas"]["Timestamp"];
|
||||
deleted_at: components["schemas"]["Timestamp"] | null;
|
||||
/** Format: int64 */
|
||||
card_version: number;
|
||||
};
|
||||
/** @description `auth_url` only in the E-37 response (null in E-38); `connection` once connected. */
|
||||
CardAttempt: {
|
||||
id: components["schemas"]["Uuid"];
|
||||
provider_id: string;
|
||||
status: components["schemas"]["CardAttemptStatus"];
|
||||
/** @description `cancelled`, `no_account` or `bank_error` for a failed attempt, else null. */
|
||||
failure: string | null;
|
||||
auth_url: string | null;
|
||||
expires_at: components["schemas"]["Timestamp"];
|
||||
connection: components["schemas"]["CardConnection"] | null;
|
||||
};
|
||||
CreateCardAuthRequest: {
|
||||
provider_id: string;
|
||||
/** @description One of 7, 30, 90, 180 (SPEC §9.6). */
|
||||
history_days: number;
|
||||
client: components["schemas"]["CardClient"];
|
||||
};
|
||||
CardSyncResponse: {
|
||||
/** Format: int64 */
|
||||
since: number;
|
||||
/**
|
||||
* Format: int64
|
||||
* @description The highest `card_version` in the page, or `since` when the page is empty.
|
||||
*/
|
||||
next_since: number;
|
||||
has_more: boolean;
|
||||
server_time: components["schemas"]["Timestamp"];
|
||||
connections: components["schemas"]["CardConnection"][];
|
||||
purchases: components["schemas"]["CardPurchase"][];
|
||||
};
|
||||
PatchCardConnectionRequest: {
|
||||
/** @description One of 7, 30, 90, 180, at most the current value (CP-3). */
|
||||
history_days: number;
|
||||
};
|
||||
CardRefreshResponse: {
|
||||
connection: components["schemas"]["CardConnection"];
|
||||
/** @description False when the 60-second rule answered without contacting the bank, or the fetch is still running. */
|
||||
fetched: boolean;
|
||||
};
|
||||
PutCardPurchaseRequest: {
|
||||
expense_id: components["schemas"]["Uuid"] | null;
|
||||
ignored: boolean;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description `invalid_request` or `invite_invalid`. */
|
||||
@@ -1197,6 +1486,16 @@ export interface components {
|
||||
"application/json; charset=utf-8": components["schemas"]["Error"];
|
||||
};
|
||||
};
|
||||
/** @description `card_bank_unavailable` (the card service or the bank did not answer or refused). */
|
||||
BadGateway: {
|
||||
headers: {
|
||||
"X-Kvitt-Server-Version": components["headers"]["ServerVersion"];
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json; charset=utf-8": components["schemas"]["Error"];
|
||||
};
|
||||
};
|
||||
/** @description `unavailable` (database busy beyond `busy_timeout`, or server shutting down). */
|
||||
Unavailable: {
|
||||
headers: {
|
||||
@@ -1228,6 +1527,12 @@ export interface components {
|
||||
/** @description Not a UUID is 404 `not_found`. */
|
||||
AccountId: components["schemas"]["Uuid"];
|
||||
/** @description Not a UUID is 404 `not_found`. */
|
||||
AttemptId: components["schemas"]["Uuid"];
|
||||
/** @description Not a UUID is 404 `not_found`. */
|
||||
ConnectionId: components["schemas"]["Uuid"];
|
||||
/** @description Not a UUID is 404 `not_found`. */
|
||||
PurchaseId: components["schemas"]["Uuid"];
|
||||
/** @description Not a UUID is 404 `not_found`. */
|
||||
CategoryId: components["schemas"]["Uuid"];
|
||||
/** @description Not a UUID is 404 `not_found`. */
|
||||
TemplateId: components["schemas"]["Uuid"];
|
||||
@@ -2719,4 +3024,396 @@ export interface operations {
|
||||
503: components["responses"]["Unavailable"];
|
||||
};
|
||||
};
|
||||
listCardProviders: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header: {
|
||||
/** @description Sync protocol version. Missing or other than `1` on an authenticated endpoint is 426 `protocol_unsupported`. */
|
||||
"X-Kvitt-Protocol": components["parameters"]["Protocol"];
|
||||
/** @description The app's `versionCode`, sent by the app on every call. */
|
||||
"X-Kvitt-App-Version"?: components["parameters"]["AppVersion"];
|
||||
/** @description The app's `versionName`, sent by the app on every call; stored per device like `X-Kvitt-App-Version` (1 to 32 characters, anything else ignored). */
|
||||
"X-Kvitt-App-Version-Name"?: components["parameters"]["AppVersionName"];
|
||||
/** @description The device locale. */
|
||||
"Accept-Language"?: components["parameters"]["AcceptLanguage"];
|
||||
};
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description The catalogue. */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Kvitt-Server-Version": components["headers"]["ServerVersion"];
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json; charset=utf-8": components["schemas"]["CardProviderList"];
|
||||
};
|
||||
};
|
||||
401: components["responses"]["Unauthorized"];
|
||||
426: components["responses"]["ProtocolUnsupported"];
|
||||
429: components["responses"]["RateLimited"];
|
||||
500: components["responses"]["Internal"];
|
||||
503: components["responses"]["Unavailable"];
|
||||
};
|
||||
};
|
||||
createCardAuth: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header: {
|
||||
/** @description Sync protocol version. Missing or other than `1` on an authenticated endpoint is 426 `protocol_unsupported`. */
|
||||
"X-Kvitt-Protocol": components["parameters"]["Protocol"];
|
||||
/** @description The app's `versionCode`, sent by the app on every call. */
|
||||
"X-Kvitt-App-Version"?: components["parameters"]["AppVersion"];
|
||||
/** @description The app's `versionName`, sent by the app on every call; stored per device like `X-Kvitt-App-Version` (1 to 32 characters, anything else ignored). */
|
||||
"X-Kvitt-App-Version-Name"?: components["parameters"]["AppVersionName"];
|
||||
/** @description The device locale. */
|
||||
"Accept-Language"?: components["parameters"]["AcceptLanguage"];
|
||||
};
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json; charset=utf-8": components["schemas"]["CreateCardAuthRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description The pending attempt with `auth_url`. */
|
||||
201: {
|
||||
headers: {
|
||||
"X-Kvitt-Server-Version": components["headers"]["ServerVersion"];
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json; charset=utf-8": components["schemas"]["CardAttempt"];
|
||||
};
|
||||
};
|
||||
/** @description `invalid_request`. */
|
||||
400: components["responses"]["BadRequest"];
|
||||
401: components["responses"]["Unauthorized"];
|
||||
/** @description `not_found` (unknown provider). */
|
||||
404: components["responses"]["NotFound"];
|
||||
/** @description `cards_unavailable`. */
|
||||
409: components["responses"]["Conflict"];
|
||||
/** @description `validation_failed` (`history_days`). */
|
||||
422: components["responses"]["Unprocessable"];
|
||||
426: components["responses"]["ProtocolUnsupported"];
|
||||
429: components["responses"]["RateLimited"];
|
||||
500: components["responses"]["Internal"];
|
||||
502: components["responses"]["BadGateway"];
|
||||
503: components["responses"]["Unavailable"];
|
||||
};
|
||||
};
|
||||
getCardAuth: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header: {
|
||||
/** @description Sync protocol version. Missing or other than `1` on an authenticated endpoint is 426 `protocol_unsupported`. */
|
||||
"X-Kvitt-Protocol": components["parameters"]["Protocol"];
|
||||
/** @description The app's `versionCode`, sent by the app on every call. */
|
||||
"X-Kvitt-App-Version"?: components["parameters"]["AppVersion"];
|
||||
/** @description The app's `versionName`, sent by the app on every call; stored per device like `X-Kvitt-App-Version` (1 to 32 characters, anything else ignored). */
|
||||
"X-Kvitt-App-Version-Name"?: components["parameters"]["AppVersionName"];
|
||||
/** @description The device locale. */
|
||||
"Accept-Language"?: components["parameters"]["AcceptLanguage"];
|
||||
};
|
||||
path: {
|
||||
/** @description Not a UUID is 404 `not_found`. */
|
||||
attempt_id: components["parameters"]["AttemptId"];
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description The attempt. */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Kvitt-Server-Version": components["headers"]["ServerVersion"];
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json; charset=utf-8": components["schemas"]["CardAttempt"];
|
||||
};
|
||||
};
|
||||
401: components["responses"]["Unauthorized"];
|
||||
/** @description `not_found`. */
|
||||
404: components["responses"]["NotFound"];
|
||||
426: components["responses"]["ProtocolUnsupported"];
|
||||
429: components["responses"]["RateLimited"];
|
||||
500: components["responses"]["Internal"];
|
||||
503: components["responses"]["Unavailable"];
|
||||
};
|
||||
};
|
||||
getCardCallback: {
|
||||
parameters: {
|
||||
query?: {
|
||||
/** @description The one-time authorisation code. */
|
||||
code?: string;
|
||||
/** @description The attempt's state. */
|
||||
state?: string;
|
||||
/** @description Set by the bank when the user cancelled. */
|
||||
error?: string;
|
||||
};
|
||||
header?: {
|
||||
/** @description Sent by the app on every call; not enforced on unauthenticated endpoints. */
|
||||
"X-Kvitt-Protocol"?: components["parameters"]["ProtocolOptional"];
|
||||
/** @description The app's `versionCode`, sent by the app on every call. */
|
||||
"X-Kvitt-App-Version"?: components["parameters"]["AppVersion"];
|
||||
/** @description The app's `versionName`, sent by the app on every call; stored per device like `X-Kvitt-App-Version` (1 to 32 characters, anything else ignored). */
|
||||
"X-Kvitt-App-Version-Name"?: components["parameters"]["AppVersionName"];
|
||||
/** @description The device locale. */
|
||||
"Accept-Language"?: components["parameters"]["AcceptLanguage"];
|
||||
};
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description The callback page. */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Kvitt-Server-Version": components["headers"]["ServerVersion"];
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"text/html; charset=utf-8": string;
|
||||
};
|
||||
};
|
||||
429: components["responses"]["RateLimited"];
|
||||
500: components["responses"]["Internal"];
|
||||
503: components["responses"]["Unavailable"];
|
||||
};
|
||||
};
|
||||
cardSync: {
|
||||
parameters: {
|
||||
query: {
|
||||
/** @description The highest `card_version` the client has applied (0 initially). */
|
||||
since: number;
|
||||
/** @description Rows per page, 1 to 1000 (`invalid_request` otherwise). */
|
||||
limit?: number;
|
||||
};
|
||||
header: {
|
||||
/** @description Sync protocol version. Missing or other than `1` on an authenticated endpoint is 426 `protocol_unsupported`. */
|
||||
"X-Kvitt-Protocol": components["parameters"]["Protocol"];
|
||||
/** @description The app's `versionCode`, sent by the app on every call. */
|
||||
"X-Kvitt-App-Version"?: components["parameters"]["AppVersion"];
|
||||
/** @description The app's `versionName`, sent by the app on every call; stored per device like `X-Kvitt-App-Version` (1 to 32 characters, anything else ignored). */
|
||||
"X-Kvitt-App-Version-Name"?: components["parameters"]["AppVersionName"];
|
||||
/** @description The device locale. */
|
||||
"Accept-Language"?: components["parameters"]["AcceptLanguage"];
|
||||
};
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description One page. */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Kvitt-Server-Version": components["headers"]["ServerVersion"];
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json; charset=utf-8": components["schemas"]["CardSyncResponse"];
|
||||
};
|
||||
};
|
||||
/** @description `invalid_request`. */
|
||||
400: components["responses"]["BadRequest"];
|
||||
401: components["responses"]["Unauthorized"];
|
||||
/** @description `cursor_ahead`. */
|
||||
409: components["responses"]["Conflict"];
|
||||
426: components["responses"]["ProtocolUnsupported"];
|
||||
429: components["responses"]["RateLimited"];
|
||||
500: components["responses"]["Internal"];
|
||||
503: components["responses"]["Unavailable"];
|
||||
};
|
||||
};
|
||||
deleteCardConnection: {
|
||||
parameters: {
|
||||
query?: {
|
||||
/** @description Also delete linked and ignored purchases. Default false. */
|
||||
erase_handled?: boolean;
|
||||
};
|
||||
header: {
|
||||
/** @description Sync protocol version. Missing or other than `1` on an authenticated endpoint is 426 `protocol_unsupported`. */
|
||||
"X-Kvitt-Protocol": components["parameters"]["Protocol"];
|
||||
/** @description The app's `versionCode`, sent by the app on every call. */
|
||||
"X-Kvitt-App-Version"?: components["parameters"]["AppVersion"];
|
||||
/** @description The app's `versionName`, sent by the app on every call; stored per device like `X-Kvitt-App-Version` (1 to 32 characters, anything else ignored). */
|
||||
"X-Kvitt-App-Version-Name"?: components["parameters"]["AppVersionName"];
|
||||
/** @description The device locale. */
|
||||
"Accept-Language"?: components["parameters"]["AcceptLanguage"];
|
||||
};
|
||||
path: {
|
||||
/** @description Not a UUID is 404 `not_found`. */
|
||||
connection_id: components["parameters"]["ConnectionId"];
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description The disconnected connection. */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Kvitt-Server-Version": components["headers"]["ServerVersion"];
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json; charset=utf-8": components["schemas"]["CardConnection"];
|
||||
};
|
||||
};
|
||||
/** @description `invalid_request`. */
|
||||
400: components["responses"]["BadRequest"];
|
||||
401: components["responses"]["Unauthorized"];
|
||||
/** @description `not_found`. */
|
||||
404: components["responses"]["NotFound"];
|
||||
426: components["responses"]["ProtocolUnsupported"];
|
||||
429: components["responses"]["RateLimited"];
|
||||
500: components["responses"]["Internal"];
|
||||
503: components["responses"]["Unavailable"];
|
||||
};
|
||||
};
|
||||
patchCardConnection: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header: {
|
||||
/** @description Sync protocol version. Missing or other than `1` on an authenticated endpoint is 426 `protocol_unsupported`. */
|
||||
"X-Kvitt-Protocol": components["parameters"]["Protocol"];
|
||||
/** @description The app's `versionCode`, sent by the app on every call. */
|
||||
"X-Kvitt-App-Version"?: components["parameters"]["AppVersion"];
|
||||
/** @description The app's `versionName`, sent by the app on every call; stored per device like `X-Kvitt-App-Version` (1 to 32 characters, anything else ignored). */
|
||||
"X-Kvitt-App-Version-Name"?: components["parameters"]["AppVersionName"];
|
||||
/** @description The device locale. */
|
||||
"Accept-Language"?: components["parameters"]["AcceptLanguage"];
|
||||
};
|
||||
path: {
|
||||
/** @description Not a UUID is 404 `not_found`. */
|
||||
connection_id: components["parameters"]["ConnectionId"];
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json; charset=utf-8": components["schemas"]["PatchCardConnectionRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description The connection. */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Kvitt-Server-Version": components["headers"]["ServerVersion"];
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json; charset=utf-8": components["schemas"]["CardConnection"];
|
||||
};
|
||||
};
|
||||
/** @description `invalid_request`. */
|
||||
400: components["responses"]["BadRequest"];
|
||||
401: components["responses"]["Unauthorized"];
|
||||
/** @description `not_found`. */
|
||||
404: components["responses"]["NotFound"];
|
||||
/** @description `validation_failed`. */
|
||||
422: components["responses"]["Unprocessable"];
|
||||
426: components["responses"]["ProtocolUnsupported"];
|
||||
429: components["responses"]["RateLimited"];
|
||||
500: components["responses"]["Internal"];
|
||||
503: components["responses"]["Unavailable"];
|
||||
};
|
||||
};
|
||||
refreshCardConnection: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header: {
|
||||
/** @description Sync protocol version. Missing or other than `1` on an authenticated endpoint is 426 `protocol_unsupported`. */
|
||||
"X-Kvitt-Protocol": components["parameters"]["Protocol"];
|
||||
/** @description The app's `versionCode`, sent by the app on every call. */
|
||||
"X-Kvitt-App-Version"?: components["parameters"]["AppVersion"];
|
||||
/** @description The app's `versionName`, sent by the app on every call; stored per device like `X-Kvitt-App-Version` (1 to 32 characters, anything else ignored). */
|
||||
"X-Kvitt-App-Version-Name"?: components["parameters"]["AppVersionName"];
|
||||
/** @description The device locale. */
|
||||
"Accept-Language"?: components["parameters"]["AcceptLanguage"];
|
||||
};
|
||||
path: {
|
||||
/** @description Not a UUID is 404 `not_found`. */
|
||||
connection_id: components["parameters"]["ConnectionId"];
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description The connection after the fetch. */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Kvitt-Server-Version": components["headers"]["ServerVersion"];
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json; charset=utf-8": components["schemas"]["CardRefreshResponse"];
|
||||
};
|
||||
};
|
||||
401: components["responses"]["Unauthorized"];
|
||||
/** @description `not_found`. */
|
||||
404: components["responses"]["NotFound"];
|
||||
/** @description `card_consent_expired`. */
|
||||
409: components["responses"]["Conflict"];
|
||||
426: components["responses"]["ProtocolUnsupported"];
|
||||
429: components["responses"]["RateLimited"];
|
||||
500: components["responses"]["Internal"];
|
||||
503: components["responses"]["Unavailable"];
|
||||
};
|
||||
};
|
||||
putCardPurchase: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header: {
|
||||
/** @description Sync protocol version. Missing or other than `1` on an authenticated endpoint is 426 `protocol_unsupported`. */
|
||||
"X-Kvitt-Protocol": components["parameters"]["Protocol"];
|
||||
/** @description The app's `versionCode`, sent by the app on every call. */
|
||||
"X-Kvitt-App-Version"?: components["parameters"]["AppVersion"];
|
||||
/** @description The app's `versionName`, sent by the app on every call; stored per device like `X-Kvitt-App-Version` (1 to 32 characters, anything else ignored). */
|
||||
"X-Kvitt-App-Version-Name"?: components["parameters"]["AppVersionName"];
|
||||
/** @description The device locale. */
|
||||
"Accept-Language"?: components["parameters"]["AcceptLanguage"];
|
||||
};
|
||||
path: {
|
||||
/** @description Not a UUID is 404 `not_found`. */
|
||||
purchase_id: components["parameters"]["PurchaseId"];
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json; charset=utf-8": components["schemas"]["PutCardPurchaseRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description The purchase. */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Kvitt-Server-Version": components["headers"]["ServerVersion"];
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json; charset=utf-8": components["schemas"]["CardPurchase"];
|
||||
};
|
||||
};
|
||||
/** @description `invalid_request`. */
|
||||
400: components["responses"]["BadRequest"];
|
||||
401: components["responses"]["Unauthorized"];
|
||||
/** @description `not_found` (the purchase, or with `field` `expense_id` an unknown or deleted expense). */
|
||||
404: components["responses"]["NotFound"];
|
||||
/** @description `expense_already_linked`. */
|
||||
409: components["responses"]["Conflict"];
|
||||
/** @description `validation_failed`. */
|
||||
422: components["responses"]["Unprocessable"];
|
||||
426: components["responses"]["ProtocolUnsupported"];
|
||||
429: components["responses"]["RateLimited"];
|
||||
500: components["responses"]["Internal"];
|
||||
503: components["responses"]["Unavailable"];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@ import { computeSplit, type SplitSpec } from './split';
|
||||
/** Tests run from web/ (npm scripts), so the repository's contracts are one level up. */
|
||||
const DIR = resolve(process.cwd(), '..', 'contracts', 'test-vectors');
|
||||
/** Functions only the app implements (SPEC §14.5.4, §14.5.6); a web feature plan that ports one moves it below. */
|
||||
const NOT_PORTED: string[] = [];
|
||||
// match_card_purchase (SPEC §14.5.8): until K3 ports it.
|
||||
const NOT_PORTED: string[] = ['match_card_purchase'];
|
||||
const PORTED = [
|
||||
'compute_split',
|
||||
'compute_balances',
|
||||
|
||||
Reference in New Issue
Block a user