- Dịch vụ custom: bảng custom_products + services.custom_product_id; createService nhận
custom{} thay product_id, giá = đơn giá tháng x số tháng chu kỳ (1/3/6/12/24/36),
gia hạn tính theo cùng đơn giá, ServiceClientDetailResource trả custom_product và specs.
- Giám sát VM: GET v1/system/vm-metrics/history gom lịch sử về tối đa 240 điểm;
lỗi lấy dung lượng đĩa không còn làm hỏng cả thẻ metrics.
- SealResponse: AES-256-GCM theo khoá phiên do proxy Next cấp qua header x-seal-key
(HMAC của session id). Client opt-in bằng x-seal; chi tiết dịch vụ, mật khẩu VM và
console ticket luôn được mã hoá. Body gửi lên dạng {enc} được giải mã trước validate;
khoá lệch trả 428 để client lấy khoá mới. Đây là che mắt DevTools, không phải bảo mật
chống chính chủ tài khoản.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
195 lines
6.5 KiB
PHP
195 lines
6.5 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\Auth;
|
|
use Illuminate\Support\Facades\DB;
|
|
use App\Support\VatNumbering;
|
|
|
|
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,
|
|
int $amountPaid = 0,
|
|
?string $label = null,
|
|
): array {
|
|
$months = OrderHelper::cycleToMonths($billingCycle);
|
|
$isDomain = $product?->product_type === 'domain';
|
|
|
|
$order = Order::create([
|
|
'order_num' => OrderHelper::generateOrderNumber(),
|
|
'customer_id' => $customerId,
|
|
'user_id' => Auth::guard('admin')->id(),
|
|
'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,
|
|
'amount_paid' => $paid ? $price : $amountPaid,
|
|
'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, $label),
|
|
'months' => $months,
|
|
'quantity' => 1,
|
|
'unit_price' => $price,
|
|
'total_price' => $price,
|
|
]);
|
|
|
|
VatNumbering::settle($invoice);
|
|
|
|
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 discardOrder(?int $orderItemId): void
|
|
{
|
|
$order = $orderItemId ? OrderItem::find($orderItemId)?->order : null;
|
|
if (! $order) {
|
|
return;
|
|
}
|
|
|
|
if (Service::whereIn('order_item_id', $order->orderItems()->pluck('id'))->exists()) {
|
|
return;
|
|
}
|
|
|
|
$order->invoices()->delete();
|
|
$order->delete();
|
|
}
|
|
|
|
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 $label = null): string
|
|
{
|
|
$name = $label ?? $product->name ?? 'Dịch vụ';
|
|
|
|
return $serverName ? "{$name} - {$serverName}" : $name;
|
|
}
|
|
}
|