Skip to content
Draft
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
102 changes: 102 additions & 0 deletions migration/1768841352156-AddBankYearlyBalances.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* @typedef {import('typeorm').MigrationInterface} MigrationInterface
* @typedef {import('typeorm').QueryRunner} QueryRunner
*/

/**
* Add yearlyBalances column to bank table and populate with historical balances.
*
* Format: { "2024": 2437.57, "2025": 0 }
* - Each year stores the closing balance (Endbestand) for that year
* - Opening balance (Anfangsbestand) is calculated as previous year's closing balance
*
* @class
* @implements {MigrationInterface}
*/
module.exports = class AddBankYearlyBalances1768841352156 {
name = 'AddBankYearlyBalances1768841352156'

/**
* @param {QueryRunner} queryRunner
*/
async up(queryRunner) {
// Add yearlyBalances column
await queryRunner.query(`
ALTER TABLE "dbo"."bank" ADD "yearlyBalances" nvarchar(MAX) NULL
`);

// Update each bank with their yearly balances (closing balances per year)
// Format: { "YYYY": closingBalance } - opening is previous year's closing
// Data source: bank anfangsbestände per 1.1.xxxx - Endbestaende.csv
const bankBalances = [
// Bank Frick EUR (ID: 1)
{
id: 1,
balances: { "2022": 11407.01, "2023": 0, "2024": 0, "2025": 0 }
},
// Bank Frick CHF (ID: 2)
{
id: 2,
balances: { "2022": 116.54, "2023": 0, "2024": 0, "2025": 0 }
},
// Bank Frick USD (ID: 3)
{
id: 3,
balances: { "2022": 6670.51, "2023": 0, "2024": 0, "2025": 0 }
},
// Olkypay EUR (ID: 4)
{
id: 4,
balances: { "2022": 15702.24, "2023": 35581.94, "2024": 11219.32, "2025": 21814.76 }
},
// Maerki Baumann EUR (ID: 5)
{
id: 5,
balances: { "2022": 67230.42, "2023": 26327.80, "2024": 3312.22, "2025": 0 }
},
// Maerki Baumann CHF (ID: 6)
{
id: 6,
balances: { "2022": 30549.23, "2023": 8011.98, "2024": 2437.57, "2025": 0 }
},
// Revolut EUR (ID: 7)
{
id: 7,
balances: { "2022": 8687.49, "2023": 3303.60, "2024": 0, "2025": 0 }
},
// Raiffeisen CHF (ID: 13)
{
id: 13,
balances: { "2022": 0, "2023": 0, "2024": 0, "2025": 1161.67 }
},
// Yapeal CHF (ID: 15)
{
id: 15,
balances: { "2022": 0, "2023": 0, "2024": 0, "2025": 1557.73 }
},
// Yapeal EUR (ID: 16)
{
id: 16,
balances: { "2022": 0, "2023": 0, "2024": 0, "2025": 2568.79 }
},
];

for (const bank of bankBalances) {
const jsonValue = JSON.stringify(bank.balances).replace(/'/g, "''");
await queryRunner.query(`
UPDATE "dbo"."bank"
SET "yearlyBalances" = '${jsonValue}'
WHERE "id" = ${bank.id}
`);
}
}

/**
* @param {QueryRunner} queryRunner
*/
async down(queryRunner) {
await queryRunner.query(`
ALTER TABLE "dbo"."bank" DROP COLUMN "yearlyBalances"
`);
}
}
90 changes: 90 additions & 0 deletions scripts/migrate-yearly-balances.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env node

/**
* Migrate yearlyBalances from old format to new format
*
* Old format: { "2025": { "opening": 2437.57, "closing": 0 } }
* New format: { "2024": 2437.57, "2025": 0 }
*
* Opening balance is now calculated as previous year's closing balance
*/

const mssql = require('mssql');
const fs = require('fs');
const path = require('path');

const mainEnvFile = path.join(__dirname, '..', '.env');
const mainEnv = {};
fs.readFileSync(mainEnvFile, 'utf-8').split('\n').forEach(line => {
const match = line.match(/^([^#=]+)=(.*)$/);
if (match) mainEnv[match[1].trim()] = match[2].trim();
});

const config = {
server: mainEnv.SQL_HOST || 'localhost',
port: parseInt(mainEnv.SQL_PORT || '1433'),
user: mainEnv.SQL_USERNAME || 'sa',
password: mainEnv.SQL_PASSWORD,
database: mainEnv.SQL_DB || 'dfx',
options: { encrypt: false, trustServerCertificate: true },
};

async function migrateBalances() {
const pool = await mssql.connect(config);

const result = await pool.request().query('SELECT id, name, iban, yearlyBalances FROM bank WHERE yearlyBalances IS NOT NULL');

console.log('Migrating yearlyBalances to new format...');
console.log('Old format: {"2025": {"opening": X, "closing": Y}}');
console.log('New format: {"2024": X, "2025": Y} (opening = previous year closing)\n');

for (const bank of result.recordset) {
const oldBalances = JSON.parse(bank.yearlyBalances);
const newBalances = {};

for (const [year, data] of Object.entries(oldBalances)) {
if (typeof data === 'object' && data !== null) {
// Old format: { opening: X, closing: Y }
const { opening, closing } = data;

// Store closing for this year
if (closing !== undefined) {
newBalances[year] = closing;
}

// Store opening as previous year's closing
if (opening && opening !== 0) {
const prevYear = (parseInt(year) - 1).toString();
newBalances[prevYear] = opening;
}
} else {
// Already in new format (just a number)
newBalances[year] = data;
}
}

const newJson = JSON.stringify(newBalances);

console.log(bank.name + ' (' + bank.iban + '):');
console.log(' Old: ' + bank.yearlyBalances);
console.log(' New: ' + newJson);

await pool.request()
.input('id', mssql.Int, bank.id)
.input('balances', mssql.NVarChar, newJson)
.query('UPDATE bank SET yearlyBalances = @balances WHERE id = @id');
}

console.log('\nMigration complete!');

const verify = await pool.request().query('SELECT name, iban, yearlyBalances FROM bank WHERE yearlyBalances IS NOT NULL');
console.log('\nVerification:');
verify.recordset.forEach(b => console.log(' ' + b.name + ': ' + b.yearlyBalances));

await pool.close();
}

migrateBalances().catch(e => {
console.error('Error:', e.message);
process.exit(1);
});
193 changes: 193 additions & 0 deletions scripts/sync-bank-tx-iban.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
#!/usr/bin/env node

/**
* Sync bank_tx.accountIban from Production to Local DB
*
* This script:
* 1. Fetches accountIban values from production DB via debug endpoint (SELECT only)
* 2. Updates the local DB directly via mssql connection
*
* Usage:
* node scripts/sync-bank-tx-iban.js
*
* Requirements:
* - .env.db-debug with DEBUG_ADDRESS and DEBUG_SIGNATURE
* - Local SQL Server running with credentials from .env
* - Production API accessible
*/

const fs = require('fs');
const path = require('path');
const mssql = require('mssql');

// Load db-debug environment
const dbDebugEnvFile = path.join(__dirname, '.env.db-debug');
if (!fs.existsSync(dbDebugEnvFile)) {
console.error('Error: .env.db-debug not found');
process.exit(1);
}

const dbDebugEnv = {};
fs.readFileSync(dbDebugEnvFile, 'utf-8').split('\n').forEach(line => {
const [key, ...valueParts] = line.split('=');
if (key && valueParts.length) {
dbDebugEnv[key.trim()] = valueParts.join('=').trim();
}
});

// Load main .env for local DB credentials
const mainEnvFile = path.join(__dirname, '..', '.env');
if (!fs.existsSync(mainEnvFile)) {
console.error('Error: .env not found');
process.exit(1);
}

const mainEnv = {};
fs.readFileSync(mainEnvFile, 'utf-8').split('\n').forEach(line => {
const match = line.match(/^([^#=]+)=(.*)$/);
if (match) {
mainEnv[match[1].trim()] = match[2].trim();
}
});

const PROD_API_URL = dbDebugEnv.DEBUG_API_URL || 'https://api.dfx.swiss/v1';
const DEBUG_ADDRESS = dbDebugEnv.DEBUG_ADDRESS;
const DEBUG_SIGNATURE = dbDebugEnv.DEBUG_SIGNATURE;

// Local DB config
const localDbConfig = {
server: mainEnv.SQL_HOST || 'localhost',
port: parseInt(mainEnv.SQL_PORT || '1433'),
user: mainEnv.SQL_USERNAME || 'sa',
password: mainEnv.SQL_PASSWORD,
database: mainEnv.SQL_DB || 'dfx',
options: {
encrypt: false,
trustServerCertificate: true,
},
};

async function getToken(apiUrl, address, signature) {
const response = await fetch(`${apiUrl}/auth/signIn`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address, signature }),
});
const data = await response.json();
if (!data.accessToken) {
throw new Error(`Auth failed for ${apiUrl}: ${JSON.stringify(data)}`);
}
return data.accessToken;
}

async function executeQuery(apiUrl, token, sql) {
const response = await fetch(`${apiUrl}/gs/debug`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({ sql }),
});
const data = await response.json();
if (data.statusCode && data.statusCode >= 400) {
throw new Error(`Query failed: ${data.message}`);
}
return data;
}

async function main() {
console.log('=== Sync bank_tx.accountIban from Production to Local ===\n');

// Connect to local DB
console.log('Connecting to local database...');
console.log(` Server: ${localDbConfig.server}:${localDbConfig.port}`);
console.log(` Database: ${localDbConfig.database}`);

let pool;
try {
pool = await mssql.connect(localDbConfig);
console.log('✓ Local database connected\n');
} catch (e) {
console.error('Failed to connect to local database:', e.message);
process.exit(1);
}

// Get production token
console.log('Authenticating to Production...');
const prodToken = await getToken(PROD_API_URL, DEBUG_ADDRESS, DEBUG_SIGNATURE);
console.log('✓ Production authenticated\n');

// Get distinct accountIbans from production with their IDs
console.log('Fetching accountIban data from Production...');

const BATCH_SIZE = 1000;
let lastId = 0;
let totalUpdated = 0;
let hasMore = true;

while (hasMore) {
console.log(`\nFetching batch after id ${lastId}...`);

// Get batch of IDs with their accountIban from production (using TOP + WHERE id > lastId)
const prodData = await executeQuery(
PROD_API_URL,
prodToken,
`SELECT TOP ${BATCH_SIZE} id, accountIban FROM bank_tx WHERE accountIban IS NOT NULL AND id > ${lastId} ORDER BY id`
);

if (!Array.isArray(prodData) || prodData.length === 0) {
console.log('No more data to fetch.');
hasMore = false;
break;
}

console.log(`Fetched ${prodData.length} records from production.`);

// Group by accountIban to minimize updates
const byIban = {};
for (const row of prodData) {
if (!row.accountIban) continue;
if (!byIban[row.accountIban]) {
byIban[row.accountIban] = [];
}
byIban[row.accountIban].push(row.id);
if (row.id > lastId) lastId = row.id;
}

// Update local DB directly
for (const [iban, ids] of Object.entries(byIban)) {
const idList = ids.join(',');

try {
const request = pool.request();
request.input('iban', mssql.NVarChar, iban);
await request.query(`UPDATE bank_tx SET accountIban = @iban WHERE id IN (${idList})`);
totalUpdated += ids.length;
process.stdout.write(`\r Updated ${totalUpdated} records...`);
} catch (e) {
console.error(`\nFailed to update IDs for IBAN ${iban}: ${e.message}`);
}
}

if (prodData.length < BATCH_SIZE) {
hasMore = false;
}
}

// Close connection
await pool.close();

console.log(`\n\n=== Sync Complete ===`);
console.log(`Total records updated: ${totalUpdated}`);

// Verify the result
console.log('\n=== Verification ===');
console.log('Run this to check Maerki Baumann CHF 2025:');
console.log(' curl -s "http://localhost:3000/v1/accounting/balance-sheet/CH3408573177975200001/2025" -H "Authorization: Bearer $TOKEN" | jq .');
}

main().catch(e => {
console.error('Error:', e.message);
process.exit(1);
});
Loading
Loading