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