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']); } }