You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
DFX now holds corporate accounts at Bank Frick AG (Balzers, LI). Integrate Bank Frick as a new bank in the API, on par with the existing Olkypay / Yapeal / Raiffeisen integrations:
Registry — register Bank Frick in the bank registry so it can be selected for receiving and sending.
Inbound — import incoming account statements into bank_tx (customer deposits) via Bank Frick's WebAPI.
Outbound — send fiat payouts through Bank Frick.
Bank Frick exposes a REST WebAPI (public docs: https://developers.bankfrick.li/api_docs/#introduction). Two mechanisms are new to our codebase: a two‑stage JWT auth and a per‑request RSA‑SHA512 body signature. Everything else (camt.053 parsing, pain.001 generation, the poll→bank_tx pipeline, the fiat‑output dispatch) can reuse existing building blocks.
This issue is written so it can be implemented from public sources only (this repo + the public Bank Frick API docs). All secret values (API key, RSA private key, concrete IBANs / customer number) are provided separately by the DFX team — see What the DFX team provides. Do not put any secret or concrete IBAN/customer number into code, tests, or commits.
Bank Frick WebAPI — the relevant facts (from the public docs)
Base URL:https://olb.bankfrick.li/webapi/ — API version v2 (paths are /v2/...). Test/sandbox hosts exist (olbtest, olbsandbox).
Auth, stage 1 — JWT:POST /v2/authorize with { "key": "<api-key>", "password": "<optional>" } → returns a JWT. Send it on every subsequent call as Authorization: Bearer <jwt>. JWT lifetime is bounded by the API‑key expiry.
Auth, stage 2 — request signing:Every request body must be signed with the client's RSA private key and sent as headers:
Signature: Base64 of RSA-SHA512(body) (PKCS#1 v1.5; RSA‑PSS is not accepted)
algorithm: rsa-sha512 (default; rsa-sha384 / rsa-sha256 also supported)
The matching public key is uploaded once in the Bank Frick online‑banking GUI ("SSH‑Schlüssel" / Manage Key). The POST /v2/authorize request itself must already be signed — signing precedes the first token.
Endpoints we need:
GET /v2/accounts and GET /v2/accounts/{customer}/{account} — accounts & balances
GET /v2/accounts/{customer}/{account}/transactions — transaction history (Frick JSON)
GET /v2/camt.053 — full statement (ISO‑20022 camt.053 XML), GET /v2/camt.052 — intraday
PUT /v2/pain.001 — payment initiation (ISO‑20022 pain.001 XML)
POST /v2/requestTan + POST /v2/signTransactionWithTan — TAN challenge & approval for payments; POST /v2/signTransactionWithoutTan exists but requires backend enablement
Constraints: optional IP whitelist on the API token; TAN release is required to approve payments unless the token/beneficiary is backend‑exempted; prepared transactions auto‑expire after 7 days. Transaction lifecycle: PREPARED → IN_PROGRESS → EXECUTED → BOOKED (or REJECTED/EXPIRED/DELETED).
How DFX models banks today (orientation)
There is no generic multi‑bank dispatcher — each bank is wired individually. Three existing patterns:
Bank
Auth
Inbound (statements)
Outbound (payout)
Olkypay
OAuth2 token in header, refresh on 403
Poll via cron + lastBankOlkyDate watermark
proprietary order/vir, two‑step approval (TO_VALIDATE → status poll)
Yapeal
static x-api-key + mTLS client cert
Webhook (camt.054 → RxJS Subject → handler)
REST pain.001 (JSON), no TAN
Raiffeisen
EBICS (keys AES‑encrypted in DB)
camt via EBICS (client exists)
pain.001 XML via EBICS — dead code, never called
Maerki Baumann
—
SEPA file upload (POST /bankTx)
manual/out‑of‑band file upload, only detected via a log entry
BankTxService.create() (bank-tx.service.ts:299) — dedup on accountServiceRef (ConflictException swallowed by callers = "already imported").
Closest template for the new client: Olkypay (src/integration/bank/services/olkypay.service.ts) — token‑in‑header + refresh maps cleanly onto Frick's JWT lifecycle, and its poll‑with‑watermark inbound is the safest fit. Use Yapeal (yapeal.service.ts) as the template for the REST payout method shape, but do not copy Yapeal's auth (static key + mTLS) — Frick uses JWT + body signature, no client cert.
Design decision: reuse the Olkypay poll pattern for inbound
Reasons: Frick's JWT‑bearer lifecycle mirrors Olkypay's token‑in‑header + refresh; Frick's statement read is a GET camt.053 pull (same spirit as Olkypay reporting/ecritures and Raiffeisen EBICS C53); it plugs straight into the existing, idempotent checkBankTx cron with a lastBankFrickDate watermark and create() dedup, without exposing a new inbound webhook surface; and polling is self‑healing (a missed tick recovers on the next run — a missed webhook is lost forever), which matches our fail‑closed backend doctrine. Frick's instant-transaction-notification-rules webhook, if used at all, should only be a latency trigger that kicks the same poll — never the source of truth (Phase 3, optional).
Scope (phased)
Ship behind a DisabledProcess kill‑switch and default the payout rail off until credentials + reconciliation are verified in production (same as the Yapeal/Olky rollout).
Phase 0 — Registry & configuration
Add FRICK = 'Bank Frick' to IbanBankName (src/subdomains/supporting/bank/bank/dto/bank.dto.ts:17).
⚠️ Beware name collisions: 'Bank Frick (SIC/SEPA) International' already exists as Kraken correspondence‑bank method labels in log-job.service.ts, and sift.dto.ts has FRICK = '$frick'. Those are unrelated to our house bank — do not reuse or overwrite them. Decide the enum value with the team ('Bank Frick' vs. a suffixed value, see the CT/Operating decision below).
Add a frick block to Config.bank (src/config/config.ts:1181, next to olkypay/raiffeisen/yapeal), e.g.:
frick: {baseUrl: process.env.FRICK_BASE_URL,// https://olb.bankfrick.li/webapi/v2apiKey: process.env.FRICK_API_KEY,privateKey: process.env.FRICK_PRIVATE_KEY?.split('<br>').join('\n'),// PEM, <br>-encoded (same convention as YAPEAL_CERT, config.ts:1213)customer: process.env.FRICK_CUSTOMER,// customer number (provided by DFX team)},
Do not add ?? default fallbacks — fail loud when unconfigured (see Gotchas).
Bank rows. Add the Frick account rows to migration/seed/bank.csv (loc seed) and as a hand‑written data migration (INSERT INTO "bank" ...) because prod DBs are not re‑seeded (seed.js is loc‑only; no migration/enum inserts bank rows today). Fields per row: name=<Frick enum>, bic='BFRILI22', currency='CHF'|'EUR', iban=<provided by DFX team>, receive, send, sctInst, amlEnabled=true. Use placeholders / the DFX‑team‑provided values — do not commit real IBANs if the repo policy is to keep them out; confirm with the team.
(Optional, only if Frick balances must feed totalBalanceChf / the financial log) add Blockchain.BANK_FRICK (src/integration/blockchain/shared/enums/blockchain.enum.ts), extend blockchainToBankName() in bank.service.ts, create CHF/EUR liquidity assets, and add a case IbanBankName.FRICK in the balance adapter bank.adapter.ts. Without this, Frick balance falls into the default branch (derived from the last bank_tx_batch).
@Injectable()exportclassBankFrickService{privatereadonlylogger=newDfxLogger(BankFrickService);privateaccessToken?: string;privatetokenExpiry?: Date;constructor(privatereadonlyhttp: HttpService){}isAvailable(): boolean{/* !!Config.bank.frick.apiKey && !!Config.bank.frick.privateKey — fail loud otherwise */}// --- ACCOUNT / BALANCE ---asyncgetBalances(): Promise<{iban: string;currency: string;balance: number}[]>// GET /v2/accounts// --- TRANSACTIONS (READ / POLL) ---asyncgetFrickTransactions(lastModificationTime: Date,accountIban: string): Promise<Partial<BankTx>[]>{// GET /v2/camt.053 (XML) -> Iso20022Service.parseCamt053Xml(xml, accountIban) -> map with parseTransaction()}privateparseTransaction(tx: CamtTransaction,accountIban: string): Partial<BankTx>// template: raiffeisen.service.ts parseTransaction// --- AUTH + SIGNING ---privateasynccallApi<T>(path: string,method: Method,body?: any): Promise<T>{// 1. ensure valid JWT (authorize() if missing/expired)// 2. serialize body ONCE to the exact string that is sent// 3. sign that exact string: Util.createSign(bodyString, Config.bank.frick.privateKey, 'sha512', 'base64')// 4. headers: Authorization: Bearer <jwt>, Signature: <sig>, algorithm: 'rsa-sha512'// 5. on 401 -> authorize() once and retry (Olkypay 403-retry pattern)}privateasyncauthorize(): Promise<void>// POST /v2/authorize {key: apiKey} — SIGNED — cache token + expiry}
Add a frick.dto.ts for Frick JSON shapes (auth response, account) if the JSON transactions endpoint is used instead of camt.053 (see open questions).
Do not create a recipient entity (Olkypay's OlkyRecipient is Olky‑specific; Frick takes the full creditor inline in the payment body).
Register BankFrickService in src/integration/bank/bank.module.ts under providersandexports (one‑module rule).
Wire the poll into BankTxService:
inject BankFrickService into the constructor (bank-tx.service.ts, next to olkyService);
add a checkFrickTransactions() modeled on checkTransactions() (bank-tx.service.ts:171): read watermark lastBankFrickDate (default new Date(0)), resolve the account IBAN via bankService.getBankInternal(IbanBankName.FRICK, 'CHF'|'EUR') (skip‑with‑warn if not configured, like the Olky guard at :181), fetch, loop this.create(tx) (swallow ConflictException), and advance the watermark only when length > 0 (self‑healing, :199);
call it from the checkBankTx() cron (bank-tx.service.ts:124, @DfxCron(EVERY_30_SECONDS, { process: Process.BANK_TX })).
Iterate over all Frick account IBANs that should receive (loop per account).
Imported bank_tx then flow through the existing assignTransactions() → updateInternal() → fillBankTx() chain unchanged (deposit → buy‑crypto matching). No changes needed there.
Phase 2 — Outbound: fiat payout
Payout is driven by FiatOutputJobService.fillFiatOutput() (src/subdomains/supporting/fiat-output/fiat-output-job.service.ts:59), sequence at :63–67: createBatches → transmitYapealPayments → transmitOlkypayPayments → searchOutgoingBankTx.
Add transmitFrickPayments() modeled on transmitYapealPayments() (:381): build a Pain001Payment and call bankFrickService.sendPayment().
sendPayment() → PUT /v2/pain.001 reusing Iso20022Service.createPain001Xml(payment)orPUT /v2/transactions with a Frick JSON body (decision below). Set debtor.bic = 'BFRILI22' — createPain001Xml renders <BIC>undefined</BIC> if it is missing.
TAN handling — the key decision. If Frick requires interactive TAN per payment, full automation is impossible and the rail must behave like Olkypay's two‑step model: transmit submits + stores a frickOrderId/frickTxId and sets isTransmittedDate; a separate approval step sets isApprovedDate. There is already a blueprint: checkOlkypayOrderStatus() (:71) polls TO_VALIDATE orders. Map Frick's requestTan/signTransactionWithTan onto the same isTransmittedDate/isApprovedDate/isConfirmedDate fields on FiatOutput. If a technical‑user/whitelisted‑beneficiary exemption or a batch‑wide TAN is available, a fully/near‑automated rail (Yapeal‑style) is possible instead.
Migration: add frickTxId (and frickOrderId if two‑step) columns to FiatOutput (fiat-output.entity.ts), analogous to yapealMsgId/olkyOrderId.
Routing: exclude FRICK from createBatches() (like YAPEAL/OLKY); include it in the setReadyDate() pending/liquidity filter; extend the EUR routing guard in setReadyDate (today EUR that is not Olky is skipped) so Frick EUR payouts (if any) become ready.
Reconciliation is rail‑independent: searchOutgoingBankTx() (:504) matches the outgoing camt bank_tx by remittanceInfo/endToEndId and only then sets isComplete. Ensure the Frick statement feed echoes the endToEndId/remittance so the match succeeds — otherwise the FiatOutput never completes.
Guard the whole payout rail behind a DisabledProcess flag (default disabled).
Phase 3 — Instant notifications (optional, later)
Register a Frick instant-transaction-notification-rule and add a frick-webhook.controller.ts (@ApiExcludeEndpoint, header‑secret validation like YapealWebhookController) that only triggers the Phase‑1 poll for lower latency. Keep polling as the source of truth.
The signing helper (critical detail)
Frick verifies the signature over the exact bytes it receives. If the string you sign differs from the string axios transmits (whitespace, key order, re‑serialization), every call returns 401.
Build the request body as a fixed string, sign that string, and send that string:
constbodyString=JSON.stringify(payload);constsignature=Util.createSign(bodyString,Config.bank.frick.privateKey,'sha512','base64');// send bodyString as the raw body + headers { Signature: signature, algorithm: 'rsa-sha512' }
The POST /v2/authorize body must be signed too (signing happens before the first token).
For GET requests (no body), confirm Frick's rule for body‑less requests (empty string vs. no signature) — see open questions.
EbicsKeyEncryptor (AES, symmetric) is not usable here — this is asymmetric RSA signing.
Gotchas (pulled from the code)
One‑bank‑per‑(name, currency) assumption.getBankInternal keys on `${name}-${currency}` and BankService's static ibanCache keeps the first row on collision. Two Frick CHF rows (CT + Operating) under the same name break selection. The DB unique key is (iban, bic) — not(name, currency) — so the DB happily accepts both; the break is purely in code. → Either give CT and Operating distinct enum names, or keep only the customer‑transaction (CT) accounts as bank entities and treat Operating as pure liquidity accounts.
name is varchar(256) but code compares against exact enum strings. A free‑text name without the enum entry is ignored by isCountryEnabled, blockchainToBankName, getBankInternal, and the balance‑adapter switch (falls through to default / return true).
Prod rows need a data migration. No migration and no code inserts bank rows today; seed.js/bank.csv is loc‑only.
JWT is runtime‑derived, not a stored secret — never put it in config.ts; cache in memory with expiry and refresh on 401.
PEM <br> decoding is mandatory for FRICK_PRIVATE_KEY (.split('<br>').join('\n')), else crypto cannot parse the key and every signature fails.
No silent fallbacks (CONTRIBUTING + repo policy): fail loud (throw / ServiceUnavailableException) when Frick is unconfigured — mirror Yapeal's "not configured" behavior. Keep at least a logger.error on poll failures; don't swallow silently.
createPain001Xml is hard‑wired to SvcLvl Cd=SEPA, NbOfTxs=1. FOREIGN / QR‑Bill / LI‑domestic CHF (SIC) may need a different payment type or the PUT /v2/transactions endpoint instead.
pain.001 version: the generator emits pain.001.001.03. Verify Frick accepts it (some LI/CH banks require .09).
camt.052 shares the camt.053 parser, which hard‑sets status=BOOKED. Intraday pending entries would be mislabeled — use camt.053 for booked statements.
LOC mock:HttpService mocks external hosts by regex in the LOC environment — add olb.bankfrick.li to the mock patterns or local calls return mock/empty.
LI is domestic (Config.isDomesticIban covers CH/LI), and the new Frick IBANs are auto‑blocked as DFX‑own IBANs (is-dfx-iban.validator.ts) once the rows exist — intended.
Open questions / decisions needed
These affect the shape of the implementation; the DFX team should confirm (some require checking against the Frick test environment):
TAN automatability — is TAN required per payment, or is there a batch‑wide TAN / technical‑user or whitelisted‑beneficiary exemption? Determines auto (Yapeal‑style) vs. submit‑then‑approve (Olky‑style) vs. manual (Maerki‑style) payout rail. Does the RSA request signature already count as the second factor for API‑submitted payments?
Inbound source — read booked statements via GET /v2/camt.053 (reuses parseCamt053Xml) or via the Frick JSON GET /v2/accounts/{customer}/{account}/transactions (needs a frick.dto.ts + custom mapping)? Recommended: camt.053 for reuse.
Outbound format — PUT /v2/pain.001 (reuse createPain001Xml) or PUT /v2/transactions (Frick JSON, needed for FOREIGN/QR‑Bill)? And which pain.001 version does Frick accept?
Account roles / flags — which IBAN is receive (customer deposits) vs. send (payout)? Does Frick support SEPA‑Instant / SIC‑Instant (sctInst)? There are 4 accounts: CHF/EUR × customer‑transaction (CT) + operating.
CT vs. Operating naming — one name with per‑currency rows, or distinct names per account role (resolves the (name,currency) collision)?
Currency ownership — does Frick handle CHF, EUR, or both, and does it replace or complement Yapeal (CHF) / Olkypay (EUR)? Determines getSenderBank priority when multiple send=true banks exist per currency.
Liquidity balance — must Frick balances feed totalBalanceChf / financial log (→ new Blockchain.BANK_FRICK, liquidity assets, balance‑adapter case)?
GET signing rule — how does Frick expect body‑less (GET) requests to be signed?
Reconciliation channel — does the outgoing statement feed reliably echo endToEndId/remittance for searchOutgoingBankTx to match?
What the DFX team provides (not part of this issue)
Handled privately by the DFX team; the implementer only wires the env‑var names:
Env values (set in the deployment environment): FRICK_API_KEY, FRICK_PRIVATE_KEY (PEM, <br>‑encoded), FRICK_BASE_URL, FRICK_CUSTOMER.
Key registration: the matching RSA public key uploaded in the Bank Frick OLB; the API access token created (with IP whitelist for the API host and TAN‑release configuration).
Bank rows in prod: the concrete IBANs, customer/contact numbers inserted via the data migration (values supplied to the implementer or applied by DFX directly).
Acceptance criteria
Build gates pass: npm run format, npm run lint, npm run type-check, npm test.
4‑point PR checklist satisfied: migration (bank rows + FiatOutput columns), env/infra (.env.example + config), service updates, frontend sync if any API contract changes.
Phase 1: a Frick camt.053 statement (test env / fixture) imports into bank_tx with correct accountIban, dedup on accountServiceRef, watermark advancing only on non‑empty fetch.
Phase 2: a payout produces a signed, authorized Frick request; the TAN/approval flow matches the chosen model; searchOutgoingBankTx reconciles it to isComplete. Rail behind a default‑off DisabledProcess flag.
Unit tests for the signing helper (exact‑body signature) and the camt.053 → bank_tx mapping.
No silent fallbacks; secrets never logged or returned in DTOs.
Reference: files to mirror / touch
File
Why
src/integration/bank/services/olkypay.service.ts
Primary template for BankFrickService (token auth + refresh, poll, parseTransaction)
src/integration/bank/services/yapeal.service.ts
Template for the REST payout method shape (sendPayment via pain.001) — not the auth
Goal
DFX now holds corporate accounts at Bank Frick AG (Balzers, LI). Integrate Bank Frick as a new bank in the API, on par with the existing Olkypay / Yapeal / Raiffeisen integrations:
bank_tx(customer deposits) via Bank Frick's WebAPI.Bank Frick exposes a REST WebAPI (public docs: https://developers.bankfrick.li/api_docs/#introduction). Two mechanisms are new to our codebase: a two‑stage JWT auth and a per‑request RSA‑SHA512 body signature. Everything else (camt.053 parsing, pain.001 generation, the poll→
bank_txpipeline, the fiat‑output dispatch) can reuse existing building blocks.Bank Frick WebAPI — the relevant facts (from the public docs)
https://olb.bankfrick.li/webapi/— API version v2 (paths are/v2/...). Test/sandbox hosts exist (olbtest,olbsandbox).POST /v2/authorizewith{ "key": "<api-key>", "password": "<optional>" }→ returns a JWT. Send it on every subsequent call asAuthorization: Bearer <jwt>. JWT lifetime is bounded by the API‑key expiry.Signature: Base64 ofRSA-SHA512(body)(PKCS#1 v1.5; RSA‑PSS is not accepted)algorithm:rsa-sha512(default;rsa-sha384/rsa-sha256also supported)POST /v2/authorizerequest itself must already be signed — signing precedes the first token.GET /v2/accountsandGET /v2/accounts/{customer}/{account}— accounts & balancesGET /v2/accounts/{customer}/{account}/transactions— transaction history (Frick JSON)GET /v2/camt.053— full statement (ISO‑20022 camt.053 XML),GET /v2/camt.052— intradayPUT /v2/pain.001— payment initiation (ISO‑20022 pain.001 XML)PUT /v2/transactions— create payment orders (SEPA / SEPA‑Instant / FOREIGN / QR‑Bill);GET/DELETEvariantsPOST /v2/requestTan+POST /v2/signTransactionWithTan— TAN challenge & approval for payments;POST /v2/signTransactionWithoutTanexists but requires backend enablement…/instant-transaction-notification-rules— webhook rules (optional, Phase 3)PREPARED → IN_PROGRESS → EXECUTED → BOOKED(orREJECTED/EXPIRED/DELETED).How DFX models banks today (orientation)
There is no generic multi‑bank dispatcher — each bank is wired individually. Three existing patterns:
lastBankOlkyDatewatermarkorder/vir, two‑step approval (TO_VALIDATE→ status poll)x-api-key+ mTLS client certSubject→ handler)pain.001(JSON), no TANPOST /bankTx)Reusable building blocks (do not rebuild):
Iso20022Service(src/integration/bank/services/iso20022.service.ts) — static, transport‑agnostic:parseCamt053Xml(xml, accountIban): CamtTransaction[](also used for camt.052)createPain001Xml(payment: Pain001Payment): string(pain.001.001.03, SEPA) andcreatePain001Json(...)Util.createSign(data, key, algo='sha256', encoding='base64')(src/shared/utils/util.ts:457) — wraps Nodecrypto.createSign; call it with'sha512'for Frick.Util.verifySignis the counterpart. Reference usage:payment-webhook.service.ts(body → sign → header).HttpService(src/shared/services/http.service.ts) — axios wrapper with retry + LOC mock mode.BankService(src/subdomains/supporting/bank/bank/bank.service.ts) —getBank({currency,paymentMethod}),getSenderBank(currency),getBankByIban(iban),getBankInternal(name,currency).BankTxService.create()(bank-tx.service.ts:299) — dedup onaccountServiceRef(ConflictExceptionswallowed by callers = "already imported").Closest template for the new client: Olkypay (
src/integration/bank/services/olkypay.service.ts) — token‑in‑header + refresh maps cleanly onto Frick's JWT lifecycle, and its poll‑with‑watermark inbound is the safest fit. Use Yapeal (yapeal.service.ts) as the template for the REST payout method shape, but do not copy Yapeal's auth (static key + mTLS) — Frick uses JWT + body signature, no client cert.Design decision: reuse the Olkypay poll pattern for inbound
Reasons: Frick's JWT‑bearer lifecycle mirrors Olkypay's token‑in‑header + refresh; Frick's statement read is a
GET camt.053pull (same spirit as Olkypayreporting/ecrituresand Raiffeisen EBICS C53); it plugs straight into the existing, idempotentcheckBankTxcron with alastBankFrickDatewatermark andcreate()dedup, without exposing a new inbound webhook surface; and polling is self‑healing (a missed tick recovers on the next run — a missed webhook is lost forever), which matches our fail‑closed backend doctrine. Frick'sinstant-transaction-notification-ruleswebhook, if used at all, should only be a latency trigger that kicks the same poll — never the source of truth (Phase 3, optional).Scope (phased)
Ship behind a
DisabledProcesskill‑switch and default the payout rail off until credentials + reconciliation are verified in production (same as the Yapeal/Olky rollout).Phase 0 — Registry & configuration
FRICK = 'Bank Frick'toIbanBankName(src/subdomains/supporting/bank/bank/dto/bank.dto.ts:17).'Bank Frick (SIC/SEPA) International'already exists as Kraken correspondence‑bank method labels inlog-job.service.ts, andsift.dto.tshasFRICK = '$frick'. Those are unrelated to our house bank — do not reuse or overwrite them. Decide the enum value with the team ('Bank Frick'vs. a suffixed value, see the CT/Operating decision below).frickblock toConfig.bank(src/config/config.ts:1181, next toolkypay/raiffeisen/yapeal), e.g.:?? defaultfallbacks — fail loud when unconfigured (see Gotchas).FRICK_BASE_URL,FRICK_API_KEY,FRICK_PRIVATE_KEY,FRICK_CUSTOMERto.env.example(PR‑checklist item 2). Leave values blank.migration/seed/bank.csv(loc seed) and as a hand‑written data migration (INSERT INTO "bank" ...) because prod DBs are not re‑seeded (seed.jsis loc‑only; no migration/enum inserts bank rows today). Fields per row:name=<Frick enum>,bic='BFRILI22',currency='CHF'|'EUR',iban=<provided by DFX team>,receive,send,sctInst,amlEnabled=true. Use placeholders / the DFX‑team‑provided values — do not commit real IBANs if the repo policy is to keep them out; confirm with the team.totalBalanceChf/ the financial log) addBlockchain.BANK_FRICK(src/integration/blockchain/shared/enums/blockchain.enum.ts), extendblockchainToBankName()inbank.service.ts, create CHF/EUR liquidity assets, and add acase IbanBankName.FRICKin the balance adapterbank.adapter.ts. Without this, Frick balance falls into the default branch (derived from the lastbank_tx_batch).Phase 1 — Inbound: statement import (deposits)
src/integration/bank/services/frick.service.ts—@Injectable,DfxLogger, injectsHttpService. Mirror Olkypay's structure. Sketch:frick.dto.tsfor Frick JSON shapes (auth response, account) if the JSONtransactionsendpoint is used instead of camt.053 (see open questions).OlkyRecipientis Olky‑specific; Frick takes the full creditor inline in the payment body).BankFrickServiceinsrc/integration/bank/bank.module.tsunderprovidersandexports(one‑module rule).BankTxService:BankFrickServiceinto the constructor (bank-tx.service.ts, next toolkyService);checkFrickTransactions()modeled oncheckTransactions()(bank-tx.service.ts:171): read watermarklastBankFrickDate(defaultnew Date(0)), resolve the account IBAN viabankService.getBankInternal(IbanBankName.FRICK, 'CHF'|'EUR')(skip‑with‑warn if not configured, like the Olky guard at:181), fetch, loopthis.create(tx)(swallowConflictException), and advance the watermark only whenlength > 0(self‑healing,:199);checkBankTx()cron (bank-tx.service.ts:124,@DfxCron(EVERY_30_SECONDS, { process: Process.BANK_TX })).bank_txthen flow through the existingassignTransactions()→updateInternal()→fillBankTx()chain unchanged (deposit → buy‑crypto matching). No changes needed there.Phase 2 — Outbound: fiat payout
Payout is driven by
FiatOutputJobService.fillFiatOutput()(src/subdomains/supporting/fiat-output/fiat-output-job.service.ts:59), sequence at:63–67:createBatches → transmitYapealPayments → transmitOlkypayPayments → searchOutgoingBankTx.transmitFrickPayments()modeled ontransmitYapealPayments()(:381): build aPain001Paymentand callbankFrickService.sendPayment().sendPayment()→PUT /v2/pain.001reusingIso20022Service.createPain001Xml(payment)orPUT /v2/transactionswith a Frick JSON body (decision below). Setdebtor.bic = 'BFRILI22'—createPain001Xmlrenders<BIC>undefined</BIC>if it is missing.transmitsubmits + stores africkOrderId/frickTxIdand setsisTransmittedDate; a separate approval step setsisApprovedDate. There is already a blueprint:checkOlkypayOrderStatus()(:71) pollsTO_VALIDATEorders. Map Frick'srequestTan/signTransactionWithTanonto the sameisTransmittedDate/isApprovedDate/isConfirmedDatefields onFiatOutput. If a technical‑user/whitelisted‑beneficiary exemption or a batch‑wide TAN is available, a fully/near‑automated rail (Yapeal‑style) is possible instead.frickTxId(andfrickOrderIdif two‑step) columns toFiatOutput(fiat-output.entity.ts), analogous toyapealMsgId/olkyOrderId.FRICKfromcreateBatches()(likeYAPEAL/OLKY); include it in thesetReadyDate()pending/liquidity filter; extend the EUR routing guard insetReadyDate(today EUR that is not Olky is skipped) so Frick EUR payouts (if any) become ready.searchOutgoingBankTx()(:504) matches the outgoing camtbank_txbyremittanceInfo/endToEndIdand only then setsisComplete. Ensure the Frick statement feed echoes theendToEndId/remittance so the match succeeds — otherwise theFiatOutputnever completes.DisabledProcessflag (default disabled).Phase 3 — Instant notifications (optional, later)
instant-transaction-notification-ruleand add africk-webhook.controller.ts(@ApiExcludeEndpoint, header‑secret validation likeYapealWebhookController) that only triggers the Phase‑1 poll for lower latency. Keep polling as the source of truth.The signing helper (critical detail)
Frick verifies the signature over the exact bytes it receives. If the string you sign differs from the string axios transmits (whitespace, key order, re‑serialization), every call returns 401.
POST /v2/authorizebody must be signed too (signing happens before the first token).EbicsKeyEncryptor(AES, symmetric) is not usable here — this is asymmetric RSA signing.Gotchas (pulled from the code)
(name, currency)assumption.getBankInternalkeys on`${name}-${currency}`andBankService's staticibanCachekeeps the first row on collision. Two Frick CHF rows (CT + Operating) under the samenamebreak selection. The DB unique key is(iban, bic)— not(name, currency)— so the DB happily accepts both; the break is purely in code. → Either give CT and Operating distinct enum names, or keep only the customer‑transaction (CT) accounts as bank entities and treat Operating as pure liquidity accounts.nameisvarchar(256)but code compares against exact enum strings. A free‑textnamewithout the enum entry is ignored byisCountryEnabled,blockchainToBankName,getBankInternal, and the balance‑adapter switch (falls through to default /return true).seed.js/bank.csvis loc‑only.config.ts; cache in memory with expiry and refresh on 401.<br>decoding is mandatory forFRICK_PRIVATE_KEY(.split('<br>').join('\n')), elsecryptocannot parse the key and every signature fails.throw/ServiceUnavailableException) when Frick is unconfigured — mirror Yapeal's "not configured" behavior. Keep at least alogger.erroron poll failures; don't swallow silently.createPain001Xmlis hard‑wired toSvcLvl Cd=SEPA,NbOfTxs=1. FOREIGN / QR‑Bill / LI‑domestic CHF (SIC) may need a different payment type or thePUT /v2/transactionsendpoint instead.pain.001.001.03. Verify Frick accepts it (some LI/CH banks require.09).status=BOOKED. Intraday pending entries would be mislabeled — use camt.053 for booked statements.HttpServicemocks external hosts by regex in the LOC environment — addolb.bankfrick.lito the mock patterns or local calls return mock/empty.Config.isDomesticIbancoversCH/LI), and the new Frick IBANs are auto‑blocked as DFX‑own IBANs (is-dfx-iban.validator.ts) once the rows exist — intended.Open questions / decisions needed
These affect the shape of the implementation; the DFX team should confirm (some require checking against the Frick test environment):
GET /v2/camt.053(reusesparseCamt053Xml) or via the Frick JSONGET /v2/accounts/{customer}/{account}/transactions(needs africk.dto.ts+ custom mapping)? Recommended: camt.053 for reuse.PUT /v2/pain.001(reusecreatePain001Xml) orPUT /v2/transactions(Frick JSON, needed for FOREIGN/QR‑Bill)? And which pain.001 version does Frick accept?receive(customer deposits) vs.send(payout)? Does Frick support SEPA‑Instant / SIC‑Instant (sctInst)? There are 4 accounts: CHF/EUR × customer‑transaction (CT) + operating.namewith per‑currency rows, or distinct names per account role (resolves the(name,currency)collision)?getSenderBankpriority when multiplesend=truebanks exist per currency.totalBalanceChf/ financial log (→ newBlockchain.BANK_FRICK, liquidity assets, balance‑adapter case)?endToEndId/remittance forsearchOutgoingBankTxto match?What the DFX team provides (not part of this issue)
Handled privately by the DFX team; the implementer only wires the env‑var names:
FRICK_API_KEY,FRICK_PRIVATE_KEY(PEM,<br>‑encoded),FRICK_BASE_URL,FRICK_CUSTOMER.Acceptance criteria
npm run format,npm run lint,npm run type-check,npm test.FiatOutputcolumns), env/infra (.env.example+ config), service updates, frontend sync if any API contract changes.bank_txwith correctaccountIban, dedup onaccountServiceRef, watermark advancing only on non‑empty fetch.searchOutgoingBankTxreconciles it toisComplete. Rail behind a default‑offDisabledProcessflag.bank_txmapping.Reference: files to mirror / touch
src/integration/bank/services/olkypay.service.tsBankFrickService(token auth + refresh, poll,parseTransaction)src/integration/bank/services/yapeal.service.tssendPaymentvia pain.001) — not the authsrc/integration/bank/services/iso20022.service.tsparseCamt053Xml,createPain001Xml,CamtTransaction/Pain001Paymentsrc/integration/bank/services/raiffeisen.service.tsparseTransaction(CamtTransaction, accountIban): Partial<BankTx>mapping templatesrc/shared/utils/util.ts:457Util.createSign— RSA‑SHA512 signature primitivesrc/subdomains/core/payment-link/services/payment-webhook.service.tssrc/subdomains/supporting/bank/bank/dto/bank.dto.ts:17IbanBankNameenum — addFRICKsrc/subdomains/supporting/bank/bank/bank.service.tsgetBankInternal/getSenderBank/getBankByIban/ibanCachesrc/config/config.ts:1181Config.bank— addfrickblock; PEM<br>convention at:1213.env.exampleFRICK_*varsmigration/seed/bank.csv+ a new data migrationsrc/subdomains/supporting/bank-tx/bank-tx/services/bank-tx.service.ts:124/171checkBankTxcron +checkTransactions— addcheckFrickTransactions()(lastBankFrickDate)src/subdomains/supporting/bank-tx/bank-tx/services/bank-transaction-handler.service.tssrc/subdomains/supporting/fiat-output/fiat-output-job.service.ts:63/381/444/71/504fillFiatOutputchain — addtransmitFrickPayments(); TAN model =checkOlkypayOrderStatussrc/subdomains/supporting/fiat-output/fiat-output.entity.tsfrickTxId/frickOrderIdsrc/integration/bank/bank.module.tsBankFrickService(providers + exports)src/integration/bank/controllers/yapeal-webhook.controller.tsPublic API docs: https://developers.bankfrick.li/api_docs/#introduction