'Full Administrator', 'guard_name' => 'admin']); User::factory()->create()->assignRole('Full Administrator'); Setting::updateOrCreate(['key' => 'setting'], ['value' => json_encode(['admin_email' => 'admin@example.com'])]); $this->owner = Customer::factory()->create(); $this->service = Service::create(['customer_id' => $this->owner->id, 'type' => 'ssl', 'status' => 'active', 'server_name' => 'example.com']); } private function as(Customer $customer): static { return $this->actingAs($customer, 'client')->withHeaders(['x-access-token' => $this->buildClientAccessToken()]); } public function test_request_notifies_admins_and_blocks_duplicates_until_withdrawn(): void { $url = "/v1/client/client-service/{$this->service->uuid}/cancel-request"; $this->as($this->owner)->postJson($url, ['reason_code' => 'other', 'reason' => 'Chuyển sang nhà cung cấp khác']) ->assertCreated() ->assertJsonPath('data.reason', 'Chuyển sang nhà cung cấp khác'); $this->assertSame(1, Notification::where('type', 'service_cancellation_request')->where('recipient_type', 'admin')->count()); Mail::assertQueued(ServiceCancellationRequestMail::class, fn ($mail) => $mail->hasTo('admin@example.com')); $this->as($this->owner)->postJson($url, ['reason' => 'Lặp lại'])->assertStatus(422); $this->as($this->owner)->deleteJson($url)->assertOk(); $this->as($this->owner)->postJson($url, ['reason' => 'Gửi lại sau khi rút'])->assertCreated(); } public function test_other_customer_cannot_request_and_reason_is_required(): void { $url = "/v1/client/client-service/{$this->service->uuid}/cancel-request"; $this->as(Customer::factory()->create())->postJson($url, ['reason' => 'Hủy giúp'])->assertNotFound(); $this->as($this->owner)->postJson($url, [])->assertStatus(422); $this->assertSame(0, Notification::where('type', 'service_cancellation_request')->count()); } public function test_admin_approves_or_rejects_once_and_customer_is_notified(): void { $admin = User::first(); $pending = $this->service->cancellationRequests()->create(['customer_id' => $this->owner->id, 'reason' => 'Không dùng nữa']); $base = "/api/v1/administrator/services/{$this->service->id}/cancellation-requests"; $this->actingAs($admin, 'admin')->postJson("{$base}/{$pending->id}/resolve", ['action' => 'reject'])->assertStatus(422); $this->actingAs($admin, 'admin')->postJson("{$base}/{$pending->id}/resolve", ['action' => 'reject', 'note' => 'Còn hợp đồng'])->assertOk(); $this->assertSame('active', $this->service->fresh()->status); $this->actingAs($admin, 'admin')->postJson("{$base}/{$pending->id}/resolve", ['action' => 'approve'])->assertStatus(422); $second = $this->service->cancellationRequests()->create(['customer_id' => $this->owner->id, 'reason' => 'Lần hai']); $this->actingAs($admin, 'admin')->postJson("{$base}/{$second->id}/resolve", ['action' => 'approve'])->assertOk(); $this->assertSame('cancelled', $this->service->fresh()->status); $this->assertSame(2, Notification::where('recipient_type', 'client')->where('type', 'service_cancellation_resolved')->count()); $this->actingAs($admin, 'admin')->getJson($base)->assertJsonCount(2, 'data'); } public function test_approved_cancellation_keeps_service_until_expiry_then_cancels(): void { $this->service->update(['expires_at' => now()->addDays(5), 'auto_renew' => true]); $pending = $this->service->cancellationRequests()->create(['customer_id' => $this->owner->id, 'reason' => 'Không dùng nữa']); $this->actingAs(User::first(), 'admin') ->postJson("/api/v1/administrator/services/{$this->service->id}/cancellation-requests/{$pending->id}/resolve", ['action' => 'approve']) ->assertOk(); $service = $this->service->fresh(); $this->assertSame('active', $service->status); $this->assertTrue($service->cancel_at_expiry); $this->assertFalse($service->auto_renew); $this->assertNull($service->cancelled_at); $this->as($this->owner)->postJson("/v1/client/client-service/{$service->uuid}/cancel-request", ['reason' => 'Gửi lại'])->assertStatus(422); $this->artisan('service:expiry')->assertSuccessful(); $this->assertSame('active', $service->fresh()->status); $service->update(['expires_at' => now()->subHour()]); $this->artisan('service:expiry')->assertSuccessful(); $this->assertSame('cancelled', $service->fresh()->status); $this->assertNotNull($service->fresh()->cancelled_at); } }