- 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>
87 lines
3.3 KiB
PHP
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, ',', '.'),
|
|
];
|
|
}
|
|
}
|