- 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>
101 lines
3.2 KiB
PHP
101 lines
3.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support;
|
|
|
|
use App\Models\Addon;
|
|
use App\Models\CustomProduct;
|
|
use App\Models\Product;
|
|
use App\Models\Service;
|
|
|
|
class RenewalPricing
|
|
{
|
|
/** @return list<array{billing_cycle: string, months: int, price: int}> */
|
|
public static function options(Product|CustomProduct|null $product): array
|
|
{
|
|
if ($product instanceof CustomProduct) {
|
|
return $product->unit_price > 0
|
|
? array_map(static fn (int $months): array => [
|
|
'billing_cycle' => CustomProduct::cycleCode($months),
|
|
'months' => $months,
|
|
'price' => (int) $product->unit_price * $months,
|
|
], CustomProduct::CYCLE_MONTHS)
|
|
: [];
|
|
}
|
|
|
|
if (! $product) {
|
|
return [];
|
|
}
|
|
|
|
$options = [];
|
|
foreach ($product->pricings()->where('is_active', 1)->get() as $pricing) {
|
|
$cycle = (string) $pricing->billing_cycle;
|
|
$months = BillingCycleCatalog::monthsFor($cycle);
|
|
|
|
if (! $months || (int) $pricing->price <= 0 || ! ProductBillingCycles::allows($product->product_type, $cycle)) {
|
|
continue;
|
|
}
|
|
|
|
$options[$months] = ['billing_cycle' => $cycle, 'months' => $months, 'price' => (int) $pricing->price];
|
|
}
|
|
ksort($options);
|
|
|
|
return array_values($options);
|
|
}
|
|
|
|
public static function option(Product|CustomProduct|null $product, int $months): ?array
|
|
{
|
|
foreach (self::options($product) as $option) {
|
|
if ($option['months'] === $months) {
|
|
return $option;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public static function addonTotal(Service $service, float $cycles): int
|
|
{
|
|
return (int) array_sum(array_column(self::addonInvoiceItems($service, $cycles, 0), 'total_price'));
|
|
}
|
|
|
|
/** @return list<array<string, mixed>> */
|
|
public static function addonInvoiceItems(Service $service, float $cycles, int $months): array
|
|
{
|
|
$meta = array_filter(
|
|
(array) ($service->orderItem?->addons_meta ?? []),
|
|
static fn ($entry): bool => is_array($entry) && (int) ($entry['qty'] ?? 0) > 0,
|
|
);
|
|
|
|
if ($meta === []) {
|
|
return [];
|
|
}
|
|
|
|
$currentPrices = Addon::whereIn('id', array_column($meta, 'addon_id'))->pluck('price', 'id');
|
|
|
|
$items = [];
|
|
foreach ($meta as $entry) {
|
|
$qty = (int) $entry['qty'];
|
|
$unitPrice = (int) ($currentPrices[$entry['addon_id'] ?? null] ?? $entry['price'] ?? 0);
|
|
$totalPrice = (int) round($unitPrice * $qty * $cycles);
|
|
|
|
if ($totalPrice <= 0) {
|
|
continue;
|
|
}
|
|
|
|
$items[] = [
|
|
'product_id' => $service->product_id,
|
|
'type' => 'addon',
|
|
'description' => 'Addon: ' . ($entry['name'] ?? strtoupper((string) ($entry['code'] ?? ''))) . ' x' . $qty,
|
|
'months' => $months,
|
|
'quantity' => $qty,
|
|
'unit_price' => (int) round($unitPrice * $cycles),
|
|
'total_price' => $totalPrice,
|
|
];
|
|
}
|
|
|
|
return $items;
|
|
}
|
|
}
|