Files
portal-api/app/Support/RenewalQuote.php
baodev0andClaude Sonnet 5.5 55a834a2f8 feat: nhắc gia hạn QR theo dịch vụ, failover server hosting, CCCD admin, log cấp phát dịch vụ
- Mail campaign: mẫu nhắc gia hạn thanh toán QR, chọn dịch vụ + chu kỳ từng dòng (renewal_services)
- Hosting: nhiều server Plesk cho một gói (service_plan_servers), chuyển server khi server chết
- Admin xem/sửa giấy tờ định danh khách, tuỳ chọn gia hạn cho khách, nhật ký cấp phát dịch vụ
- Đánh dấu mẫu email dịch vụ là non-marketing

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 18:36:41 +07:00

87 lines
3.3 KiB
PHP

<?php
namespace App\Support;
use App\Helpers\OrderHelper;
use App\Models\Service;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
class RenewalQuote
{
private const TYPE_LABELS = [
'domain' => 'tên miền',
'hosting' => 'hosting',
'vps' => 'Cloud VPS',
'ssl' => 'SSL',
];
/**
* @param array<int, int> $customerIds
* @return Collection<int, Service>
*/
public static function dueServices(array $customerIds, int $days): Collection
{
return Service::query()
->with(['customer:id,uuid,fullname,email', 'orderItem', 'product', 'customProduct'])
->whereIn('customer_id', $customerIds)
->whereNotIn('status', ['terminated', 'cancelled'])
->whereNotNull('expires_at')
->where('expires_at', '<=', now()->addDays($days)->endOfDay())
->orderBy('expires_at')
->get();
}
public static function forService(Service $service, ?int $months = null): ?array
{
$orderItem = $service->orderItem;
$product = $service->customProduct ?? $service->product;
if (! $orderItem || ! $product) {
return null;
}
$baseMonths = OrderHelper::cycleToMonths($orderItem->billing_cycle);
$options = RenewalPricing::options($product);
$base = RenewalPricing::option($product, $baseMonths);
if (! $base) {
return null;
}
$rate = VatSettings::rateFor($service->product?->product_type);
$start = ($service->expires_at ?? now())->copy();
$target = $service->server_name ?: $product->name;
$label = self::TYPE_LABELS[$service->type] ?? null;
$terms = fn (array $option) => self::terms($service, $option, $baseMonths, $start, $rate);
return [
'service_id' => $service->id,
'service_name' => 'Gia hạn ' . ($label ? $label . ' - ' : '') . $target,
'period_start' => $start->format('d/m/Y'),
'expires_date' => $start->format('d/m/Y'),
'vat_rate' => rtrim(rtrim(number_format($rate, 2, '.', ''), '0'), '.'),
'payment_content' => Str::limit('GIA HAN ' . Str::upper(Str::ascii($target)), 50, ''),
...$terms(RenewalPricing::option($product, (int) $months) ?? $base),
'cycles' => array_map($terms, $options),
];
}
/**
* @param array{months: int, price: int} $option
* @return array{months: int, billing_cycle: string, period_end: string, total_amount: string, vat_amount: string}
*/
private static function terms(Service $service, array $option, int $baseMonths, Carbon $start, float $rate): array
{
$months = $option['months'];
$amount = $option['price'] + RenewalPricing::addonTotal($service, $months / $baseMonths);
return [
'months' => $months,
'billing_cycle' => sprintf('%02d %s', $months % 12 === 0 ? $months / 12 : $months, $months % 12 === 0 ? 'năm' : 'tháng'),
'period_end' => $start->copy()->addMonthsNoOverflow($months)->subDay()->format('d/m/Y'),
'total_amount' => number_format($amount, 0, ',', '.'),
'vat_amount' => number_format((int) round($amount * $rate / (100 + $rate)), 0, ',', '.'),
];
}
}