@@ -445,35 +445,48 @@ public function creditApplied()
445445 */
446446 public function handlePaymentFailed (MolliePayment $ molliePayment )
447447 {
448- $ localPayment = DB ::transaction (function () use ($ molliePayment ) {
449- if ($ this ->creditApplied ()) {
450- $ this ->owner ->addCredit ($ this ->getCreditUsed ());
448+ $ handled = false ;
449+ $ localPayment = DB ::transaction (function () use ($ molliePayment , &$ handled ) {
450+ // Webhook retries can arrive while callers still hold stale Order instances.
451+ // Reload the current row under lock so credit restoration and item hooks run once.
452+ /** @var static $order */
453+ $ order = static ::whereKey ($ this ->getKey ())->lockForUpdate ()->firstOrFail ();
454+
455+ if ($ order ->mollie_payment_status === PaymentStatus::FAILED ) {
456+ return null ;
451457 }
452458
453- $ this ->update ([
454- 'mollie_payment_status ' => 'failed ' ,
459+ if ($ order ->creditApplied ()) {
460+ $ order ->owner ->addCredit ($ order ->getCreditUsed ());
461+ }
462+
463+ $ order ->update ([
464+ 'mollie_payment_status ' => PaymentStatus::FAILED ,
455465 'balance_before ' => 0 ,
456466 'credit_used ' => 0 ,
457467 ]);
458468
459- // It's possible a payment from Cashier v1 is not yet tracked in the Cashier database.
460- // In that case we create a record here.
461- $ localPayment = Cashier::$ paymentModel ::findByMolliePaymentOrCreate ($ molliePayment , $ this ->owner );
469+ $ localPayment = Cashier::$ paymentModel ::findByPaymentIdOrFail ($ molliePayment ->id );
462470 $ localPayment ->update ([
463- 'mollie_payment_status ' => ' failed ' ,
464- 'order_id ' => $ this ->id ,
471+ 'mollie_payment_status ' => PaymentStatus:: FAILED ,
472+ 'order_id ' => $ order ->id ,
465473 ]);
466474
467- $ this ->items ->each (function (OrderItem $ item ) {
475+ $ order ->items ->each (function (OrderItem $ item ) {
468476 $ item ->handlePaymentFailed ();
469477 });
470478
471- $ this ->owner ->validateMollieMandate ();
479+ $ order ->owner ->validateMollieMandate ();
472480
481+ $ handled = true ;
473482 return $ localPayment ;
474483 });
475484
476- Event::dispatch (new OrderPaymentFailed ($ this , $ localPayment ));
485+ $ this ->refresh ();
486+
487+ if ($ handled ) {
488+ Event::dispatch (new OrderPaymentFailed ($ this , $ localPayment ));
489+ }
477490
478491 return $ this ;
479492 }
@@ -522,25 +535,38 @@ public function handlePaymentFailedDueToInvalidMandate()
522535 */
523536 public function handlePaymentPaid (MolliePayment $ molliePayment )
524537 {
525- $ localPayment = DB ::transaction (function () use ($ molliePayment ) {
526- $ this ->update (['mollie_payment_status ' => 'paid ' ]);
538+ $ handled = false ;
539+ $ localPayment = DB ::transaction (function () use ($ molliePayment , &$ handled ) {
540+ // Paid webhooks have the same stale-instance problem: re-check under lock so the
541+ // order transition, local payment update, and item hooks are idempotent.
542+ /** @var static $order */
543+ $ order = static ::whereKey ($ this ->getKey ())->lockForUpdate ()->firstOrFail ();
544+
545+ if ($ order ->mollie_payment_status === PaymentStatus::PAID ) {
546+ return null ;
547+ }
527548
528- // It's possible a payment from Cashier v1 is not yet tracked in the Cashier database.
529- // In that case we create a record here.
530- $ localPayment = Cashier::$ paymentModel ::findByMolliePaymentOrCreate ($ molliePayment, $ this -> owner );
549+ $ order -> update ([ ' mollie_payment_status ' => PaymentStatus:: PAID ]);
550+
551+ $ localPayment = Cashier::$ paymentModel ::findByPaymentIdOrFail ($ molliePayment-> id );
531552 $ localPayment ->update ([
532- 'mollie_payment_status ' => ' paid ' ,
533- 'order_id ' => $ this ->id ,
553+ 'mollie_payment_status ' => PaymentStatus:: PAID ,
554+ 'order_id ' => $ order ->id ,
534555 ]);
535556
536- $ this ->items ->each (function (OrderItem $ item ) {
557+ $ order ->items ->each (function (OrderItem $ item ) {
537558 $ item ->handlePaymentPaid ();
538559 });
539560
561+ $ handled = true ;
540562 return $ localPayment ;
541563 });
542564
543- Event::dispatch (new OrderPaymentPaid ($ this , $ localPayment ));
565+ $ this ->refresh ();
566+
567+ if ($ handled ) {
568+ Event::dispatch (new OrderPaymentPaid ($ this , $ localPayment ));
569+ }
544570
545571 return $ this ;
546572 }
0 commit comments