Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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"`);
}
};
51 changes: 51 additions & 0 deletions migration/1784102462000-BackfillRealUnitServiceProvider2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* @typedef {import('typeorm').MigrationInterface} MigrationInterface
* @typedef {import('typeorm').QueryRunner} QueryRunner
*/

/**
* Re-runs the RealUnit service-provider backfill (same selection and idempotent update as
* 1782990000010-BackfillRealUnitServiceProvider) to heal the accounts onboarded between the first
* backfill and the sign-up/sign-in marker hooks shipping: the runtime path only set the marker in
* `registerEmail`, which existing DFX customers never reach (mail-merge early return, or the app
* skips the email step entirely for accounts with mail + KYC level >= 10). Their RealUnit wallet
* users survive the merge on the master account, so the wallet join below still finds them.
*
* @class
* @implements {MigrationInterface}
*/
module.exports = class BackfillRealUnitServiceProvider21784102462000 {
name = 'BackfillRealUnitServiceProvider21784102462000'

/**
* @param {QueryRunner} queryRunner
*/
async up(queryRunner) {
await queryRunner.query(`
UPDATE "user_data" SET "serviceProviders" =
CASE
WHEN "serviceProviders" IS NULL OR "serviceProviders" = '' THEN 'RealUnit'
WHEN ';' || "serviceProviders" || ';' LIKE '%;RealUnit;%' THEN "serviceProviders"
ELSE "serviceProviders" || ';RealUnit'
END
WHERE "id" IN (
SELECT u."userDataId"
FROM "user" u
INNER JOIN "wallet" w ON w."id" = u."walletId"
WHERE w."name" = 'RealUnit'
UNION
SELECT ks."userDataId"
FROM "kyc_step" ks
WHERE ks."name" = 'RealUnitRegistration'
)
`);
}

/**
* @param {QueryRunner} queryRunner
*/
async down(queryRunner) {
// No-op: the first backfill (1782990000010) already set the marker for an unknown subset of these
// accounts, so removing it here would also strip legitimately backfilled customers.
}
}
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
Loading
Loading