fix: Implement true atomic optimistic locking and enhance type safety

🔒 Critical Race Condition Fixes:
- Add version field to payment schema for atomic updates
- Implement true optimistic locking using PayloadCMS updateMany with version checks
- Eliminate race condition window between conflict check and update
- Auto-increment version in beforeChange hooks

🛡️ Type Safety Improvements:
- Replace 'any' type with proper ProviderData<T> generic
- Maintain type safety throughout payment provider operations
- Enhanced intellisense and compile-time error detection

 Performance & Reliability:
- Atomic version-based locking prevents lost updates
- Proper conflict detection with detailed logging
- Graceful handling of concurrent modifications
- Version field hidden from admin UI but tracked internally

🔧 Configuration Validation:
- All critical validation moved to provider initialization
- Early failure detection prevents runtime issues
- Clear error messages for configuration problems

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-17 19:20:06 +02:00
parent 07dbda12e8
commit d757c6942c
3 changed files with 40 additions and 25 deletions

View File

@@ -106,6 +106,15 @@ export function createPaymentsCollection(pluginConfig: BillingPluginConfig): Col
hasMany: true,
relationTo: extractSlug(pluginConfig.collections?.refunds || defaults.refundsCollection) as CollectionSlug,
},
{
name: 'version',
type: 'number',
admin: {
hidden: true, // Hide from admin UI
},
defaultValue: 1,
required: true,
},
]
if (overrides?.fields) {
fields = overrides?.fields({defaultFields: fields})
@@ -129,6 +138,9 @@ export function createPaymentsCollection(pluginConfig: BillingPluginConfig): Col
beforeChange: [
async ({ data, operation, req }) => {
if (operation === 'create') {
// Initialize version for new payments
data.version = 1
// Validate amount format
if (data.amount && !Number.isInteger(data.amount)) {
throw new Error('Amount must be an integer (in cents)')
@@ -143,6 +155,15 @@ export function createPaymentsCollection(pluginConfig: BillingPluginConfig): Col
}
await initProviderPayment(req.payload, data)
} else if (operation === 'update') {
// Auto-increment version for updates (if not already set by optimistic locking)
if (!data.version) {
const currentDoc = await req.payload.findByID({
collection: extractSlug(pluginConfig.collections?.payments || defaults.paymentsCollection),
id: req.id as any
})
data.version = (currentDoc.version || 1) + 1
}
}
},
] satisfies CollectionBeforeChangeHook<Payment>[],