Files
portal-api/tests/Feature/Api/Service/RenewServiceTest.php
baodev0andClaude Sonnet 5.5 b44a88bb52 feat(service): huỷ dịch vụ, SSL, đổi mật khẩu hosting, lịch sử thanh toán theo hình thức
- Lịch sử thanh toán hoá đơn trả thêm `method` (tự động gia hạn / số dư / admin / chuyển khoản / hoàn tiền), bỏ đuôi `(method)` khỏi ghi chú
- `DnsServer::panelUrl()` = https://<domain>/ dùng cho email cấp hosting và nút Plesk của khách
- Order item trả thêm `uuid`/`type` của dịch vụ để FE dẫn tới trang quản lý
- Yêu cầu huỷ dịch vụ, chứng chỉ SSL, lịch sử gia hạn, đổi mật khẩu hosting cho khách

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 16:40:24 +07:00

165 lines
6.6 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Feature\Api\Service;
use App\Enums\Invoice\InvoiceStatus;
use App\Models\Customer;
use App\Models\Invoice;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Product;
use App\Models\ProductPricings;
use App\Models\Service;
use App\Mail\ServiceRenewedMail;
use App\Models\Notification;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Mail;
use Spatie\Permission\Models\Role;
use Tests\TestCase;
class RenewServiceTest extends TestCase
{
use RefreshDatabase;
private Customer $customer;
private Service $service;
private Order $order;
protected function setUp(): void
{
parent::setUp();
$this->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());
}
}