128 lines
5.0 KiB
PHP
128 lines
5.0 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Customer;
|
|
use App\Models\Invoice;
|
|
use App\Services\Email\EmailTemplateService;
|
|
use App\Support\AutoEmailGate;
|
|
use App\Support\EmailTemplateShell;
|
|
use App\Support\InvoiceLookup;
|
|
use Carbon\Carbon;
|
|
|
|
/**
|
|
* Nhắc thanh toán hoá đơn. Cột invoices.reminder_step có từ migration đầu tiên nhưng chưa
|
|
* từng được đọc/ghi — khách chỉ biết mình nợ khi dịch vụ đã bị tắt. Ở đây reminder_step là
|
|
* SỐ MỐC NHẮC ĐÃ GỬI, nên chạy lại command trong ngày không gửi trùng.
|
|
*/
|
|
class InvoiceReminderService
|
|
{
|
|
public function __construct(
|
|
private readonly EmailTemplateService $templates,
|
|
) {}
|
|
|
|
public const SCENARIO = 'invoice_payment_reminder';
|
|
|
|
/** Hoá đơn còn phải thu thì mới nhắc. */
|
|
public function isRemindable(Invoice $invoice): bool
|
|
{
|
|
return in_array((int) $invoice->status, InvoicePaymentService::PAYABLE_STATUSES, true);
|
|
}
|
|
|
|
/**
|
|
* Số mốc nhắc đã đến hạn tính tới hôm nay, theo config('billing.reminder_offsets')
|
|
* (số âm = trước hạn, dương = sau hạn).
|
|
*/
|
|
public function dueStep(Invoice $invoice, ?Carbon $now = null): int
|
|
{
|
|
if (! $invoice->due_date) {
|
|
return 0;
|
|
}
|
|
|
|
$now = $now ?? now();
|
|
$daysFromDue = (int) $invoice->due_date->copy()->startOfDay()->diffInDays($now->copy()->startOfDay(), false);
|
|
|
|
$offsets = (array) config('billing.reminder_offsets', []);
|
|
if ($offsets === []) {
|
|
return 0;
|
|
}
|
|
sort($offsets);
|
|
|
|
// Quá mốc nhắc cuối cùng (cộng ân hạn) thì chuỗi nhắc đã đóng — không mở lại.
|
|
//
|
|
// Không có guard này, một lần import dữ liệu cũ (WHMCS) sẽ khiến mọi hoá đơn chưa
|
|
// thanh toán từ nhiều năm trước có reminder_step = 0 và due_date quá hạn rất lâu,
|
|
// cron 08:00 hôm sau bắn email nhắc nợ cho toàn bộ khách hàng cũ cùng lúc.
|
|
if ($daysFromDue > self::reminderCutoff($offsets)) {
|
|
return 0;
|
|
}
|
|
|
|
return count(array_filter($offsets, fn ($offset) => $daysFromDue >= $offset));
|
|
}
|
|
|
|
/** Ngày quá hạn tối đa còn nằm trong chuỗi nhắc. */
|
|
public static function reminderCutoff(array $offsets): int
|
|
{
|
|
return (int) max($offsets) + (int) config('billing.reminder_grace_days', 7);
|
|
}
|
|
|
|
/**
|
|
* Gửi nhắc nếu có mốc mới chưa gửi. Trả về true khi thực sự gửi.
|
|
* $force dùng cho nút "Nhắc thanh toán" thủ công của admin — bỏ qua việc đã gửi mốc đó rồi.
|
|
*/
|
|
public function remind(Invoice $invoice, bool $force = false, ?Carbon $now = null): bool
|
|
{
|
|
if (! $this->isRemindable($invoice)) {
|
|
return false;
|
|
}
|
|
|
|
$dueStep = $this->dueStep($invoice, $now);
|
|
|
|
if (! $force && $dueStep <= (int) $invoice->reminder_step) {
|
|
return false;
|
|
}
|
|
|
|
if (! $force && AutoEmailGate::blocks(self::SCENARIO)) {
|
|
return false;
|
|
}
|
|
|
|
$customer = $invoice->customer;
|
|
if (! $customer?->email) {
|
|
return false;
|
|
}
|
|
|
|
$this->templates->send(self::SCENARIO, $customer->email, $this->vars($invoice, $customer, $now));
|
|
|
|
$invoice->update(['reminder_step' => max($dueStep, (int) $invoice->reminder_step + ($force ? 1 : 0))]);
|
|
|
|
return true;
|
|
}
|
|
|
|
private function vars(Invoice $invoice, Customer $customer, ?Carbon $now): array
|
|
{
|
|
$now = $now ?? now();
|
|
$dueDate = $invoice->due_date;
|
|
$daysLate = $dueDate ? (int) $dueDate->copy()->startOfDay()->diffInDays($now->copy()->startOfDay(), false) : 0;
|
|
$outstanding = max(0, (int) $invoice->total - (int) ($invoice->amount_paid ?? 0));
|
|
|
|
return [
|
|
'client_name' => $customer->fullname ?? '',
|
|
'invoice_number' => $invoice->invoice_number,
|
|
'total' => number_format((int) $invoice->total, 0, ',', '.'),
|
|
'outstanding' => number_format($outstanding, 0, ',', '.'),
|
|
'due_date' => $dueDate?->format('d/m/Y') ?? '-',
|
|
'status_line' => $daysLate > 0
|
|
? 'Hoá đơn đã quá hạn ' . $daysLate . ' ngày.'
|
|
: ($daysLate === 0 ? 'Hôm nay là hạn thanh toán của hoá đơn này.' : 'Hoá đơn sẽ đến hạn sau ' . abs($daysLate) . ' ngày.'),
|
|
'paid_row' => (int) ($invoice->amount_paid ?? 0) > 0
|
|
? EmailTemplateShell::row('Đã thanh toán', number_format((int) $invoice->amount_paid, 0, ',', '.') . ' VNĐ')
|
|
: '',
|
|
'lookup_row' => InvoiceLookup::emailRow($invoice),
|
|
'lookup_note' => InvoiceLookup::emailNote($invoice),
|
|
'login_url' => rtrim((string) config('app.frontend_url', ''), '/') . '/dang-nhap',
|
|
'support_email' => 'support@tgs.com.vn',
|
|
'support_phone' => '(028) 7309. 7379',
|
|
];
|
|
}
|
|
}
|