Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c678339
feat(bank): integrate Bank Frick WebAPI
joshuakrueger-dfx Jul 13, 2026
f5c0270
fix(bank): harden Bank Frick status polling
joshuakrueger-dfx Jul 13, 2026
d532ffb
test(bank): enforce Bank Frick coverage
joshuakrueger-dfx Jul 13, 2026
2dfb94d
fix(bank): harden Bank Frick state tracking
joshuakrueger-dfx Jul 13, 2026
0eceaff
test(bank): lock Frick tracking metadata
joshuakrueger-dfx Jul 13, 2026
47e4289
fix(bank): widen Frick payment order lookups
joshuakrueger-dfx Jul 13, 2026
a9195fb
fix(bank): tighten Frick customer validation
joshuakrueger-dfx Jul 13, 2026
9293d24
fix(bank): keep liquidity reserved for deletion-requested Frick orders
joshuakrueger-dfx Jul 13, 2026
3a42822
fix(bank): decouple Frick auto-approval from transmission switch
joshuakrueger-dfx Jul 13, 2026
7cdc9bb
fix(bank): overlap Bank Frick statement watermark
joshuakrueger-dfx Jul 13, 2026
fa2162b
refactor(bank): isolate Bank Frick code for coverage gating
joshuakrueger-dfx Jul 13, 2026
0ae4717
test(bank): gate the full Bank Frick surface at 100% coverage
joshuakrueger-dfx Jul 13, 2026
2e39288
fix(bank): harden Bank Frick integration
joshuakrueger-dfx Jul 13, 2026
e6e08e5
Merge remote-tracking branch 'origin/develop' into agent/integrate-ba…
joshuakrueger-dfx Jul 13, 2026
77eb99e
fix(bank): avoid false-positive TODO marker
joshuakrueger-dfx Jul 14, 2026
5129342
fix: apply Ultra Test-Agent repair for PR #4196
Jul 14, 2026
5133363
fix(bank): resolve Bank Frick deep-review findings
joshuakrueger-dfx Jul 14, 2026
d3200b8
fix(bank): address re-review findings in the Bank Frick fix commits
joshuakrueger-dfx Jul 15, 2026
7c86a87
style(bank): wrap payment observer spec helper signature for prettier
joshuakrueger-dfx Jul 15, 2026
6af3006
chore(bank): isolate the Bank Frick coverage gate in its own jest config
joshuakrueger-dfx Jul 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ RAIFFEISEN_USER_ID=
RAIFFEISEN_PASSPHRASE=
RAIFFEISEN_IV=

FRICK_BASE_URL=
FRICK_API_KEY=
FRICK_PRIVATE_KEY=
FRICK_SERVER_PUBLIC_KEY=
FRICK_CUSTOMER=
FRICK_PAYOUT_ENABLED=
FRICK_APPROVE_WITHOUT_TAN=

YAPEAL_BASE_URL=
YAPEAL_PARTNERSHIP_UID=
YAPEAL_ADMIN_UID=
Expand Down Expand Up @@ -309,4 +317,4 @@ REALUNIT_BANK_NAME=

REQUEST_KNOWN_IPS=

CRON_JOB_DELAY=
CRON_JOB_DELAY=
3 changes: 3 additions & 0 deletions .github/workflows/api-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,8 @@ jobs:
env:
MIGRATION_TEST_PG: postgres://postgres:postgres@localhost:5432/postgres

- name: Enforce Bank Frick coverage
run: npm run test:frick:cov

- name: Security audit
run: npm audit --audit-level=critical
256 changes: 256 additions & 0 deletions docs/bank-frick-operations.md

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ module.exports = tseslint.config(
},
},
{
ignores: ['eslint.config.js', 'migration/**/*.js', 'scripts/*.js', 'dist/**', 'node_modules/**'],
ignores: [
'eslint.config.js',
'jest.frick.config.js',
'migration/**/*.js',
'scripts/*.js',
'dist/**',
'node_modules/**',
],
},
);
39 changes: 39 additions & 0 deletions infrastructure/bicep/api/dfx-api.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,17 @@ param olkyUser string
@secure()
param olkyPassword string

