CognxSafeTrack
feat: backlog P0→P3 — toast system, payments, tenant isolation, feedback handler, i18n parity
6dd9bad | import { logger } from '../logger'; | |
| export type PaymentProvider = 'ORANGE_MONEY' | 'WAVE'; | |
| export interface PaymentInitRequest { | |
| amount: number; | |
| currency: string; | |
| reference: string; | |
| notifyUrl: string; | |
| returnUrl: string; | |
| description: string; | |
| customerPhone?: string; | |
| } | |
| export interface PaymentInitResult { | |
| paymentUrl: string; | |
| sessionId: string; | |
| provider: PaymentProvider; | |
| } | |
| export interface WebhookPayload { | |
| status: 'SUCCESS' | 'FAILED' | 'PENDING'; | |
| reference: string; | |
| transactionId?: string; | |
| amount?: number; | |
| provider: PaymentProvider; | |
| } | |
| // Orange Money Merchant API (West Africa) | |
| async function initOrangeMoney(req: PaymentInitRequest): Promise<PaymentInitResult> { | |
| const apiKey = process.env.ORANGE_MONEY_API_KEY; | |
| const merchantId = process.env.ORANGE_MONEY_MERCHANT_ID; | |
| const apiUrl = process.env.ORANGE_MONEY_API_URL || 'https://api.orange.com/orange-money-webpay/dev/v1'; | |
| if (!apiKey || !merchantId) throw new Error('Orange Money credentials not configured'); | |
| const res = await fetch(`${apiUrl}/webpayment`, { | |
| method: 'POST', | |
| headers: { | |
| 'Authorization': `Bearer ${apiKey}`, | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| merchant_key: merchantId, | |
| currency: req.currency, | |
| order_id: req.reference, | |
| amount: req.amount, | |
| return_url: req.returnUrl, | |
| cancel_url: req.returnUrl, | |
| notif_url: req.notifyUrl, | |
| lang: 'fr', | |
| reference: req.reference, | |
| }), | |
| }); | |
| if (!res.ok) { | |
| const err = await res.json().catch(() => ({}) ) as { message?: string }; | |
| throw new Error(`Orange Money init failed: ${err.message || res.status}`); | |
| } | |
| const data = await res.json() as { payment_url: string; pay_token: string }; | |
| return { | |
| paymentUrl: data.payment_url, | |
| sessionId: data.pay_token, | |
| provider: 'ORANGE_MONEY', | |
| }; | |
| } | |
| // Wave API (Senegal) | |
| async function initWave(req: PaymentInitRequest): Promise<PaymentInitResult> { | |
| const apiKey = process.env.WAVE_API_KEY; | |
| const apiUrl = 'https://api.wave.com/v1/checkout/sessions'; | |
| if (!apiKey) throw new Error('Wave API key not configured'); | |
| const res = await fetch(apiUrl, { | |
| method: 'POST', | |
| headers: { | |
| 'Authorization': `Bearer ${apiKey}`, | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| amount: String(req.amount), | |
| currency: req.currency, | |
| error_url: req.returnUrl, | |
| success_url: `${req.returnUrl}?success=1&ref=${req.reference}`, | |
| client_reference: req.reference, | |
| }), | |
| }); | |
| if (!res.ok) { | |
| const err = await res.json().catch(() => ({}) ) as { message?: string }; | |
| throw new Error(`Wave init failed: ${err.message || res.status}`); | |
| } | |
| const data = await res.json() as { wave_launch_url: string; id: string }; | |
| return { | |
| paymentUrl: data.wave_launch_url, | |
| sessionId: data.id, | |
| provider: 'WAVE', | |
| }; | |
| } | |
| export async function initiatePayment(provider: PaymentProvider, req: PaymentInitRequest): Promise<PaymentInitResult> { | |
| logger.info({ provider, reference: req.reference, amount: req.amount }, '[PAYMENT] Initiating payment'); | |
| if (provider === 'ORANGE_MONEY') return initOrangeMoney(req); | |
| if (provider === 'WAVE') return initWave(req); | |
| throw new Error(`Unsupported provider: ${provider}`); | |
| } | |
| // Parse and normalize webhooks from Orange Money and Wave into a unified format | |
| export function parseWebhook(provider: PaymentProvider, body: Record<string, unknown>): WebhookPayload { | |
| if (provider === 'ORANGE_MONEY') { | |
| const statusMap: Record<string, WebhookPayload['status']> = { | |
| SUCCESS: 'SUCCESS', | |
| FAILED: 'FAILED', | |
| INITIATED: 'PENDING', | |
| }; | |
| return { | |
| provider, | |
| reference: String(body.order_id || body.reference || ''), | |
| transactionId: String(body.txnid || ''), | |
| amount: Number(body.amount || 0), | |
| status: statusMap[String(body.status)] ?? 'FAILED', | |
| }; | |
| } | |
| if (provider === 'WAVE') { | |
| return { | |
| provider, | |
| reference: String(body.client_reference || ''), | |
| transactionId: String(body.id || ''), | |
| amount: Number(body.amount || 0), | |
| status: body.payment_status === 'succeeded' ? 'SUCCESS' : 'FAILED', | |
| }; | |
| } | |
| throw new Error(`Unknown provider: ${provider}`); | |
| } | |