Files
portal-api/tests/Feature/Api/Admin/Billing/BillingDuplicateDetectorTest.php

168 lines
8.0 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Feature\Api\Admin\Billing;
use App\Models\Customer;
use App\Models\Invoice;
use App\Models\InvoiceItem;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Product;
use App\Models\User;
use App\Support\Billing\BillingDuplicateDetector as Detector;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Spatie\Permission\Models\Role;
use Tests\TestCase;
class BillingDuplicateDetectorTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Detector::flush();
}
private function order(Customer $customer, Product $product, string $name, int $total = 100_000, string $at = 'now', int $status = 0, string $domain = ''): Order
{
$order = Order::create([
'order_num' => 'ORD-' . uniqid(), 'customer_id' => $customer->id, 'sub_total' => $total, 'total' => $total,
'status' => $status, 'created_at' => $at, 'updated_at' => $at,
]);
OrderItem::create([
'order_id' => $order->id, 'product_id' => $product->id, 'billing_cycle' => 'monthly', 'months' => 1,
'quantity' => 1, 'price' => $total, 'server_name' => $name, 'domain_name' => $domain ?: null,
]);
return $order;
}
private function invoice(Customer $customer, ?Order $order, int $type, int $total, string $at = 'now', ?string $period = null, string $item = 'VPS'): Invoice
{
$invoice = Invoice::create([
'invoice_number' => 'INV-' . uniqid(), 'customer_id' => $customer->id, 'order_id' => $order?->id, 'invoice_type' => $type,
'sub_total' => $total, 'tax' => 0, 'total' => $total, 'status' => 1, 'billing_period_start' => $period,
'created_at' => $at, 'updated_at' => $at,
]);
InvoiceItem::create(['invoice_id' => $invoice->id, 'type' => 'service', 'description' => $item, 'months' => 1, 'quantity' => 1, 'unit_price' => $total, 'total_price' => $total]);
return $invoice;
}
public function test_same_customer_product_and_service_name_is_flagged_even_days_apart(): void
{
$customer = Customer::factory()->create();
$product = Product::factory()->create();
$a = $this->order($customer, $product, 'Web-01', at: '2026-10-01 10:00:00');
$b = $this->order($customer, $product, ' web-01 ', at: '2026-10-09 10:00:00');
$other = $this->order($customer, $product, 'web-02');
$found = Detector::orders();
$this->assertSame(Detector::REASON_SAME_SERVICE, $found[$a->id]['reason']);
$this->assertSame([$a->order_num], $found[$b->id]['with']);
$this->assertArrayNotHasKey($other->id, $found);
}
public function test_rapid_resubmit_needs_same_items_and_window(): void
{
$customer = Customer::factory()->create();
$product = Product::factory()->create();
$a = $this->order($customer, $product, '', at: '2026-10-01 10:00:00');
$b = $this->order($customer, $product, '', at: '2026-10-01 11:00:00');
$late = $this->order($customer, $product, '', at: '2026-10-05 11:00:00');
$differentTotal = $this->order($customer, $product, '', total: 200_000, at: '2026-10-01 10:30:00');
$found = Detector::orders();
$this->assertSame(Detector::REASON_RESUBMIT, $found[$a->id]['reason']);
$this->assertSame([$a->order_num], $found[$b->id]['with']);
$this->assertArrayNotHasKey($late->id, $found);
$this->assertArrayNotHasKey($differentTotal->id, $found);
}
public function test_domain_ordered_by_two_customers_is_flagged_but_cancelled_orders_are_ignored(): void
{
$product = Product::factory()->create(['product_type' => 'domain']);
$first = $this->order(Customer::factory()->create(), $product, 'shop.vn', domain: 'Shop.vn');
$second = $this->order(Customer::factory()->create(), $product, 'shop.vn', domain: 'shop.vn');
$cancelled = $this->order(Customer::factory()->create(), $product, 'shop.vn', status: 2, domain: 'shop.vn');
$found = Detector::orders();
$this->assertSame(Detector::REASON_DOMAIN_TAKEN, $found[$first->id]['reason']);
$this->assertSame(Detector::REASON_DOMAIN_TAKEN, $found[$second->id]['reason']);
$this->assertArrayNotHasKey($cancelled->id, $found);
}
public function test_invoices_same_order_and_type_are_flagged_but_renewal_periods_and_upgrades_are_not(): void
{
$customer = Customer::factory()->create();
$order = $this->order($customer, Product::factory()->create(), 'vm-1');
$a = $this->invoice($customer, $order, 1, 100_000);
$b = $this->invoice($customer, $order, 1, 150_000);
$renewal = $this->invoice($customer, $order, 2, 100_000, period: '2026-11-01');
$next = $this->invoice($customer, $order, 2, 100_000, period: '2026-12-01');
$upgradeA = $this->invoice($customer, $order, 3, 50_000, at: '2026-10-01 10:00:00', item: 'Addon RAM');
$upgradeB = $this->invoice($customer, $order, 3, 70_000, at: '2026-10-01 10:30:00', item: 'Addon CPU');
$found = Detector::invoices();
$this->assertSame(Detector::REASON_SAME_PERIOD, $found[$a->id]['reason']);
$this->assertSame([$a->invoice_number], $found[$b->id]['with']);
$this->assertArrayNotHasKey($renewal->id, $found);
$this->assertArrayNotHasKey($next->id, $found);
$this->assertArrayNotHasKey($upgradeA->id, $found);
$this->assertArrayNotHasKey($upgradeB->id, $found);
}
public function test_invoice_rapid_resubmit_requires_identical_items(): void
{
$customer = Customer::factory()->create();
$a = $this->invoice($customer, null, 1, 100_000, at: '2026-10-01 10:00:00');
$b = $this->invoice($customer, null, 1, 100_000, at: '2026-10-01 10:05:00');
$diff = $this->invoice($customer, null, 1, 100_000, at: '2026-10-01 10:06:00', item: 'Khác');
$found = Detector::invoices();
$this->assertSame(Detector::REASON_RESUBMIT, $found[$a->id]['reason']);
$this->assertSame([$a->invoice_number], $found[$b->id]['with']);
$this->assertArrayNotHasKey($diff->id, $found);
}
public function test_list_endpoints_expose_flag_and_filter(): void
{
Role::firstOrCreate(['name' => 'Full Administrator', 'guard_name' => 'admin']);
$admin = tap(User::factory()->create())->assignRole('Full Administrator');
$customer = Customer::factory()->create();
$product = Product::factory()->create();
$a = $this->order($customer, $product, 'dup-1');
$b = $this->order($customer, $product, 'dup-1');
$solo = $this->order($customer, $product, 'solo');
$inv = $this->invoice($customer, $a, 1, 100_000);
$this->invoice($customer, $a, 1, 100_000);
$orders = $this->actingAs($admin, 'admin')->getJson('/api/v1/administrator/orders?duplicates=1')->assertOk();
$this->assertEqualsCanonicalizing([$a->id, $b->id], collect($orders->json('data.items'))->pluck('id')->all());
$this->assertSame(Detector::REASON_RESUBMIT, $orders->json('data.items.0.duplicate.reason'));
$all = $this->actingAs($admin, 'admin')->getJson('/api/v1/administrator/orders')->assertOk();
$this->assertCount(3, $all->json('data.items'));
$this->assertNull(collect($all->json('data.items'))->firstWhere('id', $solo->id)['duplicate']);
$invoices = $this->actingAs($admin, 'admin')->getJson('/api/v1/administrator/invoices?duplicates=1')->assertOk();
$this->assertCount(2, $invoices->json('data.items'));
$this->assertSame(Detector::REASON_SAME_PERIOD, collect($invoices->json('data.items'))->firstWhere('id', $inv->id)['duplicate']['reason']);
}
public function test_order_date_defaults_to_today_when_not_given(): void
{
$order = Order::create(['order_num' => 'ORD-DATE', 'customer_id' => Customer::factory()->create()->id, 'sub_total' => 1, 'total' => 1, 'status' => 0]);
$this->assertSame(now()->toDateString(), $order->fresh()->date->toDateString());
}
}