92 lines
3.4 KiB
PHP
92 lines
3.4 KiB
PHP
<?php
|
|
|
|
namespace App\Integrations\Whmcs\Transformers;
|
|
|
|
use App\Integrations\Whmcs\DTOs\InvoiceData;
|
|
use App\Integrations\Whmcs\Extractors\InvoiceExtractor;
|
|
use App\Integrations\Whmcs\Support\CustomerIdResolver;
|
|
use App\Integrations\Whmcs\Support\OrderIdResolver;
|
|
use Illuminate\Support\Carbon;
|
|
|
|
class InvoiceTransformer
|
|
{
|
|
public function __construct(
|
|
private CustomerIdResolver $customers,
|
|
private OrderIdResolver $orders,
|
|
private InvoiceExtractor $extractor,
|
|
private InvoiceItemTransformer $itemTransformer,
|
|
) {
|
|
}
|
|
|
|
public function transform(array|object $row): InvoiceData
|
|
{
|
|
$row = (array) $row;
|
|
$legacyId = (int) $row['id'];
|
|
|
|
$dueDate = $this->nullableDate($row['duedate'] ?? null);
|
|
$status = $this->mapStatus($row['status'], $dueDate);
|
|
$total = (int) round((float) $row['total']);
|
|
$whmcsOrderId = $this->extractor->orderIdFor($legacyId);
|
|
|
|
$rawItems = $this->extractor->items($legacyId);
|
|
|
|
// WHMCS records discounts as negative-amount line items; invoice_items.unit_price
|
|
// is unsigned, so fold them into the header's discount_amount instead.
|
|
$discountAmount = (int) round(-$rawItems->sum(fn ($item) => min(0, (float) $item->amount)));
|
|
|
|
$items = $rawItems
|
|
->map(fn ($item) => $this->itemTransformer->transform($item))
|
|
->filter()
|
|
->all();
|
|
|
|
return new InvoiceData(
|
|
legacyId: $legacyId,
|
|
invoiceNumber: $row['invoicenum'] ?: "WHMCS-{$legacyId}",
|
|
customerId: $this->customers->resolveFor('invoice', $row['invoicenum'] ?: $row['id'], (int) $row['userid']),
|
|
orderId: $whmcsOrderId ? $this->orders->resolve($whmcsOrderId) : null,
|
|
date: $this->nullableDate($row['date'] ?? null),
|
|
subTotal: (int) round((float) $row['subtotal']),
|
|
discountAmount: $discountAmount,
|
|
tax: (int) round((float) $row['tax'] + (float) $row['tax2']),
|
|
total: $total,
|
|
// Không có amount_paid thì mọi hoá đơn WHMCS đã thanh toán sẽ hiện "chưa thu"
|
|
// và outstanding = total trên toàn bộ dữ liệu import.
|
|
amountPaid: $status === 2 ? $total : (int) round((float) ($row['credit'] ?? 0)),
|
|
status: $status,
|
|
paymentMethod: $row['paymentmethod'] ?: null,
|
|
dueDate: $dueDate,
|
|
paidAt: $this->nullableDate($row['datepaid'] ?? null),
|
|
note: $row['notes'] ?: null,
|
|
createdAt: $this->nullableDate($row['created_at'] ?? null),
|
|
items: $items,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* invoices.status: 1=unpaid, 2=paid, 3=cancelled, 4=overdue, 5=refunded.
|
|
*/
|
|
private function mapStatus(string $status, ?string $dueDate): int
|
|
{
|
|
return match ($status) {
|
|
'Paid' => 2,
|
|
'Cancelled' => 3,
|
|
'Refunded' => 5,
|
|
'Collections' => 4,
|
|
default => ($dueDate && Carbon::parse($dueDate)->isPast()) ? 4 : 1,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* WHMCS has zero-dates ('0000-00-00...') and, in this data, at least one bogus
|
|
* year (e.g. '0004-04-22') — both invalid for MySQL strict mode, so nullify them.
|
|
*/
|
|
private function nullableDate(?string $value): ?string
|
|
{
|
|
if (! $value || (int) substr($value, 0, 4) < 1970) {
|
|
return null;
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
}
|