CluPilotCloud/tests/Feature/Billing/IssueInvoiceTest.php

128 lines
5.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
use App\Models\Customer;
use App\Models\Invoice;
use App\Models\InvoiceSeries;
use App\Models\Order;
use App\Services\Billing\IssueInvoice;
use App\Services\Billing\InvoiceRenderer;
use App\Support\CompanyProfile;
/**
* Issuing a document that cannot be changed afterwards.
*/
beforeEach(function () {
CompanyProfile::put([
'name' => 'CluPilot Cloud e.U.',
'address' => 'Dreherstraße 66/1/8',
'postcode' => '1110',
'city' => 'Wien',
'vat_id' => 'ATU00000000',
]);
});
function paidOrders(Customer $customer, int ...$amounts)
{
return collect($amounts)->map(fn (int $cents) => Order::factory()->create([
'customer_id' => $customer->id,
'amount_cents' => $cents,
'currency' => 'EUR',
'status' => 'paid',
]));
}
it('issues one invoice for one purchase, with every item on its own line', function () {
// A customer who buys a plan and two add-ons in one go has bought once.
// Three invoices for one purchase is three times the paperwork for the
// same money — and the add-ons were asked for as separate lines, not
// separate documents.
$customer = Customer::factory()->create();
$invoice = app(IssueInvoice::class)->forOrders($customer, paidOrders($customer, 1900, 500, 900));
expect($invoice->snapshot['lines'])->toHaveCount(3)
->and($invoice->net_cents)->toBe(3300)
->and($invoice->tax_cents)->toBe(660)
->and($invoice->gross_cents)->toBe(3960)
->and($invoice->number)->toStartWith('RE-');
});
it('refuses to issue before the company details are complete', function () {
// An invoice without a registered name, an address or a VAT number is not
// a valid invoice here — and issuing one consumes a number that can never
// be handed out again.
App\Support\Settings::forget('company.vat_id');
$customer = Customer::factory()->create();
expect(fn () => app(IssueInvoice::class)->forOrders($customer, paidOrders($customer, 1900)))
->toThrow(RuntimeException::class);
expect(Invoice::query()->count())->toBe(0)
// And the number was not taken either.
->and(InvoiceSeries::query()->where('kind', 'invoice')->value('next_number'))->toBe(1);
});
it('refuses to issue a document with nothing on it', function () {
expect(fn () => app(IssueInvoice::class)->forOrders(Customer::factory()->create(), collect()))
->toThrow(RuntimeException::class);
});
it('freezes what it says, so later settings cannot rewrite it', function () {
// The whole reason no PDF is stored. Change the address in 2028 and an
// invoice from 2026 still shows the address it was issued under.
$customer = Customer::factory()->create(['name' => 'Muster GmbH']);
$invoice = app(IssueInvoice::class)->forOrders($customer, paidOrders($customer, 1900));
CompanyProfile::put(['name' => 'CluPilot Cloud GmbH', 'address' => 'Anderswo 9']);
$customer->update(['name' => 'Muster GmbH & Co KG']);
$frozen = $invoice->refresh()->snapshot;
expect($frozen['issuer']['name'])->toBe('CluPilot Cloud e.U.')
->and($frozen['issuer']['address'])->toBe('Dreherstraße 66/1/8')
->and($frozen['customer']['name'])->toBe('Muster GmbH');
// And the rendered document agrees with the snapshot, not with today.
expect(app(InvoiceRenderer::class)->forInvoice($invoice))->toBeString()->not->toBeEmpty();
});
it('takes the number and writes the invoice together, or neither', function () {
// A number taken and then lost to a failure is a gap in a series that must
// not have one.
$customer = Customer::factory()->create();
app(IssueInvoice::class)->forOrders($customer, paidOrders($customer, 1900));
app(IssueInvoice::class)->forOrders($customer, paidOrders($customer, 2900));
expect(Invoice::query()->pluck('number')->all())->toBe(['RE-2026-0001', 'RE-2026-0002'])
->and(InvoiceSeries::query()->where('kind', 'invoice')->value('next_number'))->toBe(3);
})->skip(fn () => now()->format('Y') !== '2026', 'The expected numbers carry the current year.');
it('carries the customers billing address as it was written', function () {
// One free-text field today. Guessing which line is the postcode would put
// it where the street belongs, on a document nobody can correct afterwards.
$customer = Customer::factory()->create([
'billing_address' => "Beispielgasse 12/3\n1030 Wien\nÖsterreich",
]);
$invoice = app(IssueInvoice::class)->forOrders($customer, paidOrders($customer, 1900));
expect($invoice->snapshot['customer']['address_lines'])
->toBe(['Beispielgasse 12/3', '1030 Wien', 'Österreich']);
});
it('prints the reverse-charge note exactly when no VAT is charged', function () {
// A zero-rated invoice without the note says nothing about why, which is
// the one thing an auditor looks for.
$customer = Customer::factory()->create([
'vat_id' => 'DE123456789',
'vat_id_verified_at' => now(),
'vat_id_verified_value' => 'DE123456789',
]);
$invoice = app(IssueInvoice::class)->forOrders($customer, paidOrders($customer, 1900));
expect($invoice->tax_cents)->toBe(0)
->and($invoice->snapshot['meta']['closing'])->toBe(__('invoice.reverse_charge'));
});