Files
portal-api/database/seeders/CustomerBillingDemoSeeder.php
T
2026-09-28 16:39:22 +07:00

206 lines
9.1 KiB
PHP

<?php
namespace Database\Seeders;
use App\Enums\Invoice\InvoiceStatus;
use App\Enums\Order\OrderStatus;
use App\Enums\TransactionStatus;
use App\Helpers\OrderHelper;
use App\Models\Customer;
use App\Models\Invoice;
use App\Models\InvoiceItem;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Product;
use App\Models\Service;
use App\Models\Transaction;
use Carbon\Carbon;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
/**
* Chạy: php artisan db:seed --class=CustomerBillingDemoSeeder
*/
class CustomerBillingDemoSeeder extends Seeder
{
private const MARKER = 'seed:customer-billing-demo';
private const SCENARIOS = [
['product' => 'email-doanh-nghiep-goi-10-tai-khoan', 'cycle' => 'monthly', 'status' => InvoiceStatus::PAID, 'paid' => 1.0, 'created' => -25, 'due' => -18],
['product' => 'switch-cisco-catalyst-24-port', 'cycle' => 'onetime', 'status' => InvoiceStatus::PAID, 'paid' => 1.0, 'created' => -60, 'due' => -53],
['product' => 'thue-may-chu-vat-ly-dell-r740', 'cycle' => 'monthly', 'status' => InvoiceStatus::UNPAID, 'paid' => 0.0, 'created' => -1, 'due' => 6],
['product' => 'it-support-goi-doanh-nghiep', 'cycle' => 'monthly', 'status' => InvoiceStatus::UNPAID, 'paid' => 0.5, 'created' => -3, 'due' => 4],
['product' => 'website-landing-page-co-ban', 'cycle' => 'onetime', 'status' => InvoiceStatus::OVERDUE, 'paid' => 0.0, 'created' => -17, 'due' => -10],
['product' => 'email-doanh-nghiep-goi-10-tai-khoan', 'cycle' => 'monthly', 'status' => InvoiceStatus::OVERDUE, 'paid' => 0.3, 'created' => -10, 'due' => -3],
['product' => 'it-support-goi-doanh-nghiep', 'cycle' => 'monthly', 'status' => InvoiceStatus::CANCELLED, 'paid' => 0.0, 'created' => -40, 'due' => -33],
['product' => 'thue-may-chu-vat-ly-dell-r740', 'cycle' => 'monthly', 'status' => InvoiceStatus::REFUNDED, 'paid' => 1.0, 'created' => -45, 'due' => -38],
];
public function run(): void
{
$this->call([TestAccountSeeder::class, ProductCategorySeeder::class, ProductTypeFieldSeeder::class, ProductMockupSeeder::class]);
$customer = Customer::where('email', 'pbao.dev@gmail.com')->firstOrFail();
DB::transaction(function () use ($customer) {
$this->purgePreviousRun($customer);
foreach (self::SCENARIOS as $scenario) {
$this->seedScenario($customer, $scenario);
}
});
$this->command?->info('Đã tạo ' . count(self::SCENARIOS) . ' đơn hàng/hoá đơn mẫu cho ' . $customer->email . '.');
}
private function purgePreviousRun(Customer $customer): void
{
$orderIds = Order::withTrashed()->where('customer_id', $customer->id)->where('note', self::MARKER)->pluck('id');
$invoices = Invoice::withTrashed()->whereIn('order_id', $orderIds)->get(['id', 'invoice_number']);
Transaction::withTrashed()
->where('customer_id', $customer->id)
->where(function ($query) use ($invoices) {
foreach ($invoices->pluck('invoice_number') as $number) {
$query->orWhere('trans', 'like', $number . '%');
}
})
->when($invoices->isNotEmpty(), fn ($query) => $query->forceDelete());
$orderItemIds = OrderItem::whereIn('order_id', $orderIds)->pluck('id');
Service::withTrashed()->whereIn('order_item_id', $orderItemIds)->forceDelete();
InvoiceItem::whereIn('invoice_id', $invoices->pluck('id'))->delete();
Invoice::withTrashed()->whereIn('id', $invoices->pluck('id'))->forceDelete();
OrderItem::whereIn('id', $orderItemIds)->delete();
Order::withTrashed()->whereIn('id', $orderIds)->forceDelete();
}
/**
* @param array{product: string, cycle: string, status: InvoiceStatus, paid: float, created: int, due: int} $scenario
*/
private function seedScenario(Customer $customer, array $scenario): void
{
$product = Product::where('slug', $scenario['product'])->firstOrFail();
$price = (int) $product->pricings()->where('billing_cycle', $scenario['cycle'])->value('price');
$months = OrderHelper::cycleToMonths($scenario['cycle']);
$status = $scenario['status'];
$createdAt = Carbon::now()->addDays($scenario['created']);
$amountPaid = (int) round($price * $scenario['paid'], -3);
$isSettled = in_array($status, [InvoiceStatus::PAID, InvoiceStatus::REFUNDED], true);
$paidAt = $isSettled ? $createdAt->copy()->addHour() : null;
$order = Order::query()->forceCreate([
'order_num' => OrderHelper::generateOrderNumber(),
'date' => $createdAt->toDateString(),
'customer_id' => $customer->id,
'sub_total' => $price,
'discount_amount' => 0,
'total' => $price,
'status' => match ($status) {
InvoiceStatus::PAID => OrderStatus::ACTIVE->value,
InvoiceStatus::CANCELLED, InvoiceStatus::REFUNDED => OrderStatus::CANCELLED->value,
default => OrderStatus::PENDING->value,
},
'payment_method' => $amountPaid > 0 ? 'balance' : null,
'paid_at' => $paidAt,
'note' => self::MARKER,
'created_at' => $createdAt,
'updated_at' => $createdAt,
]);
$orderItem = OrderItem::query()->forceCreate([
'order_id' => $order->id,
'product_id' => $product->id,
'product_pricing_id' => $product->pricings()->where('billing_cycle', $scenario['cycle'])->value('id'),
'billing_cycle' => $scenario['cycle'],
'months' => $months,
'quantity' => 1,
'price' => $price,
'created_at' => $createdAt,
'updated_at' => $createdAt,
]);
$invoice = Invoice::query()->forceCreate([
'invoice_number' => OrderHelper::generateOrderNumber('INV'),
'date' => $createdAt->toDateString(),
'customer_id' => $customer->id,
'order_id' => $order->id,
'invoice_type' => 1,
'sub_total' => $price,
'discount_amount' => 0,
'tax' => 0,
'total' => $price,
'amount_paid' => $amountPaid,
'status' => $status->value,
'payment_method' => $amountPaid > 0 ? 'balance' : null,
'due_date' => Carbon::now()->addDays($scenario['due'])->toDateString(),
'billing_period_start' => $createdAt->toDateString(),
'billing_period_end' => $createdAt->copy()->addMonths($months)->toDateString(),
'paid_at' => $paidAt,
'created_at' => $createdAt,
'updated_at' => $createdAt,
]);
InvoiceItem::query()->forceCreate([
'invoice_id' => $invoice->id,
'product_id' => $product->id,
'type' => 'service',
'description' => $product->name,
'months' => $months,
'quantity' => 1,
'unit_price' => $price,
'total_price' => $price,
'created_at' => $createdAt,
'updated_at' => $createdAt,
]);
if ($amountPaid > 0) {
$this->recordPayment($invoice, $amountPaid, $isSettled, $paidAt ?? $createdAt->copy()->addHour());
}
if ($isSettled) {
$this->seedService($order, $orderItem, $invoice, $status, $paidAt);
}
}
private function recordPayment(Invoice $invoice, int $amount, bool $isSettled, Carbon $paidAt): void
{
Transaction::query()->forceCreate([
'uuid' => (string) Str::uuid(),
'trans' => $invoice->invoice_number . ($isSettled ? '' : '-P' . $amount),
'public_code' => 'INV-PAY-' . $paidAt->format('Ymd') . '-' . strtoupper(Str::random(6)),
'amount' => $amount,
'received_amount' => $amount,
'customer_id' => $invoice->customer_id,
'status' => TransactionStatus::COMPLETED->value,
'type' => 'invoice',
'note' => 'Khách hàng thanh toán hóa đơn #' . $invoice->invoice_number . ' (balance)',
'created_at' => $paidAt,
'updated_at' => $paidAt,
]);
}
private function seedService(Order $order, OrderItem $orderItem, Invoice $invoice, InvoiceStatus $status, Carbon $startedAt): void
{
$isRefunded = $status === InvoiceStatus::REFUNDED;
$refundedAt = $startedAt->copy()->addDays(2);
Service::query()->forceCreate([
'uuid' => (string) Str::uuid(),
'customer_id' => $order->customer_id,
'product_id' => $orderItem->product_id,
'order_item_id' => $orderItem->id,
'invoice_id' => $invoice->id,
'status' => $isRefunded ? 'cancelled' : 'active',
'auto_renew' => false,
'started_at' => $startedAt,
'expires_at' => $startedAt->copy()->addMonths($orderItem->months),
'cancelled_at' => $isRefunded ? $refundedAt : null,
'refunded_at' => $isRefunded ? $refundedAt : null,
'created_at' => $startedAt,
'updated_at' => $isRefunded ? $refundedAt : $startedAt,
]);
}
}