CluPilotCloud/tests/Feature/CartTest.php

152 lines
6.6 KiB
PHP

<?php
use App\Livewire\Billing;
use App\Livewire\ConfirmRemoveOrder;
use App\Models\Customer;
use App\Models\Order;
use App\Models\User;
function cartCustomer(): array
{
$customer = Customer::factory()->create();
$user = User::factory()->create(['email' => $customer->email]);
return [$customer, $user];
}
it('says what was ordered instead of counting it', function () {
[$customer, $user] = cartCustomer();
Order::factory()->for($customer)->create(['type' => 'traffic', 'plan' => 'team', 'amount_cents' => 500, 'status' => 'pending']);
Order::factory()->for($customer)->create(['type' => 'upgrade', 'plan' => 'business', 'amount_cents' => 39900, 'status' => 'pending']);
Livewire\Livewire::actingAs($user)->test(Billing::class)
->assertSee(__('billing.cart.title'))
->assertSee(__('billing.cart.traffic', ['gb' => 1000]))
->assertSee(__('billing.cart.upgrade', ['plan' => 'Business']))
// …and what it all comes to.
->assertSee('404,00');
});
it('leaves paid orders out of the cart', function () {
[$customer, $user] = cartCustomer();
Order::factory()->for($customer)->create(['type' => 'storage', 'amount_cents' => 1000, 'status' => 'paid']);
Livewire\Livewire::actingAs($user)->test(Billing::class)->assertDontSee(__('billing.cart.title'));
});
it('removes an entry the customer changed their mind about', function () {
[$customer, $user] = cartCustomer();
$order = Order::factory()->for($customer)->create(['type' => 'storage', 'amount_cents' => 1000, 'status' => 'pending']);
Livewire\Livewire::actingAs($user)
->test(ConfirmRemoveOrder::class, ['uuid' => $order->uuid])
->assertSee(__('billing.cart.storage', ['gb' => 100]))
->call('remove');
expect(Order::query()->count())->toBe(0);
});
it('will not let one customer remove another customer\'s order', function () {
[, $user] = cartCustomer();
$stranger = Customer::factory()->create();
$order = Order::factory()->for($stranger)->create(['status' => 'pending']);
// Modals are reachable without the page's guards, so this is the real check.
Livewire\Livewire::actingAs($user)
->test(ConfirmRemoveOrder::class, ['uuid' => $order->uuid])
->assertNotFound();
expect(Order::query()->whereKey($order->id)->exists())->toBeTrue();
});
it('refuses to remove an order that is already paid', function () {
[$customer, $user] = cartCustomer();
$order = Order::factory()->for($customer)->create(['status' => 'paid']);
Livewire\Livewire::actingAs($user)
->test(ConfirmRemoveOrder::class, ['uuid' => $order->uuid])
->assertNotFound();
expect(Order::query()->whereKey($order->id)->exists())->toBeTrue();
});
it('keeps at most one plan change in the cart', function () {
[$customer, $user] = cartCustomer();
App\Models\Instance::factory()->for($customer)->create(['plan' => 'start', 'status' => 'active']);
$component = Livewire\Livewire::actingAs($user)->test(Billing::class);
$component->call('purchase', 'upgrade', 'business');
$component->call('purchase', 'upgrade', 'enterprise');
// Two contradicting plan changes cannot both be paid for — the second
// replaces the first instead of stacking.
$upgrades = Order::query()->where('type', 'upgrade')->where('status', 'pending')->get();
expect($upgrades)->toHaveCount(1)
->and($upgrades->first()->plan)->toBe('enterprise');
});
it('still lets add-ons stack', function () {
[$customer, $user] = cartCustomer();
App\Models\Instance::factory()->for($customer)->create(['plan' => 'team', 'status' => 'active']);
$component = Livewire\Livewire::actingAs($user)->test(Billing::class);
$component->call('purchase', 'storage');
$component->call('purchase', 'storage');
$component->call('purchase', 'traffic');
// Buying 200 GB as two packs is a perfectly sensible thing to want.
expect(Order::query()->where('type', 'storage')->count())->toBe(2)
->and(Order::query()->where('type', 'traffic')->count())->toBe(1);
});
it('says net, gross and how often for every price', function () {
[$customer, $user] = cartCustomer();
config()->set('provisioning.tax.rate_percent', 20);
Order::factory()->for($customer)->create(['type' => 'storage', 'amount_cents' => 1000, 'status' => 'pending']);
Order::factory()->for($customer)->create(['type' => 'traffic', 'amount_cents' => 500, 'status' => 'pending']);
Livewire\Livewire::actingAs($user)->test(Billing::class)
->assertSee(__('billing.cart.net'))
->assertSee(__('billing.cart.per_month')) // storage recurs
->assertSee(__('billing.cart.once')) // traffic does not
->assertSee('12,00') // 15,00 net → 18,00 gross
->assertSee('18,00')
->assertSee(__('billing.cart.total_gross'));
});
it('separates what recurs from a one-off top-up', function () {
[$customer, $user] = cartCustomer();
config()->set('provisioning.tax.rate_percent', 20);
Order::factory()->for($customer)->create(['type' => 'storage', 'amount_cents' => 1000, 'status' => 'pending']);
Order::factory()->for($customer)->create(['type' => 'traffic', 'amount_cents' => 500, 'status' => 'pending']);
// 10,00 net recurring → 12,00 gross per month, not the 18,00 total.
// Formatted through the same helper the view uses, or the non-breaking
// space in the currency makes this a test about whitespace.
$expected = __('billing.cart.recurring_note', [
'amount' => Illuminate\Support\Number::currency(12.00, in: 'EUR', locale: 'de'),
]);
Livewire\Livewire::actingAs($user)->test(Billing::class)->assertSee($expected);
});
it('keeps the recurring figure consistent with the total to the cent', function () {
[$customer, $user] = cartCustomer();
config()->set('provisioning.tax.rate_percent', 20);
// Cent-level prices: rounding after aggregating instead of per order makes
// the two figures disagree, and a customer who spots that stops trusting
// every other number on the page.
Order::factory()->for($customer)->create(['type' => 'storage', 'amount_cents' => 3, 'status' => 'pending']);
Order::factory()->for($customer)->create(['type' => 'storage', 'amount_cents' => 3, 'status' => 'pending']);
$expected = Illuminate\Support\Number::currency(0.08, in: 'EUR', locale: 'de');
Livewire\Livewire::actingAs($user)->test(Billing::class)
->assertSee($expected) // total gross
->assertSee(__('billing.cart.recurring_note', ['amount' => $expected])); // and the note
});