- IP pool: sheet import (xlsx/csv) matched by address, bulk allocate/revoke with notification email (ip_allocated/ip_revoked), ip_pool_logs history. - Orders/invoices: Excel export on branded template (BrandedReportWriter), shared billing filters, order item service info in list responses. - Web design quote Excel export; activity log export. - Service attachments with copy-to-invoice; deployment invoice attachments. - Customer email attachments stored on disk and attached by path. - Admin password change endpoint; user.fullname backfill migration; orders.user_id backfill from logs. - Disable deploy workflow until CI/CD is set up. - Ignore stored customer email and service attachment uploads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
159 lines
6.3 KiB
PHP
159 lines
6.3 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Customer;
|
|
|
|
use App\Models\Invoice;
|
|
use App\Http\Resources\Api\Admin\OrderItemServiceResource;
|
|
use App\Models\Order;
|
|
use App\Enums\TransactionStatus;
|
|
use App\Models\Transaction;
|
|
use App\Repositories\Customer\CustomerRepositoryInterface;
|
|
use App\Repositories\Invoice\InvoiceRepositoryInterface;
|
|
use App\Repositories\Order\OrderRepositoryInterface;
|
|
use App\Repositories\Transaction\TransactionRepositoryInterface;
|
|
use App\Services\Customer\Concerns\ResolvesCustomer;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Str;
|
|
|
|
class CustomerBillingService
|
|
{
|
|
use ResolvesCustomer;
|
|
|
|
public function __construct(
|
|
private readonly CustomerRepositoryInterface $customerRepository,
|
|
private readonly InvoiceRepositoryInterface $invoiceRepository,
|
|
private readonly TransactionRepositoryInterface $transactionRepository,
|
|
private readonly OrderRepositoryInterface $orderRepository,
|
|
) {}
|
|
|
|
public function getInvoices(string $uuid): Collection
|
|
{
|
|
$customer = $this->resolveCustomer($uuid);
|
|
|
|
$invoices = $this->invoiceRepository->filter(['customer_id' => $customer->id], ['payment']);
|
|
// Frontend đọc ngày thanh toán qua `date_paid` — alias từ cột paid_at.
|
|
$invoices->each(fn($invoice) => $invoice->date_paid = $invoice->paid_at);
|
|
|
|
return $invoices;
|
|
}
|
|
|
|
/**
|
|
* Sửa hóa đơn của khách hàng, kể cả số tiền (sub_total/tax/total). Không
|
|
* tự đồng bộ lại invoice_items hay customer.total.
|
|
*/
|
|
public function updateInvoice(string $uuid, int $invoiceId, array $data): Invoice
|
|
{
|
|
$customer = $this->resolveCustomer($uuid);
|
|
|
|
$invoice = Invoice::where('customer_id', $customer->id)->where('id', $invoiceId)->first();
|
|
abort_if(!$invoice, 404, 'Không tìm thấy hóa đơn');
|
|
|
|
// `date_paid` là tên field phía frontend — cột thật trong DB là paid_at.
|
|
if (array_key_exists('date_paid', $data)) {
|
|
$data['paid_at'] = $data['date_paid'];
|
|
unset($data['date_paid']);
|
|
}
|
|
|
|
// Đi qua repository (không gọi $invoice->update() trực tiếp) để cache
|
|
// của invoiceRepository->filter() (dùng ở getInvoices) được xoá theo,
|
|
// nếu không danh sách hóa đơn sẽ hiển thị dữ liệu cũ cho tới khi cache hết hạn.
|
|
$this->invoiceRepository->update($invoiceId, $data);
|
|
|
|
$invoice = $invoice->refresh();
|
|
$invoice->date_paid = $invoice->paid_at;
|
|
|
|
return $invoice->load('payment');
|
|
}
|
|
|
|
public function getTransactions(string $uuid): Collection
|
|
{
|
|
$customer = $this->resolveCustomer($uuid);
|
|
|
|
return $this->transactionRepository->filter(['customer_id' => $customer->id], ['user']);
|
|
}
|
|
|
|
/**
|
|
* Sửa thông tin giao dịch, kể cả amount (để sửa số tiền nhập/hiển thị sai).
|
|
* Không tự động cộng/trừ lại customer.total khi sửa amount — chiều +/- của
|
|
* amount phụ thuộc vào type (balance trừ tiền, transfer/admin_add_total
|
|
* cộng tiền,...) và không có field nào lưu lại chiều đó, nên tự suy luận
|
|
* dễ làm sai số dư hơn là sửa đúng. Nếu cần đồng bộ lại số dư, dùng
|
|
* "Cộng tiền" ở tab Tổng quan sau khi sửa xong.
|
|
*/
|
|
public function updateTransaction(string $uuid, int $transactionId, array $data): Transaction
|
|
{
|
|
$customer = $this->resolveCustomer($uuid);
|
|
|
|
$transaction = Transaction::where('customer_id', $customer->id)->where('id', $transactionId)->first();
|
|
abort_if(!$transaction, 404, 'Không tìm thấy giao dịch');
|
|
|
|
// Đi qua repository để cache của transactionRepository->filter()
|
|
// (dùng ở getTransactions) được xoá theo — xem ghi chú ở updateInvoice.
|
|
$this->transactionRepository->update($transactionId, $data);
|
|
|
|
return $transaction->refresh()->load('user');
|
|
}
|
|
|
|
public function createTransaction(string $uuid, array $data, ?int $adminId): Transaction
|
|
{
|
|
$customer = $this->resolveCustomer($uuid);
|
|
|
|
$transaction = $this->transactionRepository->create([
|
|
'uuid' => (string) Str::uuid(),
|
|
'public_code' => Transaction::newPublicCode('ADM'),
|
|
'trans' => $data['trans'] ?? null,
|
|
'amount' => $data['amount'],
|
|
'received_amount' => $data['amount'],
|
|
'customer_id' => $customer->id,
|
|
'user_id' => $adminId,
|
|
'status' => $data['status'] ?? TransactionStatus::COMPLETED->value,
|
|
'type' => 'admin_record',
|
|
'note' => $data['note'] ?? null,
|
|
]);
|
|
|
|
return $transaction->load('user');
|
|
}
|
|
|
|
public function deleteTransaction(string $uuid, int $transactionId): void
|
|
{
|
|
$customer = $this->resolveCustomer($uuid);
|
|
|
|
$exists = Transaction::where('customer_id', $customer->id)->where('id', $transactionId)->exists();
|
|
abort_if(!$exists, 404, 'Không tìm thấy giao dịch');
|
|
|
|
$this->transactionRepository->delete($transactionId);
|
|
}
|
|
|
|
public function getOrders(string $uuid): Collection
|
|
{
|
|
$customer = $this->resolveCustomer($uuid);
|
|
|
|
return $this->orderRepository->filter(['client' => $customer->id], [
|
|
'customer',
|
|
'user:id,username',
|
|
'payment',
|
|
'invoices',
|
|
...OrderItemServiceResource::relationsFor('orderItems'),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Sửa đơn hàng của khách hàng, kể cả số tiền (sub_total/discount_amount/
|
|
* total). Không tự đồng bộ lại invoice/invoice_item hay customer.total —
|
|
* nếu số tiền hóa đơn liên kết cũng cần sửa, sửa riêng ở updateInvoice.
|
|
*/
|
|
public function updateOrder(string $uuid, int $orderId, array $data): Order
|
|
{
|
|
$customer = $this->resolveCustomer($uuid);
|
|
|
|
$order = Order::where('customer_id', $customer->id)->where('id', $orderId)->first();
|
|
abort_if(!$order, 404, 'Không tìm thấy đơn hàng');
|
|
|
|
// Đi qua repository để cache của orderRepository->filter()
|
|
// (dùng ở getOrders) được xoá theo — xem ghi chú ở updateInvoice.
|
|
$this->orderRepository->update($orderId, $data);
|
|
|
|
return $order->refresh()->load(['payment', 'invoices']);
|
|
}
|
|
}
|