mirror of
https://github.com/xtr-dev/payload-billing.git
synced 2025-12-10 02:43:24 +00:00
Simplify payment updates by removing race condition logic
- Remove complex optimistic locking and version-based updates - Simplify payment status updates assuming providers don't send duplicates - Remove version field from Payment type and collection schema - Clean up webhook detection logic in payment hooks - Streamline updatePaymentStatus function for better maintainability Payment providers typically don't send duplicate webhook requests, so the complex race condition handling was unnecessary overhead. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,6 @@ import type { AccessArgs, CollectionBeforeChangeHook, CollectionConfig, Field }
|
|||||||
import type { BillingPluginConfig} from '@/plugin/config';
|
import type { BillingPluginConfig} from '@/plugin/config';
|
||||||
import { defaults } from '@/plugin/config'
|
import { defaults } from '@/plugin/config'
|
||||||
import { extractSlug } from '@/plugin/utils'
|
import { extractSlug } from '@/plugin/utils'
|
||||||
import { isWebhookRequest } from '@/providers/context'
|
|
||||||
import type { Payment } from '@/plugin/types/payments'
|
import type { Payment } from '@/plugin/types/payments'
|
||||||
import { initProviderPayment } from '@/collections/hooks'
|
import { initProviderPayment } from '@/collections/hooks'
|
||||||
|
|
||||||
@@ -107,16 +106,6 @@ export function createPaymentsCollection(pluginConfig: BillingPluginConfig): Col
|
|||||||
hasMany: true,
|
hasMany: true,
|
||||||
relationTo: extractSlug(pluginConfig.collections?.refunds || defaults.refundsCollection),
|
relationTo: extractSlug(pluginConfig.collections?.refunds || defaults.refundsCollection),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'version',
|
|
||||||
type: 'number',
|
|
||||||
admin: {
|
|
||||||
hidden: true, // Hide from admin UI
|
|
||||||
},
|
|
||||||
defaultValue: 1,
|
|
||||||
required: true,
|
|
||||||
index: true, // Index for faster optimistic lock queries
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
if (overrides?.fields) {
|
if (overrides?.fields) {
|
||||||
fields = overrides?.fields({defaultFields: fields})
|
fields = overrides?.fields({defaultFields: fields})
|
||||||
@@ -138,11 +127,8 @@ export function createPaymentsCollection(pluginConfig: BillingPluginConfig): Col
|
|||||||
fields,
|
fields,
|
||||||
hooks: {
|
hooks: {
|
||||||
beforeChange: [
|
beforeChange: [
|
||||||
async ({ data, operation, req, originalDoc }) => {
|
async ({ data, operation, req }) => {
|
||||||
if (operation === 'create') {
|
if (operation === 'create') {
|
||||||
// Initialize version for new payments
|
|
||||||
data.version = 1
|
|
||||||
|
|
||||||
// Validate amount format
|
// Validate amount format
|
||||||
if (data.amount && !Number.isInteger(data.amount)) {
|
if (data.amount && !Number.isInteger(data.amount)) {
|
||||||
throw new Error('Amount must be an integer (in cents)')
|
throw new Error('Amount must be an integer (in cents)')
|
||||||
@@ -157,17 +143,6 @@ export function createPaymentsCollection(pluginConfig: BillingPluginConfig): Col
|
|||||||
}
|
}
|
||||||
|
|
||||||
await initProviderPayment(req.payload, data)
|
await initProviderPayment(req.payload, data)
|
||||||
} else if (operation === 'update') {
|
|
||||||
// Handle version incrementing for manual updates
|
|
||||||
// Webhook updates from providers should already set the version via optimistic locking
|
|
||||||
if (!data.version && originalDoc?.id) {
|
|
||||||
// Check if this is a webhook update using explicit context tracking
|
|
||||||
if (!isWebhookRequest(req)) {
|
|
||||||
// This is a manual admin update, safely increment version
|
|
||||||
data.version = (originalDoc.version || 1) + 1
|
|
||||||
}
|
|
||||||
// If it's a webhook update without a version, let it proceed (optimistic locking already handled it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
] satisfies CollectionBeforeChangeHook<Payment>[],
|
] satisfies CollectionBeforeChangeHook<Payment>[],
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ export interface Payment {
|
|||||||
| boolean
|
| boolean
|
||||||
| null;
|
| null;
|
||||||
refunds?: (number | Refund)[] | null;
|
refunds?: (number | Refund)[] | null;
|
||||||
version: number;
|
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export async function updatePaymentFromWebhook(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update payment status and provider data with atomic optimistic locking
|
* Update payment status and provider data
|
||||||
*/
|
*/
|
||||||
export async function updatePaymentStatus(
|
export async function updatePaymentStatus(
|
||||||
payload: Payload,
|
payload: Payload,
|
||||||
@@ -79,45 +79,21 @@ export async function updatePaymentStatus(
|
|||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const paymentsCollection = extractSlug(pluginConfig.collections?.payments || defaults.paymentsCollection)
|
const paymentsCollection = extractSlug(pluginConfig.collections?.payments || defaults.paymentsCollection)
|
||||||
|
|
||||||
// Get current payment to check version for atomic locking
|
|
||||||
const currentPayment = await payload.findByID({
|
|
||||||
collection: paymentsCollection,
|
|
||||||
id: toPayloadId(paymentId)
|
|
||||||
}) as Payment
|
|
||||||
|
|
||||||
const now = new Date().toISOString()
|
|
||||||
const nextVersion = (currentPayment.version || 1) + 1
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use update with version check for atomic optimistic locking
|
await payload.update({
|
||||||
const updatedPayment = await payload.update({
|
|
||||||
collection: paymentsCollection,
|
collection: paymentsCollection,
|
||||||
id: toPayloadId(paymentId),
|
id: toPayloadId(paymentId),
|
||||||
data: {
|
data: {
|
||||||
status,
|
status,
|
||||||
version: nextVersion,
|
|
||||||
providerData: {
|
providerData: {
|
||||||
...providerData,
|
...providerData,
|
||||||
webhookProcessedAt: now,
|
webhookProcessedAt: new Date().toISOString()
|
||||||
previousStatus: currentPayment.status
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
where: {
|
|
||||||
version: { equals: currentPayment.version || 1 }
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// If we get here without error, the update succeeded
|
|
||||||
return true
|
return true
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
// Check if this is a version mismatch (no documents found to update)
|
|
||||||
if (error?.message?.includes('No documents found') ||
|
|
||||||
error?.name === 'ValidationError' ||
|
|
||||||
error?.status === 404) {
|
|
||||||
console.warn(`[Payment Update] Optimistic lock failed for payment ${paymentId} - version mismatch (expected: ${currentPayment.version}, may have been updated by another process)`)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error(`[Payment Update] Failed to update payment ${paymentId}:`, error)
|
console.error(`[Payment Update] Failed to update payment ${paymentId}:`, error)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -143,7 +119,7 @@ export async function updateInvoiceOnPaymentSuccess(
|
|||||||
id: toPayloadId(invoiceId),
|
id: toPayloadId(invoiceId),
|
||||||
data: {
|
data: {
|
||||||
status: 'paid',
|
status: 'paid',
|
||||||
payment: payment.id
|
payment: toPayloadId(payment.id)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -206,4 +182,4 @@ export function validateProductionUrl(url: string | undefined, urlType: string):
|
|||||||
} catch {
|
} catch {
|
||||||
throw new Error(`${urlType} URL is not a valid URL`)
|
throw new Error(`${urlType} URL is not a valid URL`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user