fix(billing): make the one-plan-change rule hold under two clicks
Both requests could finish their delete before either inserted, leaving exactly the two pending upgrades the rule exists to prevent. Replacement and insert now run in one transaction with the customer row locked. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>feat/portal-design
parent
a87a4f512d
commit
9fab251fb3
|
|
@ -3,7 +3,9 @@
|
|||
namespace App\Livewire;
|
||||
|
||||
use App\Livewire\Concerns\ResolvesCustomer;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Order;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Component;
|
||||
|
||||
|
|
@ -54,32 +56,42 @@ class Billing extends Component
|
|||
?? $customer->orders()->latest('id')->value('datacenter')
|
||||
?? 'fsn';
|
||||
|
||||
// A cart cannot hold two plan changes: they contradict each other and
|
||||
// no checkout could resolve which one the customer meant. Choosing
|
||||
// another simply replaces the pending one — add-ons still stack.
|
||||
if ($type === 'upgrade') {
|
||||
$replaced = Order::query()
|
||||
->where('customer_id', $customer->id)
|
||||
->where('status', 'pending')
|
||||
->where('type', 'upgrade')
|
||||
->delete();
|
||||
// A cart cannot hold two plan changes: they contradict each other and no
|
||||
// checkout could resolve which one the customer meant. Choosing another
|
||||
// replaces the pending one — add-ons still stack.
|
||||
//
|
||||
// Replacement and insert run together with the customer row locked:
|
||||
// two clicks in flight could otherwise both delete and then both
|
||||
// insert, leaving exactly the two upgrades this rule exists to prevent.
|
||||
$replaced = DB::transaction(function () use ($customer, $type, $plan, $addonKey, $amount, $datacenter) {
|
||||
Customer::query()->whereKey($customer->id)->lockForUpdate()->first();
|
||||
|
||||
if ($replaced > 0) {
|
||||
$this->dispatch('notify', message: __('billing.cart.plan_replaced'));
|
||||
}
|
||||
$replaced = $type === 'upgrade'
|
||||
? Order::query()
|
||||
->where('customer_id', $customer->id)
|
||||
->where('status', 'pending')
|
||||
->where('type', 'upgrade')
|
||||
->delete()
|
||||
: 0;
|
||||
|
||||
Order::create([
|
||||
'customer_id' => $customer->id,
|
||||
'plan' => $plan,
|
||||
'type' => $type,
|
||||
'addon_key' => $addonKey,
|
||||
'amount_cents' => $amount,
|
||||
'currency' => 'EUR',
|
||||
'datacenter' => $datacenter,
|
||||
'status' => 'pending',
|
||||
]);
|
||||
|
||||
return $replaced;
|
||||
});
|
||||
|
||||
if ($replaced > 0) {
|
||||
$this->dispatch('notify', message: __('billing.cart.plan_replaced'));
|
||||
}
|
||||
|
||||
Order::create([
|
||||
'customer_id' => $customer->id,
|
||||
'plan' => $plan,
|
||||
'type' => $type,
|
||||
'addon_key' => $addonKey,
|
||||
'amount_cents' => $amount,
|
||||
'currency' => 'EUR',
|
||||
'datacenter' => $datacenter,
|
||||
'status' => 'pending',
|
||||
]);
|
||||
|
||||
$this->dispatch('notify', message: __('billing.purchased'));
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue