feat: Expand Mollie provider to handle dynamic webhooks and update payment/invoice statuses

- Add webhook handling for Mollie payment status updates
- Map Mollie payment
This commit is contained in:
2025-09-16 23:02:04 +02:00
parent 2aad0d2538
commit 9fbc720d6a

View File

@@ -3,37 +3,130 @@ import type { InitPayment, PaymentProvider } from '@/plugin/types'
import type { Config, Payload } from 'payload' import type { Config, Payload } from 'payload'
import { createSingleton } from '@/plugin/singleton' import { createSingleton } from '@/plugin/singleton'
import type { createMollieClient, MollieClient } from '@mollie/api-client' import type { createMollieClient, MollieClient } from '@mollie/api-client'
import { defaults } from '@/plugin/config'
import { extractSlug } from '@/plugin/utils'
const symbol = Symbol('mollie') const symbol = Symbol('mollie')
export type MollieProviderConfig = Parameters<typeof createMollieClient>[0] export type MollieProviderConfig = Parameters<typeof createMollieClient>[0]
export const mollieProvider = (config: MollieProviderConfig) => { export const mollieProvider = (mollieConfig: MollieProviderConfig & {
webhookUrl?: string
redirectUrl?: string
}) => {
const singleton = createSingleton<MollieClient>(symbol) const singleton = createSingleton<MollieClient>(symbol)
return { return {
key: 'mollie', key: 'mollie',
onConfig: config => { onConfig: (config, pluginConfig) => {
config.endpoints = [ config.endpoints = [
...(config.endpoints || []), ...(config.endpoints || []),
{ {
path: '/payload-billing/mollie/webhook', path: '/payload-billing/mollie/webhook',
method: 'post', method: 'post',
handler: async (req) => { handler: async (req) => {
const payload = req.payload try {
const mollieClient = singleton.get(payload) const payload = req.payload
if (!req.text) { const mollieClient = singleton.get(payload)
throw new Error('No text body')
}
const molliePaymentId = (await req.text()).slice(3)
// Parse the webhook body to get the Mollie payment ID
if (!req.text) {
return Response.json({ error: 'Missing request body' }, { status: 400 })
}
const body = await req.text()
if (!body || !body.startsWith('id=')) {
return Response.json({ error: 'Invalid webhook payload' }, { status: 400 })
}
const molliePaymentId = body.slice(3) // Remove 'id=' prefix
// Fetch the payment details from Mollie
const molliePayment = await mollieClient.payments.get(molliePaymentId)
// Find the corresponding payment in our database
const paymentsCollection = extractSlug(pluginConfig.collections?.payments || defaults.paymentsCollection)
const payments = await payload.find({
collection: paymentsCollection,
where: {
providerId: {
equals: molliePaymentId
}
}
})
if (payments.docs.length === 0) {
return Response.json({ error: 'Payment not found' }, { status: 404 })
}
const paymentDoc = payments.docs[0]
// Map Mollie status to our status
let status: Payment['status'] = 'pending'
// Cast to string to avoid ESLint enum comparison warning
const mollieStatus = molliePayment.status as string
switch (mollieStatus) {
case 'paid':
status = 'succeeded'
break
case 'failed':
status = 'failed'
break
case 'canceled':
case 'expired':
status = 'canceled'
break
case 'pending':
case 'open':
case 'authorized':
status = 'pending'
break
default:
status = 'processing'
}
// Update the payment status and provider data
await payload.update({
collection: paymentsCollection,
id: paymentDoc.id,
data: {
status,
providerData: molliePayment.toPlainObject()
}
})
// If payment is successful and linked to an invoice, update the invoice
const invoicesCollection = extractSlug(pluginConfig.collections?.invoices || defaults.invoicesCollection)
const payment = paymentDoc as Payment
if (status === 'succeeded' && payment.invoice) {
const invoiceId = typeof payment.invoice === 'object'
? payment.invoice.id
: payment.invoice
await payload.update({
collection: invoicesCollection,
id: invoiceId,
data: {
status: 'paid',
payment: paymentDoc.id
}
})
}
return Response.json({ received: true }, { status: 200 })
} catch (error) {
console.error('[Mollie Webhook] Error processing webhook:', error)
return Response.json({
error: 'Webhook processing failed',
details: error instanceof Error ? error.message : 'Unknown error'
}, { status: 500 })
}
} }
} }
] ]
}, },
onInit: async (payload: Payload) => { onInit: async (payload: Payload) => {
const createMollieClient = (await import('@mollie/api-client')).default const createMollieClient = (await import('@mollie/api-client')).default
const mollieClient = createMollieClient(config) const mollieClient = createMollieClient(mollieConfig)
singleton.set(payload, mollieClient) singleton.set(payload, mollieClient)
}, },
initPayment: async (payload, payment) => { initPayment: async (payload, payment) => {
if (!payment.amount) { if (!payment.amount) {
@@ -48,8 +141,8 @@ export const mollieProvider = (config: MollieProviderConfig) => {
currency: payment.currency currency: payment.currency
}, },
description: payment.description || '', description: payment.description || '',
redirectUrl: 'https://localhost:3000/payment/success', redirectUrl: mollieConfig.redirectUrl || 'https://localhost:3000/payment/success',
webhookUrl: 'https://localhost:3000', webhookUrl: mollieConfig.webhookUrl || `${process.env.PAYLOAD_PUBLIC_SERVER_URL || 'https://localhost:3000'}/api/payload-billing/mollie/webhook`,
}); });
payment.providerId = molliePayment.id payment.providerId = molliePayment.id
payment.providerData = molliePayment.toPlainObject() payment.providerData = molliePayment.toPlainObject()