feat(admin): improve payments and server settings

This commit is contained in:
baodev0
2026-10-19 07:59:23 +07:00
parent 9ed77a85fd
commit ea9a53f742
32 changed files with 680 additions and 78 deletions
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Api\Admin\DnsManager;
use App\Models\DnsServer;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Spatie\Permission\Models\Role;
use Tests\TestCase;
class DnsServerDomainTest extends TestCase
{
use RefreshDatabase;
private function admin(): User
{
Role::firstOrCreate(['name' => 'Full Administrator', 'guard_name' => 'admin']);
$admin = User::factory()->create();
$admin->assignRole('Full Administrator');
return $admin;
}
public function test_public_host_prefers_domain_over_ip(): void
{
$server = new DnsServer(['ip_address' => '103.42.58.99', 'domain' => 'host99.tgs.com.vn']);
$this->assertSame('host99.tgs.com.vn', $server->publicHost());
$server->domain = null;
$this->assertSame('103.42.58.99', $server->publicHost());
}
public function test_store_saves_domain_and_rejects_invalid_hostname(): void
{
$admin = $this->admin();
$payload = ['name' => 'Host 99', 'ip_address' => '103.42.58.99', 'username' => 'root', 'password' => 'secret'];
$this->actingAs($admin, 'admin')
->postJson('/api/v1/administrator/dns-servers', $payload + ['domain' => 'not a domain'])
->assertStatus(422);
$this->actingAs($admin, 'admin')
->postJson('/api/v1/administrator/dns-servers', $payload + ['domain' => 'host99.tgs.com.vn'])
->assertStatus(201)
->assertJsonPath('data.domain', 'host99.tgs.com.vn');
$this->assertDatabaseHas('dns_manager_servers', ['ip_address' => '103.42.58.99', 'domain' => 'host99.tgs.com.vn']);
}
}
@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Api\Service;
use App\Enums\Invoice\InvoiceStatus;
use App\Enums\Order\OrderStatus;
use App\Jobs\Mail\Admin\AdminPaymentReceivedJob;
use App\Models\Customer;
use App\Models\Invoice;
use App\Models\Order;
use App\Services\Api\Order\OrderService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Bus;
use Tests\TestCase;
class OrderPaymentAdminNotificationTest extends TestCase
{
use RefreshDatabase;
private function orderFor(Customer $customer, int $invoiceStatus): Order
{
$order = Order::create([
'order_num' => 'ORD-' . uniqid(),
'customer_id' => $customer->id,
'sub_total' => 100_000,
'discount_amount' => 0,
'total' => 100_000,
'status' => OrderStatus::PENDING->value,
]);
Invoice::create([
'invoice_number' => 'INV-' . uniqid(),
'customer_id' => $customer->id,
'order_id' => $order->id,
'invoice_type' => 1,
'sub_total' => 100_000,
'tax' => 0,
'total' => 100_000,
'status' => $invoiceStatus,
]);
return $order->fresh(['invoice', 'customer']);
}
public function test_successful_payment_notifies_admin_once(): void
{
Bus::fake([AdminPaymentReceivedJob::class]);
$customer = Customer::factory()->create(['total' => 200_000]);
$order = $this->orderFor($customer, InvoiceStatus::UNPAID->value);
app(OrderService::class)->pay($order, 'balance');
Bus::assertDispatchedTimes(AdminPaymentReceivedJob::class, 1);
}
public function test_insufficient_balance_does_not_notify_admin(): void
{
Bus::fake([AdminPaymentReceivedJob::class]);
$customer = Customer::factory()->create(['total' => 0]);
$order = $this->orderFor($customer, InvoiceStatus::UNPAID->value);
try {
app(OrderService::class)->pay($order, 'balance');
$this->fail('Expected InvalidArgumentException');
} catch (\InvalidArgumentException) {
}
Bus::assertNotDispatched(AdminPaymentReceivedJob::class);
}
public function test_paying_an_already_paid_invoice_is_rejected_without_notifying_admin(): void
{
Bus::fake([AdminPaymentReceivedJob::class]);
$customer = Customer::factory()->create(['total' => 200_000]);
$order = $this->orderFor($customer, InvoiceStatus::PAID->value);
try {
app(OrderService::class)->pay($order, 'balance');
$this->fail('Expected InvalidArgumentException');
} catch (\InvalidArgumentException) {
}
Bus::assertNotDispatched(AdminPaymentReceivedJob::class);
$this->assertSame(200_000, (int) $customer->fresh()->total);
}
}
@@ -5,12 +5,17 @@ declare(strict_types=1);
namespace Tests\Feature\Api\Transaction;
use App\Enums\TransactionStatus;
use App\Events\AdminNotificationCreated;
use App\Models\Customer;
use App\Models\Transaction;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Spatie\Permission\Models\Role;
use Tests\TestCase;
class ManualDepositProofTest extends TestCase
@@ -108,8 +113,13 @@ class ManualDepositProofTest extends TestCase
public function test_upload_then_confirm_binds_proof_to_owner(): void
{
Storage::fake('local');
Mail::fake();
Event::fake([AdminNotificationCreated::class]);
$customer = Customer::factory()->create();
Role::firstOrCreate(['name' => 'Full Administrator', 'guard_name' => 'admin']);
$admin = User::factory()->create();
$admin->assignRole('Full Administrator');
$transaction = Transaction::create([
'uuid' => Str::uuid()->toString(),
@@ -138,5 +148,15 @@ class ManualDepositProofTest extends TestCase
$confirm->assertStatus(200)->assertJsonPath('error', false);
$this->assertSame($path, $transaction->fresh()->proof_image);
$this->assertDatabaseHas('notifications', [
'recipient_type' => 'admin',
'recipient_id' => $admin->id,
'type' => 'manual_deposit_pending',
'link' => '/transactions',
]);
Event::assertDispatched(
AdminNotificationCreated::class,
fn (AdminNotificationCreated $event) => $event->type === 'manual_deposit_pending',
);
}
}
@@ -7,8 +7,11 @@ namespace Tests\Feature\Mail;
use App\Http\Facade\SignRSA;
use App\Models\Setting;
use App\Support\MailSettings;
use App\Listeners\Mail\ApplySystemRecipients;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Mail\Events\MessageSending;
use Illuminate\Support\Facades\Cache;
use Symfony\Component\Mime\Email;
use Tests\TestCase;
class MailSettingsQueueTest extends TestCase
@@ -45,4 +48,21 @@ class MailSettingsQueueTest extends TestCase
$this->saveSmtp('smtp.settings-two.test');
$this->assertSame('smtp.settings-two.test', $this->hostSeenByQueuedJob());
}
public function test_system_cc_and_bcc_are_applied_without_duplicate_recipients(): void
{
$this->saveSmtp('smtp.settings.test');
$smtp = Setting::where('key', 'smtp')->firstOrFail();
$values = json_decode((string) $smtp->value, true);
$values['cc'] = ['audit@example.com', 'customer@example.com'];
$values['bcc'] = ['archive@example.com', 'audit@example.com'];
$smtp->update(['value' => json_encode($values)]);
Cache::forget(MailSettings::cacheKey());
$message = (new Email())->to('customer@example.com')->subject('Test');
app(ApplySystemRecipients::class)->handle(new MessageSending($message, []));
$this->assertSame(['audit@example.com'], array_map(fn ($address) => $address->getAddress(), $message->getCc()));
$this->assertSame(['archive@example.com'], array_map(fn ($address) => $address->getAddress(), $message->getBcc()));
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Tests\Unit;
use App\Enums\Product\ProductBillingCycle;
use PHPUnit\Framework\TestCase;
class ProductBillingCycleLabelTest extends TestCase
{
public function test_code_is_translated_to_vietnamese_label(): void
{
$this->assertSame('Hàng tháng', ProductBillingCycle::vietnameseLabelFor('monthly'));
$this->assertSame('6 tháng', ProductBillingCycle::vietnameseLabelFor('semi_annually'));
$this->assertSame('Hàng năm', ProductBillingCycle::vietnameseLabelFor('annually'));
}
public function test_unknown_or_missing_code_falls_back_safely(): void
{
$this->assertSame('custom', ProductBillingCycle::vietnameseLabelFor('custom'));
$this->assertSame('N/A', ProductBillingCycle::vietnameseLabelFor(null));
}
public function test_months_are_translated_with_fallback(): void
{
$this->assertSame('Hàng tháng', ProductBillingCycle::vietnameseLabelForMonths(1));
$this->assertSame('2 năm', ProductBillingCycle::vietnameseLabelForMonths(24));
$this->assertSame('9 tháng', ProductBillingCycle::vietnameseLabelForMonths(9));
$this->assertSame('0 tháng', ProductBillingCycle::vietnameseLabelForMonths(0));
}
}