param frickBaseUrl string
@secure()
param frickApiKey string
@secure()
param frickPrivateKey string
param frickServerPublicKey string
@secure()
param frickCustomer string
param frickPayoutEnabled string
param frickApproveWithoutTan string

param letterUrl string
param letterUser string
@secure()
Expand Down Expand Up @@ -1209,6 +1220,34 @@ resource apiAppService 'Microsoft.Web/sites@2018-11-01' = {
name: 'OLKY_PASSWORD'
value: olkyPassword
}
{
name: 'FRICK_BASE_URL'
value: frickBaseUrl
}
{
name: 'FRICK_API_KEY'
value: frickApiKey
}
{
name: 'FRICK_PRIVATE_KEY'
value: frickPrivateKey
}
{
name: 'FRICK_SERVER_PUBLIC_KEY'
value: frickServerPublicKey
}
{
name: 'FRICK_CUSTOMER'
value: frickCustomer
}
{
name: 'FRICK_PAYOUT_ENABLED'
value: frickPayoutEnabled
}
{
name: 'FRICK_APPROVE_WITHOUT_TAN'
value: frickApproveWithoutTan
}
{
name: 'COIN_GECKO_API_KEY'
value: coinGeckoApiKey
Expand Down
21 changes: 21 additions & 0 deletions infrastructure/bicep/api/parameters/dev.json
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,27 @@
"olkyPassword": {
"value": "xxx"
},
"frickBaseUrl": {
"value": "https://olbtest.bankfrick.li/webapi/v2"
},
"frickApiKey": {
"value": "xxx"
},
"frickPrivateKey": {
"value": "xxx"
},
"frickServerPublicKey": {
"value": "xxx"
},
"frickCustomer": {
"value": "xxx"
},
"frickPayoutEnabled": {
"value": "false"
},
"frickApproveWithoutTan": {
"value": "false"
},
"letterUrl": {
"value": "https://sandbox.letterxpress.de/v1"
},
Expand Down
21 changes: 21 additions & 0 deletions infrastructure/bicep/api/parameters/loc.json
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,27 @@
"olkyPassword": {
"value": "xxx"
},
"frickBaseUrl": {
"value": "https://olbtest.bankfrick.li/webapi/v2"
},
"frickApiKey": {
"value": "xxx"
},
"frickPrivateKey": {
"value": "xxx"
},
"frickServerPublicKey": {
"value": "xxx"
},
"frickCustomer": {
"value": "xxx"
},
"frickPayoutEnabled": {
"value": "false"
},
"frickApproveWithoutTan": {
"value": "false"
},
"letterUrl": {
"value": "https://sandbox.letterxpress.de/v1"
},
Expand Down
21 changes: 21 additions & 0 deletions infrastructure/bicep/api/parameters/prd.json
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,27 @@
"olkyPassword": {
"value": "xxx"
},
"frickBaseUrl": {
"value": "https://olb.bankfrick.li/webapi/v2"
},
"frickApiKey": {
"value": "xxx"
},
"frickPrivateKey": {
"value": "xxx"
},
"frickServerPublicKey": {
"value": "xxx"
},
"frickCustomer": {
"value": "xxx"
},
"frickPayoutEnabled": {
"value": "false"
},
"frickApproveWithoutTan": {
"value": "false"
},
"letterUrl": {
"value": "https://api.letterxpress.de/v1"
},
Expand Down
37 changes: 37 additions & 0 deletions jest.frick.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Bank Frick coverage gate. Kept out of package.json's shared Jest config so the strict per-file 100%
// threshold on the shared iso20022.service.ts cannot red an unrelated `test:cov` run - only the
// dedicated test:frick:cov step (with its own --collectCoverageFrom scope) enforces it.
const base = require('./package.json').jest;

module.exports = {
...base,
coverageThreshold: {
'src/integration/bank/dto/frick.dto.ts': { branches: 100, functions: 100, lines: 100, statements: 100 },
'src/integration/bank/services/frick.service.ts': { branches: 100, functions: 100, lines: 100, statements: 100 },
'src/integration/bank/services/iso20022.service.ts': {
branches: 100,
functions: 100,
lines: 100,
statements: 100,
},
'src/config/frick.config.ts': { branches: 100, functions: 100, lines: 100, statements: 100 },
'src/subdomains/supporting/bank-tx/bank-tx/services/bank-tx-frick.service.ts': {
branches: 100,
functions: 100,
lines: 100,
statements: 100,
},
'src/subdomains/supporting/bank-tx/bank-tx/services/bank-tx-outgoing-match.service.ts': {
branches: 100,
functions: 100,
lines: 100,
statements: 100,
},
'src/subdomains/supporting/fiat-output/fiat-output-frick.service.ts': {
branches: 100,
functions: 100,
lines: 100,
statements: 100,
},
},
};
88 changes: 88 additions & 0 deletions migration/1783944000000-AddBankFrickPayoutTracking.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* @typedef {import('typeorm').MigrationInterface} MigrationInterface
* @typedef {import('typeorm').QueryRunner} QueryRunner
*/

/**
* Schema-only migration: `fiat_output` payout-tracking columns, the `bank.sendPriority` sender
* tie-breaker column (backfilled to a neutral default on every existing row), and the default-off
* process switches. It deliberately never inserts, updates or deletes `bank` rows - the only prior
* migration that ever did that (`1768943778000-AddYapealEurManualBank.js`) was reverted because the
* row was already inserted manually (`f897b98a2`). The two new Bank Frick account rows, and any
* cleanup of the legacy Bank Frick rows, are manual production steps documented in
* `docs/bank-frick-operations.md` §3, exactly like the existing Yapeal EUR account. A fresh local
* registry is populated from migration/seed/bank.csv instead.
*
* @class
* @implements {MigrationInterface}
*/
module.exports = class AddBankFrickPayoutTracking1783944000000 {
name = 'AddBankFrickPayoutTracking1783944000000';

/**
* @param {QueryRunner} queryRunner
*/
async up(queryRunner) {
await queryRunner.query(`SET LOCAL lock_timeout = '5s'`);
// Bank Frick's orderId is a plain integer (max 16 digits); varchar(64) is deliberately generous
// rather than the unrelated 256-character width used for the other string-identifier columns below.
await queryRunner.query(`ALTER TABLE "fiat_output" ADD "frickOrderId" character varying(64)`);
// Stores DFX's own generated customId (e.g. "DFX-FO-42"), not a Bank Frick transaction id - named
// accordingly rather than "frickTxId".
await queryRunner.query(`ALTER TABLE "fiat_output" ADD "frickCustomId" character varying(256)`);
await queryRunner.query(`ALTER TABLE "fiat_output" ADD "frickOrderStatus" character varying(256)`);
await queryRunner.query(`ALTER TABLE "fiat_output" ADD "frickError" character varying(256)`);
// Holds exactly the reference string sent to Bank Frick (customId-prefixed, bank-bound) so
// reconciliation can match on it without ever overwriting the customer-facing remittanceInfo.
await queryRunner.query(`ALTER TABLE "fiat_output" ADD "frickReference" character varying(256)`);
// Sender priority is a deliberate, operator-controlled tie-breaker: lower value tried first. Every
// pre-existing bank row is backfilled to the neutral default (1000) so this column changes nothing
// about today's routing. The new Frick rows are seeded worse than that default (2000) so simply
// flipping Frick's `send` flag on cannot silently steal traffic from a working incumbent (Olkypay,
// Yapeal) — Ops must deliberately lower Frick's priority below the incumbent's to cut over.
await queryRunner.query(`ALTER TABLE "bank" ADD "sendPriority" integer NOT NULL DEFAULT 1000`);
await queryRunner.query(`
INSERT INTO "setting" ("key", "value", "updated", "created")
VALUES (
'disabledProcess',
'["FiatOutputFrickTransmission","FiatOutputFrickStatusCheck"]',
NOW(),
NOW()
)
ON CONFLICT ("key") DO UPDATE SET "value" = (
COALESCE(NULLIF("setting"."value", ''), '[]')::jsonb
|| CASE
WHEN COALESCE(NULLIF("setting"."value", ''), '[]')::jsonb @> '["FiatOutputFrickTransmission"]'::jsonb
THEN '[]'::jsonb ELSE '["FiatOutputFrickTransmission"]'::jsonb
END
|| CASE
WHEN COALESCE(NULLIF("setting"."value", ''), '[]')::jsonb @> '["FiatOutputFrickStatusCheck"]'::jsonb
THEN '[]'::jsonb ELSE '["FiatOutputFrickStatusCheck"]'::jsonb
END
)::text, "updated" = NOW()
`);
}

/**
* @param {QueryRunner} queryRunner
*/
async down(queryRunner) {
await queryRunner.query(`SET LOCAL lock_timeout = '5s'`);
await queryRunner.query(`
UPDATE "setting"
SET "value" = COALESCE((
SELECT jsonb_agg(process)
FROM jsonb_array_elements_text(COALESCE(NULLIF("setting"."value", ''), '[]')::jsonb) AS processes(process)
WHERE process NOT IN ('FiatOutputFrickTransmission', 'FiatOutputFrickStatusCheck')
), '[]'::jsonb)::text,
"updated" = NOW()
WHERE "key" = 'disabledProcess'
`);
await queryRunner.query(`ALTER TABLE "bank" DROP COLUMN "sendPriority"`);
await queryRunner.query(`ALTER TABLE "fiat_output" DROP COLUMN "frickReference"`);
await queryRunner.query(`ALTER TABLE "fiat_output" DROP COLUMN "frickError"`);
await queryRunner.query(`ALTER TABLE "fiat_output" DROP COLUMN "frickOrderStatus"`);
await queryRunner.query(`ALTER TABLE "fiat_output" DROP COLUMN "frickCustomId"`);
await queryRunner.query(`ALTER TABLE "fiat_output" DROP COLUMN "frickOrderId"`);
}
};
16 changes: 9 additions & 7 deletions migration/seed/bank.csv
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
id,updated,created,name,iban,bic,currency,receive,send,sctInst,amlEnabled,assetId
16,2025-12-05 14:27:39,2025-12-05 14:27:39,Yapeal,CH6783019DFXEUR000002,YAPECHZ2,EUR,TRUE,TRUE,FALSE,TRUE,405
15,2025-12-05 14:27:39,2025-12-05 14:27:39,Yapeal,CH5283019DFXCHF000001,YAPECHZ2,CHF,TRUE,TRUE,FALSE,TRUE,404
14,2025-09-10 23:30:56,2025-09-10 23:30:56,Maerki Baumann,LU116060002000005040,OLKILUL1,EUR,FALSE,FALSE,FALSE,TRUE,
13,2025-09-10 23:30:07,2025-09-10 23:30:07,Maerki Baumann,CH4880808002186504370,RAIFCH22,CHF,FALSE,FALSE,FALSE,TRUE,
12,2025-09-10 23:30:07,2025-09-10 23:30:07,Maerki Baumann,CH7780808002608614092,RAIFCH22,EUR,FALSE,FALSE,FALSE,TRUE,
8,2025-09-10 23:30:06,2025-09-10 23:30:06,Maerki Baumann,CH3808573109968202333,MAEBCHZZ,USD,FALSE,FALSE,FALSE,TRUE,
id,updated,created,name,iban,bic,currency,receive,send,sctInst,amlEnabled,assetId
9001,2026-07-13 00:00:00,2026-07-13 00:00:00,Bank Frick,LI5600000FRICKEUR0001,BFRILI22,EUR,FALSE,FALSE,FALSE,TRUE,
9002,2026-07-13 00:00:00,2026-07-13 00:00:00,Bank Frick,LI4200000FRICKCHF0001,BFRILI22,CHF,FALSE,FALSE,FALSE,TRUE,
16,2025-12-05 14:27:39,2025-12-05 14:27:39,Yapeal,CH6783019DFXEUR000002,YAPECHZ2,EUR,TRUE,TRUE,FALSE,TRUE,405
15,2025-12-05 14:27:39,2025-12-05 14:27:39,Yapeal,CH5283019DFXCHF000001,YAPECHZ2,CHF,TRUE,TRUE,FALSE,TRUE,404
14,2025-09-10 23:30:56,2025-09-10 23:30:56,Maerki Baumann,LU116060002000005040,OLKILUL1,EUR,FALSE,FALSE,FALSE,TRUE,
13,2025-09-10 23:30:07,2025-09-10 23:30:07,Maerki Baumann,CH4880808002186504370,RAIFCH22,CHF,FALSE,FALSE,FALSE,TRUE,
12,2025-09-10 23:30:07,2025-09-10 23:30:07,Maerki Baumann,CH7780808002608614092,RAIFCH22,EUR,FALSE,FALSE,FALSE,TRUE,
8,2025-09-10 23:30:06,2025-09-10 23:30:06,Maerki Baumann,CH3808573109968202333,MAEBCHZZ,USD,FALSE,FALSE,FALSE,TRUE,
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"test": "jest --silent",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:frick:cov": "jest --config jest.frick.config.js integration/bank/services/__tests__/frick.service.spec.ts integration/bank/services/__tests__/iso20022.service.spec.ts config/__tests__/frick.config.spec.ts config/__tests__/bank-frick-config.spec.ts subdomains/supporting/bank-tx/bank-tx/services/__tests__/bank-tx-frick.service.spec.ts subdomains/supporting/bank-tx/bank-tx/services/__tests__/bank-tx-outgoing-match.service.spec.ts subdomains/supporting/fiat-output/__tests__/fiat-output-frick.service.spec.ts --coverage --runInBand --collectCoverageFrom=integration/bank/dto/frick.dto.ts --collectCoverageFrom=integration/bank/services/frick.service.ts --collectCoverageFrom=integration/bank/services/iso20022.service.ts --collectCoverageFrom=config/frick.config.ts --collectCoverageFrom=subdomains/supporting/bank-tx/bank-tx/services/bank-tx-frick.service.ts --collectCoverageFrom=subdomains/supporting/bank-tx/bank-tx/services/bank-tx-outgoing-match.service.ts --collectCoverageFrom=subdomains/supporting/fiat-output/fiat-output-frick.service.ts",
"type-check": "tsc --noEmit",
"format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"",
"check": "npm run lint && npm run test",
Expand Down
20 changes: 20 additions & 0 deletions src/config/__tests__/bank-frick-config.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { GetConfig } from '../config';

describe('Bank Frick config', () => {
it('restores PEM line breaks in the private key', () => {
const previousPrivateKey = process.env.FRICK_PRIVATE_KEY;
const previousServerPublicKey = process.env.FRICK_SERVER_PUBLIC_KEY;
process.env.FRICK_PRIVATE_KEY = 'synthetic-line-one<br>synthetic-line-two';
process.env.FRICK_SERVER_PUBLIC_KEY = 'synthetic-public-one<br>synthetic-public-two';

try {
expect(GetConfig().bank.frick.privateKey).toBe('synthetic-line-one\nsynthetic-line-two');
expect(GetConfig().bank.frick.serverPublicKey).toBe('synthetic-public-one\nsynthetic-public-two');
} finally {
if (previousPrivateKey === undefined) delete process.env.FRICK_PRIVATE_KEY;
else process.env.FRICK_PRIVATE_KEY = previousPrivateKey;
if (previousServerPublicKey === undefined) delete process.env.FRICK_SERVER_PUBLIC_KEY;
else process.env.FRICK_SERVER_PUBLIC_KEY = previousServerPublicKey;
}
});
});
Loading