- Lịch sử thanh toán hoá đơn trả thêm `method` (tự động gia hạn / số dư / admin / chuyển khoản / hoàn tiền), bỏ đuôi `(method)` khỏi ghi chú - `DnsServer::panelUrl()` = https://<domain>/ dùng cho email cấp hosting và nút Plesk của khách - Order item trả thêm `uuid`/`type` của dịch vụ để FE dẫn tới trang quản lý - Yêu cầu huỷ dịch vụ, chứng chỉ SSL, lịch sử gia hạn, đổi mật khẩu hosting cho khách Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
123 lines
4.6 KiB
PHP
123 lines
4.6 KiB
PHP
<?php
|
|
|
|
namespace App\Support\Billing;
|
|
|
|
use App\Models\ActivityLog;
|
|
use App\Models\Invoice;
|
|
use App\Models\Service;
|
|
use App\Models\Transaction;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Str;
|
|
|
|
class InvoicePaymentHistory
|
|
{
|
|
private const SERVICE_CREATION_PREFIX = 'SVC-';
|
|
|
|
/**
|
|
* @param Collection<int, Invoice> $invoices
|
|
* @return array<int, list<array<string, mixed>>> keyed by invoice id, oldest payment first
|
|
*/
|
|
public static function forInvoices(Collection $invoices): array
|
|
{
|
|
$history = $invoices->mapWithKeys(fn (Invoice $invoice) => [$invoice->id => []])->all();
|
|
if ($invoices->isEmpty()) {
|
|
return $history;
|
|
}
|
|
|
|
$invoiceByOrderNum = $invoices
|
|
->filter(fn (Invoice $invoice) => (int) $invoice->invoice_type === 1 && $invoice->order?->order_num)
|
|
->keyBy(fn (Invoice $invoice) => $invoice->order->order_num);
|
|
|
|
$transactions = Transaction::with('user')
|
|
->whereIn('customer_id', $invoices->pluck('customer_id')->unique())
|
|
->where(function ($query) use ($invoices, $invoiceByOrderNum) {
|
|
foreach ($invoices as $invoice) {
|
|
$query->orWhere('trans', $invoice->invoice_number)
|
|
->orWhere('trans', 'like', $invoice->invoice_number . '-%');
|
|
}
|
|
if ($invoiceByOrderNum->isNotEmpty()) {
|
|
$query->orWhere(fn ($creation) => $creation
|
|
->whereIn('trans', $invoiceByOrderNum->keys())
|
|
->where('public_code', 'like', self::SERVICE_CREATION_PREFIX . '%'));
|
|
}
|
|
})
|
|
->oldest('id')
|
|
->get();
|
|
|
|
$invoiceByNumber = $invoices->keyBy('invoice_number');
|
|
$renewalCausers = self::renewalCausers($invoices);
|
|
|
|
foreach ($transactions as $transaction) {
|
|
$invoice = self::matchInvoice($transaction, $invoiceByNumber, $invoiceByOrderNum);
|
|
if ($invoice) {
|
|
$history[$invoice->id][] = self::present($transaction, $renewalCausers[$invoice->invoice_number] ?? null);
|
|
}
|
|
}
|
|
|
|
return $history;
|
|
}
|
|
|
|
private static function matchInvoice(Transaction $transaction, Collection $invoiceByNumber, Collection $invoiceByOrderNum): ?Invoice
|
|
{
|
|
if (Str::startsWith((string) $transaction->public_code, self::SERVICE_CREATION_PREFIX)) {
|
|
return $invoiceByOrderNum->get($transaction->trans);
|
|
}
|
|
|
|
$number = preg_replace('/-(P\d+|REFUND)$/', '', (string) $transaction->trans);
|
|
|
|
return $invoiceByNumber->get($number);
|
|
}
|
|
|
|
private static function renewalCausers(Collection $invoices): array
|
|
{
|
|
$numbers = $invoices->where('invoice_type', 2)->pluck('invoice_number')->all();
|
|
if ($numbers === []) {
|
|
return [];
|
|
}
|
|
|
|
return ActivityLog::where('subject_type', Service::class)
|
|
->where('event', 'renewed')
|
|
->whereIn('new_values->invoice_number', $numbers)
|
|
->get(['causer_type', 'new_values'])
|
|
->mapWithKeys(fn (ActivityLog $log) => [$log->new_values['invoice_number'] => $log->causer_type])
|
|
->all();
|
|
}
|
|
|
|
private static function method(Transaction $transaction, bool $isRefund, ?string $renewalCauser): string
|
|
{
|
|
if ($isRefund) {
|
|
return 'refund';
|
|
}
|
|
|
|
if (in_array($transaction->type, ['transfer', 'balance'], true)) {
|
|
return $transaction->type;
|
|
}
|
|
|
|
if (preg_match('/\(([a-z_]+)\)$/', (string) $transaction->note, $match) !== 1) {
|
|
return 'other';
|
|
}
|
|
|
|
return match (true) {
|
|
$match[1] === 'balance' && $renewalCauser === 'system' => 'auto_renew',
|
|
default => $match[1],
|
|
};
|
|
}
|
|
|
|
private static function present(Transaction $transaction, ?string $renewalCauser): array
|
|
{
|
|
$isRefund = Str::endsWith((string) $transaction->trans, '-REFUND');
|
|
|
|
return [
|
|
'id' => $transaction->id,
|
|
'public_code' => $transaction->public_code,
|
|
'kind' => $isRefund ? 'refund' : 'payment',
|
|
'amount' => abs((int) $transaction->amount),
|
|
'type' => $transaction->type,
|
|
'method' => self::method($transaction, $isRefund, $renewalCauser),
|
|
'note' => preg_replace('/ \([a-z_]+\)$/', '', (string) $transaction->note) ?: null,
|
|
'recorded_by' => $transaction->user?->fullname ?: $transaction->user?->username,
|
|
'created_at' => $transaction->created_at,
|
|
];
|
|
}
|
|
}
|