Initial commit: add .gitignore and README
This commit is contained in:
16
scripts/create-test-db.sql
Normal file
16
scripts/create-test-db.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
-- Create test database for DBIS Core Lite
|
||||
-- Run with: psql -U postgres -f scripts/create-test-db.sql
|
||||
|
||||
-- Drop database if it exists (use with caution)
|
||||
-- DROP DATABASE IF EXISTS dbis_core_test;
|
||||
|
||||
-- Create test database
|
||||
CREATE DATABASE dbis_core_test;
|
||||
|
||||
-- Connect to test database and create schema
|
||||
\c dbis_core_test
|
||||
|
||||
-- The schema will be created by running migrations
|
||||
-- After creating the database, run:
|
||||
-- DATABASE_URL=postgresql://postgres:postgres@localhost:5432/dbis_core_test npm run migrate
|
||||
|
||||
130
scripts/ensure-account-balance.ts
Normal file
130
scripts/ensure-account-balance.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Script to ensure account has required balance
|
||||
* Usage: ts-node -r tsconfig-paths/register scripts/ensure-account-balance.ts
|
||||
*/
|
||||
|
||||
import { LedgerAdapterFactory } from '../src/ledger/adapter/factory';
|
||||
import { Currency } from '../src/models/payment';
|
||||
import { TransactionType } from '../src/models/transaction';
|
||||
import { query } from '../src/database/connection';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const ACCOUNT_NUMBER = 'US64000000000000000000001';
|
||||
const CURRENCY = 'EUR' as Currency;
|
||||
const REQUIRED_BALANCE = 97000000000.00;
|
||||
|
||||
async function ensureAccountBalance() {
|
||||
try {
|
||||
console.log(`Checking balance for account ${ACCOUNT_NUMBER} (${CURRENCY})...`);
|
||||
|
||||
const adapter = LedgerAdapterFactory.getAdapter();
|
||||
const currentBalance = await adapter.getBalance(ACCOUNT_NUMBER, CURRENCY);
|
||||
|
||||
console.log('Current balance:', {
|
||||
totalBalance: currentBalance.totalBalance,
|
||||
availableBalance: currentBalance.availableBalance,
|
||||
reservedBalance: currentBalance.reservedBalance,
|
||||
});
|
||||
|
||||
if (currentBalance.totalBalance >= REQUIRED_BALANCE) {
|
||||
console.log(`✓ Account already has sufficient balance: ${currentBalance.totalBalance.toFixed(2)} ${CURRENCY}`);
|
||||
console.log(` Required: ${REQUIRED_BALANCE.toFixed(2)} ${CURRENCY}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const difference = REQUIRED_BALANCE - currentBalance.totalBalance;
|
||||
console.log(`Account balance is insufficient. Adding ${difference.toFixed(2)} ${CURRENCY}...`);
|
||||
|
||||
// Create a system payment record for the initial balance seed
|
||||
const systemPaymentId = uuidv4();
|
||||
await query(
|
||||
`INSERT INTO payments (
|
||||
id, payment_id, type, amount, currency,
|
||||
sender_account, sender_bic, receiver_account, receiver_bic,
|
||||
beneficiary_name, maker_operator_id, status
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
|
||||
(SELECT id FROM operators LIMIT 1), $11)
|
||||
ON CONFLICT (payment_id) DO NOTHING`,
|
||||
[
|
||||
systemPaymentId,
|
||||
'SYSTEM-INITIAL-BALANCE',
|
||||
'FI_TO_FI',
|
||||
difference,
|
||||
CURRENCY,
|
||||
'SYSTEM',
|
||||
'SYSTEM',
|
||||
ACCOUNT_NUMBER,
|
||||
'SYSTEM',
|
||||
'Initial Balance Seed',
|
||||
'SETTLED',
|
||||
]
|
||||
);
|
||||
|
||||
// Get the payment ID (may have been created or already exists)
|
||||
const paymentResult = await query(
|
||||
`SELECT id FROM payments WHERE payment_id = $1`,
|
||||
['SYSTEM-INITIAL-BALANCE']
|
||||
);
|
||||
|
||||
if (paymentResult.rows.length === 0) {
|
||||
throw new Error('Failed to create system payment record');
|
||||
}
|
||||
|
||||
const paymentId = paymentResult.rows[0].id;
|
||||
|
||||
// Create a credit transaction to bring balance to required amount
|
||||
const transactionId = uuidv4();
|
||||
await query(
|
||||
`INSERT INTO ledger_postings (
|
||||
internal_transaction_id, payment_id, account_number, transaction_type,
|
||||
amount, currency, status, posting_timestamp, reference
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
||||
[
|
||||
transactionId,
|
||||
paymentId,
|
||||
ACCOUNT_NUMBER,
|
||||
TransactionType.CREDIT,
|
||||
difference,
|
||||
CURRENCY,
|
||||
'POSTED',
|
||||
new Date(),
|
||||
'INITIAL_BALANCE_SEED',
|
||||
]
|
||||
);
|
||||
|
||||
// Verify new balance
|
||||
const newBalance = await adapter.getBalance(ACCOUNT_NUMBER, CURRENCY);
|
||||
console.log('✓ Balance updated successfully!');
|
||||
console.log('New balance:', {
|
||||
totalBalance: newBalance.totalBalance.toFixed(2),
|
||||
availableBalance: newBalance.availableBalance.toFixed(2),
|
||||
reservedBalance: newBalance.reservedBalance.toFixed(2),
|
||||
});
|
||||
|
||||
if (newBalance.totalBalance >= REQUIRED_BALANCE) {
|
||||
console.log(`✓ Account now has sufficient balance: ${newBalance.totalBalance.toFixed(2)} ${CURRENCY}`);
|
||||
} else {
|
||||
console.error(`✗ Error: Balance still insufficient: ${newBalance.totalBalance.toFixed(2)} ${CURRENCY}`);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Error ensuring account balance:', error.message);
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run if executed directly
|
||||
if (require.main === module) {
|
||||
ensureAccountBalance()
|
||||
.then(() => {
|
||||
console.log('Script completed successfully');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Script failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
export { ensureAccountBalance };
|
||||
55
scripts/quick-test-setup.sh
Executable file
55
scripts/quick-test-setup.sh
Executable file
@@ -0,0 +1,55 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Quick test database setup script
|
||||
# This script provides simple commands to set up the test database
|
||||
|
||||
set -e
|
||||
|
||||
echo "🔧 DBIS Core Lite - Quick Test Database Setup"
|
||||
echo "=============================================="
|
||||
echo ""
|
||||
|
||||
DB_NAME="dbis_core_test"
|
||||
DEFAULT_URL="postgresql://postgres:postgres@localhost:5432/${DB_NAME}"
|
||||
|
||||
# Function to check if command exists
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Check for PostgreSQL
|
||||
if command_exists psql; then
|
||||
echo "✅ PostgreSQL client found"
|
||||
elif command_exists docker; then
|
||||
echo "⚠️ PostgreSQL client not found, but Docker is available"
|
||||
echo " You can use Docker to run PostgreSQL (see README_TEST_DATABASE.md)"
|
||||
else
|
||||
echo "❌ Neither PostgreSQL client nor Docker found"
|
||||
echo " Please install PostgreSQL or Docker to continue"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "📋 Quick Setup Commands:"
|
||||
echo ""
|
||||
echo "1. Create test database:"
|
||||
echo " createdb ${DB_NAME}"
|
||||
echo ""
|
||||
echo "2. Set environment variable:"
|
||||
echo " export TEST_DATABASE_URL=\"${DEFAULT_URL}\""
|
||||
echo " # Or create .env.test file (already created)"
|
||||
echo ""
|
||||
echo "3. Run migrations:"
|
||||
echo " DATABASE_URL=\$TEST_DATABASE_URL npm run migrate"
|
||||
echo ""
|
||||
echo "4. Run tests:"
|
||||
echo " npm test"
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo "💡 Tip: Create .env.test file with:"
|
||||
echo " TEST_DATABASE_URL=${DEFAULT_URL}"
|
||||
echo ""
|
||||
echo "📖 For detailed instructions, see: README_TEST_DATABASE.md"
|
||||
echo ""
|
||||
|
||||
135
scripts/setup-test-db-docker.sh
Executable file
135
scripts/setup-test-db-docker.sh
Executable file
@@ -0,0 +1,135 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Docker-based test database setup for DBIS Core Lite
|
||||
|
||||
set -e
|
||||
|
||||
echo "🐳 DBIS Core Lite - Docker Test Database Setup"
|
||||
echo "=============================================="
|
||||
echo ""
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Check if Docker is available
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo -e "${RED}❌ Docker is not installed${NC}"
|
||||
echo " Please install Docker or use manual PostgreSQL setup"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✅ Docker found${NC}"
|
||||
echo ""
|
||||
|
||||
# Check if docker-compose is available
|
||||
if command -v docker-compose &> /dev/null; then
|
||||
COMPOSE_CMD="docker-compose"
|
||||
elif docker compose version &> /dev/null; then
|
||||
COMPOSE_CMD="docker compose"
|
||||
else
|
||||
echo -e "${RED}❌ Docker Compose is not available${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✅ Docker Compose found${NC}"
|
||||
echo ""
|
||||
|
||||
# Start PostgreSQL container
|
||||
echo "🚀 Starting PostgreSQL container..."
|
||||
$COMPOSE_CMD -f docker-compose.test.yml up -d postgres-test
|
||||
|
||||
echo ""
|
||||
echo "⏳ Waiting for PostgreSQL to be ready..."
|
||||
sleep 5
|
||||
|
||||
# Wait for PostgreSQL to be healthy
|
||||
MAX_WAIT=30
|
||||
WAITED=0
|
||||
while [ $WAITED -lt $MAX_WAIT ]; do
|
||||
if docker exec dbis_core_test_db pg_isready -U postgres > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}✅ PostgreSQL is ready${NC}"
|
||||
break
|
||||
fi
|
||||
echo -n "."
|
||||
sleep 1
|
||||
WAITED=$((WAITED + 1))
|
||||
done
|
||||
|
||||
if [ $WAITED -ge $MAX_WAIT ]; then
|
||||
echo -e "${RED}❌ PostgreSQL did not become ready in time${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Create test database
|
||||
echo "📦 Creating test database..."
|
||||
docker exec dbis_core_test_db psql -U postgres -c "CREATE DATABASE dbis_core_test;" 2>/dev/null || {
|
||||
echo -e "${YELLOW}⚠️ Database may already exist${NC}"
|
||||
}
|
||||
|
||||
echo -e "${GREEN}✅ Test database created${NC}"
|
||||
echo ""
|
||||
|
||||
# Apply schema
|
||||
echo "📋 Applying database schema..."
|
||||
docker exec -i dbis_core_test_db psql -U postgres -d dbis_core_test < src/database/schema.sql > /dev/null 2>&1
|
||||
echo -e "${GREEN}✅ Schema applied${NC}"
|
||||
echo ""
|
||||
|
||||
# Update .env.test with Docker connection
|
||||
TEST_DB_URL="postgresql://postgres:postgres@localhost:5434/dbis_core_test"
|
||||
echo "📝 Updating .env.test with Docker connection..."
|
||||
cat > .env.test << EOF
|
||||
# Test Database Configuration (Docker)
|
||||
TEST_DATABASE_URL=${TEST_DB_URL}
|
||||
|
||||
# Test Environment Variables
|
||||
NODE_ENV=test
|
||||
JWT_SECRET=test-secret-key-for-testing-only
|
||||
EOF
|
||||
|
||||
echo -e "${GREEN}✅ .env.test updated${NC}"
|
||||
echo ""
|
||||
|
||||
# Run migrations (if any)
|
||||
echo "🔄 Running database migrations..."
|
||||
export TEST_DATABASE_URL="${TEST_DB_URL}"
|
||||
export DATABASE_URL="${TEST_DB_URL}"
|
||||
|
||||
if npm run migrate > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}✅ Migrations completed${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Migrations completed (or none needed)${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Verify tables
|
||||
echo "🔍 Verifying database schema..."
|
||||
TABLE_COUNT=$(docker exec dbis_core_test_db psql -U postgres -d dbis_core_test -t -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';" 2>/dev/null | tr -d ' ')
|
||||
|
||||
if [ -n "$TABLE_COUNT" ] && [ "$TABLE_COUNT" -gt "0" ]; then
|
||||
echo -e "${GREEN}✅ Database schema verified (${TABLE_COUNT} tables)${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ No tables found - please check schema${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}✅ Docker test database setup complete!${NC}"
|
||||
echo ""
|
||||
echo "📋 Connection Details:"
|
||||
echo " Host: localhost"
|
||||
echo " Port: 5434"
|
||||
echo " Database: dbis_core_test"
|
||||
echo " User: postgres"
|
||||
echo " Password: postgres"
|
||||
echo ""
|
||||
echo "🚀 Next steps:"
|
||||
echo " 1. Run tests: npm test"
|
||||
echo " 2. Stop container: $COMPOSE_CMD -f docker-compose.test.yml down"
|
||||
echo " 3. Start container: $COMPOSE_CMD -f docker-compose.test.yml up -d"
|
||||
echo ""
|
||||
128
scripts/setup-test-db.sh
Executable file
128
scripts/setup-test-db.sh
Executable file
@@ -0,0 +1,128 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to set up test database for DBIS Core Lite
|
||||
|
||||
set -e
|
||||
|
||||
echo "🔧 Setting up test database for DBIS Core Lite"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Default values
|
||||
DB_USER="${POSTGRES_USER:-postgres}"
|
||||
DB_PASSWORD="${POSTGRES_PASSWORD:-postgres}"
|
||||
DB_HOST="${POSTGRES_HOST:-localhost}"
|
||||
DB_PORT="${POSTGRES_PORT:-5432}"
|
||||
TEST_DB_NAME="dbis_core_test"
|
||||
|
||||
# Test database URL
|
||||
TEST_DATABASE_URL="postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${TEST_DB_NAME}"
|
||||
|
||||
echo "📋 Configuration:"
|
||||
echo " Database: ${TEST_DB_NAME}"
|
||||
echo " User: ${DB_USER}"
|
||||
echo " Host: ${DB_HOST}:${DB_PORT}"
|
||||
echo ""
|
||||
|
||||
# Check if PostgreSQL is accessible
|
||||
echo "🔍 Checking PostgreSQL connection..."
|
||||
if ! PGPASSWORD="${DB_PASSWORD}" psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d postgres -c "SELECT 1" > /dev/null 2>&1; then
|
||||
echo -e "${RED}❌ Cannot connect to PostgreSQL${NC}"
|
||||
echo " Please ensure PostgreSQL is running and credentials are correct"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}✅ PostgreSQL connection successful${NC}"
|
||||
echo ""
|
||||
|
||||
# Check if test database exists
|
||||
echo "🔍 Checking if test database exists..."
|
||||
if PGPASSWORD="${DB_PASSWORD}" psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -lqt 2>/dev/null | cut -d \| -f 1 | grep -qw "${TEST_DB_NAME}"; then
|
||||
echo -e "${YELLOW}⚠️ Test database '${TEST_DB_NAME}' already exists${NC}"
|
||||
read -p "Do you want to drop and recreate it? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "🗑️ Dropping existing test database..."
|
||||
PGPASSWORD="${DB_PASSWORD}" psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d postgres -c "DROP DATABASE IF EXISTS ${TEST_DB_NAME};" > /dev/null 2>&1
|
||||
echo -e "${GREEN}✅ Database dropped${NC}"
|
||||
else
|
||||
echo "⏭️ Keeping existing database"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create test database if it doesn't exist
|
||||
if ! PGPASSWORD="${DB_PASSWORD}" psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -lqt 2>/dev/null | cut -d \| -f 1 | grep -qw "${TEST_DB_NAME}"; then
|
||||
echo "📦 Creating test database '${TEST_DB_NAME}'..."
|
||||
PGPASSWORD="${DB_PASSWORD}" psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d postgres -c "CREATE DATABASE ${TEST_DB_NAME};" > /dev/null 2>&1
|
||||
echo -e "${GREEN}✅ Test database created${NC}"
|
||||
else
|
||||
echo -e "${GREEN}✅ Test database already exists${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Run migrations
|
||||
echo "🔄 Running database migrations..."
|
||||
export DATABASE_URL="${TEST_DATABASE_URL}"
|
||||
if npm run migrate > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}✅ Migrations completed successfully${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠️ Migrations may have failed or already applied${NC}"
|
||||
echo " Checking database schema..."
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Verify tables exist
|
||||
echo "🔍 Verifying database schema..."
|
||||
TABLES=$(PGPASSWORD="${DB_PASSWORD}" psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d "${TEST_DB_NAME}" -t -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';" 2>/dev/null | tr -d ' ')
|
||||
if [ -n "$TABLES" ] && [ "$TABLES" -gt 0 ]; then
|
||||
echo -e "${GREEN}✅ Database schema verified (${TABLES} tables found)${NC}"
|
||||
|
||||
# List tables
|
||||
echo ""
|
||||
echo "📊 Tables in test database:"
|
||||
PGPASSWORD="${DB_PASSWORD}" psql -h "${DB_HOST}" -p "${DB_PORT}" -U "${DB_USER}" -d "${TEST_DB_NAME}" -c "\dt" 2>/dev/null || echo " (Unable to list tables)"
|
||||
else
|
||||
echo -e "${RED}❌ No tables found in test database${NC}"
|
||||
echo " Please check migrations"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Set environment variable in .env.test if it exists, or create it
|
||||
ENV_FILE=".env.test"
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
echo "📝 Updating ${ENV_FILE}..."
|
||||
if grep -q "TEST_DATABASE_URL" "$ENV_FILE"; then
|
||||
sed -i "s|^TEST_DATABASE_URL=.*|TEST_DATABASE_URL=${TEST_DATABASE_URL}|" "$ENV_FILE"
|
||||
else
|
||||
echo "TEST_DATABASE_URL=${TEST_DATABASE_URL}" >> "$ENV_FILE"
|
||||
fi
|
||||
echo -e "${GREEN}✅ ${ENV_FILE} updated${NC}"
|
||||
else
|
||||
echo "📝 Creating ${ENV_FILE}..."
|
||||
cat > "$ENV_FILE" << EOF
|
||||
# Test Database Configuration
|
||||
TEST_DATABASE_URL=${TEST_DATABASE_URL}
|
||||
|
||||
# Test Environment
|
||||
NODE_ENV=test
|
||||
JWT_SECRET=test-secret-key-for-testing-only
|
||||
EOF
|
||||
echo -e "${GREEN}✅ ${ENV_FILE} created${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo -e "${GREEN}✅ Test database setup complete!${NC}"
|
||||
echo ""
|
||||
echo "📋 Next steps:"
|
||||
echo " 1. Run tests with: npm test"
|
||||
echo " 2. Or run specific test suite: npm test -- tests/unit"
|
||||
echo ""
|
||||
echo "💡 Tip: The TEST_DATABASE_URL is set in ${ENV_FILE}"
|
||||
echo " Make sure to load it in your test environment"
|
||||
|
||||
316
scripts/submit-template-transactions.ts
Normal file
316
scripts/submit-template-transactions.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
/**
|
||||
* 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 };
|
||||
406
scripts/test-frontend-flow.ts
Normal file
406
scripts/test-frontend-flow.ts
Normal file
@@ -0,0 +1,406 @@
|
||||
/**
|
||||
* Comprehensive Frontend Flow Test
|
||||
* Tests all possible actions from login through all features
|
||||
* Usage: ts-node -r tsconfig-paths/register scripts/test-frontend-flow.ts
|
||||
*/
|
||||
|
||||
const API_BASE = 'http://localhost:3000/api/v1';
|
||||
let authToken: string = '';
|
||||
let operator: any = null;
|
||||
let createdPaymentId: 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 };
|
||||
}
|
||||
}
|
||||
|
||||
async function testLogin() {
|
||||
log('\n=== TEST 1: LOGIN ===', colors.blue);
|
||||
logInfo('Attempting login with ADMIN001/admin123...');
|
||||
|
||||
const result = await makeRequest('POST', '/auth/login', {
|
||||
operatorId: 'ADMIN001',
|
||||
password: 'admin123',
|
||||
}, false);
|
||||
|
||||
if (result.ok && result.data.token) {
|
||||
authToken = result.data.token;
|
||||
operator = result.data.operator;
|
||||
logSuccess(`Login successful - Operator: ${operator.operatorId} (${operator.name}) - Role: ${operator.role}`);
|
||||
return true;
|
||||
} else {
|
||||
logError(`Login failed: ${result.data?.error || result.error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testGetMe() {
|
||||
log('\n=== TEST 2: GET CURRENT OPERATOR INFO ===', colors.blue);
|
||||
logInfo('Fetching current operator information...');
|
||||
|
||||
const result = await makeRequest('GET', '/auth/me');
|
||||
|
||||
if (result.ok && result.data.operatorId) {
|
||||
logSuccess(`Retrieved operator info: ${result.data.operatorId} (${result.data.role})`);
|
||||
return true;
|
||||
} else {
|
||||
logError(`Failed to get operator info: ${result.data?.error || result.error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testCheckAccountBalance() {
|
||||
log('\n=== TEST 3: CHECK ACCOUNT BALANCE ===', colors.blue);
|
||||
logInfo('Checking balance for account US64000000000000000000001 (EUR)...');
|
||||
|
||||
const result = await makeRequest('GET', '/accounts/US64000000000000000000001/balance?currency=EUR');
|
||||
|
||||
if (result.ok && result.data.totalBalance !== undefined) {
|
||||
logSuccess(`Account balance retrieved successfully`);
|
||||
logInfo(` Total Balance: ${parseFloat(result.data.totalBalance).toLocaleString()} ${result.data.currency}`);
|
||||
logInfo(` Available: ${parseFloat(result.data.availableBalance).toLocaleString()} ${result.data.currency}`);
|
||||
logInfo(` Reserved: ${parseFloat(result.data.reservedBalance).toLocaleString()} ${result.data.currency}`);
|
||||
return true;
|
||||
} else {
|
||||
logError(`Failed to get balance: ${result.data?.error || result.error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testListMessageTemplates() {
|
||||
log('\n=== TEST 4: LIST MESSAGE TEMPLATES ===', colors.blue);
|
||||
logInfo('Fetching available message templates...');
|
||||
|
||||
const result = await makeRequest('GET', '/message-templates');
|
||||
|
||||
if (result.ok && Array.isArray(result.data.templates)) {
|
||||
logSuccess(`Found ${result.data.templates.length} template(s)`);
|
||||
result.data.templates.forEach((template: string) => {
|
||||
logInfo(` - ${template}`);
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
logError(`Failed to list templates: ${result.data?.error || result.error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testLoadMessageTemplate() {
|
||||
log('\n=== TEST 5: LOAD MESSAGE TEMPLATE ===', colors.blue);
|
||||
logInfo('Loading pacs008-template-a.xml template...');
|
||||
|
||||
const result = await makeRequest('POST', '/message-templates/pacs008-template-a.xml', {});
|
||||
|
||||
if (result.ok && result.data.message) {
|
||||
logSuccess('Template loaded successfully');
|
||||
logInfo(` Message ID: ${result.data.message.msgId}`);
|
||||
logInfo(` UETR: ${result.data.message.uetr}`);
|
||||
logInfo(` XML length: ${result.data.message.xml.length} bytes`);
|
||||
return true;
|
||||
} else {
|
||||
logError(`Failed to load template: ${result.data?.error || result.error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testCreatePayment() {
|
||||
log('\n=== TEST 6: CREATE PAYMENT ===', colors.blue);
|
||||
logInfo('Creating a test payment...');
|
||||
|
||||
const paymentData = {
|
||||
type: 'CUSTOMER_CREDIT_TRANSFER',
|
||||
amount: 1000.00,
|
||||
currency: 'EUR',
|
||||
senderAccount: 'US64000000000000000000001',
|
||||
senderBIC: 'DFCUUGKA',
|
||||
receiverAccount: '02650010158937',
|
||||
receiverBIC: 'DFCUUGKA',
|
||||
beneficiaryName: 'Test Beneficiary',
|
||||
purpose: 'Test Payment',
|
||||
remittanceInfo: 'Test remittance information',
|
||||
};
|
||||
|
||||
const result = await makeRequest('POST', '/payments', paymentData);
|
||||
|
||||
if (result.ok && result.data.paymentId) {
|
||||
createdPaymentId = result.data.paymentId;
|
||||
logSuccess(`Payment created successfully`);
|
||||
logInfo(` Payment ID: ${createdPaymentId}`);
|
||||
logInfo(` Status: ${result.data.status}`);
|
||||
return true;
|
||||
} else {
|
||||
const errorMsg = result.data?.error || result.data?.details || JSON.stringify(result.data) || result.error || 'Unknown error';
|
||||
logError(`Failed to create payment: ${errorMsg}`);
|
||||
if (result.data?.details) {
|
||||
logInfo(` Details: ${JSON.stringify(result.data.details)}`);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testGetPaymentStatus() {
|
||||
if (!createdPaymentId) {
|
||||
log('\n=== TEST 7: GET PAYMENT STATUS ===', colors.yellow);
|
||||
logInfo('Skipping - No payment ID available');
|
||||
return false;
|
||||
}
|
||||
|
||||
log('\n=== TEST 7: GET PAYMENT STATUS ===', colors.blue);
|
||||
logInfo(`Fetching status for payment ${createdPaymentId}...`);
|
||||
|
||||
const result = await makeRequest('GET', `/payments/${createdPaymentId}`);
|
||||
|
||||
if (result.ok && result.data.paymentId) {
|
||||
logSuccess('Payment status retrieved successfully');
|
||||
logInfo(` Payment ID: ${result.data.paymentId}`);
|
||||
logInfo(` Status: ${result.data.status}`);
|
||||
logInfo(` Amount: ${result.data.amount} ${result.data.currency}`);
|
||||
logInfo(` UETR: ${result.data.uetr || 'Not yet generated'}`);
|
||||
return true;
|
||||
} else {
|
||||
logError(`Failed to get payment status: ${result.data?.error || result.error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testListPayments() {
|
||||
log('\n=== TEST 8: LIST PAYMENTS ===', colors.blue);
|
||||
logInfo('Fetching list of payments...');
|
||||
|
||||
const result = await makeRequest('GET', '/payments?limit=10&offset=0');
|
||||
|
||||
if (result.ok && Array.isArray(result.data.payments)) {
|
||||
logSuccess(`Retrieved ${result.data.payments.length} payment(s)`);
|
||||
result.data.payments.slice(0, 3).forEach((payment: any) => {
|
||||
logInfo(` - ${payment.payment_id}: ${payment.amount} ${payment.currency} (${payment.status})`);
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
logError(`Failed to list payments: ${result.data?.error || result.error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testApprovePayment() {
|
||||
if (!createdPaymentId) {
|
||||
log('\n=== TEST 9: APPROVE PAYMENT ===', colors.yellow);
|
||||
logInfo('Skipping - No payment ID available');
|
||||
return false;
|
||||
}
|
||||
|
||||
log('\n=== TEST 9: APPROVE PAYMENT ===', colors.blue);
|
||||
logInfo(`Approving payment ${createdPaymentId}...`);
|
||||
|
||||
// Note: This requires CHECKER role, but we're logged in as ADMIN which should work
|
||||
const result = await makeRequest('POST', `/payments/${createdPaymentId}/approve`);
|
||||
|
||||
if (result.ok) {
|
||||
logSuccess('Payment approved successfully');
|
||||
logInfo(` Message: ${result.data.message}`);
|
||||
return true;
|
||||
} else {
|
||||
// This might fail if payment is already approved or requires checker role
|
||||
logError(`Failed to approve payment: ${result.data?.error || result.error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testGetPaymentStatusAfterApproval() {
|
||||
if (!createdPaymentId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
log('\n=== TEST 10: GET PAYMENT STATUS (AFTER APPROVAL) ===', colors.blue);
|
||||
logInfo(`Checking payment status after approval...`);
|
||||
|
||||
const result = await makeRequest('GET', `/payments/${createdPaymentId}`);
|
||||
|
||||
if (result.ok && result.data.paymentId) {
|
||||
logSuccess('Payment status retrieved');
|
||||
logInfo(` Status: ${result.data.status}`);
|
||||
logInfo(` UETR: ${result.data.uetr || 'Not yet generated'}`);
|
||||
return true;
|
||||
} else {
|
||||
logError(`Failed to get payment status: ${result.data?.error || result.error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testMessageTemplateSend() {
|
||||
log('\n=== TEST 11: SEND MESSAGE TEMPLATE ===', colors.blue);
|
||||
logInfo('Sending pacs008-template-a.xml template...');
|
||||
|
||||
const result = await makeRequest('POST', '/message-templates/pacs008-template-a.xml/send', {});
|
||||
|
||||
if (result.ok) {
|
||||
logSuccess('Template message sent successfully');
|
||||
logInfo(` Message ID: ${result.data.messageDetails.msgId}`);
|
||||
logInfo(` UETR: ${result.data.messageDetails.uetr}`);
|
||||
return true;
|
||||
} else {
|
||||
logError(`Failed to send template: ${result.data?.error || result.error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testLogout() {
|
||||
log('\n=== TEST 12: LOGOUT ===', colors.blue);
|
||||
logInfo('Logging out...');
|
||||
|
||||
const result = await makeRequest('POST', '/auth/logout');
|
||||
|
||||
if (result.ok) {
|
||||
logSuccess('Logout successful');
|
||||
authToken = '';
|
||||
operator = null;
|
||||
return true;
|
||||
} else {
|
||||
logError(`Logout failed: ${result.data?.error || result.error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testProtectedEndpointAfterLogout() {
|
||||
log('\n=== TEST 13: TEST PROTECTED ENDPOINT AFTER LOGOUT ===', colors.blue);
|
||||
logInfo('Attempting to access protected endpoint without token...');
|
||||
|
||||
const result = await makeRequest('GET', '/auth/me');
|
||||
|
||||
if (!result.ok && (result.data?.error || result.error)) {
|
||||
logSuccess('Correctly rejected request without valid token');
|
||||
logInfo(` Error: ${result.data?.error || result.error}`);
|
||||
return true;
|
||||
} else {
|
||||
logError('Security issue: Should have rejected request');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function runAllTests() {
|
||||
log('\n' + '='.repeat(60), colors.cyan);
|
||||
log('COMPREHENSIVE FRONTEND FLOW TEST', colors.cyan);
|
||||
log('='.repeat(60), colors.cyan);
|
||||
|
||||
const results: { test: string; passed: boolean }[] = [];
|
||||
|
||||
// Test 1: Login
|
||||
results.push({ test: 'Login', passed: await testLogin() });
|
||||
if (!results[0].passed) {
|
||||
log('\n❌ Login failed. Cannot continue with other tests.', colors.red);
|
||||
return;
|
||||
}
|
||||
|
||||
// Test 2: Get current operator
|
||||
results.push({ test: 'Get Operator Info', passed: await testGetMe() });
|
||||
|
||||
// Test 3: Check account balance
|
||||
results.push({ test: 'Check Account Balance', passed: await testCheckAccountBalance() });
|
||||
|
||||
// Test 4: List message templates
|
||||
results.push({ test: 'List Message Templates', passed: await testListMessageTemplates() });
|
||||
|
||||
// Test 5: Load message template
|
||||
results.push({ test: 'Load Message Template', passed: await testLoadMessageTemplate() });
|
||||
|
||||
// Test 6: Create payment
|
||||
results.push({ test: 'Create Payment', passed: await testCreatePayment() });
|
||||
|
||||
// Test 7: Get payment status
|
||||
results.push({ test: 'Get Payment Status', passed: await testGetPaymentStatus() });
|
||||
|
||||
// Test 8: List payments
|
||||
results.push({ test: 'List Payments', passed: await testListPayments() });
|
||||
|
||||
// Test 9: Approve payment
|
||||
results.push({ test: 'Approve Payment', passed: await testApprovePayment() });
|
||||
|
||||
// Test 10: Get payment status after approval
|
||||
results.push({ test: 'Get Payment Status (After Approval)', passed: await testGetPaymentStatusAfterApproval() });
|
||||
|
||||
// Test 11: Send message template
|
||||
results.push({ test: 'Send Message Template', passed: await testMessageTemplateSend() });
|
||||
|
||||
// Test 12: Logout
|
||||
results.push({ test: 'Logout', passed: await testLogout() });
|
||||
|
||||
// Test 13: Test protected endpoint after logout
|
||||
results.push({ test: 'Protected Endpoint After Logout', passed: await testProtectedEndpointAfterLogout() });
|
||||
|
||||
// Print summary
|
||||
log('\n' + '='.repeat(60), colors.cyan);
|
||||
log('TEST SUMMARY', colors.cyan);
|
||||
log('='.repeat(60), colors.cyan);
|
||||
|
||||
const passed = results.filter(r => r.passed).length;
|
||||
const total = results.length;
|
||||
|
||||
results.forEach((result, index) => {
|
||||
const status = result.passed ? '✓' : '✗';
|
||||
const color = result.passed ? colors.green : colors.red;
|
||||
log(`${status} Test ${index + 1}: ${result.test}`, color);
|
||||
});
|
||||
|
||||
log('\n' + '='.repeat(60), colors.cyan);
|
||||
log(`Total: ${passed}/${total} tests passed`, passed === total ? colors.green : colors.yellow);
|
||||
log('='.repeat(60) + '\n', colors.cyan);
|
||||
|
||||
process.exit(passed === total ? 0 : 1);
|
||||
}
|
||||
|
||||
// Run tests
|
||||
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);
|
||||
}
|
||||
|
||||
runAllTests().catch((error) => {
|
||||
logError(`Test suite failed: ${error.message}`);
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
export { runAllTests };
|
||||
115
scripts/ux-review.ts
Normal file
115
scripts/ux-review.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* UX/UI Review Script
|
||||
* Tests key UX flows and identifies issues
|
||||
*/
|
||||
|
||||
const API_BASE = 'http://localhost:3000/api/v1';
|
||||
|
||||
async function testUXFlows() {
|
||||
console.log('\n=== UX/UI REVIEW ===\n');
|
||||
|
||||
const issues: string[] = [];
|
||||
const suggestions: string[] = [];
|
||||
|
||||
// Test 1: Login form validation
|
||||
console.log('1. Checking login form...');
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ operatorId: '', password: '' }),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (response.status === 400 || response.status === 401) {
|
||||
console.log(' ✓ Empty form validation works');
|
||||
} else {
|
||||
issues.push('Login form should validate empty fields');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(' ✓ Login endpoint accessible');
|
||||
}
|
||||
|
||||
// Test 2: Error message format
|
||||
console.log('\n2. Testing error handling...');
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ operatorId: 'INVALID', password: 'WRONG' }),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.error) {
|
||||
console.log(' ✓ Error messages returned');
|
||||
if (typeof data.error === 'string') {
|
||||
console.log(` Error: ${data.error}`);
|
||||
} else {
|
||||
issues.push('Error response format should be consistent (string)');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
issues.push('Error handling may not be working correctly');
|
||||
}
|
||||
|
||||
// Test 3: Payment form requirements
|
||||
console.log('\n3. Checking payment form requirements...');
|
||||
suggestions.push('Payment form should have client-side validation');
|
||||
suggestions.push('Form fields should show required indicators (*)');
|
||||
suggestions.push('Amount field should prevent negative values');
|
||||
suggestions.push('Account numbers should have format validation');
|
||||
|
||||
// Test 4: Loading states
|
||||
console.log('\n4. Checking loading states...');
|
||||
suggestions.push('Buttons should show loading state during API calls');
|
||||
suggestions.push('Forms should be disabled during submission');
|
||||
suggestions.push('Loading spinners should be shown for async operations');
|
||||
|
||||
// Test 5: Success feedback
|
||||
console.log('\n5. Checking success feedback...');
|
||||
suggestions.push('Success messages should be clear and actionable');
|
||||
suggestions.push('Payment ID should be easily copyable');
|
||||
suggestions.push('Next steps should be clearly indicated');
|
||||
|
||||
// Test 6: Navigation flow
|
||||
console.log('\n6. Checking navigation...');
|
||||
suggestions.push('Clear visual indication of current section');
|
||||
suggestions.push('Breadcrumb or navigation indicators');
|
||||
suggestions.push('Keyboard navigation support (Tab, Enter)');
|
||||
|
||||
// Test 7: Accessibility
|
||||
console.log('\n7. Accessibility considerations...');
|
||||
suggestions.push('Form labels should be properly associated with inputs');
|
||||
suggestions.push('Error messages should be associated with form fields');
|
||||
suggestions.push('Keyboard shortcuts should be documented');
|
||||
suggestions.push('Color contrast should meet WCAG standards');
|
||||
|
||||
// Summary
|
||||
console.log('\n=== SUMMARY ===\n');
|
||||
if (issues.length > 0) {
|
||||
console.log('⚠️ Issues found:');
|
||||
issues.forEach((issue, i) => console.log(` ${i + 1}. ${issue}`));
|
||||
} else {
|
||||
console.log('✓ No critical issues found');
|
||||
}
|
||||
|
||||
if (suggestions.length > 0) {
|
||||
console.log('\n💡 UX Improvements suggested:');
|
||||
suggestions.forEach((suggestion, i) => console.log(` ${i + 1}. ${suggestion}`));
|
||||
}
|
||||
|
||||
console.log('\n');
|
||||
}
|
||||
|
||||
// Run if executed directly
|
||||
if (require.main === module) {
|
||||
if (typeof fetch === 'undefined') {
|
||||
console.error('Error: fetch is not available. Please use Node.js 18+');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
testUXFlows().catch((error) => {
|
||||
console.error('Review failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
export { testUXFlows };
|
||||
Reference in New Issue
Block a user