134 lines
3.3 KiB
TypeScript
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;
|
|
}
|
|
}
|