customer = Customer::factory()->create(['total' => 100_000]); $product = Product::factory()->create(['product_type' => 'hosting', 'is_active' => true]); ProductPricings::create(['product_id' => $product->id, 'billing_cycle' => 'monthly', 'price' => 15_000, 'is_active' => true]); ProductPricings::create(['product_id' => $product->id, 'billing_cycle' => 'quarterly', 'price' => 40_000, 'is_active' => true]); $this->order = Order::create([ 'order_num' => 'ORD-RENEW-TEST', 'customer_id' => $this->customer->id, 'total' => 15_000, 'status' => 1, ]); $orderItem = OrderItem::create([ 'order_id' => $this->order->id, 'product_id' => $product->id, 'billing_cycle' => 'monthly', 'months' => 1, 'quantity' => 1, 'price' => 15_000, 'addons_meta' => [], ]); $this->service = Service::create([ 'customer_id' => $this->customer->id, 'product_id' => $product->id, 'order_item_id' => $orderItem->id, 'type' => 'hosting', 'status' => 'active', 'started_at' => now()->subMonth(), 'expires_at' => now()->addDays(5), ]); } private function renew(int $months) { return $this->actingAs($this->customer, 'client') ->withHeaders(['x-access-token' => $this->buildClientAccessToken()]) ->postJson("/v1/client/dich-vu/{$this->service->id}/renew", ['months' => $months]); } public function test_renew_charges_balance_and_extends_expiry_on_the_same_order(): void { $expiresAt = $this->service->expires_at->copy(); $this->renew(3)->assertOk()->assertJsonPath('success', true); $invoice = Invoice::where('invoice_type', 2)->sole(); $this->assertSame($this->order->id, (int) $invoice->order_id); $this->assertSame(InvoiceStatus::PAID->value, (int) $invoice->status); $this->assertSame(40_000, (int) $invoice->total); $this->assertSame(60_000, (int) $this->customer->fresh()->total); $this->assertSame(1, Order::count()); $this->assertTrue($this->service->fresh()->expires_at->isSameDay($expiresAt->addMonths(3))); $this->assertDatabaseHas('transactions', ['customer_id' => $this->customer->id, 'amount' => 40_000]); } public function test_insufficient_balance_keeps_unpaid_invoice_and_does_not_extend(): void { $this->customer->update(['total' => 10_000]); $expiresAt = $this->service->expires_at->copy(); $this->renew(1)->assertStatus(422)->assertJsonPath('data.status', InvoiceStatus::UNPAID->value); $this->assertSame(10_000, (int) $this->customer->fresh()->total); $this->assertTrue($this->service->fresh()->expires_at->equalTo($expiresAt)); $this->customer->update(['total' => 100_000]); $this->renew(3)->assertOk(); $invoice = Invoice::where('invoice_type', 2)->sole(); $this->assertSame(InvoiceStatus::PAID->value, (int) $invoice->status); $this->assertSame(40_000, (int) $invoice->total); $this->assertSame(1, $invoice->invoiceItems()->count()); $this->assertSame(60_000, (int) $this->customer->fresh()->total); } public function test_months_without_a_priced_cycle_are_rejected(): void { $this->renew(2)->assertStatus(422); $this->assertSame(0, Invoice::where('invoice_type', 2)->count()); } public function test_customer_renewal_notifies_admins_by_notification_and_email(): void { Mail::fake(); Role::firstOrCreate(['name' => 'Full Administrator', 'guard_name' => 'admin']); User::factory()->create()->assignRole('Full Administrator'); Setting::updateOrCreate(['key' => 'setting'], ['value' => json_encode(['admin_email' => 'admin@example.com'])]); $this->renew(3)->assertOk(); $notification = Notification::where('type', 'service_renewed_by_customer')->where('recipient_type', 'admin')->sole(); $this->assertSame('/services/' . $this->service->id, $notification->link); Mail::assertQueued(ServiceRenewedMail::class, fn ($mail) => $mail->hasTo('admin@example.com')); } public function test_customer_reads_own_renewal_history_only(): void { $this->renew(3)->assertOk(); $path = "/v1/client/client-service/{$this->service->uuid}/renewals"; $headers = ['x-access-token' => $this->buildClientAccessToken()]; $this->actingAs($this->customer, 'client')->withHeaders($headers)->getJson($path) ->assertOk() ->assertJsonCount(1, 'data') ->assertJsonPath('data.0.months', 3) ->assertJsonPath('data.0.by', 'Bạn') ->assertJsonPath('data.0.amount', 40000); $this->actingAs(Customer::factory()->create(), 'client')->withHeaders($headers)->getJson($path)->assertNotFound(); } public function test_renewal_invoice_detail_reports_renewal_term_and_next_date(): void { $expiresAt = $this->service->expires_at->copy(); $this->renew(3)->assertOk(); $invoice = Invoice::where('invoice_type', 2)->sole(); $this->actingAs($this->customer, 'client') ->withHeaders(['x-access-token' => $this->buildClientAccessToken()]) ->getJson("/v1/client/invoices/order/ORD-RENEW-TEST?invoice={$invoice->invoice_number}") ->assertOk() ->assertJsonPath('data.service.months', 3) ->assertJsonPath('data.items.0.months', 3) ->assertJsonPath('data.items.0.price', 40000) ->assertJsonPath('data.billing_period_end', $expiresAt->copy()->addMonths(3)->toDateString()) ->assertJsonPath('data.next_renewal_date', $expiresAt->copy()->addMonths(3)->toDateString()); } }