@@ -157,11 +157,7 @@ export async function POST(request: NextRequest) {
157157 logWarn ( 'Failed to parse cart payload' , {
158158 reason : error instanceof Error ? error . message : String ( error ) ,
159159 } ) ;
160- return errorResponse (
161- 'INVALID_PAYLOAD' ,
162- 'Unable to process cart data.' ,
163- 400
164- ) ;
160+ return errorResponse ( 'INVALID_PAYLOAD' , 'Unable to process cart data.' , 400 ) ;
165161 }
166162
167163 const idempotencyKey = getIdempotencyKey ( request ) ;
@@ -241,22 +237,13 @@ export async function POST(request: NextRequest) {
241237 const paymentsEnabled = isPaymentsEnabled ( ) ;
242238
243239 if ( ! paymentsEnabled ) {
244- // If the order already failed (inventory or other), return a stable conflict instead of 500.
245- if (
246- order . paymentProvider === 'none' &&
247- order . paymentStatus === 'failed'
248- ) {
249- return errorResponse (
250- 'CHECKOUT_FAILED' ,
251- 'Order could not be completed.' ,
252- 409 ,
253- { orderId : order . id }
254- ) ;
240+ if ( order . paymentProvider === 'none' && order . paymentStatus === 'failed' ) {
241+ return errorResponse ( 'CHECKOUT_FAILED' , 'Order could not be completed.' , 409 , {
242+ orderId : order . id ,
243+ } ) ;
255244 }
256- if (
257- order . paymentProvider === 'stripe' &&
258- order . paymentStatus !== 'paid'
259- ) {
245+
246+ if ( order . paymentProvider === 'stripe' && order . paymentStatus !== 'paid' ) {
260247 return errorResponse (
261248 'PAYMENTS_DISABLED' ,
262249 'Payments are disabled. This order requires payment and cannot be processed.' ,
@@ -266,16 +253,9 @@ export async function POST(request: NextRequest) {
266253 }
267254
268255 if ( order . paymentProvider === 'none' ) {
269- if (
270- ! [ 'paid' , 'failed' ] . includes ( order . paymentStatus ) ||
271- order . paymentIntentId
272- ) {
256+ if ( ! [ 'paid' , 'failed' ] . includes ( order . paymentStatus ) || order . paymentIntentId ) {
273257 logError (
274- `Payments disabled but order is not paid/none. orderId=${
275- order . id
276- } provider=${ order . paymentProvider } status=${
277- order . paymentStatus
278- } intent=${ order . paymentIntentId ?? 'null' } `,
258+ `Payments disabled but order is not paid/none. orderId=${ order . id } provider=${ order . paymentProvider } status=${ order . paymentStatus } intent=${ order . paymentIntentId ?? 'null' } ` ,
279259 new Error ( 'ORDER_STATE_INVALID' )
280260 ) ;
281261 return errorResponse (
@@ -287,16 +267,16 @@ export async function POST(request: NextRequest) {
287267 }
288268 }
289269
290- const stripePaymentFlow =
291- paymentsEnabled && order . paymentProvider === 'stripe' ;
270+ const stripePaymentFlow = paymentsEnabled && order . paymentProvider === 'stripe' ;
292271
272+ // =========================
273+ // Existing order path
274+ // =========================
293275 if ( ! result . isNew ) {
276+ // Existing order already has PI: retrieve client_secret
294277 if ( stripePaymentFlow && order . paymentIntentId ) {
295278 try {
296- const paymentIntent = await retrievePaymentIntent (
297- order . paymentIntentId
298- ) ;
299-
279+ const paymentIntent = await retrievePaymentIntent ( order . paymentIntentId ) ;
300280 return buildCheckoutResponse ( {
301281 order : {
302282 id : order . id ,
@@ -312,23 +292,27 @@ export async function POST(request: NextRequest) {
312292 } ) ;
313293 } catch ( error ) {
314294 logError ( 'Checkout payment intent retrieval failed' , error ) ;
315- return errorResponse (
316- 'STRIPE_ERROR' ,
317- 'Unable to initiate payment.' ,
318- 400
319- ) ;
295+ return errorResponse ( 'STRIPE_ERROR' , 'Unable to initiate payment.' , 502 ) ;
320296 }
321297 }
322298
299+ // Existing order without PI: create PI then attach (post-create => never 400)
323300 if ( stripePaymentFlow && ! order . paymentIntentId ) {
301+ let paymentIntent : { paymentIntentId : string ; clientSecret : string } ;
302+
324303 try {
325- const paymentIntent = await createPaymentIntent ( {
304+ paymentIntent = await createPaymentIntent ( {
326305 amount : totalCents ,
327306 currency : order . currency ,
328307 orderId : order . id ,
329308 idempotencyKey,
330309 } ) ;
310+ } catch ( error ) {
311+ logError ( 'Checkout payment intent creation failed' , error ) ;
312+ return errorResponse ( 'STRIPE_ERROR' , 'Unable to initiate payment.' , 502 ) ;
313+ }
331314
315+ try {
332316 const updatedOrder = await setOrderPaymentIntent ( {
333317 orderId : order . id ,
334318 paymentIntentId : paymentIntent . paymentIntentId ,
@@ -348,15 +332,30 @@ export async function POST(request: NextRequest) {
348332 status : 200 ,
349333 } ) ;
350334 } catch ( error ) {
351- logError ( 'Checkout payment intent creation failed' , error ) ;
352- return errorResponse (
353- 'STRIPE_ERROR' ,
354- 'Unable to initiate payment.' ,
355- 400
356- ) ;
335+ logError ( 'Checkout payment intent attach failed' , error ) ;
336+
337+ // Post-create => conflict, not 400
338+ if ( error instanceof InvalidPayloadError ) {
339+ return errorResponse (
340+ 'CHECKOUT_CONFLICT' ,
341+ 'Order state conflict while attaching payment intent. Retry with the same Idempotency-Key.' ,
342+ 409 ,
343+ { orderId : order . id }
344+ ) ;
345+ }
346+
347+ if ( error instanceof OrderStateInvalidError ) {
348+ return errorResponse ( error . code , error . message , 500 , {
349+ orderId : error . orderId ,
350+ ...( error . details ? { details : error . details } : { } ) ,
351+ } ) ;
352+ }
353+
354+ return errorResponse ( 'INTERNAL_ERROR' , 'Unable to process checkout.' , 500 ) ;
357355 }
358356 }
359357
358+ // Not Stripe flow => return existing order as-is
360359 return buildCheckoutResponse ( {
361360 order : {
362361 id : order . id ,
@@ -372,6 +371,9 @@ export async function POST(request: NextRequest) {
372371 } ) ;
373372 }
374373
374+ // =========================
375+ // New order path
376+ // =========================
375377 if ( ! stripePaymentFlow ) {
376378 return buildCheckoutResponse ( {
377379 order : {
@@ -388,14 +390,30 @@ export async function POST(request: NextRequest) {
388390 } ) ;
389391 }
390392
393+ // Stripe new order: Phase 1 PSP call (if fails => restock best-effort, return 502)
394+ let paymentIntent : { paymentIntentId : string ; clientSecret : string } ;
395+
391396 try {
392- const paymentIntent = await createPaymentIntent ( {
397+ paymentIntent = await createPaymentIntent ( {
393398 amount : totalCents ,
394399 currency : order . currency ,
395400 orderId : order . id ,
396401 idempotencyKey,
397402 } ) ;
403+ } catch ( error ) {
404+ logError ( 'Checkout payment intent creation failed' , error ) ;
398405
406+ try {
407+ await restockOrder ( order . id , { reason : 'failed' } ) ;
408+ } catch ( restockError ) {
409+ logError ( 'Restoring stock after payment intent failure failed' , restockError ) ;
410+ }
411+
412+ return errorResponse ( 'STRIPE_ERROR' , 'Unable to initiate payment.' , 502 ) ;
413+ }
414+
415+ // Stripe new order: Phase 2 attach PI (post-create => never 400)
416+ try {
399417 const updatedOrder = await setOrderPaymentIntent ( {
400418 orderId : order . id ,
401419 paymentIntentId : paymentIntent . paymentIntentId ,
@@ -415,36 +433,34 @@ export async function POST(request: NextRequest) {
415433 status : 201 ,
416434 } ) ;
417435 } catch ( error ) {
418- logError ( 'Checkout payment intent creation failed' , error ) ;
436+ logError ( 'Checkout payment intent attach failed' , error ) ;
419437
420- try {
421- await restockOrder ( order . id , { reason : 'failed' } ) ;
422- } catch ( restockError ) {
423- logError (
424- 'Restoring stock after payment intent failure failed' ,
425- restockError
438+ if ( error instanceof InvalidPayloadError ) {
439+ // Conflict/race/state issue. Do NOT return 400.
440+ // Leave inventory reserved; retry with same idempotency key or janitor will sweep.
441+ return errorResponse (
442+ 'CHECKOUT_CONFLICT' ,
443+ 'Order state conflict while attaching payment intent. Retry with the same Idempotency-Key.' ,
444+ 409 ,
445+ { orderId : order . id }
426446 ) ;
427447 }
428448
429- if ( error instanceof Error && error . message . startsWith ( 'STRIPE_' ) ) {
430- return errorResponse (
431- 'STRIPE_ERROR' ,
432- 'Unable to initiate payment.' ,
433- 400
434- ) ;
449+ // For non-conflict attach failures: best-effort release to avoid stock lock
450+ try {
451+ await restockOrder ( order . id , { reason : 'failed' } ) ;
452+ } catch ( restockError ) {
453+ logError ( 'Restoring stock after payment intent attach failure failed' , restockError ) ;
435454 }
436455
437456 if ( error instanceof OrderStateInvalidError ) {
438457 return errorResponse ( error . code , error . message , 500 , {
439458 orderId : error . orderId ,
459+ ...( error . details ? { details : error . details } : { } ) ,
440460 } ) ;
441461 }
442462
443- return errorResponse (
444- 'INTERNAL_ERROR' ,
445- 'Unable to process checkout.' ,
446- 500
447- ) ;
463+ return errorResponse ( 'INTERNAL_ERROR' , 'Unable to process checkout.' , 500 ) ;
448464 }
449465 } catch ( error ) {
450466 if ( isExpectedBusinessError ( error ) ) {
@@ -457,11 +473,7 @@ export async function POST(request: NextRequest) {
457473 }
458474
459475 if ( error instanceof InvalidPayloadError ) {
460- return errorResponse (
461- error . code ,
462- error . message || 'Invalid checkout payload' ,
463- 400
464- ) ;
476+ return errorResponse ( error . code , error . message || 'Invalid checkout payload' , 400 ) ;
465477 }
466478
467479 if ( error instanceof InvalidVariantError ) {
@@ -512,4 +524,4 @@ export async function POST(request: NextRequest) {
512524
513525 return errorResponse ( 'INTERNAL_ERROR' , 'Unable to process checkout.' , 500 ) ;
514526 }
515- }
527+ }
0 commit comments