UNPKG

argus-demo-cli

Version:

Demo CLI tool for Argus financial control plane

530 lines (461 loc) • 22.9 kB
#!/usr/bin/env node import { Command } from 'commander'; import chalk from 'chalk'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import { readFileSync } from 'fs'; import Argus from 'argus-sdk'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const packageJson = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')); const program = new Command(); // Helper function to parse integers correctly // (avoids parseInt radix issue with Commander.js defaults) const parseIntValue = (value) => parseInt(value, 10); program .name('argus-demo-cli') .description('Demo CLI tool for Argus financial control plane') .version(packageJson.version) .helpOption('-h, --help', 'Display help for command') .addHelpText('after', ` ${chalk.yellow('Authentication Requirements:')} This CLI requires the following environment variables to be set: ${chalk.cyan('ARGUS_API_KEY')} - Your Argus API key ${chalk.cyan('ARGUS_BASE_URL')} - The base URL for the Argus API (e.g., https://api.argus.example.com) ${chalk.yellow('Example:')} export ARGUS_API_KEY="your-api-key-here" export ARGUS_BASE_URL="https://api.argus.example.com" argus-demo-cli evaluate --amount 100 --currency USD ${chalk.yellow('Getting Started:')} 1. Set your environment variables 2. Run 'argus-demo-cli --help' to see available commands 3. Use 'argus-demo-cli <command> --help' for command-specific help `); // Example command to demonstrate functionality program .command('evaluate') .description('Evaluate a transaction against policies') .option('-a, --amount <amount>', 'Transaction amount', parseFloat) .option('-c, --currency <currency>', 'Currency code (e.g., USD, EUR)', 'USD') .option('-m, --merchant <merchant>', 'Merchant name') .action((options) => { // Check for required environment variables if (!process.env.ARGUS_API_KEY || !process.env.ARGUS_BASE_URL) { console.error(chalk.red('Error: Missing required environment variables')); console.error(chalk.yellow('Please set ARGUS_API_KEY and ARGUS_BASE_URL')); console.error(chalk.gray('Run "argus-demo-cli --help" for more information')); process.exit(1); } console.log(chalk.green('Evaluating transaction...')); console.log(chalk.gray(`Amount: ${options.amount} ${options.currency}`)); if (options.merchant) { console.log(chalk.gray(`Merchant: ${options.merchant}`)); } console.log(chalk.gray(`Using API: ${process.env.ARGUS_BASE_URL}`)); // This is where you would make the actual API call console.log(chalk.yellow('\nThis is a demo - actual API integration not implemented')); }); // Demo command that runs 5 test transactions program .command('demo') .description('Run a demo with 5 preset test transactions') .option('-i, --iterations <number>', 'Number of iterations to run', parseIntValue, 1) .action(async (options) => { // Check for required environment variables const apiKey = process.env.ARGUS_API_KEY; const baseURL = process.env.ARGUS_BASE_URL || 'http://localhost:8000'; if (!apiKey) { console.error(chalk.red('Error: Missing required environment variables')); console.error(chalk.yellow('Please set ARGUS_API_KEY and ARGUS_BASE_URL')); console.error(chalk.gray('Run "argus-demo-cli --help" for more information')); process.exit(1); } // Preset transactions for demo - Realistic B2B finance scenarios const transactions = [ { name: "AWS Monthly Services", amount: "3847.23", currency: "USD", payment_method: "card", recipient_name: "Amazon Web Services", merchant_category_code: "5734", // Computer Software Stores metadata: { category: "cloud_infrastructure", recurring: true, vendor_id: "aws_prod_01" } }, { name: "Office Supplies Vendor", amount: "892.47", currency: "USD", payment_method: "ach", recipient_name: "Staples Business Advantage", merchant_category_code: "5943", // Office Equipment/Supplies metadata: { category: "office_supplies", po_number: "PO-2024-0187" } }, { name: "Software License Renewal", amount: "4999.00", currency: "USD", payment_method: "card", recipient_name: "Salesforce Inc", merchant_category_code: "5734", // Computer Software Stores metadata: { category: "software_licenses", license_type: "enterprise", seats: 50 } }, { name: "Suspicious Wire Transfer", amount: "45000.00", currency: "USD", payment_method: "wire", recipient_name: "Offshore Holdings LLC", merchant_category_code: "6051", // Cryptocurrency/Digital Currency recipient_type: "business", metadata: { category: "investment", first_time_vendor: true, country: "Cayman Islands" } }, { name: "International Contractor Payment", amount: "8500.00", currency: "EUR", payment_method: "wire", recipient_name: "TechConsult GmbH", recipient_type: "business", metadata: { category: "professional_services", country: "Germany", tax_form: "W-8BEN-E" } } ]; console.log(chalk.blue('šŸš€ Starting Argus Demo - B2B Finance Operations')); console.log(chalk.gray(`šŸ“ API URL: ${baseURL}`)); console.log(chalk.gray(`šŸ”‘ API Key: ${apiKey.substring(0, 20)}...${apiKey.substring(apiKey.length - 4)}`)); console.log(chalk.gray(`šŸ”„ Running ${transactions.length} transactions${options.iterations > 1 ? ` (${options.iterations} iterations)` : ''}...\n`)); // Initialize client const client = new Argus({ apiKey, baseURL, }); // Debug: log the actual request being made console.log(chalk.gray('Debug: Initializing Argus client with:')); console.log(chalk.gray(` - baseURL: ${baseURL}`)); console.log(chalk.gray(` - apiKey starts with: ${apiKey.substring(0, 10)}...`)); // Track results const results = { approved: 0, rejected: 0, manual_review: 0, total: 0 }; const timings = []; try { // Run iterations for (let iter = 0; iter < options.iterations; iter++) { if (options.iterations > 1) { console.log(chalk.gray(`\nšŸ”„ Iteration ${iter + 1}/${options.iterations}`)); } // Process each transaction for (let i = 0; i < transactions.length; i++) { const tx = transactions[i]; const startTime = Date.now(); try { const response = await client.evaluate.transaction({ amount: tx.amount, currency: tx.currency, payment_method: tx.payment_method, recipient_name: tx.recipient_name, recipient_type: tx.recipient_type, merchant_category_code: tx.merchant_category_code, metadata: tx.metadata, }); const duration = Date.now() - startTime; timings.push(duration); results.total++; // Update counters switch (response.decision.toUpperCase()) { case 'APPROVED': results.approved++; console.log(chalk.green(`āœ… APPROVED`) + ` Transaction ${i + 1}: ${tx.name} (${tx.currency} ${tx.amount})`); break; case 'REJECTED': results.rejected++; console.log(chalk.red(`āŒ REJECTED`) + ` Transaction ${i + 1}: ${tx.name} (${tx.currency} ${tx.amount})`); break; case 'MANUAL_REVIEW': results.manual_review++; console.log(chalk.yellow(`āš ļø MANUAL REVIEW`) + ` Transaction ${i + 1}: ${tx.name} (${tx.currency} ${tx.amount})`); break; } console.log(chalk.gray(` Decision ID: ${response.decision_id}`)); if (response.failed_rule) { console.log(chalk.gray(` Failed Rule: ${response.failed_rule}`)); } console.log(chalk.gray(` Time: ${duration}ms\n`)); } catch (error) { console.error(chalk.red(`āŒ Error processing ${tx.name}:`), error instanceof Error ? error.message : error); if (error.response) { console.error(chalk.gray('Response status:'), error.response.status); console.error(chalk.gray('Response data:'), JSON.stringify(error.response.data, null, 2)); } results.total++; } } } // Display summary const avgTime = timings.length > 0 ? Math.round(timings.reduce((a, b) => a + b, 0) / timings.length) : 0; const approvalRate = results.total > 0 ? Math.round((results.approved / results.total) * 100) : 0; console.log(chalk.blue('\nšŸ“Š Summary:')); console.log(chalk.gray(` Total: ${results.total} transactions`)); console.log(chalk.gray(` Approved: ${results.approved} (${approvalRate}%)`)); console.log(chalk.gray(` Rejected: ${results.rejected} (${Math.round((results.rejected / results.total) * 100)}%)`)); console.log(chalk.gray(` Manual Review: ${results.manual_review} (${Math.round((results.manual_review / results.total) * 100)}%)`)); if (timings.length > 0) { console.log(chalk.gray(` Avg Response Time: ${avgTime}ms`)); } } catch (error) { console.error(chalk.red('\nāŒ Fatal error:'), error instanceof Error ? error.message : error); process.exit(1); } }); // Seed dashboard command program .command('seed-dashboard') .description('Generate historical transaction data for dashboard analytics') .option('-d, --days <number>', 'Number of days of history to generate', parseIntValue, 30) .option('-t, --transactions-per-day <number>', 'Average transactions per day', parseIntValue, 25) .action(async (options) => { // Check for required environment variables const apiKey = process.env.ARGUS_API_KEY; const baseURL = process.env.ARGUS_BASE_URL || 'http://localhost:8000'; if (!apiKey) { console.error(chalk.red('Error: Missing required environment variables')); console.error(chalk.yellow('Please set ARGUS_API_KEY')); console.error(chalk.gray('Run "argus-demo-cli --help" for more information')); process.exit(1); } const days = options.days; const avgTransactionsPerDay = options.transactionsPerDay; console.log(chalk.blue('🌱 Seeding Dashboard with Historical Data')); console.log(chalk.gray(`šŸ“ API URL: ${baseURL}`)); console.log(chalk.gray(`šŸ“… Days of history: ${days}`)); console.log(chalk.gray(`šŸ“Š Average transactions per day: ${avgTransactionsPerDay}`)); console.log(chalk.gray(`šŸ“¦ Estimated total transactions: ~${days * avgTransactionsPerDay}\n`)); // Initialize client const client = new Argus({ apiKey, baseURL, }); // Merchant templates for variety const merchantTemplates = [ // Software/SaaS (30%) { name: "Amazon Web Services", mcc: "5734", category: "cloud_infrastructure", weight: 8 }, { name: "Microsoft Azure", mcc: "5734", category: "cloud_infrastructure", weight: 7 }, { name: "Salesforce Inc", mcc: "5734", category: "software_licenses", weight: 5 }, { name: "Adobe Creative Cloud", mcc: "5734", category: "software_licenses", weight: 4 }, { name: "Slack Technologies", mcc: "5734", category: "software_subscriptions", weight: 3 }, { name: "Zoom Video Communications", mcc: "5734", category: "software_subscriptions", weight: 3 }, // Office Supplies (20%) { name: "Staples Business", mcc: "5943", category: "office_supplies", weight: 10 }, { name: "Office Depot", mcc: "5943", category: "office_supplies", weight: 7 }, { name: "W.B. Mason", mcc: "5943", category: "office_supplies", weight: 3 }, // Professional Services (20%) { name: "McKinsey & Company", mcc: "8999", category: "consulting", weight: 5 }, { name: "Deloitte Consulting", mcc: "8999", category: "consulting", weight: 5 }, { name: "Legal Services LLC", mcc: "8111", category: "legal_services", weight: 5 }, { name: "BDO Accounting", mcc: "8931", category: "accounting", weight: 5 }, // Travel/Hotels (10%) { name: "United Airlines", mcc: "3000", category: "travel", weight: 3 }, { name: "Marriott Hotels", mcc: "7011", category: "travel", weight: 3 }, { name: "Hertz Car Rental", mcc: "7512", category: "travel", weight: 2 }, { name: "Uber for Business", mcc: "4121", category: "travel", weight: 2 }, // Electronics (10%) { name: "Apple Store", mcc: "5732", category: "electronics", weight: 5 }, { name: "Dell Technologies", mcc: "5732", category: "electronics", weight: 5 }, // High-risk (5%) { name: "Crypto Exchange Ltd", mcc: "6051", category: "cryptocurrency", weight: 2 }, { name: "Online Gaming Platform", mcc: "5967", category: "gambling", weight: 2 }, { name: "Betting Services Inc", mcc: "7995", category: "gambling", weight: 1 }, // Other business expenses (5%) { name: "FedEx", mcc: "4215", category: "shipping", weight: 2 }, { name: "AT&T Business", mcc: "4814", category: "telecommunications", weight: 2 }, { name: "Business Insurance Co", mcc: "6300", category: "insurance", weight: 1 } ]; // Calculate total weight for probability distribution const totalWeight = merchantTemplates.reduce((sum, m) => sum + m.weight, 0); // Helper function to get random merchant based on weights function getRandomMerchant() { let random = Math.random() * totalWeight; for (const merchant of merchantTemplates) { random -= merchant.weight; if (random <= 0) return merchant; } return merchantTemplates[0]; // Fallback } // Helper function to generate transaction amount based on category function generateAmount(category) { const ranges = { cloud_infrastructure: { min: 1000, max: 15000, decimal: true }, software_licenses: { min: 500, max: 10000, decimal: false }, software_subscriptions: { min: 50, max: 2000, decimal: true }, office_supplies: { min: 50, max: 2000, decimal: true }, consulting: { min: 5000, max: 50000, decimal: false }, legal_services: { min: 2000, max: 25000, decimal: false }, accounting: { min: 1000, max: 10000, decimal: false }, travel: { min: 100, max: 5000, decimal: true }, electronics: { min: 500, max: 5000, decimal: true }, cryptocurrency: { min: 10000, max: 50000, decimal: false }, gambling: { min: 1000, max: 10000, decimal: false }, shipping: { min: 20, max: 500, decimal: true }, telecommunications: { min: 200, max: 2000, decimal: true }, insurance: { min: 1000, max: 5000, decimal: false } }; const range = ranges[category] || { min: 100, max: 5000, decimal: true }; const amount = Math.random() * (range.max - range.min) + range.min; return range.decimal ? amount.toFixed(2) : Math.round(amount).toFixed(2); } // Helper function to determine if transaction happens on given hour function shouldTransactAtHour(hour, isWeekend) { // Business hours pattern (9 AM - 6 PM) if (isWeekend) { // Much lower chance on weekends return Math.random() < 0.1; } if (hour >= 9 && hour <= 18) { // Peak business hours return Math.random() < 0.7; } else if (hour >= 7 && hour <= 21) { // Extended business hours return Math.random() < 0.3; } else { // Night time return Math.random() < 0.05; } } // Track results const results = { approved: 0, rejected: 0, manual_review: 0, total: 0, errors: 0 }; const startTime = Date.now(); try { // Generate transactions for each day for (let dayOffset = days - 1; dayOffset >= 0; dayOffset--) { const date = new Date(); date.setDate(date.getDate() - dayOffset); date.setHours(0, 0, 0, 0); const dayOfWeek = date.getDay(); const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; // Generate random transaction count with variance let baseTxCount; if (isWeekend) { // Weekends: 20-40% of average baseTxCount = avgTransactionsPerDay * (0.2 + Math.random() * 0.2); } else if (dayOfWeek === 1) { // Mondays: 110-130% (busy catch-up day) baseTxCount = avgTransactionsPerDay * (1.1 + Math.random() * 0.2); } else if (dayOfWeek === 5) { // Fridays: 80-100% (wind down) baseTxCount = avgTransactionsPerDay * (0.8 + Math.random() * 0.2); } else { // Tuesday-Thursday: 90-110% of average baseTxCount = avgTransactionsPerDay * (0.9 + Math.random() * 0.2); } // Add some additional random variance (±20%) const variance = 0.2; const randomFactor = 1 + (Math.random() - 0.5) * variance; const dayTransactions = Math.max(1, Math.floor(baseTxCount * randomFactor)); console.log(chalk.gray(`šŸ“… Generating ${date.toDateString()} (${dayTransactions} transactions)...`)); // Generate transactions throughout the day const transactionTimes = []; for (let hour = 0; hour < 24; hour++) { if (shouldTransactAtHour(hour, isWeekend)) { const minute = Math.floor(Math.random() * 60); const second = Math.floor(Math.random() * 60); transactionTimes.push({ hour, minute, second }); } } // Ensure we have enough transaction times while (transactionTimes.length < dayTransactions) { const hour = Math.floor(Math.random() * 24); const minute = Math.floor(Math.random() * 60); const second = Math.floor(Math.random() * 60); transactionTimes.push({ hour, minute, second }); } // Shuffle and limit to desired number transactionTimes.sort(() => Math.random() - 0.5); transactionTimes.splice(dayTransactions); // Generate transactions for this day for (const time of transactionTimes) { const merchant = getRandomMerchant(); const amount = generateAmount(merchant.category); // Determine currency (70% USD, 20% EUR, 10% GBP) const currencyRand = Math.random(); const currency = currencyRand < 0.7 ? "USD" : currencyRand < 0.9 ? "EUR" : "GBP"; // Determine payment method based on amount and category let payment_method; const amountNum = parseFloat(amount); if (amountNum > 10000) { payment_method = "wire"; } else if (merchant.category.includes("consulting") || merchant.category.includes("legal")) { payment_method = Math.random() < 0.7 ? "ach" : "wire"; } else { payment_method = Math.random() < 0.6 ? "card" : "ach"; } // Create transaction timestamp const transactionDate = new Date(date); transactionDate.setHours(time.hour, time.minute, time.second); try { const response = await client.evaluate.transaction({ amount, currency, payment_method, recipient_name: merchant.name, merchant_category_code: merchant.mcc, created_at: transactionDate.toISOString(), metadata: { category: merchant.category, day_of_week: transactionDate.getDay(), hour_of_day: transactionDate.getHours() } }); results.total++; // Update counters switch (response.decision.toUpperCase()) { case 'APPROVED': results.approved++; break; case 'REJECTED': results.rejected++; break; case 'MANUAL_REVIEW': results.manual_review++; break; } } catch (error) { results.errors++; if (results.errors <= 5) { console.error(chalk.red(` āŒ Error: ${error instanceof Error ? error.message : error}`)); } } } // Show progress const progress = ((days - dayOffset) / days * 100).toFixed(0); console.log(chalk.green(` āœ“ Day complete. Progress: ${progress}%`)); } // Calculate summary statistics const duration = Math.round((Date.now() - startTime) / 1000); const approvalRate = results.total > 0 ? Math.round((results.approved / results.total) * 100) : 0; const rejectionRate = results.total > 0 ? Math.round((results.rejected / results.total) * 100) : 0; const reviewRate = results.total > 0 ? Math.round((results.manual_review / results.total) * 100) : 0; console.log(chalk.blue('\n✨ Dashboard Seeding Complete!')); console.log(chalk.gray('─'.repeat(50))); console.log(chalk.gray(`ā±ļø Duration: ${duration} seconds`)); console.log(chalk.gray(`šŸ“Š Total Transactions: ${results.total}`)); console.log(chalk.green(`āœ… Approved: ${results.approved} (${approvalRate}%)`)); console.log(chalk.red(`āŒ Rejected: ${results.rejected} (${rejectionRate}%)`)); console.log(chalk.yellow(`āš ļø Manual Review: ${results.manual_review} (${reviewRate}%)`)); if (results.errors > 0) { console.log(chalk.red(`🚫 Errors: ${results.errors}`)); } console.log(chalk.gray('─'.repeat(50))); console.log(chalk.cyan('\nšŸŽÆ Your dashboard should now display:')); console.log(chalk.gray(' • Transaction volume trends over time')); console.log(chalk.gray(' • Decision breakdown pie chart')); console.log(chalk.gray(' • Top rejection reasons')); console.log(chalk.gray(' • Payment method distribution')); console.log(chalk.gray('\nšŸ“ˆ Visit your dashboard to see the analytics!')); } catch (error) { console.error(chalk.red('\nāŒ Fatal error:'), error instanceof Error ? error.message : error); process.exit(1); } }); // Parse command line arguments program.parse(process.argv); // Show help if no command is provided if (!process.argv.slice(2).length) { program.outputHelp(); }