178 lines
6.6 KiB
PHP
178 lines
6.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Feature\Api\Admin;
|
|
|
|
use App\Enums\Invoice\InvoiceStatus;
|
|
use App\Mail\InvoiceNotificationMail;
|
|
use App\Models\Customer;
|
|
use App\Models\Invoice;
|
|
use App\Models\InvoiceItem;
|
|
use App\Models\Product;
|
|
use App\Models\Setting;
|
|
use App\Models\User;
|
|
use App\Support\VatCalculator;
|
|
use App\Support\VatNumbering;
|
|
use App\Support\VatSettings;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Spatie\Permission\Models\Permission;
|
|
use Spatie\Permission\Models\Role;
|
|
use Tests\TestCase;
|
|
|
|
class VatAutomationTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
VatSettings::forget();
|
|
}
|
|
|
|
private function rates(array $rates): void
|
|
{
|
|
Setting::updateOrCreate(['key' => 'vat_settings'], ['value' => json_encode(
|
|
collect($rates)->map(fn ($rate, $type) => ['type' => $type, 'type_label' => $type, 'vat_rate' => $rate])->values()
|
|
)]);
|
|
VatSettings::forget();
|
|
}
|
|
|
|
private function invoice(array $lines, int $discount = 0, int $status = InvoiceStatus::UNPAID->value): Invoice
|
|
{
|
|
$customer = Customer::factory()->create();
|
|
$gross = array_sum(array_column($lines, 1));
|
|
$invoice = Invoice::create([
|
|
'invoice_number' => 'INV-' . uniqid(), 'customer_id' => $customer->id, 'invoice_type' => 1,
|
|
'sub_total' => $gross, 'discount_amount' => $discount, 'tax' => 0, 'total' => $gross - $discount, 'status' => $status,
|
|
]);
|
|
foreach ($lines as [$type, $amount]) {
|
|
InvoiceItem::create([
|
|
'invoice_id' => $invoice->id, 'product_id' => Product::factory()->create(['product_type' => $type])->id,
|
|
'type' => 'service', 'description' => $type, 'months' => 1, 'quantity' => 1,
|
|
'unit_price' => $amount, 'total_price' => $amount,
|
|
]);
|
|
}
|
|
|
|
return $invoice;
|
|
}
|
|
|
|
private function admin(): User
|
|
{
|
|
$role = Role::create(['name' => 'role-' . uniqid(), 'guard_name' => 'admin']);
|
|
foreach (['invoice.view', 'invoice.edit', 'setting.manager'] as $name) {
|
|
Permission::firstOrCreate(['name' => $name, 'guard_name' => 'admin']);
|
|
$role->givePermissionTo($name);
|
|
}
|
|
|
|
return tap(User::factory()->create())->assignRole($role);
|
|
}
|
|
|
|
public function test_tax_is_extracted_from_inclusive_price_per_service_type(): void
|
|
{
|
|
$this->rates(['vps' => 10, 'domain' => 0]);
|
|
$invoice = $this->invoice([['vps', 110_000], ['domain', 500_000]]);
|
|
|
|
VatCalculator::refresh($invoice);
|
|
|
|
$this->assertSame(10_000, (int) $invoice->fresh()->tax);
|
|
$this->assertSame(610_000, (int) $invoice->fresh()->total);
|
|
}
|
|
|
|
public function test_discount_reduces_taxable_amount_proportionally(): void
|
|
{
|
|
$this->rates(['vps' => 10]);
|
|
$invoice = $this->invoice([['vps', 220_000]], discount: 110_000);
|
|
|
|
VatCalculator::refresh($invoice);
|
|
|
|
$this->assertSame(10_000, (int) $invoice->fresh()->tax);
|
|
}
|
|
|
|
public function test_no_configured_rate_keeps_tax_zero(): void
|
|
{
|
|
$invoice = $this->invoice([['vps', 110_000]]);
|
|
|
|
VatCalculator::refresh($invoice);
|
|
|
|
$this->assertSame(0, (int) $invoice->fresh()->tax);
|
|
}
|
|
|
|
public function test_issued_invoice_tax_is_frozen(): void
|
|
{
|
|
$this->rates(['vps' => 10]);
|
|
$invoice = $this->invoice([['vps', 110_000]]);
|
|
VatNumbering::issue($invoice);
|
|
$this->rates(['vps' => 8]);
|
|
|
|
VatCalculator::refresh($invoice->fresh());
|
|
|
|
$this->assertSame(10_000, (int) $invoice->fresh()->tax);
|
|
}
|
|
|
|
public function test_issue_assigns_sequential_numbers_and_skips_manual_collisions(): void
|
|
{
|
|
VatSettings::saveNumbering(['serial' => '1C26TCP', 'next_number' => 5, 'padding' => 4, 'auto_issue' => false]);
|
|
$first = $this->invoice([['vps', 100_000]]);
|
|
$second = $this->invoice([['vps', 100_000]]);
|
|
$manual = $this->invoice([['vps', 100_000]]);
|
|
$manual->update(['vat_serial' => '1C26TCP', 'vat_number' => '0006']);
|
|
|
|
$a = VatNumbering::issue($first);
|
|
$b = VatNumbering::issue($second);
|
|
|
|
$this->assertSame('0005', $a->vat_number);
|
|
$this->assertSame('0007', $b->vat_number);
|
|
$this->assertSame(8, VatSettings::numbering()['next_number']);
|
|
$this->assertSame($a->vat_number, VatNumbering::issue($a)->vat_number);
|
|
$this->assertNotNull($a->buyer_snapshot);
|
|
}
|
|
|
|
public function test_auto_issue_runs_only_when_enabled_and_invoice_becomes_paid(): void
|
|
{
|
|
VatSettings::saveNumbering(['serial' => 'AB', 'next_number' => 1, 'padding' => 3, 'auto_issue' => true]);
|
|
$invoice = $this->invoice([['vps', 100_000]]);
|
|
$this->assertNull($invoice->fresh()->vat_number);
|
|
|
|
$invoice->update(['status' => InvoiceStatus::PAID->value]);
|
|
|
|
$this->assertSame('001', $invoice->fresh()->vat_number);
|
|
|
|
VatSettings::saveNumbering(['serial' => 'AB', 'next_number' => 2, 'padding' => 3, 'auto_issue' => false]);
|
|
$other = $this->invoice([['vps', 100_000]]);
|
|
$other->update(['status' => InvoiceStatus::PAID->value]);
|
|
$this->assertNull($other->fresh()->vat_number);
|
|
}
|
|
|
|
public function test_admin_can_issue_and_configure_numbering(): void
|
|
{
|
|
$admin = $this->admin();
|
|
$invoice = $this->invoice([['vps', 100_000]]);
|
|
|
|
$this->actingAs($admin, 'admin')->putJson('/api/v1/administrator/vat-numbering', [
|
|
'serial' => '1c26tcp', 'next_number' => 10, 'padding' => 6, 'auto_issue' => false,
|
|
])->assertOk()->assertJsonPath('data.serial', '1C26TCP');
|
|
|
|
$this->actingAs($admin, 'admin')->postJson("/api/v1/administrator/invoices/{$invoice->id}/vat/issue")
|
|
->assertOk()->assertJsonPath('data.vat_number', '000010')->assertJsonPath('data.vat_serial', '1C26TCP');
|
|
|
|
$this->actingAs($admin, 'admin')->postJson("/api/v1/administrator/invoices/{$invoice->id}/vat/issue")
|
|
->assertStatus(422);
|
|
|
|
$this->actingAs($admin, 'admin')->putJson('/api/v1/administrator/vat-numbering', [
|
|
'serial' => '1C26TCP', 'next_number' => 10, 'padding' => 6, 'auto_issue' => false,
|
|
])->assertStatus(422);
|
|
}
|
|
|
|
public function test_invoice_notification_email_carries_lookup_code(): void
|
|
{
|
|
$this->seed(\Database\Seeders\TemplateSeeder::class);
|
|
$invoice = $this->invoice([['vps', 100_000]]);
|
|
|
|
$html = InvoiceNotificationMail::forInvoice($invoice->load('customer'))->render();
|
|
|
|
$this->assertStringContainsString($invoice->lookup_code, $html);
|
|
$this->assertStringContainsString('/tra-cuu-hoa-don', $html);
|
|
}
|
|
}
|