129 lines
4.8 KiB
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'));
|
|
});
|