Files
boc/services/frilans-payout/routes.mjs
T
Bernt 1a12fb870b dev-api: add developer portal, OpenAPI spec, and Flatenbadet case study
- New /developers/ page with API docs, SDKs, pricing, use cases
- OpenAPI 3.0 spec for Orders, Missions, Photos, Analytics
- Case study: Glasskiosken i Flatenbadet — complete ROI analysis
- Updated /order/ with recurring missions and frequency dropdown
2026-07-14 15:27:33 +00:00

281 lines
8.5 KiB
JavaScript

/**
* 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;