Files
boc/quixzoom-token-system/api/server.js
T
Bernt 58ca4e68db feat(boc): Complete Business Operations Center v1.0
- Go backend API with full CRUD for all modules (CRM, Sales, Finance, HR, Legal, Marketing, Support, Purchase, Inventory, Projects, Automation, Analytics)
- Rust analytics service with parallel report generation
- C runtime with POSIX shared memory IPC
- PostgreSQL schema with 30+ tables, full migrations
- Redis cache, sessions, pub/sub
- Kafka event streaming with Zookeeper
- WebSocket hub for real-time updates
- Automation engine with cron jobs, workflows, event triggers
- JWT authentication, multi-tenant from start
- Docker Compose with all services
- Nginx reverse proxy with rate limiting
- Integration tests passing
- Feature gap analysis against Fortnox/Odoo/Visma

Refs: BOC-001
2026-07-12 12:41:35 +00:00

473 lines
15 KiB
JavaScript

// ============================================================
// QZ TOKEN API SERVER
// ============================================================
// Endpoints för:
// - Zoomer wallet-hantering
// - Token-transaktioner
// - Mission-prissättning
// - Payout-hantering
// - Stripe Connect-integration
const express = require('express');
const { Pool } = require('pg');
const crypto = require('crypto');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const app = express();
app.use(express.json());
// PostgreSQL pool
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
});
// Konstanta
const QZ_TO_USD_RATE = 1.0; // 1 QZ TOKEN = 1 USD (fast kurs, test)
const MIN_MISSION_PRICE = 0.5; // Minimum $0.50 per mission
// ============================================================
// HJÄLPFUNKTIONER
// ============================================================
function generateTxHash() {
const timestamp = Date.now();
const random = crypto.randomBytes(16).toString('hex');
return `qz_tx_${timestamp}_${random}`;
}
function signTransaction(txData, privateKey) {
const sign = crypto.createSign('SHA256');
sign.update(JSON.stringify(txData));
sign.end();
return sign.sign(privateKey, 'hex');
}
function qzToUsd(qzAmount) {
return qzAmount * QZ_TO_USD_RATE;
}
function usdToQz(usdAmount) {
return usdAmount / QZ_TO_USD_RATE;
}
// ============================================================
// ZOOMER WALLET
// ============================================================
// Skapa wallet för ny Zoomer
app.post('/api/v1/wallets', async (req, res) => {
const { zoomer_id, stripe_connect_account_id } = req.body;
try {
const result = await pool.query(
`INSERT INTO zoomer_wallets (zoomer_id, stripe_connect_account_id, stripe_connect_status)
VALUES ($1, $2, $3)
RETURNING *`,
[zoomer_id, stripe_connect_account_id, stripe_connect_account_id ? 'active' : 'pending']
);
// Skapa default payout schema (måndagar)
await pool.query(
`INSERT INTO payout_schedules (zoomer_id, wallet_id, day_of_week, time_of_day)
VALUES ($1, $2, 1, '09:00:00')`,
[zoomer_id, result.rows[0].id]
);
res.json({
success: true,
wallet: result.rows[0]
});
} catch (error) {
console.error('Error creating wallet:', error);
res.status(500).json({ error: 'Failed to create wallet' });
}
});
// Hämta wallet-info
app.get('/api/v1/wallets/:zoomer_id', async (req, res) => {
const { zoomer_id } = req.params;
try {
const result = await pool.query(
`SELECT * FROM zoomer_balance WHERE zoomer_id = $1`,
[zoomer_id]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Wallet not found' });
}
res.json({
success: true,
wallet: result.rows[0]
});
} catch (error) {
console.error('Error fetching wallet:', error);
res.status(500).json({ error: 'Failed to fetch wallet' });
}
});
// ============================================================
// TOKEN TRANSAKTIONER
// ============================================================
// Skapa mission reward (när uppdrag godkänns)
app.post('/api/v1/transactions/mission-reward', async (req, res) => {
const { zoomer_id, mission_id, observation_id, amount_qz_token, description } = req.body;
const client = await pool.connect();
try {
await client.query('BEGIN');
// Hämta wallet
const walletResult = await client.query(
`SELECT id FROM zoomer_wallets WHERE zoomer_id = $1`,
[zoomer_id]
);
if (walletResult.rows.length === 0) {
throw new Error('Wallet not found');
}
const wallet_id = walletResult.rows[0].id;
const txHash = generateTxHash();
const amount_usd = qzToUsd(amount_qz_token);
// Skapa transaktion
const txResult = await client.query(
`INSERT INTO token_transactions
(tx_hash, tx_type, to_wallet_id, amount_qz_token, amount_usd, exchange_rate, mission_id, observation_id, description, status, confirmed_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW())
RETURNING *`,
[txHash, 'mission_reward', wallet_id, amount_qz_token, amount_usd, QZ_TO_USD_RATE, mission_id, observation_id, description, 'confirmed']
);
// Uppdatera wallet-balans
await client.query(
`UPDATE zoomer_wallets
SET balance_qz_token = balance_qz_token + $1
WHERE id = $2`,
[amount_qz_token, wallet_id]
);
await client.query('COMMIT');
res.json({
success: true,
transaction: txResult.rows[0],
message: `Rewarded ${amount_qz_token} QZ TOKEN ($${amount_usd.toFixed(2)}) to zoomer ${zoomer_id}`
});
} catch (error) {
await client.query('ROLLBACK');
console.error('Error creating mission reward:', error);
res.status(500).json({ error: 'Failed to create mission reward' });
} finally {
client.release();
}
});
// Hämta transaktionshistorik
app.get('/api/v1/transactions/:zoomer_id', async (req, res) => {
const { zoomer_id } = req.params;
const { limit = 50, offset = 0 } = req.query;
try {
const walletResult = await pool.query(
`SELECT id FROM zoomer_wallets WHERE zoomer_id = $1`,
[zoomer_id]
);
if (walletResult.rows.length === 0) {
return res.status(404).json({ error: 'Wallet not found' });
}
const wallet_id = walletResult.rows[0].id;
const result = await pool.query(
`SELECT * FROM token_transactions
WHERE from_wallet_id = $1 OR to_wallet_id = $1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3`,
[wallet_id, limit, offset]
);
res.json({
success: true,
transactions: result.rows
});
} catch (error) {
console.error('Error fetching transactions:', error);
res.status(500).json({ error: 'Failed to fetch transactions' });
}
});
// ============================================================
// MISSION PRICING
// ============================================================
// Hämta pris för mission-typ
app.get('/api/v1/pricing/:mission_type', async (req, res) => {
const { mission_type } = req.params;
const { country_code, city } = req.query;
try {
let query = `SELECT * FROM mission_pricing WHERE mission_type = $1 AND status = 'active'`;
let params = [mission_type];
if (country_code) {
query += ` AND (country_code = $2 OR country_code IS NULL)`;
params.push(country_code);
}
if (city) {
query += ` AND (city = $3 OR city IS NULL)`;
params.push(city);
}
query += ` ORDER BY country_code DESC, city DESC LIMIT 1`;
const result = await pool.query(query, params);
if (result.rows.length === 0) {
// Returnera default-pris
return res.json({
success: true,
pricing: {
mission_type,
base_price_qz_token: usdToQz(MIN_MISSION_PRICE),
base_price_usd: MIN_MISSION_PRICE,
complexity_multiplier: 1.0,
min_price_qz_token: usdToQz(MIN_MISSION_PRICE),
max_price_qz_token: 100.0
}
});
}
res.json({
success: true,
pricing: result.rows[0]
});
} catch (error) {
console.error('Error fetching pricing:', error);
res.status(500).json({ error: 'Failed to fetch pricing' });
}
});
// ============================================================
// PAYOUT
// ============================================================
// Initiera payout (körs varje måndag via cron)
app.post('/api/v1/payouts/execute', async (req, res) => {
const { zoomer_id } = req.body; // Optional: specific zoomer, or all
try {
// Hitta zoomers med payout idag
const today = new Date().getDay(); // 1 = Monday
let query = `
SELECT ps.*, zw.balance_qz_token, zw.stripe_connect_account_id, z.name as zoomer_name
FROM payout_schedules ps
JOIN zoomer_wallets zw ON zw.id = ps.wallet_id
JOIN zoomers z ON z.id = ps.zoomer_id
WHERE ps.day_of_week = $1
AND ps.status = 'active'
AND zw.stripe_connect_status = 'active'
AND zw.balance_qz_token >= ps.minimum_qz_token
`;
let params = [today];
if (zoomer_id) {
query += ` AND ps.zoomer_id = $2`;
params.push(zoomer_id);
}
const schedules = await pool.query(query, params);
const results = [];
for (const schedule of schedules.rows) {
try {
const payoutResult = await executePayout(schedule);
results.push(payoutResult);
} catch (error) {
results.push({
zoomer_id: schedule.zoomer_id,
status: 'failed',
error: error.message
});
}
}
res.json({
success: true,
payouts_executed: results.length,
results
});
} catch (error) {
console.error('Error executing payouts:', error);
res.status(500).json({ error: 'Failed to execute payouts' });
}
});
// Utför payout via Stripe Connect
async function executePayout(schedule) {
const client = await pool.connect();
try {
await client.query('BEGIN');
const amount_qz_token = schedule.balance_qz_token;
const amount_usd = qzToUsd(amount_qz_token);
// Skapa Stripe Transfer
const transfer = await stripe.transfers.create({
amount: Math.round(amount_usd * 100), // Stripe använder cent
currency: 'usd',
destination: schedule.stripe_connect_account_id,
description: `quiXzoom payout for ${schedule.zoomer_name}`
});
// Skapa payout record
const payoutResult = await client.query(
`INSERT INTO payout_executions
(zoomer_id, wallet_id, amount_qz_token, amount_usd, exchange_rate, stripe_transfer_id, status, processed_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
RETURNING *`,
[schedule.zoomer_id, schedule.wallet_id, amount_qz_token, amount_usd, QZ_TO_USD_RATE, transfer.id, 'completed']
);
// Skapa token transaction (burn/payout)
const txHash = generateTxHash();
await client.query(
`INSERT INTO token_transactions
(tx_hash, tx_type, from_wallet_id, amount_qz_token, amount_usd, exchange_rate, stripe_payout_id, description, status, confirmed_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW())`,
[txHash, 'payout', schedule.wallet_id, amount_qz_token, amount_usd, QZ_TO_USD_RATE, payoutResult.rows[0].id, `Weekly payout to ${schedule.zoomer_name}`, 'confirmed']
);
// Nollställ wallet-balans
await client.query(
`UPDATE zoomer_wallets SET balance_qz_token = 0 WHERE id = $1`,
[schedule.wallet_id]
);
await client.query('COMMIT');
return {
zoomer_id: schedule.zoomer_id,
status: 'completed',
amount_qz_token,
amount_usd,
stripe_transfer_id: transfer.id
};
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
// ============================================================
// CUSTOMER TOP-UP
// ============================================================
// Skapa top-up (kund köper QZ TOKENS)
app.post('/api/v1/topups', async (req, res) => {
const { customer_id, amount_usd } = req.body;
try {
const amount_qz_token = usdToQz(amount_usd);
// Skapa Stripe Payment Intent
const paymentIntent = await stripe.paymentIntents.create({
amount: Math.round(amount_usd * 100),
currency: 'usd',
customer: customer_id,
description: `quiXzoom Top-up: ${amount_qz_token} QZ TOKEN`
});
const result = await pool.query(
`INSERT INTO customer_topups
(customer_id, amount_qz_token, amount_usd, exchange_rate, stripe_payment_intent_id, status)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *`,
[customer_id, amount_qz_token, amount_usd, QZ_TO_USD_RATE, paymentIntent.id, 'pending']
);
res.json({
success: true,
topup: result.rows[0],
client_secret: paymentIntent.client_secret
});
} catch (error) {
console.error('Error creating top-up:', error);
res.status(500).json({ error: 'Failed to create top-up' });
}
});
// Webhook för Stripe-händelser
app.post('/api/v1/webhooks/stripe', express.raw({ type: 'application/json' }), async (req, res) => {
const sig = req.headers['stripe-signature'];
try {
const event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
switch (event.type) {
case 'payment_intent.succeeded':
await handlePaymentSuccess(event.data.object);
break;
case 'transfer.paid':
await handleTransferPaid(event.data.object);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
res.json({ received: true });
} catch (error) {
console.error('Webhook error:', error);
res.status(400).send(`Webhook Error: ${error.message}`);
}
});
async function handlePaymentSuccess(paymentIntent) {
await pool.query(
`UPDATE customer_topups
SET status = 'completed', completed_at = NOW()
WHERE stripe_payment_intent_id = $1`,
[paymentIntent.id]
);
}
async function handleTransferPaid(transfer) {
await pool.query(
`UPDATE payout_executions
SET status = 'completed', completed_at = NOW()
WHERE stripe_transfer_id = $1`,
[transfer.id]
);
}
// ============================================================
// HEALTH CHECK
// ============================================================
app.get('/health', (req, res) => {
res.json({ status: 'ok', service: 'qz-token-api' });
});
// Starta server
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
console.log(`QZ Token API running on port ${PORT}`);
});
module.exports = app;