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
@@ -36,7 +36,9 @@ class CheckOverdueInvoices extends Command
}
$invoices = Invoice::query()
->with('order:id,status')
// customer_id must be selected too: OrderObserver::updated() reads it when
// logging the status change below, and a NULL there violates log.customer_id NOT NULL.
->with('order:id,status,customer_id')
->whereNotNull('due_date')
->whereDate('due_date', '<', $today)
->whereIn('status', [InvoiceStatus::UNPAID->value, InvoiceStatus::OVERDUE->value])
@@ -0,0 +1,94 @@
<?php
namespace App\Console\Commands\Domain;
use App\Integrations\Domain\Extractors\DnsDomainExtractor;
use App\Integrations\Domain\Extractors\DnsDomainUserExtractor;
use App\Integrations\Domain\Extractors\DnsRecordExtractor;
use App\Integrations\Domain\Extractors\DnsServerExtractor;
use App\Integrations\Domain\Transformers\DnsDomainTransformer;
use App\Integrations\Domain\Transformers\DnsDomainUserTransformer;
use App\Integrations\Domain\Transformers\DnsRecordTransformer;
use App\Integrations\Domain\Transformers\DnsServerTransformer;
use App\Integrations\Domain\Writers\DnsDomainUserWriter;
use App\Integrations\Domain\Writers\DnsDomainWriter;
use App\Integrations\Domain\Writers\DnsRecordWriter;
use App\Integrations\Domain\Writers\DnsServerWriter;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class SyncDomainsCommand extends Command
{
protected $signature = 'domain:sync';
protected $description = 'Extract DNS servers/domains/records/owners from the domain admin (tgs_domain_prod) database and upsert them locally';
public function handle(
DnsServerExtractor $serverExtractor,
DnsServerTransformer $serverTransformer,
DnsServerWriter $serverWriter,
DnsDomainExtractor $domainExtractor,
DnsDomainTransformer $domainTransformer,
DnsDomainWriter $domainWriter,
DnsRecordExtractor $recordExtractor,
DnsRecordTransformer $recordTransformer,
DnsRecordWriter $recordWriter,
DnsDomainUserExtractor $domainUserExtractor,
DnsDomainUserTransformer $domainUserTransformer,
DnsDomainUserWriter $domainUserWriter,
): int {
// Order matters: servers before domains (FK), domains before records/owners (FK).
$this->info('Syncing servers...');
$bar = $this->output->createProgressBar(DB::connection('domain')->table('dns_manager_servers')->count());
$bar->start();
foreach ($serverExtractor->extract() as $row) {
$serverWriter->write($serverTransformer->transform($row));
$bar->advance();
}
$bar->finish();
$this->newLine();
$this->info('Syncing domains...');
$bar = $this->output->createProgressBar(DB::connection('domain')->table('dns_manager_domain')->count());
$bar->start();
$skippedDomains = 0;
foreach ($domainExtractor->extract() as $row) {
if (! $domainWriter->write($domainTransformer->transform($row))) {
$skippedDomains++;
}
$bar->advance();
}
$bar->finish();
$this->newLine();
$this->info('Syncing records...');
$bar = $this->output->createProgressBar(DB::connection('domain')->table('dns_manager_ns')->count());
$bar->start();
$skippedRecords = 0;
foreach ($recordExtractor->extract() as $row) {
if (! $recordWriter->write($recordTransformer->transform($row))) {
$skippedRecords++;
}
$bar->advance();
}
$bar->finish();
$this->newLine();
$this->info('Syncing domain owners...');
$bar = $this->output->createProgressBar(DB::connection('domain')->table('dns_domain_user')->count());
$bar->start();
$skippedOwners = 0;
foreach ($domainUserExtractor->extract() as $row) {
if (! $domainUserWriter->write($domainUserTransformer->transform($row))) {
$skippedOwners++;
}
$bar->advance();
}
$bar->finish();
$this->newLine();
$this->info("Skipped: {$skippedDomains} domain(s), {$skippedRecords} record(s), {$skippedOwners} owner link(s) (unresolved FK).");
return Command::SUCCESS;
}
}
+12 -29
View File
@@ -3,13 +3,10 @@
namespace App\Console\Commands;
use App\Enums\Invoice\InvoiceStatus;
use App\Enums\Order\OrderStatus;
use App\Helpers\OrderHelper;
use App\Jobs\VirtualMachine\Action\VMJobs;
use App\Models\Invoice;
use App\Models\InvoiceItem;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Service;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
@@ -142,38 +139,22 @@ class ServiceExpiry extends Command
}
$price = (int) $orderItem->price;
$months = $orderItem->months ?? 1;
if ($dryRun) {
$this->line(" [dry-run:invoice] Would create renewal order + invoice for Service #{$service->id}, amount " . number_format($price) . ' VND.');
$this->line(" [dry-run:invoice] Would create renewal invoice for Service #{$service->id} on Order #{$orderItem->order_id}, amount " . number_format($price) . ' VND.');
return;
}
$order = Order::create([
'order_num' => OrderHelper::generateOrderNumber(),
'customer_id' => $service->customer_id,
'sub_total' => $price,
'discount_amount' => 0,
'total' => $price,
'status' => OrderStatus::PENDING->value,
'client_ip' => null,
'note' => 'Đơn hàng gia hạn dịch vụ #' . $service->id,
]);
OrderItem::create([
'order_id' => $order->id,
'product_id' => $service->product_id,
'billing_cycle' => $orderItem->billing_cycle,
'months' => $orderItem->months ?? 1,
'os_id' => $orderItem->os_id,
'quantity' => 1,
'server_name' => $service->server_name,
'price' => $price,
]);
// Gia hạn là một billing cycle mới của CÙNG một Order/Service — không tạo Order mới.
// Order gốc chính là order đã tạo ra service này (qua order_item -> order_id).
$periodStart = $service->expires_at && $service->expires_at->isFuture() ? $service->expires_at->copy() : now();
$periodEnd = $periodStart->copy()->addMonths($months);
$invoice = Invoice::create([
'invoice_number' => OrderHelper::generateOrderNumber('INV'),
'customer_id' => $service->customer_id,
'order_id' => $order->id,
'order_id' => $orderItem->order_id,
'invoice_type' => 2, // renewal
'sub_total' => $price,
'discount_amount' => 0,
@@ -183,6 +164,8 @@ class ServiceExpiry extends Command
'due_date' => $service->expires_at && $service->expires_at->isFuture()
? $service->expires_at->toDateString()
: now()->addDays(7)->toDateString(),
'billing_period_start' => $periodStart->toDateString(),
'billing_period_end' => $periodEnd->toDateString(),
]);
InvoiceItem::create([
@@ -199,11 +182,11 @@ class ServiceExpiry extends Command
// Update service to point to the new renewal invoice
$service->update(['invoice_id' => $invoice->id]);
Log::info('Renewal order and invoice created', [
Log::info('Renewal invoice created', [
'service_id' => $service->id,
'order_num' => $order->order_num,
'order_id' => $orderItem->order_id,
'invoice_number' => $invoice->invoice_number,
]);
$this->line(" [invoice] Renewal {$order->order_num} / {$invoice->invoice_number} for Service #{$service->id}.");
$this->line(" [invoice] Renewal invoice {$invoice->invoice_number} on Order #{$orderItem->order_id} for Service #{$service->id}.");
}
}
-54
View File
@@ -1,54 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\OrderItem;
use Carbon\Carbon;
use Illuminate\Console\Command;
class UpdateExpiryDate extends Command
{
protected $signature = 'app:update-expiry-date {--dry-run : Show what would be updated without making changes}';
protected $description = 'Update expiry_date for existing order items based on created_at + months';
public function handle()
{
$dryRun = $this->option('dry-run');
$query = OrderItem::whereNull('expiry_date')
->whereNotNull('months')
->where('months', '>', 0);
$count = $query->count();
if ($count === 0) {
$this->info('No order items need to be updated.');
return 0;
}
$this->info("Found {$count} order items to update.");
if ($dryRun) {
$this->warn('DRY RUN - No changes will be made.');
$query->chunk(100, function ($items) {
foreach ($items as $item) {
$expiryDate = Carbon::parse($item->created_at)->addMonths($item->months);
$this->line("Order #{$item->order_id}, Item #{$item->id}: would set expiry_date to {$expiryDate}");
}
});
} else {
$updated = 0;
$query->chunk(100, function ($items) use (&$updated) {
foreach ($items as $item) {
$expiryDate = Carbon::parse($item->created_at)->addMonths($item->months);
$item->update(['expiry_date' => $expiryDate]);
$updated++;
}
});
$this->info("Updated {$updated} order items.");
}
return 0;
}
}
@@ -0,0 +1,36 @@
<?php
namespace App\Console\Commands\Whmcs;
use App\Integrations\Whmcs\Extractors\ClientExtractor;
use App\Jobs\Whmcs\SyncClientJob;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class SyncClientsCommand extends Command
{
protected $signature = 'whmcs:sync-clients {--sync : Run each record inline instead of queueing it}';
protected $description = 'Extract clients from the WHMCS database and upsert them into customers';
public function handle(ClientExtractor $extractor): int
{
$sync = (bool) $this->option('sync');
$total = DB::connection('whmcs')->table('tblclients')->count();
$bar = $this->output->createProgressBar($total);
$bar->start();
foreach ($extractor->extract() as $row) {
$job = new SyncClientJob((array) $row);
$sync ? dispatch_sync($job) : dispatch($job);
$bar->advance();
}
$bar->finish();
$this->newLine();
$this->info("Dispatched {$total} client(s) for sync.");
return Command::SUCCESS;
}
}
@@ -0,0 +1,38 @@
<?php
namespace App\Console\Commands\Whmcs;
use App\Integrations\Whmcs\Extractors\CustomerDomainExtractor;
use App\Integrations\Whmcs\Transformers\CustomerDomainTransformer;
use App\Integrations\Whmcs\Writers\CustomerDomainWriter;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class SyncCustomerDomainsCommand extends Command
{
protected $signature = 'whmcs:sync-domains';
protected $description = 'Extract customer domain registrations (tbldomains) from WHMCS and upsert them into module_domain (run after whmcs:sync-clients, whmcs:sync-orders, domain:sync)';
public function handle(CustomerDomainExtractor $extractor, CustomerDomainTransformer $transformer, CustomerDomainWriter $writer): int
{
$total = DB::connection('whmcs')->table('tbldomains')->count();
$bar = $this->output->createProgressBar($total);
$bar->start();
$skipped = 0;
foreach ($extractor->extract() as $row) {
if (! $writer->write($transformer->transform($row))) {
$skipped++;
}
$bar->advance();
}
$bar->finish();
$this->newLine();
$this->info("Processed {$total} domain(s), skipped {$skipped} (unresolved customer).");
return Command::SUCCESS;
}
}
@@ -0,0 +1,36 @@
<?php
namespace App\Console\Commands\Whmcs;
use App\Integrations\Whmcs\Extractors\InvoiceExtractor;
use App\Jobs\Whmcs\SyncInvoiceJob;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class SyncInvoicesCommand extends Command
{
protected $signature = 'whmcs:sync-invoices {--sync : Run each record inline instead of queueing it}';
protected $description = 'Extract invoices (with their line items) from WHMCS and upsert them into invoices/invoice_items';
public function handle(InvoiceExtractor $extractor): int
{
$sync = (bool) $this->option('sync');
$total = DB::connection('whmcs')->table('tblinvoices')->count();
$bar = $this->output->createProgressBar($total);
$bar->start();
foreach ($extractor->extract() as $row) {
$job = new SyncInvoiceJob((array) $row);
$sync ? dispatch_sync($job) : dispatch($job);
$bar->advance();
}
$bar->finish();
$this->newLine();
$this->info("Dispatched {$total} invoice(s) for sync.");
return Command::SUCCESS;
}
}
@@ -0,0 +1,38 @@
<?php
namespace App\Console\Commands\Whmcs;
use App\Integrations\Whmcs\Extractors\OrderItemExtractor;
use App\Integrations\Whmcs\Transformers\OrderItemTransformer;
use App\Integrations\Whmcs\Writers\OrderItemWriter;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class SyncOrderItemsCommand extends Command
{
protected $signature = 'whmcs:sync-order-items';
protected $description = 'Extract provisioned services (tblhosting) from WHMCS and upsert them into order_items (run after whmcs:sync-orders)';
public function handle(OrderItemExtractor $extractor, OrderItemTransformer $transformer, OrderItemWriter $writer): int
{
$total = DB::connection('whmcs')->table('tblhosting')->count();
$bar = $this->output->createProgressBar($total);
$bar->start();
$skipped = 0;
foreach ($extractor->extract() as $row) {
if (! $writer->write($transformer->transform($row))) {
$skipped++;
}
$bar->advance();
}
$bar->finish();
$this->newLine();
$this->info("Processed {$total} order item(s), skipped {$skipped} (unresolved order).");
return Command::SUCCESS;
}
}
@@ -0,0 +1,36 @@
<?php
namespace App\Console\Commands\Whmcs;
use App\Integrations\Whmcs\Extractors\OrderExtractor;
use App\Jobs\Whmcs\SyncOrderJob;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class SyncOrdersCommand extends Command
{
protected $signature = 'whmcs:sync-orders {--sync : Run each record inline instead of queueing it}';
protected $description = 'Extract orders from the WHMCS database and upsert them into orders (run before whmcs:sync-invoices)';
public function handle(OrderExtractor $extractor): int
{
$sync = (bool) $this->option('sync');
$total = DB::connection('whmcs')->table('tblorders')->count();
$bar = $this->output->createProgressBar($total);
$bar->start();
foreach ($extractor->extract() as $row) {
$job = new SyncOrderJob((array) $row);
$sync ? dispatch_sync($job) : dispatch($job);
$bar->advance();
}
$bar->finish();
$this->newLine();
$this->info("Dispatched {$total} order(s) for sync.");
return Command::SUCCESS;
}
}
@@ -0,0 +1,72 @@
<?php
namespace App\Console\Commands\Whmcs;
use App\Integrations\Whmcs\Extractors\AddonExtractor;
use App\Integrations\Whmcs\Extractors\ProductExtractor;
use App\Integrations\Whmcs\Extractors\ProductGroupExtractor;
use App\Integrations\Whmcs\Transformers\ProductAddonTransformer;
use App\Integrations\Whmcs\Transformers\ProductCategoryTransformer;
use App\Integrations\Whmcs\Transformers\ProductTransformer;
use App\Integrations\Whmcs\Writers\ProductAddonWriter;
use App\Integrations\Whmcs\Writers\ProductCategoryWriter;
use App\Integrations\Whmcs\Writers\ProductWriter;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class SyncProductsCommand extends Command
{
protected $signature = 'whmcs:sync-products';
protected $description = 'Extract product groups, products (with pricing) and addons from WHMCS and upsert them into product_categories/products/product_pricings/product_addon';
public function handle(
ProductGroupExtractor $groupExtractor,
ProductCategoryTransformer $groupTransformer,
ProductCategoryWriter $groupWriter,
ProductExtractor $productExtractor,
ProductTransformer $productTransformer,
ProductWriter $productWriter,
AddonExtractor $addonExtractor,
ProductAddonTransformer $addonTransformer,
ProductAddonWriter $addonWriter,
): int {
// Order matters: categories before products (FK), products before addons (type guess looks at linked products).
$this->info('Syncing product categories...');
$bar = $this->output->createProgressBar(DB::connection('whmcs')->table('tblproductgroups')->count());
$bar->start();
foreach ($groupExtractor->extract() as $row) {
$groupWriter->write($groupTransformer->transform($row));
$bar->advance();
}
$bar->finish();
$this->newLine();
$this->info('Syncing products...');
$bar = $this->output->createProgressBar(DB::connection('whmcs')->table('tblproducts')->count());
$bar->start();
$skippedProducts = 0;
foreach ($productExtractor->extract() as $row) {
if (! $productWriter->write($productTransformer->transform($row))) {
$skippedProducts++;
}
$bar->advance();
}
$bar->finish();
$this->newLine();
$this->info('Syncing addons...');
$bar = $this->output->createProgressBar(DB::connection('whmcs')->table('tbladdons')->count());
$bar->start();
foreach ($addonExtractor->extract() as $row) {
$addonWriter->write($addonTransformer->transform($row));
$bar->advance();
}
$bar->finish();
$this->newLine();
$this->info("Skipped: {$skippedProducts} product(s) (unresolved category).");
return Command::SUCCESS;
}
}
@@ -0,0 +1,38 @@
<?php
namespace App\Console\Commands\Whmcs;
use App\Integrations\Whmcs\Extractors\ServiceExtractor;
use App\Integrations\Whmcs\Transformers\ServiceTransformer;
use App\Integrations\Whmcs\Writers\ServiceWriter;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class SyncServicesCommand extends Command
{
protected $signature = 'whmcs:sync-services';
protected $description = 'Extract provisioned services (tblhosting) from WHMCS and upsert them into services (run after whmcs:sync-clients, whmcs:sync-products, whmcs:sync-order-items)';
public function handle(ServiceExtractor $extractor, ServiceTransformer $transformer, ServiceWriter $writer): int
{
$total = DB::connection('whmcs')->table('tblhosting')->count();
$bar = $this->output->createProgressBar($total);
$bar->start();
$skipped = 0;
foreach ($extractor->extract() as $row) {
if (! $writer->write($transformer->transform($row))) {
$skipped++;
}
$bar->advance();
}
$bar->finish();
$this->newLine();
$this->info("Processed {$total} service(s), skipped {$skipped} (unresolved customer).");
return Command::SUCCESS;
}
}