317 lines
9.4 KiB
TypeScript
317 lines
9.4 KiB
TypeScript
/**
|
|
* 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<any> {
|
|
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<boolean> {
|
|
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 };
|