- 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>
172 lines
5.8 KiB
PHP
172 lines
5.8 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Order;
|
|
|
|
use App\Enums\Invoice\InvoiceStatus;
|
|
use App\Enums\Order\OrderStatus;
|
|
use App\Helpers\OrderHelper;
|
|
use App\Models\DomainPricing;
|
|
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 Illuminate\Support\Facades\DB;
|
|
|
|
class ServiceOrderRecorder
|
|
{
|
|
/**
|
|
* @return array{order: Order, orderItem: OrderItem, invoice: Invoice}
|
|
*/
|
|
public function create(
|
|
int $customerId,
|
|
?Product $product,
|
|
string $billingCycle,
|
|
int $price,
|
|
?string $serverName,
|
|
bool $paid,
|
|
?string $paymentMethod = null,
|
|
?string $clientIp = null,
|
|
): array {
|
|
$months = OrderHelper::cycleToMonths($billingCycle);
|
|
$isDomain = $product?->product_type === 'domain';
|
|
|
|
$order = Order::create([
|
|
'order_num' => OrderHelper::generateOrderNumber(),
|
|
'customer_id' => $customerId,
|
|
'date' => now()->toDateString(),
|
|
'sub_total' => $price,
|
|
'discount_amount' => 0,
|
|
'total' => $price,
|
|
'status' => $paid ? OrderStatus::ACTIVE->value : OrderStatus::PENDING->value,
|
|
'payment_method' => $paymentMethod,
|
|
'client_ip' => $clientIp,
|
|
]);
|
|
|
|
$orderItem = OrderItem::create([
|
|
'order_id' => $order->id,
|
|
'product_id' => $product?->id,
|
|
'billing_cycle' => $billingCycle,
|
|
'months' => $months,
|
|
'server_name' => $serverName,
|
|
'domain_name' => $isDomain ? $serverName : null,
|
|
'quantity' => 1,
|
|
'price' => $price,
|
|
]);
|
|
|
|
$invoice = Invoice::create([
|
|
'invoice_number' => OrderHelper::generateOrderNumber('INV'),
|
|
'customer_id' => $customerId,
|
|
'order_id' => $order->id,
|
|
'invoice_type' => 1,
|
|
'date' => now()->toDateString(),
|
|
'sub_total' => $price,
|
|
'discount_amount' => 0,
|
|
'tax' => 0,
|
|
'total' => $price,
|
|
'status' => $paid ? InvoiceStatus::PAID->value : InvoiceStatus::UNPAID->value,
|
|
'payment_method' => $paymentMethod,
|
|
'paid_at' => $paid ? now() : null,
|
|
'due_date' => $paid ? now()->addMonths($months)->toDateString() : now()->toDateString(),
|
|
]);
|
|
|
|
InvoiceItem::create([
|
|
'invoice_id' => $invoice->id,
|
|
'product_id' => $product?->id,
|
|
'type' => 'service',
|
|
'description' => $this->describe($product, $serverName),
|
|
'months' => $months,
|
|
'quantity' => 1,
|
|
'unit_price' => $price,
|
|
'total_price' => $price,
|
|
]);
|
|
|
|
return compact('order', 'orderItem', 'invoice');
|
|
}
|
|
|
|
public function ensureForService(Service $service, ?int $price = null, ?string $billingCycle = null): void
|
|
{
|
|
if ($service->order_item_id || ! $service->customer_id) {
|
|
return;
|
|
}
|
|
|
|
DB::transaction(function () use ($service, $price, $billingCycle) {
|
|
$product = $service->product ?? $this->defaultProductFor($service->type);
|
|
$isDomain = ($product?->product_type ?? $service->type) === 'domain';
|
|
$billingCycle ??= $isDomain ? 'annually' : 'monthly';
|
|
$price ??= $this->listPrice($product, $billingCycle, $service->server_name) ?? 0;
|
|
|
|
$records = $this->create(
|
|
(int) $service->customer_id,
|
|
$product,
|
|
$billingCycle,
|
|
$price,
|
|
$service->server_name,
|
|
paid: false,
|
|
);
|
|
|
|
$service->forceFill([
|
|
'product_id' => $service->product_id ?? $product?->id,
|
|
'order_item_id' => $records['orderItem']->id,
|
|
'invoice_id' => $records['invoice']->id,
|
|
])->save();
|
|
});
|
|
}
|
|
|
|
public function syncUnpaidInvoice(Service $service, int $price): void
|
|
{
|
|
$invoice = $service->invoice;
|
|
if (! $invoice || (int) $invoice->status !== InvoiceStatus::UNPAID->value || (int) $invoice->invoice_type !== 1) {
|
|
return;
|
|
}
|
|
|
|
$months = (int) ($service->orderItem?->months ?: 1);
|
|
|
|
$invoice->update(['sub_total' => $price, 'total' => $price]);
|
|
$invoice->invoiceItems()->where('type', 'service')->update([
|
|
'months' => $months,
|
|
'unit_price' => $price,
|
|
'total_price' => $price,
|
|
]);
|
|
$invoice->order?->update(['sub_total' => $price, 'total' => $price]);
|
|
}
|
|
|
|
public function cancelUnpaidInvoice(Service $service): void
|
|
{
|
|
$invoice = $service->invoice;
|
|
if (! $invoice || (int) $invoice->status !== InvoiceStatus::UNPAID->value) {
|
|
return;
|
|
}
|
|
|
|
$invoice->update(['status' => InvoiceStatus::CANCELLED->value]);
|
|
$invoice->order?->update(['status' => OrderStatus::CANCELLED->value]);
|
|
}
|
|
|
|
public function listPrice(?Product $product, string $billingCycle, ?string $serverName): ?int
|
|
{
|
|
if ($product?->product_type === 'domain') {
|
|
return DomainPricing::matchDomain((string) $serverName)
|
|
?->orderPrice(DomainPricing::yearsForMonths(OrderHelper::cycleToMonths($billingCycle)));
|
|
}
|
|
|
|
$price = $product?->pricings()->where('billing_cycle', $billingCycle)->value('price');
|
|
|
|
return $price === null ? null : (int) $price;
|
|
}
|
|
|
|
private function defaultProductFor(?string $type): ?Product
|
|
{
|
|
return $type
|
|
? Product::where('product_type', $type)->where('is_active', true)->orderBy('id')->first()
|
|
: null;
|
|
}
|
|
|
|
private function describe(?Product $product, ?string $serverName): string
|
|
{
|
|
$name = $product->name ?? 'Dịch vụ';
|
|
|
|
return $serverName ? "{$name} - {$serverName}" : $name;
|
|
}
|
|
}
|