import type { BillingWebhookHandlerService, IHandleWebhookEventParams } from '@kit/billing'; import { getLogger } from '@kit/shared/logger'; import { Database, Enums } from '@kit/supabase/database'; import jwt from 'jsonwebtoken'; import { MontonioServerEnvSchema } from '../schema/montonio-server-env.schema'; type UpsertOrderParams = Database['medreport']['Functions']['upsert_order']['Args']; type BillingProvider = Enums<{ schema: 'medreport' }, 'billing_provider'>; interface MontonioOrderToken { uuid: string; accessKey: string; merchantReference: string; merchantReferenceDisplay: string; paymentStatus: 'PAID' | 'FAILED' | 'CANCELLED' | 'PENDING' | 'EXPIRED' | 'REFUNDED'; paymentMethod: string; grandTotal: number; currency: string; senderIban?: string; senderName?: string; paymentProviderName?: string; paymentLinkUuid: string; iat: number; exp: number; } const { secretKey } = MontonioServerEnvSchema.parse({ apiUrl: process.env.MONTONIO_API_URL, secretKey: process.env.MONTONIO_SECRET_KEY, }); export class MontonioWebhookHandlerService implements BillingWebhookHandlerService { private readonly provider: BillingProvider = 'montonio'; private readonly namespace = 'billing.montonio'; async verifyWebhookSignature(request: Request) { const logger = await getLogger(); let token: string; try { const url = new URL(request.url); const searchParams = url.searchParams; console.info("searchParams", searchParams, url); const tokenParam = searchParams.get('order-token') as string | null; if (!tokenParam) { throw new Error('Missing order-token'); } token = tokenParam; } catch (error) { logger.error({ error, name: this.namespace, }, `Failed to parse Montonio webhook request`); throw new Error('Invalid request'); } try { const decoded = jwt.verify(token, secretKey, { algorithms: ['HS256'], }); return decoded as MontonioOrderToken; } catch (error) { logger.error({ error, name: this.namespace, }, `Failed to verify Montonio webhook signature`); throw new Error('Invalid signature'); } } async handleWebhookEvent( event: MontonioOrderToken, params: IHandleWebhookEventParams ) { const logger = await getLogger(); logger.info({ name: this.namespace, event, }, `Received Montonio webhook event`); if (event.paymentStatus === 'PAID') { const [accountId] = event.merchantReferenceDisplay.split(':'); if (!accountId) { throw new Error('Invalid merchant reference'); } const order: UpsertOrderParams = { target_account_id: accountId, target_customer_id: '', target_order_id: event.uuid, status: 'succeeded', billing_provider: this.provider, total_amount: event.grandTotal, currency: event.currency, line_items: [], }; return params.onCheckoutSessionCompleted(order); } if (event.paymentStatus === 'FAILED' || event.paymentStatus === 'CANCELLED') { return params.onPaymentFailed(event.uuid); } return; } }