update email template

This commit is contained in:
baodev0
2026-09-09 16:56:22 +07:00
parent d3d9f76ba0
commit 2365ef72e9
210 changed files with 6335 additions and 4688 deletions
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Tests\Feature\Api\Service;
use App\Enums\Invoice\InvoiceStatus;
use App\Enums\Order\OrderStatus;
use App\Models\Customer;
use App\Models\Invoice;
@@ -123,4 +124,105 @@ class RenewServiceTest extends TestCase
$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());
}
}
+130
View File
@@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Console;
use App\Enums\Invoice\InvoiceStatus;
use App\Enums\Order\OrderStatus;
use App\Models\Customer;
use App\Models\Invoice;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Product;
use App\Models\Service;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ServiceExpiryTest extends TestCase
{
use RefreshDatabase;
private function createExpiringService(int $price): Service
{
$customer = Customer::factory()->create();
$product = Product::factory()->create(['product_type' => 'hosting', 'is_active' => true]);
$order = Order::create([
'order_num' => 'ORD-' . uniqid(),
'customer_id' => $customer->id,
'sub_total' => $price,
'total' => $price,
'status' => OrderStatus::ACTIVE->value,
]);
$orderItem = OrderItem::create([
'order_id' => $order->id,
'product_id' => $product->id,
'billing_cycle' => 'monthly',
'months' => 1,
'quantity' => 1,
'price' => $price,
]);
return Service::create([
'customer_id' => $customer->id,
'product_id' => $product->id,
'order_item_id' => $orderItem->id,
'status' => 'active',
'server_name' => 'test-server',
'expires_at' => now()->addDays(3),
]);
}
public function test_expiry_command_creates_renewal_invoice_on_the_original_order(): void
{
$service = $this->createExpiringService(90_000);
$originalOrderId = $service->orderItem->order_id;
$this->artisan('service:expiry')->assertExitCode(0);
$service->refresh();
$this->assertNotNull($service->invoice_id);
$invoice = Invoice::find($service->invoice_id);
$this->assertSame($originalOrderId, $invoice->order_id);
$this->assertSame(2, $invoice->invoice_type);
$this->assertSame(InvoiceStatus::UNPAID->value, $invoice->status);
$this->assertNotNull($invoice->billing_period_start);
$this->assertNotNull($invoice->billing_period_end);
// Order 1:N Invoice — no second Order row created for the renewal.
$this->assertSame(1, Order::query()->count());
}
public function test_running_expiry_command_twice_does_not_duplicate_the_renewal_invoice(): void
{
$service = $this->createExpiringService(90_000);
$this->artisan('service:expiry')->assertExitCode(0);
$firstInvoiceId = $service->refresh()->invoice_id;
$this->artisan('service:expiry')->assertExitCode(0);
$secondInvoiceId = $service->refresh()->invoice_id;
$this->assertSame($firstInvoiceId, $secondInvoiceId);
$this->assertSame(1, Invoice::where('invoice_type', 2)->count());
}
public function test_cancelled_service_does_not_get_a_renewal_invoice(): void
{
$service = $this->createExpiringService(90_000);
$service->update(['status' => 'cancelled', 'expires_at' => now()->subDay()]);
$this->artisan('service:expiry')->assertExitCode(0);
$this->assertNull($service->refresh()->invoice_id);
$this->assertSame(0, Invoice::where('invoice_type', 2)->count());
}
/**
* Đúng flow đề bài mục 6: Invoice gia hạn quá due_date -> chuyển OVERDUE (không tạo Invoice mới).
* Test này verify bước tiếp theo: cron service:expiry chạy lại SAU KHI invoice đã OVERDUE
* (không còn UNPAID nữa) vẫn phải nhận ra invoice đang mở và KHÔNG tạo invoice #2 trùng kỳ.
*/
public function test_expiry_command_does_not_duplicate_invoice_after_it_became_overdue(): void
{
$service = $this->createExpiringService(90_000);
// Lần chạy 1: sinh invoice gia hạn (UNPAID).
$this->artisan('service:expiry')->assertExitCode(0);
$invoiceId = $service->refresh()->invoice_id;
$this->assertNotNull($invoiceId);
// Giả lập thời gian trôi qua khỏi due_date rồi cron invoices:check-overdue chạy thật,
// chuyển invoice sang OVERDUE — đúng như flow thật, không set status tay.
Invoice::whereKey($invoiceId)->update(['due_date' => now()->subDay()->toDateString()]);
$this->artisan('invoices:check-overdue')->assertExitCode(0);
$invoice = Invoice::find($invoiceId);
$this->assertSame(InvoiceStatus::OVERDUE->value, (int) $invoice->status);
// Lần chạy 2: service:expiry chạy lại (vd. mỗi 5 phút) trong khi invoice đang OVERDUE.
$this->artisan('service:expiry')->assertExitCode(0);
$this->assertSame($invoiceId, $service->refresh()->invoice_id);
$this->assertSame(1, Invoice::where('invoice_type', 2)->count());
// Vẫn đúng là OVERDUE — service:expiry không được đụng vào status của invoice.
$this->assertSame(InvoiceStatus::OVERDUE->value, (int) $invoice->fresh()->status);
}
}
+129
View File
@@ -0,0 +1,129 @@
<?php
namespace Tests\Feature;
use App\Exceptions\InvalidTemplateVariableException;
use App\Jobs\Mail\SendTemplateEmailJob;
use App\Mail\GenericTemplateMail;
use App\Models\Template;
use App\Services\Email\EmailTemplateService;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;
class EmailTemplateServiceTest extends TestCase
{
use DatabaseTransactions;
private function makeTemplate(array $overrides = []): Template
{
return Template::create(array_merge([
'type_id' => 0,
'scenario' => 'unit_test_scenario_' . uniqid(),
'format' => 'html',
'name' => 'Test scenario',
'subject' => 'Xin chào {{full_name}}',
'message' => '<p>Chào {{full_name}}, mời truy cập {{login_url}}</p>',
'metadata' => [
'variables' => [
['key' => 'full_name', 'label' => 'Họ tên', 'type' => 'string', 'example' => 'Quý khách'],
['key' => 'login_url', 'label' => 'Link đăng nhập', 'type' => 'url', 'example' => 'https://example.com'],
],
],
'status' => 1,
], $overrides));
}
public function test_send_dispatches_job_to_queue(): void
{
Bus::fake();
$template = $this->makeTemplate();
app(EmailTemplateService::class)->send($template->scenario, 'user@example.com', [
'full_name' => 'Nguyễn Văn A',
'login_url' => 'https://portal.tgs.com.vn/login',
]);
Bus::assertDispatched(SendTemplateEmailJob::class, fn (SendTemplateEmailJob $job) => $job->scenario === $template->scenario
&& $job->to === ['user@example.com']
&& $job->data['full_name'] === 'Nguyễn Văn A');
}
public function test_job_renders_and_sends_mail_with_template_sender_config(): void
{
Mail::fake();
$template = $this->makeTemplate([
'metadata' => [
'variables' => [
['key' => 'full_name', 'label' => 'Họ tên', 'type' => 'string'],
['key' => 'login_url', 'label' => 'Link', 'type' => 'url'],
],
'mail' => [
'from_email' => 'noreply@tgs.com.vn',
'from_name' => 'Thế Giới Số',
'reply_to' => 'support@tgs.com.vn',
'reply_to_name' => 'TGS Support',
],
],
]);
(new SendTemplateEmailJob($template->scenario, ['user@example.com'], [
'full_name' => 'Nguyễn Văn A',
'login_url' => 'https://portal.tgs.com.vn/login',
]))->handle(app(EmailTemplateService::class), app(\App\Services\Email\EmailTemplateRenderer::class));
Mail::assertSent(GenericTemplateMail::class, function (GenericTemplateMail $mail) {
$mail->assertTo('user@example.com');
$envelope = $mail->envelope();
return $envelope->subject === 'Xin chào Nguyễn Văn A'
&& $envelope->from->address === 'noreply@tgs.com.vn'
&& $envelope->replyTo[0]->address === 'support@tgs.com.vn'
&& str_contains($mail->renderedHtml, 'Nguyễn Văn A');
});
}
public function test_job_falls_back_to_system_default_sender_when_template_has_no_mail_config(): void
{
Mail::fake();
$template = $this->makeTemplate();
(new SendTemplateEmailJob($template->scenario, ['user@example.com'], [
'full_name' => 'A',
'login_url' => 'https://example.com',
]))->handle(app(EmailTemplateService::class), app(\App\Services\Email\EmailTemplateRenderer::class));
Mail::assertSent(GenericTemplateMail::class, function (GenericTemplateMail $mail) {
return $mail->envelope()->from === null;
});
}
public function test_preview_throws_for_undeclared_variable(): void
{
$template = $this->makeTemplate([
'message' => '<p>{{full_name}} {{not_declared}}</p>',
]);
$this->expectException(InvalidTemplateVariableException::class);
app(EmailTemplateService::class)->preview($template->scenario, ['full_name' => 'A']);
}
public function test_load_template_is_cached_by_scenario(): void
{
$template = $this->makeTemplate();
$service = app(EmailTemplateService::class);
$first = $service->loadTemplate($template->scenario);
$this->assertSame('Xin chào {{full_name}}', $first->subject);
$template->update(['subject' => 'Đã đổi']); // model event must bust the cache
$second = $service->loadTemplate($template->scenario);
$this->assertSame('Đã đổi', $second->subject);
}
}
@@ -0,0 +1,45 @@
<?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\Support\Facades\Mail;
use Tests\TestCase;
class AdminTransactionApproveTest extends TestCase
{
use RefreshDatabase;
public function test_approving_the_same_manual_deposit_twice_credits_the_customer_only_once(): void
{
Mail::fake();
$customer = Customer::factory()->create(['total' => 0, 'credit' => 0, 'total_pending' => 100_000]);
$transaction = Transaction::create([
'uuid' => (string) \Illuminate\Support\Str::uuid(),
'public_code' => 'DEP-TEST-0001',
'trans' => 'DEP-TEST-0001',
'amount' => 100_000,
'customer_id' => $customer->id,
'type' => 'manual',
'status' => TransactionStatus::PENDING->value,
]);
$controller = app(AdminTransactionController::class);
$first = $controller->approve($transaction->uuid);
$second = $controller->approve($transaction->uuid);
$this->assertSame(200, $first->getStatusCode());
// Second call finds the transaction no longer PENDING → treated as not-approvable.
$this->assertSame(404, $second->getStatusCode());
$this->assertSame(100_000, (int) $customer->fresh()->total);
}
}
@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\Payment;
use App\Enums\Invoice\InvoiceStatus;
use App\Enums\Order\OrderStatus;
use App\Models\Customer;
use App\Models\Invoice;
use App\Models\Order;
use App\Models\Transaction;
use App\Services\InvoicePaymentService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class InvoicePaymentServiceTest extends TestCase
{
use RefreshDatabase;
public function test_paying_an_already_paid_invoice_does_not_double_deduct_balance(): void
{
$customer = Customer::factory()->create(['total' => 200_000]);
$order = Order::create([
'order_num' => 'ORD-' . uniqid(),
'customer_id' => $customer->id,
'sub_total' => 100_000,
'total' => 100_000,
'status' => OrderStatus::PENDING->value,
]);
$invoice = 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::UNPAID->value,
]);
$service = app(InvoicePaymentService::class);
$service->payWithBalance($invoice, $customer, 'balance');
// Simulate a retried request (double-click / network retry) for the same invoice.
$service->payWithBalance($invoice->fresh(), $customer->fresh(), 'balance');
$this->assertSame(100_000, (int) $customer->fresh()->total);
$this->assertSame(1, Transaction::where('customer_id', $customer->id)->where('type', 'invoice')->count());
}
public function test_a_cancelled_invoice_cannot_be_paid_and_is_never_counted_as_paid(): void
{
$customer = Customer::factory()->create(['total' => 200_000]);
$order = Order::create([
'order_num' => 'ORD-' . uniqid(),
'customer_id' => $customer->id,
'sub_total' => 100_000,
'total' => 100_000,
'status' => OrderStatus::CANCELLED->value,
]);
$invoice = 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::CANCELLED->value,
]);
$service = app(InvoicePaymentService::class);
$this->expectException(\InvalidArgumentException::class);
try {
$service->payWithBalance($invoice, $customer, 'balance');
} finally {
$this->assertSame(200_000, (int) $customer->fresh()->total);
$this->assertSame(InvoiceStatus::CANCELLED->value, (int) $invoice->fresh()->status);
$this->assertNull($invoice->fresh()->paid_at);
}
}
}
+114
View File
@@ -0,0 +1,114 @@
<?php
namespace Tests\Unit;
use App\Exceptions\InvalidTemplateVariableException;
use App\Models\Template;
use App\Services\Email\EmailTemplateRenderer;
use Tests\TestCase;
class EmailTemplateRendererTest extends TestCase
{
private function makeTemplate(string $subject, string $message, array $variables): Template
{
$template = new Template([
'scenario' => 'unit_test_scenario',
'subject' => $subject,
'message' => $message,
'metadata' => ['variables' => $variables],
]);
return $template;
}
public function test_it_replaces_known_placeholders_in_subject_and_html(): void
{
$template = $this->makeTemplate(
'Xin chào {{full_name}}',
'<p>Chào mừng {{full_name}} đến với {{app_name}}</p>',
[
['key' => 'full_name', 'label' => 'Họ tên', 'type' => 'string'],
['key' => 'app_name', 'label' => 'Tên app', 'type' => 'string'],
],
);
$result = (new EmailTemplateRenderer())->render($template, [
'full_name' => 'Nguyễn Văn A',
'app_name' => 'Thế Giới Số',
]);
$this->assertSame('Xin chào Nguyễn Văn A', $result['subject']);
$this->assertSame('<p>Chào mừng Nguyễn Văn A đến với Thế Giới Số</p>', $result['html']);
}
public function test_it_falls_back_to_example_value_when_data_missing(): void
{
$template = $this->makeTemplate(
'Mã: {{code}}',
'<p>{{code}}</p>',
[['key' => 'code', 'label' => 'Mã', 'type' => 'string', 'example' => '000000']],
);
$result = (new EmailTemplateRenderer())->render($template, []);
$this->assertSame('Mã: 000000', $result['subject']);
$this->assertSame('<p>000000</p>', $result['html']);
}
public function test_it_html_escapes_string_type_by_default(): void
{
$template = $this->makeTemplate(
'subj',
'<p>{{name}}</p>',
[['key' => 'name', 'label' => 'Tên', 'type' => 'string']],
);
$result = (new EmailTemplateRenderer())->render($template, ['name' => '<script>alert(1)</script>']);
$this->assertStringNotContainsString('<script>', $result['html']);
$this->assertStringContainsString('&lt;script&gt;', $result['html']);
}
public function test_it_rejects_invalid_url_type(): void
{
$template = $this->makeTemplate(
'subj',
'<a href="{{link}}">go</a>',
[['key' => 'link', 'label' => 'Link', 'type' => 'url']],
);
$result = (new EmailTemplateRenderer())->render($template, ['link' => 'javascript:alert(1)']);
$this->assertStringNotContainsString('javascript:', $result['html']);
}
public function test_it_throws_for_undeclared_variable(): void
{
$template = $this->makeTemplate(
'subj',
'<p>{{full_name}} {{secret_admin_flag}}</p>',
[['key' => 'full_name', 'label' => 'Họ tên', 'type' => 'string']],
);
$this->expectException(InvalidTemplateVariableException::class);
$this->expectExceptionMessage('secret_admin_flag');
(new EmailTemplateRenderer())->render($template, ['full_name' => 'A']);
}
public function test_it_never_executes_placeholder_content_as_code(): void
{
$template = $this->makeTemplate(
'subj',
'<p>{{note}}</p>',
[['key' => 'note', 'label' => 'Note', 'type' => 'string']],
);
$result = (new EmailTemplateRenderer())->render($template, [
'note' => '{{ 7*7 }} @php(system("id")) <?php echo 1; ?>',
]);
$this->assertStringNotContainsString('49', $result['html']);
$this->assertStringContainsString('&lt;?php', $result['html']);
}
}
-137
View File
@@ -1,137 +0,0 @@
<?php
declare(strict_types=1);
namespace Tests\Unit;
use App\Enums\Order\OrderStatus;
use App\Jobs\VirtualMachine\ProcessOrderPaymentJob;
use App\Models\Customer;
use App\Models\Invoice;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Product;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ProcessOrderPaymentJobTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function it_can_process_order_payment_with_vm_creation()
{
// Arrange
$customer = Customer::factory()->create(['credit' => 1000000]);
$order = Order::factory()->create([
'customer_id' => $customer->id,
'amount' => 500000,
'status' => OrderStatus::PENDING->value
]);
$invoice = Invoice::factory()->create([
'order_id' => $order->id,
'amount' => 500000
]);
$product = Product::factory()->create(['product_type' => 4]); // VPS product
$orderItem = OrderItem::factory()->create([
'order_id' => $order->id,
'product_id' => $product->id,
'product_type' => 4
]);
// Act
$job = new ProcessOrderPaymentJob($order);
$job->handle(
app(\App\Http\Services\Invoice\InvoiceService::class),
app(\App\Http\Services\Discord::class)
);
// Assert
$this->assertEquals(500000, $customer->fresh()->credit); // Credit should be deducted
$this->assertEquals(OrderStatus::ACTIVE->value, $order->fresh()->status);
}
/** @test */
public function it_handles_insufficient_credit()
{
// Arrange
$customer = Customer::factory()->create(['credit' => 100000]); // Insufficient credit
$order = Order::factory()->create([
'customer_id' => $customer->id,
'amount' => 500000,
'status' => OrderStatus::PENDING->value
]);
$invoice = Invoice::factory()->create([
'order_id' => $order->id,
'amount' => 500000
]);
// Act
$job = new ProcessOrderPaymentJob($order);
$job->handle(
app(\App\Http\Services\Invoice\InvoiceService::class),
app(\App\Http\Services\Discord::class)
);
// Assert
$this->assertEquals(100000, $customer->fresh()->credit); // Credit should not be deducted
$this->assertEquals(OrderStatus::PENDING->value, $order->fresh()->status);
}
/** @test */
public function it_handles_missing_invoice()
{
// Arrange
$customer = Customer::factory()->create(['credit' => 1000000]);
$order = Order::factory()->create([
'customer_id' => $customer->id,
'amount' => 500000,
'status' => OrderStatus::PENDING->value
]);
// No invoice created
// Act
$job = new ProcessOrderPaymentJob($order);
$job->handle(
app(\App\Http\Services\Invoice\InvoiceService::class),
app(\App\Http\Services\Discord::class)
);
// Assert
$this->assertEquals(1000000, $customer->fresh()->credit); // Credit should not be deducted
$this->assertEquals(OrderStatus::PENDING->value, $order->fresh()->status);
}
/** @test */
public function it_handles_non_vps_products()
{
// Arrange
$customer = Customer::factory()->create(['credit' => 1000000]);
$order = Order::factory()->create([
'customer_id' => $customer->id,
'amount' => 500000,
'status' => OrderStatus::PENDING->value
]);
$invoice = Invoice::factory()->create([
'order_id' => $order->id,
'amount' => 500000
]);
$product = Product::factory()->create(['product_type' => 1]); // Non-VPS product
$orderItem = OrderItem::factory()->create([
'order_id' => $order->id,
'product_id' => $product->id,
'product_type' => 1
]);
// Act
$job = new ProcessOrderPaymentJob($order);
$job->handle(
app(\App\Http\Services\Invoice\InvoiceService::class),
app(\App\Http\Services\Discord::class)
);
// Assert
$this->assertEquals(1000000, $customer->fresh()->credit); // Credit should not be deducted for non-VPS
$this->assertEquals(OrderStatus::PENDING->value, $order->fresh()->status);
}
}