Files
portal-api/app/Services/Order/ServiceOrderRecorder.php
T
baodev0andClaude Sonnet 5.5 6315a392c0 feat(service): custom product, lịch sử giám sát VM và mã hoá response theo phiên
- 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>
2026-10-27 13:58:50 +07:00

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;
}
}