CluPilotCloud/tests/Feature/Billing/PortalInvoicesTest.php

129 lines
4.8 KiB
PHP

<?php
use App\Livewire\Invoices;
use App\Models\Customer;
use App\Models\Invoice;
use App\Models\Order;
use App\Models\User;
use App\Services\Billing\IssueInvoice;
use App\Support\CompanyProfile;
use Livewire\Livewire;
/**
* The customer sees their own invoices, and only those.
*
* The page used to be a mock nobody took back out: five invented documents
* numbered CP-2026-0003 to CP-2026-0007 — a prefix belonging to no series this
* installation has ever configured — all marked paid, a rising spend curve and a
* next charge of 198 € on a date somebody typed. Every signed-in customer saw it,
* while the invoices we had really issued them, and really mailed them, appeared
* nowhere in the portal at all.
*/
beforeEach(function () {
CompanyProfile::put([
'name' => 'CluPilot Cloud e.U.', 'address' => 'Dreherstraße 66/1/8',
'postcode' => '1110', 'city' => 'Wien', 'vat_id' => 'ATU00000000',
]);
});
/** A portal customer with a signed-in user behind them. */
function invoiceCustomer(string $email): array
{
$user = User::factory()->create(['email' => $email, 'email_verified_at' => now()]);
$customer = Customer::factory()->create(['email' => $email, 'user_id' => $user->id]);
return [$user, $customer];
}
function issuedFor(Customer $customer, int $chargedCents): Invoice
{
return app(IssueInvoice::class)->forOrders($customer, collect([
Order::factory()->create([
'customer_id' => $customer->id,
'amount_cents' => $chargedCents,
'currency' => 'EUR',
'status' => 'paid',
'stripe_event_id' => 'cs_'.uniqid(),
]),
]));
}
it('lists the invoices this customer was actually issued, with their real numbers and totals', function () {
[$user, $customer] = invoiceCustomer('own@invoices.test');
$older = issuedFor($customer, 21480);
$newer = issuedFor($customer, 23880);
Livewire::actingAs($user)->test(Invoices::class)
->assertSee($older->number)
->assertSee($newer->number)
// The number the customer was charged, off the document itself.
->assertSee('238,80')
->assertSee('214,80')
// Newest first: the one somebody came here to fetch is the last one.
->assertViewHas('invoices', fn ($invoices) => $invoices->first()->is($newer))
// And nothing invented. These were the mock's, and its prefix belongs to
// no configured series.
->assertDontSee('CP-2026-0007')
->assertDontSee('01.08.2026');
});
it('never shows one customer another customer\'s invoice', function () {
[$user] = invoiceCustomer('mine@invoices.test');
[, $stranger] = invoiceCustomer('theirs@invoices.test');
$theirs = issuedFor($stranger, 21480);
Livewire::actingAs($user)->test(Invoices::class)
->assertDontSee($theirs->number)
->assertSee(__('invoices.empty'));
});
it('hands over the customer\'s own PDF and refuses everybody else\'s', function () {
[$user, $customer] = invoiceCustomer('pdf@invoices.test');
[$stranger] = invoiceCustomer('nosy@invoices.test');
$invoice = issuedFor($customer, 21480);
$this->actingAs($user)->get(route('invoices.pdf', $invoice->uuid))
->assertOk()
->assertHeader('content-type', 'application/pdf');
// Checked in the query, not by leaving the link off the page: a URL anybody
// can type is not made private by not printing it.
$this->actingAs($stranger)->get(route('invoices.pdf', $invoice->uuid))->assertNotFound();
});
it('does not hand an invoice to somebody who is not signed in', function () {
[, $customer] = invoiceCustomer('guarded@invoices.test');
$invoice = issuedFor($customer, 21480);
$this->get(route('invoices.pdf', $invoice->uuid))->assertRedirect('/login');
});
it('says nothing at all about a next charge nobody has computed', function () {
[$user, $customer] = invoiceCustomer('nofuture@invoices.test');
issuedFor($customer, 21480);
// The mock announced "Nächste Abbuchung 198 € am 01.08.2026" to every
// customer on the platform. What Stripe will take next cycle depends on
// prorations, account credit and discounts, and nothing here works it out —
// so the page says nothing rather than guessing.
$html = Livewire::actingAs($user)->test(Invoices::class)->html();
expect($html)->not->toContain('Nächste Abbuchung')
->and($html)->not->toContain('Ausgaben');
});
it('marks a cancellation so it cannot be mistaken for a second charge', function () {
[$user, $customer] = invoiceCustomer('storno@invoices.test');
$original = issuedFor($customer, 21480);
$storno = app(IssueInvoice::class)->cancelling($original);
Livewire::actingAs($user)->test(Invoices::class)
->assertSee($storno->number)
->assertSee(__('invoices.storno'));
});