Files
medreport_mrb2b/packages/billing/montonio/src/services/montonio-webhook-handler.service.ts

111 lines
3.2 KiB
TypeScript

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;
}
}