* fix(enable-banking): flatten Enable Banking's bank_transaction_code object to a string
Enable Banking serializes bank_transaction_code as {description, code,
sub_code}; three places declared it a string. The direct path passed the
object through, so PostgREST wrote its JSON text into
transactions.bank_transaction_code for every Enable Banking row since
2026-08-09 (6,356 rows, 78 companies) and the label/method derivation never
matched. The Connect service forwarded the same object and the wire contract
rejected it, so every connector-canary sync failed from 2026-09-03 (Capstone
support case 2026-09-07, "banksynken mot Nordea").
One rule, one place: normalizeBankTransactionCode in the connect-contract
file (code, code/sub_code, else description, else null), applied by
convertTransaction here and by Connect's normalizeBookedTransaction in the
mirrored contract. The wire schema stays z.string().nullable();
CONTRACT_VERSION bumps to 2026-09-08. A repair migration rewrites the stored
JSON text with the same rule and touches nothing else.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnbRMhwvUigizrPar5hW47
* fix(enable-banking): skip reset-source rows in the repair and read "Kortköp/uttag" as card
Skeptic findings on 4a30bb3f3:
- The repair migration would have aborted on prod: 110 of the 6,356 rows
belong to a migration-reset source company, whose transactions are
immutable by trigger (transactions_block_migration_reset_source_mutation).
Same failure as 20260903170000. Those rows are now excluded; nothing reads
the column back for an archived company.
- With the code description reaching the keyword tables as a string,
"Kortköp/uttag" (SEB/Swedbank wording for an ordinary card purchase)
matched UTTAG before KORT in both CODE_KEYWORD_METHODS and KEYWORD_LABELS,
so 256 card rows a month would have shown "Betalsätt: Uttag". Card now
precedes withdrawal in both tables (and in the Connect mirror), matching
what TRAILING_PHRASES already says about the same phrase.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnbRMhwvUigizrPar5hW47
* chore(migrations): annotate the repair as pg-test skip and state why no rattelse log is owed
coverage-gate flagged the migration because it creates a function; the only
function is a pg_temp helper dropped in the same statement batch, and a
one-shot UPDATE cannot be re-exercised after apply, so the annotation is the
honest disposition. The header also answers the Swedish compliance review:
the column is a write-once ingest projection with no reader, the underlag is
the archived raw PSD2 page (untouched), and the verifikat lives in
journal_entries, which the statement never reads or writes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RnbRMhwvUigizrPar5hW47
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
@accounted/connect-contract
The wire contract between an Accounted ledger installation (the hosted service or a self-hosted instance) and Accounted Connect, the service that operates the provider integrations only Accounted can run: bank feeds through its PSD2 credentials, the Skatteverket API client, the Peppol access point, company lookup, the migration sources.
This package is shape only: constants, Zod schemas and the TypeScript types inferred from them. There is no behaviour and no provider code in it. Both sides of the connection validate with the same schemas so they cannot drift apart, and the package is MIT so that anyone may implement either side.
What is in it:
- key prefix, header names and the entitlements path an installation uses;
- the entitlements and sync-report payloads of the hourly key sync;
- the error envelope and the stable error codes the service answers with;
- the Peppol operations (
/api/connect/peppol/*): request and response schemas plus the operation table (method, path, company header required).
Versioning: CONTRACT_VERSION is a date. Fields are only ever added; a
breaking change is a new operation or family, never a changed one.
Use
import { peppolSubmissionSchema, PEPPOL_OPERATIONS, CONTRACT_VERSION } from '@accounted/connect-contract'
const parsed = peppolSubmissionSchema.safeParse(body)
if (!parsed.success) return badRequest(parsed.error)
Inside the Accounted repository the package is consumed from source through a path alias. To publish, build from the repository root with the root dependencies installed:
npx tsc -p packages/connect-contract/tsconfig.json
cd packages/connect-contract && npm publish --access public