/** * Submit Template Transactions to Pending Approvals * Parses XML template files and submits them as payments * Usage: ts-node -r tsconfig-paths/register scripts/submit-template-transactions.ts */ import * as fs from 'fs'; import * as path from 'path'; import { parseString } from 'xml2js'; const API_BASE = 'http://localhost:3000/api/v1'; let authToken: string = ''; // Colors for terminal output const colors = { reset: '\x1b[0m', green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m', blue: '\x1b[34m', cyan: '\x1b[36m', }; function log(message: string, color: string = colors.reset) { console.log(`${color}${message}${colors.reset}`); } function logSuccess(message: string) { log(`✓ ${message}`, colors.green); } function logError(message: string) { log(`✗ ${message}`, colors.red); } function logInfo(message: string) { log(`→ ${message}`, colors.cyan); } async function makeRequest(method: string, endpoint: string, body?: any, requireAuth: boolean = true): Promise<{ response?: Response; data?: any; error?: string; ok: boolean }> { const headers: any = { 'Content-Type': 'application/json' }; if (requireAuth && authToken) { headers['Authorization'] = `Bearer ${authToken}`; } const options: any = { method, headers }; if (body) { options.body = JSON.stringify(body); } try { const response = await fetch(`${API_BASE}${endpoint}`, options); const data = await response.json(); return { response, data, ok: response.ok }; } catch (error: any) { return { error: error.message, ok: false, data: null }; } } async function login() { log('\n=== LOGIN ===', colors.blue); logInfo('Logging in as ADMIN001...'); const result = await makeRequest('POST', '/auth/login', { operatorId: 'ADMIN001', password: 'admin123', }, false); if (result.ok && result.data.token) { authToken = result.data.token; logSuccess('Login successful'); return true; } else { logError(`Login failed: ${result.data?.error || result.error}`); return false; } } /** * Parse XML file and extract payment data */ async function parseXMLFile(filePath: string): Promise { const xmlContent = fs.readFileSync(filePath, 'utf-8'); return new Promise((resolve, reject) => { parseString(xmlContent, { explicitArray: true, mergeAttrs: false }, (err, result) => { if (err) { reject(err); } else { resolve(result); } }); }); } /** * Extract payment data from parsed XML */ function extractPaymentData(parsedXml: any): any { const docArray = parsedXml.Document?.FIToFICstmrCdtTrf; if (!docArray || !Array.isArray(docArray) || !docArray[0]) { throw new Error('Invalid XML structure: Missing FIToFICstmrCdtTrf'); } const doc = docArray[0]; if (!doc.CdtTrfTxInf?.[0]) { throw new Error('Invalid XML structure: Missing CdtTrfTxInf'); } const txInf = doc.CdtTrfTxInf[0]; // Extract amount and currency const settlementAmt = txInf.IntrBkSttlmAmt?.[0]; if (!settlementAmt) { throw new Error('Invalid XML structure: Missing IntrBkSttlmAmt'); } // Handle xml2js structure: text content is in _ property, attributes in $ property const amountStr = typeof settlementAmt === 'string' ? settlementAmt : (settlementAmt._ || settlementAmt); const amount = parseFloat(amountStr); const currency = (settlementAmt.$ && settlementAmt.$.Ccy) || 'EUR'; // Extract sender account (Debtor Account) const senderAccount = txInf.DbtrAcct?.[0]?.Id?.[0]?.Othr?.[0]?.Id?.[0]; if (!senderAccount) { throw new Error('Invalid XML structure: Missing DbtrAcct'); } // Extract sender BIC (Debtor Agent) const senderBIC = txInf.DbtrAgt?.[0]?.FinInstnId?.[0]?.BICFI?.[0]; if (!senderBIC) { throw new Error('Invalid XML structure: Missing DbtrAgt BICFI'); } // Extract receiver account (Creditor Account) const receiverAccount = txInf.CdtrAcct?.[0]?.Id?.[0]?.Othr?.[0]?.Id?.[0]; if (!receiverAccount) { throw new Error('Invalid XML structure: Missing CdtrAcct'); } // Extract receiver BIC (Creditor Agent) const receiverBIC = txInf.CdtrAgt?.[0]?.FinInstnId?.[0]?.BICFI?.[0]; if (!receiverBIC) { throw new Error('Invalid XML structure: Missing CdtrAgt BICFI'); } // Extract beneficiary name (Creditor) const beneficiaryName = txInf.Cdtr?.[0]?.Nm?.[0]; if (!beneficiaryName) { throw new Error('Invalid XML structure: Missing Cdtr Nm'); } // Extract remittance info const remittanceInfo = txInf.RmtInf?.[0]?.Ustrd?.[0] || ''; // Extract purpose (can use remittance info or set default) const purpose = remittanceInfo || 'Payment transaction'; return { type: 'CUSTOMER_CREDIT_TRANSFER', amount: amount, currency: currency, senderAccount: senderAccount, senderBIC: senderBIC, receiverAccount: receiverAccount, receiverBIC: receiverBIC, beneficiaryName: beneficiaryName, purpose: purpose, remittanceInfo: remittanceInfo, }; } /** * Submit a payment */ async function submitPayment(paymentData: any, filename: string): Promise { logInfo(`Submitting payment from ${filename}...`); logInfo(` Amount: ${paymentData.amount} ${paymentData.currency}`); logInfo(` From: ${paymentData.senderAccount} (${paymentData.senderBIC})`); logInfo(` To: ${paymentData.receiverAccount} (${paymentData.receiverBIC})`); logInfo(` Beneficiary: ${paymentData.beneficiaryName}`); const result = await makeRequest('POST', '/payments', paymentData); if (result.ok && result.data && (result.data.paymentId || result.data.id)) { const paymentId = result.data.paymentId || result.data.id; logSuccess(`Payment submitted successfully`); logInfo(` Payment ID: ${paymentId}`); logInfo(` Status: ${result.data.status}`); return true; } else { let errorMsg = 'Unknown error'; if (result.error) { errorMsg = result.error; } else if (result.data) { if (typeof result.data === 'string') { errorMsg = result.data; } else if (result.data.error) { // Handle nested error object if (typeof result.data.error === 'object' && result.data.error.message) { errorMsg = result.data.error.message; if (result.data.error.code) { errorMsg = `[${result.data.error.code}] ${errorMsg}`; } } else { errorMsg = result.data.error; } } else if (result.data.message) { errorMsg = result.data.message; } else if (Array.isArray(result.data)) { errorMsg = result.data.join(', '); } else { try { errorMsg = JSON.stringify(result.data, null, 2); } catch (e) { errorMsg = String(result.data); } } if (result.data.details) { errorMsg += `\n Details: ${JSON.stringify(result.data.details, null, 2)}`; } } logError(`Failed to submit payment: ${errorMsg}`); if (result.response && !result.ok) { logInfo(` HTTP Status: ${result.response.status}`); } return false; } } async function main() { log('\n' + '='.repeat(60), colors.cyan); log('SUBMIT TEMPLATE TRANSACTIONS TO PENDING APPROVALS', colors.cyan); log('='.repeat(60), colors.cyan); // Login const loginSuccess = await login(); if (!loginSuccess) { logError('Cannot continue without authentication'); process.exit(1); } // Process template files const templatesDir = path.join(process.cwd(), 'docs/examples'); const templateFiles = [ 'pacs008-template-a.xml', 'pacs008-template-b.xml', ]; const results: { file: string; success: boolean }[] = []; for (const templateFile of templateFiles) { const filePath = path.join(templatesDir, templateFile); if (!fs.existsSync(filePath)) { logError(`Template file not found: ${templateFile}`); results.push({ file: templateFile, success: false }); continue; } try { log(`\n=== PROCESSING ${templateFile} ===`, colors.blue); // Parse XML const parsedXml = await parseXMLFile(filePath); // Extract payment data const paymentData = extractPaymentData(parsedXml); // Submit payment const success = await submitPayment(paymentData, templateFile); results.push({ file: templateFile, success }); } catch (error: any) { logError(`Error processing ${templateFile}: ${error.message}`); results.push({ file: templateFile, success: false }); } } // Print summary log('\n' + '='.repeat(60), colors.cyan); log('SUMMARY', colors.cyan); log('='.repeat(60), colors.cyan); const successful = results.filter(r => r.success).length; const total = results.length; results.forEach((result) => { const status = result.success ? '✓' : '✗'; const color = result.success ? colors.green : colors.red; log(`${status} ${result.file}`, color); }); log('\n' + '='.repeat(60), colors.cyan); log(`Total: ${successful}/${total} payments submitted successfully`, successful === total ? colors.green : colors.yellow); log('='.repeat(60) + '\n', colors.cyan); process.exit(successful === total ? 0 : 1); } // Run script if (require.main === module) { // Check if fetch is available (Node.js 18+) if (typeof fetch === 'undefined') { console.error('Error: fetch is not available. Please use Node.js 18+ or install node-fetch'); process.exit(1); } main().catch((error) => { logError(`Script failed: ${error.message}`); console.error(error); process.exit(1); }); } export { main };