/** * quiXzoom Frilans Payout — API Routes * REST endpoints for multi-market payout */ import { Router } from 'express'; import { z } from 'zod'; import { getMarket, listMarkets, calculatePayout } from './markets.mjs'; import * as db from './db.mjs'; const router = Router(); // ── Validation Schemas ────────────────────────────────────────────────────── const registerSchema = z.object({ country: z.string().length(2), tax_handling: z.enum(['self', 'platform']), personal_number: z.string().optional(), payout_method: z.enum(['frilans_finans', 'stripe_connect', 'bank_transfer']), }); const calculateSchema = z.object({ amount: z.number().positive(), country: z.string().length(2), tax_handling: z.enum(['self', 'platform']), }); const payoutSchema = z.object({ amount: z.number().positive(), country: z.string().length(2), method: z.enum(['frilans_finans', 'stripe_connect', 'bank_transfer']), }); // ── Middleware ────────────────────────────────────────────────────────────── function validate(schema) { return (req, res, next) => { try { schema.parse(req.body); next(); } catch (error) { res.status(400).json({ error: 'Validation failed', details: error.errors }); } }; } // ── Routes ────────────────────────────────────────────────────────────────── // GET /api/aamos/frilans/markets router.get('/markets', (req, res) => { const markets = listMarkets().map(m => ({ country: m.country, currency: m.currency, language: m.language, tax_rate: m.tax_rate, platform_fee_pct: m.platform_fee_pct, processing_fee_pct: m.processing_fee_pct, min_payout: m.min_payout, payout_schedule: m.payout_schedule, frilans_available: m.frilans_available, stripe_connect_available: m.stripe_connect_available, bank_transfer_available: m.bank_transfer_available, })); res.json({ markets }); }); // GET /api/aamos/frilans/markets/:country router.get('/markets/:country', (req, res) => { const market = getMarket(req.params.country); if (!market) { return res.status(404).json({ error: 'Market not found' }); } res.json({ market }); }); // POST /api/aamos/frilans/register router.post('/register', validate(registerSchema), async (req, res) => { try { const { country, tax_handling, personal_number, payout_method } = req.body; const userId = req.user?.id; // From auth middleware const market = getMarket(country); if (!market) { return res.status(400).json({ error: 'Market not supported' }); } // Check if method is available if (payout_method === 'frilans_finans' && !market.frilans_available) { return res.status(400).json({ error: 'Frilans not available in this market' }); } const account = await db.createAccount({ userId, country: market.country, currency: market.currency, taxHandling: tax_handling, personalNumber: personal_number, payoutMethod: payout_method, }); res.status(201).json({ account_id: account.id, status: account.status, market: { country: market.country, currency: market.currency, }, tax_rate: market.tax_rate, platform_fee_pct: market.platform_fee_pct, processing_fee_pct: market.processing_fee_pct, }); } catch (error) { res.status(500).json({ error: error.message }); } }); // GET /api/aamos/frilans/account router.get('/account', async (req, res) => { try { const userId = req.user?.id; const account = await db.getAccountByUserId(userId); if (!account) { return res.status(404).json({ error: 'Account not found' }); } res.json({ account }); } catch (error) { res.status(500).json({ error: error.message }); } }); // POST /api/aamos/frilans/calculate router.post('/calculate', validate(calculateSchema), (req, res) => { try { const { amount, country, tax_handling } = req.body; const market = getMarket(country); if (!market) { return res.status(400).json({ error: 'Market not supported' }); } const calculation = calculatePayout(amount, market, tax_handling); res.json({ ...calculation, breakdown: [ { label: 'Gross Amount', amount: calculation.gross_amount, type: 'net' }, { label: `Platform Fee (${(calculation.platform_fee_pct * 100).toFixed(0)}%)`, amount: calculation.platform_fee, type: 'fee' }, { label: `Processing Fee (${(calculation.processing_fee_pct * 100).toFixed(1)}%)`, amount: calculation.processing_fee, type: 'fee' }, ...(calculation.tax_amount > 0 ? [{ label: `Withholding Tax (${(calculation.tax_rate * 100).toFixed(0)}%)`, amount: calculation.tax_amount, type: 'tax', }] : []), { label: 'Net Amount', amount: calculation.net_amount, type: 'net' }, ], }); } catch (error) { res.status(500).json({ error: error.message }); } }); // POST /api/aamos/frilans/payout router.post('/payout', validate(payoutSchema), async (req, res) => { try { const { amount, country, method } = req.body; const userId = req.user?.id; const account = await db.getAccountByUserId(userId); if (!account) { return res.status(404).json({ error: 'Account not found' }); } const market = getMarket(country); if (!market) { return res.status(400).json({ error: 'Market not supported' }); } // Calculate const calculation = calculatePayout(amount, market, account.tax_handling); // Schedule date const scheduledDate = getNextPayoutDate(market.payout_schedule); // Create payout const payout = await db.createPayout({ accountId: account.id, amount: calculation.gross_amount, netAmount: calculation.net_amount, currency: calculation.currency, platformFee: calculation.platform_fee, processingFee: calculation.processing_fee, taxAmount: calculation.tax_amount, method, scheduledDate, }); res.status(201).json({ payout_id: payout.id, status: payout.status, amount: payout.amount, net_amount: payout.net_amount, currency: payout.currency, scheduled_date: payout.scheduled_date, method: payout.method, }); } catch (error) { res.status(500).json({ error: error.message }); } }); // GET /api/aamos/frilans/payouts router.get('/payouts', async (req, res) => { try { const userId = req.user?.id; const account = await db.getAccountByUserId(userId); if (!account) { return res.status(404).json({ error: 'Account not found' }); } const payouts = await db.listPayouts(account.id); res.json({ payouts }); } catch (error) { res.status(500).json({ error: error.message }); } }); // GET /api/aamos/frilans/tax-summary router.get('/tax-summary', async (req, res) => { try { const userId = req.user?.id; const year = parseInt(req.query.year) || new Date().getFullYear(); const account = await db.getAccountByUserId(userId); if (!account) { return res.status(404).json({ error: 'Account not found' }); } const summary = await db.getTaxSummary(account.id, year); const market = getMarket(account.country); res.json({ year, country: account.country, currency: account.currency, ...summary, tax_rate: market?.tax_rate || 0, }); } catch (error) { res.status(500).json({ error: error.message }); } }); // ── Helpers ───────────────────────────────────────────────────────────────── function getNextPayoutDate(schedule) { const now = new Date(); const date = new Date(now); switch (schedule) { case 'weekly': const daysUntilFriday = (5 - now.getDay() + 7) % 7; date.setDate(now.getDate() + (daysUntilFriday === 0 ? 7 : daysUntilFriday)); break; case 'biweekly': date.setDate(now.getDate() + 14); break; case 'monthly': date.setMonth(now.getMonth() + 1); date.setDate(0); break; } return date.toISOString().split('T')[0]; } export default router;