Files
payload-billing/src/collections/payments.ts
Bas van den Aakster d5a47a05b1 fix: resolve module import issues for Next.js/Turbopack compatibility
- Remove .js extensions from all TypeScript imports throughout codebase
- Update dev config to use testProvider instead of mollieProvider for testing
- Fix module resolution issues preventing development server startup
- Enable proper testing of billing plugin functionality with test provider

This resolves the "Module not found: Can't resolve" errors that were
preventing the development server from starting with Next.js/Turbopack.
All TypeScript imports now use extension-less imports as required.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-19 12:12:39 +02:00

171 lines
5.0 KiB
TypeScript

import type { AccessArgs, CollectionBeforeChangeHook, CollectionConfig, Field } from 'payload'
import type { BillingPluginConfig} from '../plugin/config';
import { defaults } from '../plugin/config'
import { extractSlug } from '../plugin/utils'
import type { Payment } from '../plugin/types/payments'
import { initProviderPayment } from './hooks'
export function createPaymentsCollection(pluginConfig: BillingPluginConfig): CollectionConfig {
const overrides = typeof pluginConfig.collections?.payments === 'object' ? pluginConfig.collections?.payments : {}
let fields: Field[] = [
{
name: 'provider',
type: 'select',
admin: {
position: 'sidebar',
},
options: [
{ label: 'Stripe', value: 'stripe' },
{ label: 'Mollie', value: 'mollie' },
{ label: 'Test', value: 'test' },
],
required: true,
},
{
name: 'providerId',
type: 'text',
admin: {
description: 'The payment ID from the payment provider',
},
label: 'Provider Payment ID',
unique: true,
index: true, // Ensure this field is indexed for webhook lookups
},
{
name: 'status',
type: 'select',
admin: {
position: 'sidebar',
},
options: [
{ label: 'Pending', value: 'pending' },
{ label: 'Processing', value: 'processing' },
{ label: 'Succeeded', value: 'succeeded' },
{ label: 'Failed', value: 'failed' },
{ label: 'Canceled', value: 'canceled' },
{ label: 'Refunded', value: 'refunded' },
{ label: 'Partially Refunded', value: 'partially_refunded' },
],
required: true,
},
{
name: 'amount',
type: 'number',
admin: {
description: 'Amount in cents (e.g., 2000 = $20.00)',
},
min: 1,
required: true,
},
{
name: 'currency',
type: 'text',
admin: {
description: 'ISO 4217 currency code (e.g., USD, EUR)',
},
maxLength: 3,
required: true,
},
{
name: 'description',
type: 'text',
admin: {
description: 'Payment description',
},
},
{
name: 'invoice',
type: 'relationship',
admin: {
position: 'sidebar',
},
relationTo: extractSlug(pluginConfig.collections?.invoices || defaults.invoicesCollection),
},
{
name: 'metadata',
type: 'json',
admin: {
description: 'Additional metadata for the payment',
},
},
{
name: 'providerData',
type: 'json',
admin: {
description: 'Raw data from the payment provider',
readOnly: true,
},
},
{
name: 'refunds',
type: 'relationship',
admin: {
position: 'sidebar',
readOnly: true,
},
hasMany: true,
relationTo: extractSlug(pluginConfig.collections?.refunds || defaults.refundsCollection),
},
{
name: 'version',
type: 'number',
defaultValue: 1,
admin: {
hidden: true, // Hide from admin UI to prevent manual tampering
},
index: true, // Index for optimistic locking performance
},
]
if (overrides?.fields) {
fields = overrides?.fields({defaultFields: fields})
}
return {
slug: extractSlug(pluginConfig.collections?.payments || defaults.paymentsCollection),
access: overrides?.access || {
create: ({ req: { user } }: AccessArgs) => !!user,
delete: ({ req: { user } }: AccessArgs) => !!user,
read: ({ req: { user } }: AccessArgs) => !!user,
update: ({ req: { user } }: AccessArgs) => !!user,
},
admin: {
defaultColumns: ['id', 'provider', 'status', 'amount', 'currency', 'createdAt'],
group: 'Billing',
useAsTitle: 'id',
...overrides?.admin
},
fields,
hooks: {
beforeChange: [
async ({ data, operation, req, originalDoc }) => {
if (operation === 'create') {
// Validate amount format
if (data.amount && !Number.isInteger(data.amount)) {
throw new Error('Amount must be an integer (in cents)')
}
// Validate currency format
if (data.currency) {
data.currency = data.currency.toUpperCase()
if (!/^[A-Z]{3}$/.test(data.currency)) {
throw new Error('Currency must be a 3-letter ISO code')
}
}
await initProviderPayment(req.payload, data)
}
// Auto-increment version for manual updates (not webhook updates)
// Webhook updates handle their own versioning in updatePaymentStatus
if (operation === 'update' && !data.version) {
// If version is not being explicitly set (i.e., manual admin update),
// increment it automatically
const currentVersion = (originalDoc as Payment)?.version || 1
data.version = currentVersion + 1
}
},
] satisfies CollectionBeforeChangeHook<Payment>[],
},
timestamps: true,
}
}