Skip to content

Commit 950c00d

Browse files
Add stale-read chargeback coverage and simplify comparison
Replace `! lessThan` with `greaterThanOrEqual` for readability in the aftercare chargeback branch, and add a stale-read regression test that proves concurrent deliveries dispatch `ChargebackReceived` only once. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a16ffcd commit 950c00d

3 files changed

Lines changed: 92 additions & 1 deletion

File tree

src/Http/Controllers/AftercareWebhookController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public function handleWebhook(Request $request)
7070

7171
$locallyKnownAmountChargedBack = $localPayment->getAmountChargedBack();
7272

73-
if (! $locallyKnownAmountChargedBack->lessThan($molliePaymentAmountChargedBackTotal)) {
73+
if ($locallyKnownAmountChargedBack->greaterThanOrEqual($molliePaymentAmountChargedBackTotal)) {
7474
return;
7575
}
7676

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
namespace Laravel\Cashier\Tests\Fixtures;
4+
5+
class ChargebackStaleReadPayment extends Payment
6+
{
7+
protected static int $remainingStaleReads = 0;
8+
9+
protected static int $staleAmountChargedBack = 0;
10+
11+
public static function returnStaleChargebackAmountOnNextReads(int $reads, int $amountChargedBack = 0): void
12+
{
13+
static::$remainingStaleReads = $reads;
14+
static::$staleAmountChargedBack = $amountChargedBack;
15+
}
16+
17+
public static function resetStaleChargebackReads(): void
18+
{
19+
static::$remainingStaleReads = 0;
20+
static::$staleAmountChargedBack = 0;
21+
}
22+
23+
public static function findByPaymentId($id): ?self
24+
{
25+
/** @var self|null $payment */
26+
$payment = parent::findByPaymentId($id);
27+
28+
if (! $payment || static::$remainingStaleReads <= 0) {
29+
return $payment;
30+
}
31+
32+
static::$remainingStaleReads--;
33+
34+
/** @var self $stalePayment */
35+
$stalePayment = clone $payment;
36+
$stalePayment->amount_charged_back = static::$staleAmountChargedBack;
37+
38+
return $stalePayment;
39+
}
40+
}

tests/Http/Controllers/AftercareWebhookControllerTest.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@
88
use Illuminate\Support\Facades\Event;
99
use Laravel\Cashier\Cashier;
1010
use Laravel\Cashier\Events\ChargebackReceived;
11+
use Laravel\Cashier\Payment;
1112
use Laravel\Cashier\Events\RefundProcessed;
1213
use Laravel\Cashier\Http\Controllers\AftercareWebhookController;
1314
use Laravel\Cashier\Order\OrderItemCollection;
1415
use Laravel\Cashier\Refunds\RefundItemCollection;
1516
use Laravel\Cashier\Tests\BaseTestCase;
1617
use Laravel\Cashier\Tests\Database\Factories\SubscriptionFactory;
18+
use Laravel\Cashier\Tests\Fixtures\ChargebackStaleReadPayment;
1719
use Laravel\Cashier\Tests\Fixtures\User;
1820
use Mollie\Api\MollieApiClient;
1921
use Mollie\Api\Resources\Payment as MolliePayment;
@@ -25,6 +27,14 @@
2527

2628
class AftercareWebhookControllerTest extends BaseTestCase
2729
{
30+
protected function tearDown(): void
31+
{
32+
ChargebackStaleReadPayment::resetStaleChargebackReads();
33+
Cashier::usePaymentModel(Payment::class);
34+
35+
parent::tearDown();
36+
}
37+
2838
#[Test]
2939
public function itDetectsNewChargebacks()
3040
{
@@ -66,6 +76,47 @@ public function itDetectsNewChargebacks()
6676
Event::assertDispatched(ChargebackReceived::class);
6777
}
6878

79+
#[Test]
80+
public function itHandlesDuplicateChargebackDeliveriesWhenTheOuterPaymentReadIsStale()
81+
{
82+
Event::fake();
83+
Cashier::usePaymentModel(ChargebackStaleReadPayment::class);
84+
85+
$molliePayment = new MolliePayment(new MollieApiClient);
86+
$molliePayment->id = 'tr_duplicate_chargeback';
87+
$molliePayment->status = 'paid';
88+
$molliePayment->amount = (object) [
89+
'currency' => 'EUR',
90+
'value' => '20.00',
91+
];
92+
$molliePayment->amountRefunded = null;
93+
$molliePayment->amountChargedBack = null;
94+
$molliePayment->_links = (object) [];
95+
96+
$localPayment = Cashier::$paymentModel::createFromMolliePayment($molliePayment, $this->getUser());
97+
$molliePayment->amountChargedBack = (object) [
98+
'value' => '10.00',
99+
'currency' => 'EUR',
100+
];
101+
$molliePayment->_links->chargebacks = (object) [
102+
'href' => 'https://www.mollie.com/dashboard/org_12345678/payments/tr_WDqYK6vllg',
103+
'type' => 'application/json',
104+
];
105+
106+
$this->withMockedGetMolliePayment(2, $molliePayment);
107+
ChargebackStaleReadPayment::returnStaleChargebackAmountOnNextReads(2);
108+
109+
/** @var AftercareWebhookController $controller */
110+
$controller = $this->app->make(AftercareWebhookController::class);
111+
112+
$controller->handleWebhook($this->getWebhookRequest($molliePayment->id));
113+
$controller->handleWebhook($this->getWebhookRequest($molliePayment->id));
114+
115+
$localPayment->refresh();
116+
$this->assertMoney(1000, 'EUR', $localPayment->getAmountChargedBack());
117+
Event::assertDispatchedTimes(ChargebackReceived::class, 1);
118+
}
119+
69120
#[Test]
70121
public function itDetectsNewRefunds()
71122
{

0 commit comments

Comments
 (0)