Commit Graph
3 Commits
Author SHA1 Message Date
MattssonandClaude Fable 5.1 477b59453f fix(enable-banking): flatten Enable Banking's bank_transaction_code object to a string (#2398)
* 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>
2026-09-08 00:37:23 +02:00
4c6feea64d feat(connect): bank sync through the connector operation, with a per-company canary (#2205)
* feat(connect): bank sync through the connector operation, with a per-company canary

In connector mode the enable-banking sync no longer pages Enable Banking on
the instance: it calls POST /api/connect/bank/sync on the hosted service with
the session id it holds and the account, and receives booked, normalized
rows plus the raw provider pages to archive. Everything downstream is shared
with the direct path (stored external ids computed here from booking_date,
amount and the account scope; ingest; archive; balance refresh), so a company
that moves to the connector produces byte-identical keys. A 410 from the
service maps onto the same SessionExpiredError the direct path throws.

bankConnectorMode(companyId) gains CONNECT_BANK_CANARY_COMPANIES: listed
companies use the connector even while the installation has its own Enable
Banking credentials, which is how hosted Accounted moves its bank sync to
Connect a few companies at a time before dropping its keys. The contract
package gains the bank sync request/response schemas (2026-09-03).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>

* fix(connect): calendar-valid dates, body read inside the timeout, service origin as the default

Review follow-ups on #2205. The contract validates date_from, date_to and
booking_date with z.iso.date() (2026-02-30 and an empty booking date are
refused; the installation derives its stored keys from booking_date). The
connector sync reads the response body inside the timeout window so a
service that stalls the body cannot hold the sync open. DEFAULT_CONNECT_BASE_URL
now names the connector service (connect.accounted.se), which is where the
sync operation exists; the hosted app's copy of the connector routes is
legacy and hosted Accounted itself sets GNUBOK_CONNECT_URL explicitly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>

---------

Signed-off-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 11:06:47 +02:00
fcfa1ba974 feat(connect): extract the connector wire contract into packages/connect-contract (#2179)
* feat(connect): extract the connector wire contract into packages/connect-contract

The definitions in lib/connect/contract.ts (key prefix, headers, entitlements
path, entitlement and sync-report shapes) move into a standalone MIT package,
packages/connect-contract (published as @accounted/connect-contract), joined
by the error envelope, the stable error codes, and Zod schemas for every
Peppol connector operation (lookup, submit, status, evidence, register,
unregister, inbound list and xml) with an operation table. Shape only: no
behaviour, no provider code. In-repo callers import through the tsconfig and
vitest alias; lib/connect/contract.ts re-exports so nothing else changes.
The point of the package is that either side of the connection can be built
outside this repository: a self-hosted ledger talking to Accounted Connect,
or another connector service talking to the open ledger.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>

* docs(decisions): record the Connect direction, the Peppol proxy shape, and the contract package

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>

---------

Signed-off-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 20:57:47 +02:00