|
| 1 | +#!/usr/bin/env node |
| 2 | + |
| 3 | +/** |
| 4 | + * Sync bank_tx.accountIban from Production to Local DB |
| 5 | + * |
| 6 | + * This script: |
| 7 | + * 1. Fetches accountIban values from production DB via debug endpoint (SELECT only) |
| 8 | + * 2. Updates the local DB directly via mssql connection |
| 9 | + * |
| 10 | + * Usage: |
| 11 | + * node scripts/sync-bank-tx-iban.js |
| 12 | + * |
| 13 | + * Requirements: |
| 14 | + * - .env.db-debug with DEBUG_ADDRESS and DEBUG_SIGNATURE |
| 15 | + * - Local SQL Server running with credentials from .env |
| 16 | + * - Production API accessible |
| 17 | + */ |
| 18 | + |
| 19 | +const fs = require('fs'); |
| 20 | +const path = require('path'); |
| 21 | +const mssql = require('mssql'); |
| 22 | + |
| 23 | +// Load db-debug environment |
| 24 | +const dbDebugEnvFile = path.join(__dirname, '.env.db-debug'); |
| 25 | +if (!fs.existsSync(dbDebugEnvFile)) { |
| 26 | + console.error('Error: .env.db-debug not found'); |
| 27 | + process.exit(1); |
| 28 | +} |
| 29 | + |
| 30 | +const dbDebugEnv = {}; |
| 31 | +fs.readFileSync(dbDebugEnvFile, 'utf-8').split('\n').forEach(line => { |
| 32 | + const [key, ...valueParts] = line.split('='); |
| 33 | + if (key && valueParts.length) { |
| 34 | + dbDebugEnv[key.trim()] = valueParts.join('=').trim(); |
| 35 | + } |
| 36 | +}); |
| 37 | + |
| 38 | +// Load main .env for local DB credentials |
| 39 | +const mainEnvFile = path.join(__dirname, '..', '.env'); |
| 40 | +if (!fs.existsSync(mainEnvFile)) { |
| 41 | + console.error('Error: .env not found'); |
| 42 | + process.exit(1); |
| 43 | +} |
| 44 | + |
| 45 | +const mainEnv = {}; |
| 46 | +fs.readFileSync(mainEnvFile, 'utf-8').split('\n').forEach(line => { |
| 47 | + const match = line.match(/^([^#=]+)=(.*)$/); |
| 48 | + if (match) { |
| 49 | + mainEnv[match[1].trim()] = match[2].trim(); |
| 50 | + } |
| 51 | +}); |
| 52 | + |
| 53 | +const PROD_API_URL = dbDebugEnv.DEBUG_API_URL || 'https://api.dfx.swiss/v1'; |
| 54 | +const DEBUG_ADDRESS = dbDebugEnv.DEBUG_ADDRESS; |
| 55 | +const DEBUG_SIGNATURE = dbDebugEnv.DEBUG_SIGNATURE; |
| 56 | + |
| 57 | +// Local DB config |
| 58 | +const localDbConfig = { |
| 59 | + server: mainEnv.SQL_HOST || 'localhost', |
| 60 | + port: parseInt(mainEnv.SQL_PORT || '1433'), |
| 61 | + user: mainEnv.SQL_USERNAME || 'sa', |
| 62 | + password: mainEnv.SQL_PASSWORD, |
| 63 | + database: mainEnv.SQL_DB || 'dfx', |
| 64 | + options: { |
| 65 | + encrypt: false, |
| 66 | + trustServerCertificate: true, |
| 67 | + }, |
| 68 | +}; |
| 69 | + |
| 70 | +async function getToken(apiUrl, address, signature) { |
| 71 | + const response = await fetch(`${apiUrl}/auth/signIn`, { |
| 72 | + method: 'POST', |
| 73 | + headers: { 'Content-Type': 'application/json' }, |
| 74 | + body: JSON.stringify({ address, signature }), |
| 75 | + }); |
| 76 | + const data = await response.json(); |
| 77 | + if (!data.accessToken) { |
| 78 | + throw new Error(`Auth failed for ${apiUrl}: ${JSON.stringify(data)}`); |
| 79 | + } |
| 80 | + return data.accessToken; |
| 81 | +} |
| 82 | + |
| 83 | +async function executeQuery(apiUrl, token, sql) { |
| 84 | + const response = await fetch(`${apiUrl}/gs/debug`, { |
| 85 | + method: 'POST', |
| 86 | + headers: { |
| 87 | + 'Content-Type': 'application/json', |
| 88 | + 'Authorization': `Bearer ${token}`, |
| 89 | + }, |
| 90 | + body: JSON.stringify({ sql }), |
| 91 | + }); |
| 92 | + const data = await response.json(); |
| 93 | + if (data.statusCode && data.statusCode >= 400) { |
| 94 | + throw new Error(`Query failed: ${data.message}`); |
| 95 | + } |
| 96 | + return data; |
| 97 | +} |
| 98 | + |
| 99 | +async function main() { |
| 100 | + console.log('=== Sync bank_tx.accountIban from Production to Local ===\n'); |
| 101 | + |
| 102 | + // Connect to local DB |
| 103 | + console.log('Connecting to local database...'); |
| 104 | + console.log(` Server: ${localDbConfig.server}:${localDbConfig.port}`); |
| 105 | + console.log(` Database: ${localDbConfig.database}`); |
| 106 | + |
| 107 | + let pool; |
| 108 | + try { |
| 109 | + pool = await mssql.connect(localDbConfig); |
| 110 | + console.log('✓ Local database connected\n'); |
| 111 | + } catch (e) { |
| 112 | + console.error('Failed to connect to local database:', e.message); |
| 113 | + process.exit(1); |
| 114 | + } |
| 115 | + |
| 116 | + // Get production token |
| 117 | + console.log('Authenticating to Production...'); |
| 118 | + const prodToken = await getToken(PROD_API_URL, DEBUG_ADDRESS, DEBUG_SIGNATURE); |
| 119 | + console.log('✓ Production authenticated\n'); |
| 120 | + |
| 121 | + // Get distinct accountIbans from production with their IDs |
| 122 | + console.log('Fetching accountIban data from Production...'); |
| 123 | + |
| 124 | + const BATCH_SIZE = 1000; |
| 125 | + let lastId = 0; |
| 126 | + let totalUpdated = 0; |
| 127 | + let hasMore = true; |
| 128 | + |
| 129 | + while (hasMore) { |
| 130 | + console.log(`\nFetching batch after id ${lastId}...`); |
| 131 | + |
| 132 | + // Get batch of IDs with their accountIban from production (using TOP + WHERE id > lastId) |
| 133 | + const prodData = await executeQuery( |
| 134 | + PROD_API_URL, |
| 135 | + prodToken, |
| 136 | + `SELECT TOP ${BATCH_SIZE} id, accountIban FROM bank_tx WHERE accountIban IS NOT NULL AND id > ${lastId} ORDER BY id` |
| 137 | + ); |
| 138 | + |
| 139 | + if (!Array.isArray(prodData) || prodData.length === 0) { |
| 140 | + console.log('No more data to fetch.'); |
| 141 | + hasMore = false; |
| 142 | + break; |
| 143 | + } |
| 144 | + |
| 145 | + console.log(`Fetched ${prodData.length} records from production.`); |
| 146 | + |
| 147 | + // Group by accountIban to minimize updates |
| 148 | + const byIban = {}; |
| 149 | + for (const row of prodData) { |
| 150 | + if (!row.accountIban) continue; |
| 151 | + if (!byIban[row.accountIban]) { |
| 152 | + byIban[row.accountIban] = []; |
| 153 | + } |
| 154 | + byIban[row.accountIban].push(row.id); |
| 155 | + if (row.id > lastId) lastId = row.id; |
| 156 | + } |
| 157 | + |
| 158 | + // Update local DB directly |
| 159 | + for (const [iban, ids] of Object.entries(byIban)) { |
| 160 | + const idList = ids.join(','); |
| 161 | + |
| 162 | + try { |
| 163 | + const request = pool.request(); |
| 164 | + request.input('iban', mssql.NVarChar, iban); |
| 165 | + await request.query(`UPDATE bank_tx SET accountIban = @iban WHERE id IN (${idList})`); |
| 166 | + totalUpdated += ids.length; |
| 167 | + process.stdout.write(`\r Updated ${totalUpdated} records...`); |
| 168 | + } catch (e) { |
| 169 | + console.error(`\nFailed to update IDs for IBAN ${iban}: ${e.message}`); |
| 170 | + } |
| 171 | + } |
| 172 | + |
| 173 | + if (prodData.length < BATCH_SIZE) { |
| 174 | + hasMore = false; |
| 175 | + } |
| 176 | + } |
| 177 | + |
| 178 | + // Close connection |
| 179 | + await pool.close(); |
| 180 | + |
| 181 | + console.log(`\n\n=== Sync Complete ===`); |
| 182 | + console.log(`Total records updated: ${totalUpdated}`); |
| 183 | + |
| 184 | + // Verify the result |
| 185 | + console.log('\n=== Verification ==='); |
| 186 | + console.log('Run this to check Maerki Baumann CHF 2025:'); |
| 187 | + console.log(' curl -s "http://localhost:3000/v1/accounting/balance-sheet/CH3408573177975200001/2025" -H "Authorization: Bearer $TOKEN" | jq .'); |
| 188 | +} |
| 189 | + |
| 190 | +main().catch(e => { |
| 191 | + console.error('Error:', e.message); |
| 192 | + process.exit(1); |
| 193 | +}); |
0 commit comments