feat: nạp tiền thủ công mã 30 phút, gia hạn trừ số dư, đồng bộ domain từ Plesk
- Nạp tiền: mã thanh toán sống 30 phút (ManualDepositDraft), khôi phục/huỷ mã, lệnh dọn mã hết hạn; total_pending chỉ tính giao dịch đã nộp chứng từ, admin duyệt/từ chối/xoá giao dịch - Gia hạn dịch vụ: phát hành invoice gia hạn trên order gốc và trừ số dư ngay - Đồng bộ domain từ Plesk theo server, ServiceOrderRecorder tạo order/invoice cho dịch vụ admin dựng - Danh sách dịch vụ trả field theo loại (service_plan, dns_records_count), đếm dịch vụ theo loại Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
10af41c5cf
commit
c5115b52dd
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Api\Admin\Customer;
|
||||
|
||||
use App\Models\Customer;
|
||||
use App\Models\DomainPricing;
|
||||
use App\Models\Product;
|
||||
use App\Models\Service;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CreateDomainServicePricingTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private User $admin;
|
||||
|
||||
private Customer $customer;
|
||||
|
||||
private Product $product;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Role::firstOrCreate(['name' => 'Full Administrator', 'guard_name' => 'admin']);
|
||||
$this->admin = User::factory()->create();
|
||||
$this->admin->assignRole('Full Administrator');
|
||||
$this->customer = Customer::factory()->create();
|
||||
$this->product = Product::factory()->create(['product_type' => 'domain']);
|
||||
|
||||
DomainPricing::query()->delete();
|
||||
DomainPricing::create(['type' => 'domain', 'tld' => '.vn', 'setup_fee' => '100000', 'first_year' => 400_000, 'recurring_fee' => '350000', 'is_active' => true]);
|
||||
DomainPricing::create(['type' => 'domain', 'tld' => '.com.vn', 'setup_fee' => 'Miễn phí', 'first_year' => 300_000, 'recurring_fee' => '300000', 'is_active' => true]);
|
||||
}
|
||||
|
||||
private function createService(array $data)
|
||||
{
|
||||
return $this->actingAs($this->admin, 'admin')->postJson(
|
||||
"/api/v1/administrator/customers/{$this->customer->uuid}/services",
|
||||
['product_id' => $this->product->id, 'payment_method' => 'transfer', 'billing_cycle' => 'biennially', ...$data],
|
||||
);
|
||||
}
|
||||
|
||||
public function test_domain_service_uses_domain_pricing_for_matched_tld(): void
|
||||
{
|
||||
$this->createService(['server_name' => 'Example.COM.vn'])->assertCreated();
|
||||
$this->createService(['server_name' => 'example.vn'])->assertCreated();
|
||||
|
||||
$this->assertDatabaseHas('order_items', ['domain_name' => 'Example.COM.vn', 'price' => 600_000]);
|
||||
$this->assertDatabaseHas('order_items', ['domain_name' => 'example.vn', 'price' => 850_000]);
|
||||
}
|
||||
|
||||
public function test_domain_without_pricing_requires_amount(): void
|
||||
{
|
||||
$this->createService(['server_name' => 'example.xyz'])->assertStatus(422);
|
||||
$this->assertSame(0, Service::count());
|
||||
|
||||
$this->createService(['server_name' => 'example.xyz', 'amount_paid' => 250_000])->assertCreated();
|
||||
$this->assertDatabaseHas('order_items', ['domain_name' => 'example.xyz', 'price' => 250_000]);
|
||||
}
|
||||
|
||||
public function test_saving_price_on_service_without_order_creates_unpaid_order_and_invoice(): void
|
||||
{
|
||||
$service = Service::create([
|
||||
'customer_id' => $this->customer->id,
|
||||
'product_id' => $this->product->id,
|
||||
'server_name' => 'example.vn',
|
||||
'status' => 'active',
|
||||
]);
|
||||
$url = "/api/v1/administrator/customers/{$this->customer->uuid}/services/{$service->id}";
|
||||
|
||||
$this->actingAs($this->admin, 'admin')->putJson($url, ['price' => 500_000, 'billing_cycle' => 'annually'])->assertOk();
|
||||
|
||||
$service->refresh();
|
||||
$this->assertSame(500_000, (int) $service->orderItem->price);
|
||||
$this->assertSame(1, (int) $service->invoice->status);
|
||||
$this->assertSame(500_000, (int) $service->invoice->total);
|
||||
|
||||
$this->actingAs($this->admin, 'admin')->putJson($url, ['price' => 450_000])->assertOk();
|
||||
|
||||
$this->assertSame(450_000, (int) $service->invoice->fresh()->total);
|
||||
$this->assertSame(450_000, (int) $service->invoice->invoiceItems()->value('total_price'));
|
||||
$this->assertDatabaseCount('invoices', 1);
|
||||
}
|
||||
|
||||
public function test_domain_quote_endpoint(): void
|
||||
{
|
||||
$url = '/api/v1/administrator/services/domain-quote';
|
||||
|
||||
$this->actingAs($this->admin, 'admin')->getJson("{$url}?domain=abc.vn&billing_cycle=triennially")
|
||||
->assertOk()
|
||||
->assertJsonPath('data.tld', '.vn')
|
||||
->assertJsonPath('data.years', 3)
|
||||
->assertJsonPath('data.price', 1_200_000);
|
||||
|
||||
$this->actingAs($this->admin, 'admin')->getJson("{$url}?domain=abc.xyz")
|
||||
->assertOk()
|
||||
->assertJsonPath('data', null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Api\Admin;
|
||||
|
||||
use App\Models\Customer;
|
||||
use App\Models\DnsDomain;
|
||||
use App\Models\DomainPricing;
|
||||
use App\Models\Product;
|
||||
use App\Models\Service;
|
||||
use App\Services\Domain\CustomerDomainServiceSync;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CustomerDomainServiceSyncTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_assign_update_and_detach_keep_domain_service_in_sync(): void
|
||||
{
|
||||
$customer = Customer::factory()->create();
|
||||
$serverId = DB::table('dns_manager_servers')->insertGetId([
|
||||
'ip_address' => '10.0.0.1',
|
||||
'password' => 'x',
|
||||
]);
|
||||
$domain = DnsDomain::create([
|
||||
'domain_name' => 'devro-tech.com',
|
||||
'dns_manager_server_id' => $serverId,
|
||||
'is_created' => DnsDomain::STATUS_CREATED,
|
||||
]);
|
||||
DomainPricing::query()->delete();
|
||||
DomainPricing::create(['type' => 'domain', 'tld' => '.com', 'setup_fee' => '0', 'first_year' => 320_000, 'recurring_fee' => '340000', 'is_active' => true]);
|
||||
$product = Product::factory()->create(['product_type' => 'domain', 'is_active' => true]);
|
||||
$sync = app(CustomerDomainServiceSync::class);
|
||||
|
||||
$sync->syncCustomers($domain, [$customer->id]);
|
||||
|
||||
$service = Service::query()
|
||||
->where('customer_id', $customer->id)
|
||||
->where('type', 'domain')
|
||||
->where('server_name', 'devro-tech.com')
|
||||
->firstOrFail();
|
||||
|
||||
$this->assertSame('active', $service->status);
|
||||
$this->assertSame(CustomerDomainServiceSync::AUTO_CREATED_NOTE, $service->note);
|
||||
$this->assertSame($product->id, $service->product_id);
|
||||
$this->assertSame('devro-tech.com', $service->orderItem->domain_name);
|
||||
$this->assertSame(320_000, (int) $service->orderItem->price);
|
||||
$this->assertSame('annually', $service->orderItem->billing_cycle);
|
||||
$this->assertSame(1, (int) $service->invoice->status);
|
||||
$this->assertSame(320_000, (int) $service->invoice->total);
|
||||
|
||||
$sync->syncCustomers($domain, [$customer->id]);
|
||||
$this->assertSame(1, Service::query()->where('customer_id', $customer->id)->count());
|
||||
$this->assertSame(1, $service->orderItem->order()->count());
|
||||
$this->assertDatabaseCount('invoices', 1);
|
||||
|
||||
$sync->updateCustomerDomain($domain, $customer->id, '2027-07-21');
|
||||
$this->assertSame('2027-07-21', $service->fresh()->expires_at?->toDateString());
|
||||
|
||||
$sync->syncCustomers($domain, []);
|
||||
|
||||
$this->assertSoftDeleted('services', ['id' => $service->id]);
|
||||
$this->assertSame(3, (int) $service->invoice->fresh()->status);
|
||||
$this->assertDatabaseMissing('dns_domain_user', [
|
||||
'domain_id' => $domain->id,
|
||||
'user_id' => $customer->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Api\Admin\DnsManager;
|
||||
|
||||
use App\Events\DnsSyncCompleted;
|
||||
use App\Jobs\DnsManager\SyncDnsFromPlesk;
|
||||
use App\Models\DnsDomain;
|
||||
use App\Models\User;
|
||||
use App\Services\Plesk\PleskDomainService;
|
||||
use App\Services\ServerService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Mockery;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SyncDomainsFromPleskTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private int $serverId;
|
||||
|
||||
private int $otherServerId;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->serverId = DB::table('dns_manager_servers')->insertGetId(['ip_address' => '10.0.0.1', 'password' => 'x']);
|
||||
$this->otherServerId = DB::table('dns_manager_servers')->insertGetId(['ip_address' => '10.0.0.2', 'password' => 'x']);
|
||||
}
|
||||
|
||||
private function runJob(array|false $pleskDomains): void
|
||||
{
|
||||
$plesk = Mockery::mock(PleskDomainService::class);
|
||||
$plesk->shouldReceive('listDomains')->once()->andReturn($pleskDomains);
|
||||
$plesk->shouldReceive('getLastError')->andReturn('boom');
|
||||
|
||||
$server = Mockery::mock(ServerService::class);
|
||||
$server->shouldReceive('addServerById')->andReturn(true);
|
||||
$server->shouldReceive('listRecords')->andReturn(['error' => false, 'data' => []]);
|
||||
|
||||
$this->app->instance(PleskDomainService::class, $plesk);
|
||||
$this->app->instance(ServerService::class, $server);
|
||||
|
||||
$this->app->call([new SyncDnsFromPlesk($this->serverId), 'handle']);
|
||||
}
|
||||
|
||||
public function test_imports_new_domains_and_links_pending_ones_on_selected_server(): void
|
||||
{
|
||||
Event::fake([DnsSyncCompleted::class]);
|
||||
$pending = DnsDomain::create(['domain_name' => 'Pending.vn', 'dns_manager_server_id' => $this->serverId, 'is_created' => 0]);
|
||||
$otherServer = DnsDomain::create(['domain_name' => 'other.vn', 'dns_manager_server_id' => $this->otherServerId, 'is_created' => 0]);
|
||||
$cancelled = DnsDomain::create(['domain_name' => 'cancelled.vn', 'dns_manager_server_id' => $this->serverId, 'is_created' => DnsDomain::STATUS_CANCELLED]);
|
||||
|
||||
$this->runJob([
|
||||
['id' => 1, 'name' => 'new.vn', 'base_domain_id' => 0],
|
||||
['id' => 2, 'name' => 'pending.vn', 'base_domain_id' => 0],
|
||||
['id' => 3, 'name' => 'other.vn', 'base_domain_id' => 0],
|
||||
['id' => 4, 'name' => 'cancelled.vn', 'base_domain_id' => 0],
|
||||
['id' => 5, 'name' => 'sub.new.vn', 'base_domain_id' => 1],
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('dns_manager_domain', ['domain_name' => 'new.vn', 'plesk_id' => 1, 'dns_manager_server_id' => $this->serverId, 'is_created' => 1]);
|
||||
$this->assertDatabaseHas('dns_manager_domain', ['id' => $pending->id, 'plesk_id' => 2, 'is_created' => 1]);
|
||||
$this->assertDatabaseHas('dns_manager_domain', ['id' => $otherServer->id, 'plesk_id' => null]);
|
||||
$this->assertDatabaseHas('dns_manager_domain', ['id' => $cancelled->id, 'plesk_id' => null, 'is_created' => DnsDomain::STATUS_CANCELLED]);
|
||||
$this->assertDatabaseMissing('dns_manager_domain', ['domain_name' => 'sub.new.vn']);
|
||||
Event::assertDispatched(DnsSyncCompleted::class, fn ($e) => $e->imported === 2 && $e->failedDomains === []);
|
||||
}
|
||||
|
||||
public function test_reports_failure_when_plesk_domain_list_unavailable(): void
|
||||
{
|
||||
Event::fake([DnsSyncCompleted::class]);
|
||||
|
||||
$this->runJob(false);
|
||||
|
||||
$this->assertDatabaseCount('dns_manager_domain', 0);
|
||||
Event::assertDispatched(DnsSyncCompleted::class, fn ($e) => $e->failedDomains !== []);
|
||||
}
|
||||
|
||||
public function test_endpoint_queues_sync_for_selected_server(): void
|
||||
{
|
||||
Queue::fake();
|
||||
Role::firstOrCreate(['name' => 'Full Administrator', 'guard_name' => 'admin']);
|
||||
$admin = User::factory()->create();
|
||||
$admin->assignRole('Full Administrator');
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->postJson('/api/v1/administrator/domains/sync-dns', ['dns_manager_server_id' => 999999])
|
||||
->assertStatus(422);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->postJson('/api/v1/administrator/domains/sync-dns', ['dns_manager_server_id' => $this->serverId])
|
||||
->assertOk();
|
||||
|
||||
Queue::assertPushed(SyncDnsFromPlesk::class, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Api\Admin\Product;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ProductCategorySortOrderTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_store_requires_sort_order(): void
|
||||
{
|
||||
Role::firstOrCreate(['name' => 'Full Administrator', 'guard_name' => 'admin']);
|
||||
$admin = User::factory()->create();
|
||||
$admin->assignRole('Full Administrator');
|
||||
$url = '/api/v1/administrator/product-categories';
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->postJson($url, ['name' => 'Wordpress Hosting', 'sort_order' => null])
|
||||
->assertStatus(422)
|
||||
->assertJsonPath('errors.sort_order.0', 'Vui lòng nhập thứ tự hiển thị (sort_order).');
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->postJson($url, ['name' => 'Wordpress Hosting', 'sort_order' => 3])
|
||||
->assertCreated();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Api\Client;
|
||||
|
||||
use App\Models\Customer;
|
||||
use App\Models\DnsDomain;
|
||||
use App\Models\DnsManagerNS;
|
||||
use App\Models\Service;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ClientServiceListTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_customer_can_only_list_their_own_services_across_all_types(): void
|
||||
{
|
||||
$customer = Customer::factory()->create();
|
||||
$otherCustomer = Customer::factory()->create();
|
||||
|
||||
$vps = Service::create([
|
||||
'customer_id' => $customer->id,
|
||||
'type' => 'vps',
|
||||
'server_name' => 'vps.example.vn',
|
||||
'status' => 'active',
|
||||
]);
|
||||
$hosting = Service::create([
|
||||
'customer_id' => $customer->id,
|
||||
'type' => 'hosting',
|
||||
'server_name' => 'hosting.example.vn',
|
||||
'status' => 'cancelled',
|
||||
]);
|
||||
$domain = Service::create([
|
||||
'customer_id' => $customer->id,
|
||||
'type' => 'domain',
|
||||
'server_name' => 'example.vn',
|
||||
'status' => 'active',
|
||||
]);
|
||||
$dnsServerId = DB::table('dns_manager_servers')->insertGetId([
|
||||
'ip_address' => '10.0.0.1',
|
||||
'password' => 'x',
|
||||
]);
|
||||
$dnsDomain = DnsDomain::create([
|
||||
'domain_name' => 'example.vn',
|
||||
'dns_manager_server_id' => $dnsServerId,
|
||||
'is_created' => DnsDomain::STATUS_CREATED,
|
||||
]);
|
||||
$dnsDomain->customer()->attach($customer->id);
|
||||
DnsManagerNS::create([
|
||||
'dns_manager_domain_id' => $dnsDomain->id,
|
||||
'type' => 'A',
|
||||
'type_valid' => 'A',
|
||||
'name' => 'example.vn',
|
||||
'value' => '10.0.0.2',
|
||||
'ttl' => 300,
|
||||
]);
|
||||
Service::create([
|
||||
'customer_id' => $otherCustomer->id,
|
||||
'type' => 'vps',
|
||||
'server_name' => 'private.example.vn',
|
||||
'status' => 'active',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($customer, 'client')
|
||||
->withHeaders(['x-access-token' => $this->buildClientAccessToken()])
|
||||
->getJson('/v1/client/client-service')
|
||||
->assertOk()
|
||||
->assertJsonPath('error', false)
|
||||
->assertJsonCount(3, 'data.items');
|
||||
|
||||
$items = collect($response->json('data.items'))->keyBy('uuid');
|
||||
|
||||
$this->assertSame('vps', $items[$vps->uuid]['type']);
|
||||
$this->assertSame('hosting', $items[$hosting->uuid]['type']);
|
||||
$this->assertSame(1, $items[$domain->uuid]['dns_records_count']);
|
||||
$this->assertFalse($items->contains('server_name', 'private.example.vn'));
|
||||
|
||||
$this->assertSame(['os', 'specs'], array_values(array_intersect(
|
||||
array_keys($items[$vps->uuid]),
|
||||
['os', 'specs', 'service_plan', 'dns_records_count'],
|
||||
)));
|
||||
$this->assertSame(['service_plan'], array_values(array_intersect(
|
||||
array_keys($items[$hosting->uuid]),
|
||||
['os', 'specs', 'service_plan', 'dns_records_count'],
|
||||
)));
|
||||
$this->assertSame(['dns_records_count'], array_values(array_intersect(
|
||||
array_keys($items[$domain->uuid]),
|
||||
['os', 'specs', 'service_plan', 'dns_records_count'],
|
||||
)));
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@ declare(strict_types=1);
|
||||
namespace Tests\Feature\Api\Service;
|
||||
|
||||
use App\Enums\Invoice\InvoiceStatus;
|
||||
use App\Models\Addon;
|
||||
use App\Enums\Order\OrderStatus;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Order;
|
||||
@@ -14,7 +12,6 @@ use App\Models\OrderItem;
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductPricings;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceAddon;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -23,330 +20,85 @@ class RenewServiceTest extends TestCase
|
||||
use RefreshDatabase;
|
||||
|
||||
private Customer $customer;
|
||||
private Product $product;
|
||||
private Service $service;
|
||||
private Order $order;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->customer = Customer::factory()->create();
|
||||
$this->product = Product::factory()->create([
|
||||
'product_type' => 'hosting',
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
$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]);
|
||||
|
||||
private function actingAsCustomer(): static
|
||||
{
|
||||
return $this->actingAs($this->customer, 'client')
|
||||
->withHeaders(['x-access-token' => $this->buildClientAccessToken()]);
|
||||
}
|
||||
|
||||
/** Encrypts the shared secret the same way ClientAccessTokenCheck expects, for test requests. */
|
||||
|
||||
private function createServiceWithOldPrice(int $oldPrice): Service
|
||||
{
|
||||
$order = Order::create([
|
||||
'order_num' => 'ORD-' . uniqid(),
|
||||
$this->order = Order::create([
|
||||
'order_num' => 'ORD-RENEW-TEST',
|
||||
'customer_id' => $this->customer->id,
|
||||
'sub_total' => $oldPrice,
|
||||
'total' => $oldPrice,
|
||||
'status' => OrderStatus::ACTIVE->value,
|
||||
'total' => 15_000,
|
||||
'status' => 1,
|
||||
]);
|
||||
|
||||
$orderItem = OrderItem::create([
|
||||
'order_id' => $order->id,
|
||||
'product_id' => $this->product->id,
|
||||
'order_id' => $this->order->id,
|
||||
'product_id' => $product->id,
|
||||
'billing_cycle' => 'monthly',
|
||||
'months' => 1,
|
||||
'quantity' => 1,
|
||||
'price' => $oldPrice,
|
||||
'price' => 15_000,
|
||||
'addons_meta' => [],
|
||||
]);
|
||||
|
||||
return Service::create([
|
||||
$this->service = Service::create([
|
||||
'customer_id' => $this->customer->id,
|
||||
'product_id' => $this->product->id,
|
||||
'product_id' => $product->id,
|
||||
'order_item_id' => $orderItem->id,
|
||||
'type' => 'hosting',
|
||||
'status' => 'active',
|
||||
'server_name' => 'test-server',
|
||||
'started_at' => now()->subMonth(),
|
||||
'expires_at' => now()->addDays(5),
|
||||
]);
|
||||
}
|
||||
|
||||
private function createServiceWithCycle(string $billingCycle, int $cycleMonths, int $oldPrice): Service
|
||||
private function renew(int $months)
|
||||
{
|
||||
$order = Order::create([
|
||||
'order_num' => 'ORD-' . uniqid(),
|
||||
'customer_id' => $this->customer->id,
|
||||
'sub_total' => $oldPrice,
|
||||
'total' => $oldPrice,
|
||||
'status' => OrderStatus::ACTIVE->value,
|
||||
]);
|
||||
|
||||
$orderItem = OrderItem::create([
|
||||
'order_id' => $order->id,
|
||||
'product_id' => $this->product->id,
|
||||
'billing_cycle' => $billingCycle,
|
||||
'months' => $cycleMonths,
|
||||
'quantity' => 1,
|
||||
'price' => $oldPrice,
|
||||
]);
|
||||
|
||||
return Service::create([
|
||||
'customer_id' => $this->customer->id,
|
||||
'product_id' => $this->product->id,
|
||||
'order_item_id' => $orderItem->id,
|
||||
'status' => 'active',
|
||||
'server_name' => 'test-server',
|
||||
]);
|
||||
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_annual_service_for_exactly_one_cycle_charges_the_annual_price(): void
|
||||
public function test_renew_charges_balance_and_extends_expiry_on_the_same_order(): void
|
||||
{
|
||||
$service = $this->createServiceWithCycle('annually', 12, 1_800_000);
|
||||
$expiresAt = $this->service->expires_at->copy();
|
||||
|
||||
ProductPricings::create([
|
||||
'product_id' => $this->product->id,
|
||||
'billing_cycle' => 'annually',
|
||||
'price' => 1_800_000,
|
||||
'is_active' => true,
|
||||
]);
|
||||
$this->renew(3)->assertOk()->assertJsonPath('success', true);
|
||||
|
||||
$response = $this->actingAsCustomer()->postJson(
|
||||
"/v1/client/dich-vu/{$service->id}/renew",
|
||||
['months' => 12],
|
||||
);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('data.total', 1_800_000);
|
||||
$this->assertDatabaseHas('invoices', [
|
||||
'id' => $response->json('data.invoice_id'),
|
||||
'total' => 1_800_000,
|
||||
]);
|
||||
$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(45_000, (int) $invoice->total);
|
||||
$this->assertSame(55_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' => 45_000]);
|
||||
}
|
||||
|
||||
public function test_renew_annual_service_with_months_not_matching_the_cycle_is_rejected(): void
|
||||
public function test_insufficient_balance_keeps_unpaid_invoice_and_does_not_extend(): void
|
||||
{
|
||||
$service = $this->createServiceWithCycle('annually', 12, 1_800_000);
|
||||
$this->customer->update(['total' => 10_000]);
|
||||
$expiresAt = $this->service->expires_at->copy();
|
||||
|
||||
ProductPricings::create([
|
||||
'product_id' => $this->product->id,
|
||||
'billing_cycle' => 'annually',
|
||||
'price' => 1_800_000,
|
||||
'is_active' => true,
|
||||
]);
|
||||
$this->renew(1)->assertStatus(422)->assertJsonPath('data.status', InvoiceStatus::UNPAID->value);
|
||||
|
||||
$response = $this->actingAsCustomer()->postJson(
|
||||
"/v1/client/dich-vu/{$service->id}/renew",
|
||||
['months' => 1],
|
||||
);
|
||||
$this->assertSame(10_000, (int) $this->customer->fresh()->total);
|
||||
$this->assertTrue($this->service->fresh()->expires_at->equalTo($expiresAt));
|
||||
|
||||
$response->assertStatus(422);
|
||||
$this->assertSame(0, Invoice::where('customer_id', $this->customer->id)->count());
|
||||
}
|
||||
$this->customer->update(['total' => 100_000]);
|
||||
$this->renew(2)->assertOk();
|
||||
|
||||
public function test_renew_charges_the_recurring_addon_price_on_top_of_the_service_price(): void
|
||||
{
|
||||
$service = $this->createServiceWithOldPrice(90_000);
|
||||
|
||||
$addon = Addon::create([
|
||||
'type' => $this->product->product_type,
|
||||
'code' => 'ram',
|
||||
'name' => 'RAM',
|
||||
'price' => 50_000,
|
||||
'min' => 1,
|
||||
'max' => 64,
|
||||
'step' => 1,
|
||||
'status' => true,
|
||||
]);
|
||||
|
||||
$service->orderItem->update(['addons_meta' => [[
|
||||
'addon_id' => $addon->id,
|
||||
'code' => 'ram',
|
||||
'name' => 'RAM',
|
||||
'qty' => 2,
|
||||
'price' => 40_000,
|
||||
'subtotal' => 80_000,
|
||||
]]]);
|
||||
|
||||
ServiceAddon::create([
|
||||
'service_id' => $service->id,
|
||||
'addon_type' => 'ram',
|
||||
'addon_value' => '6144',
|
||||
'price' => 50_000,
|
||||
]);
|
||||
|
||||
ProductPricings::create([
|
||||
'product_id' => $this->product->id,
|
||||
'billing_cycle' => 'monthly',
|
||||
'price' => 100_000,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$response = $this->actingAsCustomer()->postJson(
|
||||
"/v1/client/dich-vu/{$service->id}/renew",
|
||||
['months' => 1],
|
||||
);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('data.total', 200_000);
|
||||
$invoiceId = $response->json('data.invoice_id');
|
||||
$this->assertDatabaseHas('invoice_items', [
|
||||
'invoice_id' => $invoiceId,
|
||||
'type' => 'renewal',
|
||||
'unit_price' => 100_000,
|
||||
]);
|
||||
$this->assertDatabaseHas('invoice_items', [
|
||||
'invoice_id' => $invoiceId,
|
||||
'type' => 'addon',
|
||||
'quantity' => 2,
|
||||
'total_price' => 100_000,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_renew_uses_current_catalog_price_not_the_original_order_price(): void
|
||||
{
|
||||
$service = $this->createServiceWithOldPrice(90_000);
|
||||
|
||||
// Giá sản phẩm hiện tại đã tăng lên 100.000 kể từ lúc khách đặt mua.
|
||||
ProductPricings::create([
|
||||
'product_id' => $this->product->id,
|
||||
'billing_cycle' => 'monthly',
|
||||
'price' => 100_000,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$response = $this->actingAsCustomer()->postJson(
|
||||
"/v1/client/dich-vu/{$service->id}/renew",
|
||||
['months' => 2],
|
||||
);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('data.total', 200_000);
|
||||
|
||||
$this->assertDatabaseHas('invoices', [
|
||||
'id' => $response->json('data.invoice_id'),
|
||||
'total' => 200_000,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_renew_fails_when_product_pricing_is_no_longer_active(): void
|
||||
{
|
||||
$service = $this->createServiceWithOldPrice(90_000);
|
||||
|
||||
ProductPricings::create([
|
||||
'product_id' => $this->product->id,
|
||||
'billing_cycle' => 'monthly',
|
||||
'price' => 100_000,
|
||||
'is_active' => false,
|
||||
]);
|
||||
|
||||
$response = $this->actingAsCustomer()->postJson(
|
||||
"/v1/client/dich-vu/{$service->id}/renew",
|
||||
['months' => 1],
|
||||
);
|
||||
|
||||
$response->assertStatus(400);
|
||||
$this->assertSame(0, Invoice::where('customer_id', $this->customer->id)->count());
|
||||
}
|
||||
|
||||
public function test_renew_reuses_original_order_instead_of_creating_a_new_one(): void
|
||||
{
|
||||
$service = $this->createServiceWithOldPrice(90_000);
|
||||
$originalOrderId = $service->orderItem->order_id;
|
||||
|
||||
ProductPricings::create([
|
||||
'product_id' => $this->product->id,
|
||||
'billing_cycle' => 'monthly',
|
||||
'price' => 100_000,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$response = $this->actingAsCustomer()->postJson(
|
||||
"/v1/client/dich-vu/{$service->id}/renew",
|
||||
['months' => 1],
|
||||
);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('data.order_id', $originalOrderId);
|
||||
|
||||
// Order 1:N Invoice — no second Order row is ever created for a renewal.
|
||||
$this->assertSame(1, Order::where('customer_id', $this->customer->id)->count());
|
||||
$this->assertDatabaseHas('invoices', [
|
||||
'id' => $response->json('data.invoice_id'),
|
||||
'order_id' => $originalOrderId,
|
||||
'invoice_type' => 2,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_renewal_invoice_has_a_billing_period_and_is_not_duplicated_for_the_same_period(): void
|
||||
{
|
||||
$service = $this->createServiceWithOldPrice(90_000);
|
||||
|
||||
ProductPricings::create([
|
||||
'product_id' => $this->product->id,
|
||||
'billing_cycle' => 'monthly',
|
||||
'price' => 100_000,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$response = $this->actingAsCustomer()->postJson(
|
||||
"/v1/client/dich-vu/{$service->id}/renew",
|
||||
['months' => 1],
|
||||
);
|
||||
$response->assertOk();
|
||||
|
||||
$invoiceId = $response->json('data.invoice_id');
|
||||
$this->assertDatabaseHas('invoices', ['id' => $invoiceId]);
|
||||
$invoice = Invoice::find($invoiceId);
|
||||
$this->assertNotNull($invoice->billing_period_start);
|
||||
$this->assertNotNull($invoice->billing_period_end);
|
||||
|
||||
// Calling renew again while the invoice is still open must return the SAME
|
||||
// invoice, not create a duplicate for the same billing period.
|
||||
$second = $this->actingAsCustomer()->postJson(
|
||||
"/v1/client/dich-vu/{$service->id}/renew",
|
||||
['months' => 1],
|
||||
);
|
||||
$second->assertOk();
|
||||
$second->assertJsonPath('data.invoice_id', $invoiceId);
|
||||
$this->assertSame(1, Invoice::where('customer_id', $this->customer->id)->where('invoice_type', 2)->count());
|
||||
}
|
||||
|
||||
/**
|
||||
* Đúng flow đề bài mục 6: invoice gia hạn quá due_date -> OVERDUE, KHÔNG tạo invoice mới.
|
||||
* Verify khách bấm "Gia hạn" lại LÚC invoice đang OVERDUE (chưa qua billing cycle tiếp theo)
|
||||
* vẫn nhận về đúng invoice cũ, không bị tạo trùng.
|
||||
*/
|
||||
public function test_renew_while_existing_invoice_is_overdue_returns_the_same_invoice(): void
|
||||
{
|
||||
$service = $this->createServiceWithOldPrice(90_000);
|
||||
|
||||
ProductPricings::create([
|
||||
'product_id' => $this->product->id,
|
||||
'billing_cycle' => 'monthly',
|
||||
'price' => 100_000,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$first = $this->actingAsCustomer()->postJson(
|
||||
"/v1/client/dich-vu/{$service->id}/renew",
|
||||
['months' => 1],
|
||||
);
|
||||
$first->assertOk();
|
||||
$invoiceId = $first->json('data.invoice_id');
|
||||
|
||||
// Invoice quá due_date -> cron invoices:check-overdue chuyển sang OVERDUE (flow thật).
|
||||
Invoice::whereKey($invoiceId)->update(['due_date' => now()->subDay()->toDateString()]);
|
||||
$this->artisan('invoices:check-overdue')->assertExitCode(0);
|
||||
$this->assertSame(InvoiceStatus::OVERDUE->value, (int) Invoice::find($invoiceId)->status);
|
||||
|
||||
// Khách bấm "Gia hạn" lại trong khi invoice đang OVERDUE — không được tạo invoice #2.
|
||||
$second = $this->actingAsCustomer()->postJson(
|
||||
"/v1/client/dich-vu/{$service->id}/renew",
|
||||
['months' => 1],
|
||||
);
|
||||
$second->assertOk();
|
||||
$second->assertJsonPath('data.invoice_id', $invoiceId);
|
||||
$this->assertSame(1, Invoice::where('customer_id', $this->customer->id)->where('invoice_type', 2)->count());
|
||||
$invoice = Invoice::where('invoice_type', 2)->sole();
|
||||
$this->assertSame(InvoiceStatus::PAID->value, (int) $invoice->status);
|
||||
$this->assertSame(30_000, (int) $invoice->total);
|
||||
$this->assertSame(1, $invoice->invoiceItems()->count());
|
||||
$this->assertSame(70_000, (int) $this->customer->fresh()->total);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Api\Transaction;
|
||||
|
||||
use App\Enums\TransactionStatus;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Transaction;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ManualDepositDraftExpiryTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private function actingAsCustomer(Customer $customer): static
|
||||
{
|
||||
return $this->actingAs($customer, 'client')
|
||||
->withHeaders(['x-access-token' => $this->buildClientAccessToken()]);
|
||||
}
|
||||
|
||||
private function prepare(Customer $customer, int $amount = 500000): array
|
||||
{
|
||||
return $this->actingAsCustomer($customer)
|
||||
->postJson('/v1/transaction/manual-deposit/prepare', ['amount' => $amount])
|
||||
->assertOk()
|
||||
->json('data');
|
||||
}
|
||||
|
||||
public function test_prepare_returns_expiry_and_show_restores_draft(): void
|
||||
{
|
||||
$customer = Customer::factory()->create(['total_pending' => 0]);
|
||||
|
||||
$draft = $this->prepare($customer);
|
||||
|
||||
$this->assertSame(500000, $draft['amount']);
|
||||
$this->assertEqualsWithDelta(now()->addMinutes(30)->timestamp, strtotime($draft['expires_at']), 5);
|
||||
|
||||
$this->actingAsCustomer($customer)
|
||||
->getJson("/v1/transaction/manual-deposit/{$draft['uuid']}")
|
||||
->assertOk()
|
||||
->assertJsonPath('data.public_code', $draft['public_code'])
|
||||
->assertJsonPath('data.amount', 500000);
|
||||
}
|
||||
|
||||
public function test_show_hides_draft_of_another_customer(): void
|
||||
{
|
||||
$owner = Customer::factory()->create();
|
||||
$other = Customer::factory()->create();
|
||||
|
||||
$draft = $this->prepare($owner);
|
||||
|
||||
$this->actingAsCustomer($other)
|
||||
->getJson("/v1/transaction/manual-deposit/{$draft['uuid']}")
|
||||
->assertNotFound();
|
||||
}
|
||||
|
||||
public function test_expired_draft_is_discarded_on_show_and_rejected_on_confirm(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
$customer = Customer::factory()->create(['total_pending' => 0]);
|
||||
|
||||
$first = $this->prepare($customer);
|
||||
$this->travel(31)->minutes();
|
||||
|
||||
$this->actingAsCustomer($customer)
|
||||
->getJson("/v1/transaction/manual-deposit/{$first['uuid']}")
|
||||
->assertStatus(410);
|
||||
$this->assertNull(Transaction::withTrashed()->where('uuid', $first['uuid'])->first());
|
||||
$this->assertSame(0, (int) $customer->fresh()->total_pending);
|
||||
|
||||
$second = $this->prepare($customer);
|
||||
$this->travel(31)->minutes();
|
||||
$path = UploadedFile::fake()->image('proof.jpg')->store('deposit-proofs/' . $customer->id, 'local');
|
||||
|
||||
$this->actingAsCustomer($customer)
|
||||
->postJson('/v1/transaction/manual-deposit/confirm', ['uuid' => $second['uuid'], 'proof_image' => $path])
|
||||
->assertStatus(422);
|
||||
$this->assertNull(Transaction::where('uuid', $second['uuid'])->value('proof_image'));
|
||||
}
|
||||
|
||||
public function test_purge_command_removes_only_expired_drafts(): void
|
||||
{
|
||||
$customer = Customer::factory()->create(['total_pending' => 0]);
|
||||
|
||||
$expired = $this->prepare($customer, 300000);
|
||||
$this->travel(31)->minutes();
|
||||
$other = Customer::factory()->create(['total_pending' => 0]);
|
||||
$fresh = $this->prepare($other, 200000);
|
||||
|
||||
$submitted = Transaction::create([
|
||||
'amount' => 100000,
|
||||
'customer_id' => $customer->id,
|
||||
'type' => 'manual',
|
||||
'status' => TransactionStatus::PENDING->value,
|
||||
'proof_image' => 'deposit-proofs/x.jpg',
|
||||
'trans' => 'MANTEST',
|
||||
]);
|
||||
|
||||
$this->artisan('deposits:purge-expired-drafts')->assertSuccessful();
|
||||
|
||||
$this->assertNull(Transaction::withTrashed()->where('uuid', $expired['uuid'])->first());
|
||||
$this->assertNotNull(Transaction::where('uuid', $fresh['uuid'])->first());
|
||||
$this->assertNotNull($submitted->fresh());
|
||||
$this->assertSame(0, (int) $customer->fresh()->total_pending);
|
||||
$this->assertSame(0, (int) $other->fresh()->total_pending);
|
||||
}
|
||||
|
||||
public function test_total_pending_only_counts_amount_after_proof_is_submitted(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
$customer = Customer::factory()->create(['total_pending' => 0]);
|
||||
|
||||
$draft = $this->prepare($customer, 700000);
|
||||
$this->assertSame(0, (int) $customer->fresh()->total_pending);
|
||||
|
||||
$path = $this->actingAsCustomer($customer)
|
||||
->post('/v1/transaction/manual-deposit/upload-proof', ['file' => UploadedFile::fake()->image('proof.jpg')])
|
||||
->assertOk()
|
||||
->json('path');
|
||||
$this->assertSame(0, (int) $customer->fresh()->total_pending);
|
||||
|
||||
$this->actingAsCustomer($customer)
|
||||
->postJson('/v1/transaction/manual-deposit/confirm', ['uuid' => $draft['uuid'], 'proof_image' => $path])
|
||||
->assertOk();
|
||||
$this->assertSame(700000, (int) $customer->fresh()->total_pending);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Payment;
|
||||
|
||||
use App\Enums\TransactionStatus;
|
||||
use App\Http\Controllers\Api\Admin\Transaction\AdminTransactionController;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Transaction;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AdminManualDepositActionsTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private function deposit(Customer $customer, int $amount, ?string $proof, TransactionStatus $status = TransactionStatus::PENDING): Transaction
|
||||
{
|
||||
return Transaction::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'trans' => 'MANTEST' . Str::random(6),
|
||||
'amount' => $amount,
|
||||
'customer_id' => $customer->id,
|
||||
'type' => 'manual',
|
||||
'status' => $status->value,
|
||||
'proof_image' => $proof,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_draft_without_proof_can_be_rejected_and_approved_without_touching_pending(): void
|
||||
{
|
||||
Mail::fake();
|
||||
$customer = Customer::factory()->create(['total' => 0, 'total_pending' => 300_000]);
|
||||
$controller = app(AdminTransactionController::class);
|
||||
|
||||
$rejected = $this->deposit($customer, 5_000_000, null);
|
||||
$this->assertSame(200, $controller->reject($rejected->uuid)->getStatusCode());
|
||||
$this->assertSame(TransactionStatus::FAILED->value, (int) $rejected->fresh()->status);
|
||||
|
||||
$approved = $this->deposit($customer, 1_000_000, null);
|
||||
$this->assertSame(200, $controller->approve(new Request(), $approved->uuid)->getStatusCode());
|
||||
|
||||
$customer->refresh();
|
||||
$this->assertSame(300_000, (int) $customer->total_pending);
|
||||
$this->assertSame(1_000_000, (int) $customer->total);
|
||||
}
|
||||
|
||||
public function test_signed_proof_image_url_serves_the_file(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
$customer = Customer::factory()->create();
|
||||
$path = UploadedFile::fake()->image('proof.png')->store('deposit-proofs/' . $customer->id, 'local');
|
||||
$transaction = $this->deposit($customer, 300_000, $path);
|
||||
|
||||
$this->get($transaction->proof_image_urls[0])->assertOk();
|
||||
$this->get(strtok($transaction->proof_image_urls[0], '?'))->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_deleting_submitted_pending_deposit_releases_pending_amount(): void
|
||||
{
|
||||
$customer = Customer::factory()->create(['total' => 0, 'total_pending' => 300_000]);
|
||||
$transaction = $this->deposit($customer, 300_000, 'deposit-proofs/a.jpg');
|
||||
|
||||
$response = app(AdminTransactionController::class)->destroy($transaction->uuid);
|
||||
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
$this->assertSoftDeleted($transaction);
|
||||
$this->assertSame(0, (int) $customer->fresh()->total_pending);
|
||||
}
|
||||
|
||||
public function test_deleting_draft_or_completed_deposit_keeps_balances(): void
|
||||
{
|
||||
$customer = Customer::factory()->create(['total' => 2_000_000, 'total_pending' => 300_000]);
|
||||
$controller = app(AdminTransactionController::class);
|
||||
|
||||
$draft = $this->deposit($customer, 500_000, null);
|
||||
$completed = $this->deposit($customer, 2_000_000, 'deposit-proofs/b.jpg', TransactionStatus::COMPLETED);
|
||||
|
||||
$this->assertSame(200, $controller->destroy($draft->uuid)->getStatusCode());
|
||||
$this->assertSame(200, $controller->destroy($completed->uuid)->getStatusCode());
|
||||
$this->assertSame(404, $controller->destroy($completed->uuid)->getStatusCode());
|
||||
|
||||
$customer->refresh();
|
||||
$this->assertSame(300_000, (int) $customer->total_pending);
|
||||
$this->assertSame(2_000_000, (int) $customer->total);
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ class AdminTransactionApproveTest extends TestCase
|
||||
'customer_id' => $customer->id,
|
||||
'type' => 'manual',
|
||||
'status' => TransactionStatus::PENDING->value,
|
||||
'proof_image' => 'deposit-proofs/test.jpg',
|
||||
]);
|
||||
|
||||
$controller = app(AdminTransactionController::class);
|
||||
@@ -61,6 +62,7 @@ class AdminTransactionApproveTest extends TestCase
|
||||
'customer_id' => $customer->id,
|
||||
'type' => 'manual',
|
||||
'status' => TransactionStatus::PENDING->value,
|
||||
'proof_image' => 'deposit-proofs/test.jpg',
|
||||
]);
|
||||
|
||||
$request = Request::create('/', 'POST', ['received_amount' => 4_000_000]);
|
||||
@@ -92,6 +94,7 @@ class AdminTransactionApproveTest extends TestCase
|
||||
'customer_id' => $customer->id,
|
||||
'type' => 'manual',
|
||||
'status' => TransactionStatus::PENDING->value,
|
||||
'proof_image' => 'deposit-proofs/test.jpg',
|
||||
]);
|
||||
|
||||
app(AdminTransactionController::class)->approve(new Request(), $transaction->uuid);
|
||||
|
||||
Reference in New Issue
Block a user