Files
medreport_mrb2b/packages/billing/montonio/src/services/montonio-webhook-handler.service.ts
Danel Kungla 0c2cfe6d18 prettier fix
2025-09-19 17:22:36 +03:00

134 lines
3.3 KiB
TypeScript

import jwt from 'jsonwebtoken';
import type {
BillingWebhookHandlerService,
IHandleWebhookEventParams,
} from '@kit/billing';
import { getLogger } from '@kit/shared/logger';
import { Database, Enums } from '@kit/supabase/database';
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;
}
}