feat(billing): renewals, coupons, VAT numbering, billing cycles and invoice payment tracking
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
ea9a53f742
commit
25f23d541e
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Api\Client;
|
||||
|
||||
use App\Enums\Invoice\InvoiceStatus;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\InvoiceItem;
|
||||
use App\Support\InvoiceBuyer;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class InvoiceLookupTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private Invoice $invoice;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$customer = Customer::factory()->create([
|
||||
'fullname' => 'Nguyen Van A',
|
||||
'address' => '1 Le Loi',
|
||||
'invoice_company' => 'Cong ty ABC',
|
||||
'tax_code' => '0312345678',
|
||||
'invoice_address' => '99 Nguyen Hue',
|
||||
]);
|
||||
|
||||
$this->invoice = Invoice::create([
|
||||
'invoice_number' => 'INV-LOOKUP-1',
|
||||
'customer_id' => $customer->id,
|
||||
'invoice_type' => 1,
|
||||
'sub_total' => 100_000,
|
||||
'tax' => 10_000,
|
||||
'total' => 110_000,
|
||||
'status' => InvoiceStatus::PAID->value,
|
||||
]);
|
||||
InvoiceItem::create([
|
||||
'invoice_id' => $this->invoice->id,
|
||||
'type' => 'service',
|
||||
'description' => 'VPS 1 thang',
|
||||
'months' => 1,
|
||||
'quantity' => 1,
|
||||
'unit_price' => 100_000,
|
||||
'total_price' => 100_000,
|
||||
]);
|
||||
}
|
||||
|
||||
private function lookup(string $uri, array $body)
|
||||
{
|
||||
return $this->withHeaders(['x-access-token' => $this->buildClientAccessToken()])->postJson($uri, $body);
|
||||
}
|
||||
|
||||
public function test_lookup_code_is_generated_on_create(): void
|
||||
{
|
||||
$this->assertMatchesRegularExpression('/^[A-Z0-9]{10}$/', $this->invoice->lookup_code);
|
||||
}
|
||||
|
||||
public function test_correct_code_returns_limited_payload(): void
|
||||
{
|
||||
$response = $this->lookup('/v1/invoice-lookup', [
|
||||
'invoice_number' => 'INV-LOOKUP-1',
|
||||
'lookup_code' => strtolower($this->invoice->lookup_code),
|
||||
])->assertOk();
|
||||
|
||||
$response->assertJsonPath('data.total', 110_000)
|
||||
->assertJsonPath('data.buyer.tax_code', '0312345678')
|
||||
->assertJsonPath('data.buyer.company', 'Cong ty ABC');
|
||||
|
||||
$body = json_encode($response->json());
|
||||
$this->assertStringNotContainsString('Nguyen Van A', $body);
|
||||
$this->assertStringNotContainsString('99 Nguyen Hue', $body);
|
||||
$this->assertStringNotContainsString($this->invoice->lookup_code, $body);
|
||||
}
|
||||
|
||||
public function test_wrong_code_or_unknown_number_is_404(): void
|
||||
{
|
||||
$this->lookup('/v1/invoice-lookup', ['invoice_number' => 'INV-LOOKUP-1', 'lookup_code' => 'WRONGCODE1'])->assertNotFound();
|
||||
$this->lookup('/v1/invoice-lookup', ['invoice_number' => 'INV-NOPE', 'lookup_code' => $this->invoice->lookup_code])->assertNotFound();
|
||||
$this->lookup('/v1/invoice-lookup/pdf', ['invoice_number' => 'INV-LOOKUP-1', 'lookup_code' => 'WRONGCODE1'])->assertNotFound();
|
||||
}
|
||||
|
||||
public function test_cancelled_invoice_is_not_found(): void
|
||||
{
|
||||
$this->invoice->update(['status' => InvoiceStatus::CANCELLED->value]);
|
||||
|
||||
$this->lookup('/v1/invoice-lookup', [
|
||||
'invoice_number' => 'INV-LOOKUP-1',
|
||||
'lookup_code' => $this->invoice->lookup_code,
|
||||
])->assertNotFound();
|
||||
}
|
||||
|
||||
public function test_pdf_is_returned_for_correct_code(): void
|
||||
{
|
||||
$response = $this->lookup('/v1/invoice-lookup/pdf', [
|
||||
'invoice_number' => 'INV-LOOKUP-1',
|
||||
'lookup_code' => $this->invoice->lookup_code,
|
||||
])->assertOk();
|
||||
|
||||
$this->assertSame('application/pdf', $response->headers->get('Content-Type'));
|
||||
$this->assertStringStartsWith('%PDF', $response->getContent());
|
||||
}
|
||||
|
||||
public function test_requires_client_access_token(): void
|
||||
{
|
||||
$this->postJson('/v1/invoice-lookup', ['invoice_number' => 'INV-LOOKUP-1', 'lookup_code' => $this->invoice->lookup_code])
|
||||
->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_throttles_repeated_guesses_for_one_invoice(): void
|
||||
{
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$this->lookup('/v1/invoice-lookup', ['invoice_number' => 'INV-LOOKUP-1', 'lookup_code' => 'GUESS' . $i])->assertNotFound();
|
||||
}
|
||||
|
||||
$this->lookup('/v1/invoice-lookup', ['invoice_number' => 'INV-LOOKUP-1', 'lookup_code' => $this->invoice->lookup_code])
|
||||
->assertStatus(429);
|
||||
}
|
||||
|
||||
public function test_buyer_snapshot_freezes_buyer_after_vat_is_recorded(): void
|
||||
{
|
||||
$this->invoice->update(['buyer_snapshot' => InvoiceBuyer::fromCustomer($this->invoice->customer)]);
|
||||
$this->invoice->customer->update(['tax_code' => '0999999999']);
|
||||
|
||||
$this->assertSame('0312345678', InvoiceBuyer::forInvoice($this->invoice->fresh())['tax_code']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user