Files
InvoiceShelf/app/Platform/Modules/Infrastructure/EloquentCompanyDataReader.php
T
Darko Gjorgjijoski 0b9ae9ea00 refactor(ai): extract assistant into an official module (#749)
* feat(modules): expose host data boundaries

* feat(modules): add frontend extension surfaces

* refactor(ai): remove assistant from core

* chore(ai): prepare the module extraction

* fix(modules): load extension styles after the host bundle

* chore(modules): lock SDK 3.3.0
2026-08-05 22:10:21 +02:00

298 lines
15 KiB
PHP

<?php
namespace App\Platform\Modules\Infrastructure;
use App\Domains\Catalog\Models\Item;
use App\Domains\Contacts\Models\Address;
use App\Domains\Contacts\Models\Customer;
use App\Domains\Purchases\Models\Expense;
use App\Domains\Purchases\Models\ExpenseCategory;
use App\Domains\Receivables\Models\Payment;
use App\Domains\Sales\Models\Invoice;
use App\Domains\Sales\Models\InvoiceItem;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use InvoiceShelf\Modules\Contracts\Host\CompanyDataReader;
class EloquentCompanyDataReader implements CompanyDataReader
{
public function companyStats(int $companyId, string $startDate, string $endDate): array
{
return [
'invoices' => [
'count' => Invoice::query()->where('company_id', $companyId)->where('type', Invoice::TYPE_INVOICE)->whereBetween('invoice_date', [$startDate, $endDate])->count(),
'total' => (float) Invoice::query()->where('company_id', $companyId)->whereBetween('invoice_date', [$startDate, $endDate])->sum('total'),
],
'payments' => [
'count' => Payment::query()->where('company_id', $companyId)->whereBetween('payment_date', [$startDate, $endDate])->count(),
'total' => (float) Payment::query()->where('company_id', $companyId)->whereBetween('payment_date', [$startDate, $endDate])->sum('amount'),
],
'expenses' => [
'count' => Expense::query()->where('company_id', $companyId)->whereBetween('expense_date', [$startDate, $endDate])->count(),
'total' => (float) Expense::query()->where('company_id', $companyId)->whereBetween('expense_date', [$startDate, $endDate])->sum('amount'),
],
];
}
public function findCustomer(int $companyId, int $customerId): ?array
{
$customer = Customer::query()
->where('company_id', $companyId)
->whereKey($customerId)
->with(['billingAddress', 'shippingAddress'])
->first();
if ($customer === null) {
return null;
}
return [
'id' => $customer->id,
'name' => $customer->name,
'display_name' => $customer->display_name,
'email' => $customer->email,
'phone' => $customer->phone,
'contact_name' => $customer->contact_name,
'company_name' => $customer->company_name,
'website' => $customer->website,
'enable_portal' => (bool) $customer->enable_portal,
'billing_address' => $this->address($customer->billingAddress),
'shipping_address' => $this->address($customer->shippingAddress),
'totals' => [
'invoice_count' => Invoice::query()->where('company_id', $companyId)->where('customer_id', $customer->id)->where('type', Invoice::TYPE_INVOICE)->count(),
'outstanding_amount' => (float) Invoice::query()->where('company_id', $companyId)->where('customer_id', $customer->id)->whereIn('paid_status', ['UNPAID', 'PARTIALLY_PAID'])->sum('due_amount'),
],
];
}
public function searchCustomers(int $companyId, ?string $query, int $limit): array
{
$customers = Customer::query()->where('company_id', $companyId)->orderBy('name')->limit($limit);
if ($query !== null && $query !== '') {
$customers->where(function ($builder) use ($query) {
$builder->where('name', 'like', "%{$query}%")
->orWhere('display_name', 'like', "%{$query}%")
->orWhere('email', 'like', "%{$query}%")
->orWhere('company_name', 'like', "%{$query}%")
->orWhere('contact_name', 'like', "%{$query}%");
});
}
return $customers->get()->map(fn (Customer $customer): array => [
'id' => $customer->id,
'name' => $customer->name,
'display_name' => $customer->display_name,
'email' => $customer->email,
'phone' => $customer->phone,
'company_name' => $customer->company_name,
])->all();
}
public function rankCustomers(int $companyId, string $metric, ?string $startDate, ?string $endDate, int $limit): array
{
$rows = match ($metric) {
'invoiced_total' => $this->customerInvoiceRanking($companyId, $startDate, $endDate, $limit, 'SUM(total)'),
'paid_total' => $this->customerPaymentRanking($companyId, $startDate, $endDate, $limit),
'invoice_count' => $this->customerInvoiceRanking($companyId, $startDate, $endDate, $limit, 'COUNT(*)'),
'outstanding_balance' => Invoice::query()->where('company_id', $companyId)->whereNotNull('customer_id')->whereIn('paid_status', ['UNPAID', 'PARTIALLY_PAID'])->selectRaw('customer_id, SUM(due_amount) as metric_value, COUNT(*) as invoice_count')->groupBy('customer_id')->orderByDesc('metric_value')->limit($limit)->get(),
};
$customers = Customer::query()->where('company_id', $companyId)->whereIn('id', $rows->pluck('customer_id'))->get()->keyBy('id');
return $rows->map(function ($row) use ($customers, $metric): array {
$customer = $customers->get($row->customer_id);
return [
'customer_id' => (int) $row->customer_id,
'name' => $customer?->name,
'display_name' => $customer?->display_name,
'company_name' => $customer?->company_name,
'metric_value' => $metric === 'invoice_count' ? (int) $row->metric_value : (float) $row->metric_value,
'invoice_count' => isset($row->invoice_count) ? (int) $row->invoice_count : null,
];
})->all();
}
public function findInvoice(int $companyId, string $invoiceNumber): ?array
{
$invoice = Invoice::query()->where('company_id', $companyId)->where('invoice_number', $invoiceNumber)->with(['customer:id,name,email,phone', 'items', 'taxes'])->first();
if ($invoice === null) {
return null;
}
return [
'id' => $invoice->id,
'invoice_number' => $invoice->invoice_number,
'reference_number' => $invoice->reference_number,
'status' => $invoice->status,
'paid_status' => $invoice->paid_status,
'invoice_date' => $this->date($invoice->invoice_date),
'due_date' => $this->date($invoice->due_date),
'sub_total' => $invoice->sub_total,
'tax' => $invoice->tax,
'discount' => $invoice->discount,
'total' => $invoice->total,
'due_amount' => $invoice->due_amount,
'overdue' => (bool) $invoice->overdue,
'notes' => $invoice->notes,
'customer' => $invoice->customer ? ['id' => $invoice->customer->id, 'name' => $invoice->customer->name, 'email' => $invoice->customer->email, 'phone' => $invoice->customer->phone] : null,
'items' => $invoice->items->map(fn ($item): array => ['name' => $item->name, 'description' => $item->description, 'quantity' => $item->quantity, 'price' => $item->price, 'total' => $item->total])->all(),
'taxes' => $invoice->taxes->map(fn ($tax): array => ['name' => $tax->name, 'percent' => $tax->percent, 'amount' => $tax->amount])->all(),
];
}
public function searchInvoices(int $companyId, ?string $query, ?string $status, ?int $customerId, int $limit): array
{
$invoices = Invoice::query()->where('company_id', $companyId)->with('customer:id,name')->latest('invoice_date')->limit($limit);
if ($query !== null && $query !== '') {
$invoices->where(fn ($builder) => $builder->where('invoice_number', 'like', "%{$query}%")->orWhere('reference_number', 'like', "%{$query}%"));
}
if ($status !== null && $status !== '') {
$status = strtoupper($status);
if (in_array($status, ['PAID', 'UNPAID', 'PARTIALLY_PAID'], true)) {
$invoices->where('paid_status', $status);
} elseif ($status === 'OVERDUE') {
$invoices->where('overdue', true);
} else {
$invoices->where('status', $status);
}
}
if ($customerId !== null) {
$invoices->where('customer_id', $customerId);
}
return $invoices->get()->map(fn (Invoice $invoice): array => $this->invoiceSummary($invoice))->all();
}
public function overdueInvoices(int $companyId, int $limit): array
{
return Invoice::query()->where('company_id', $companyId)->where('overdue', true)->with('customer:id,name')->orderBy('due_date')->limit($limit)->get()->map(fn (Invoice $invoice): array => $this->invoiceSummary($invoice))->all();
}
public function recentPayments(int $companyId, string $startDate, int $limit): array
{
return Payment::query()->where('company_id', $companyId)->where('payment_date', '>=', $startDate)->with(['allocations:id,payment_id,invoice_id,amount', 'customer:id,name', 'paymentMethod:id,name'])->latest('payment_date')->limit($limit)->get()->map(function (Payment $payment): array {
$allocated = (int) $payment->allocations->sum('amount');
return [
'id' => $payment->id,
'payment_number' => $payment->payment_number,
'payment_date' => $this->date($payment->payment_date),
'amount' => $payment->amount,
'customer_id' => $payment->customer_id,
'customer_name' => $payment->customer?->name,
'allocations' => $payment->allocations->map(fn ($allocation): array => ['invoice_id' => $allocation->invoice_id, 'amount' => $allocation->amount])->all(),
'allocated_amount' => $allocated,
'unallocated_amount' => (int) $payment->amount - $allocated,
'payment_method' => $payment->paymentMethod?->name,
];
})->all();
}
public function expenseCategories(int $companyId): array
{
return ExpenseCategory::query()->where('company_id', $companyId)->orderBy('name')->get(['id', 'name', 'description'])->map(fn (ExpenseCategory $category): array => ['id' => $category->id, 'name' => $category->name, 'description' => $category->description])->all();
}
public function rankExpenseCategories(int $companyId, ?string $startDate, ?string $endDate, int $limit): array
{
$expenses = Expense::query()->where('company_id', $companyId)->whereNotNull('expense_category_id')->selectRaw('expense_category_id, SUM(amount) as total_amount, COUNT(*) as expense_count')->groupBy('expense_category_id')->orderByDesc('total_amount')->limit($limit);
if ($startDate !== null && $endDate !== null) {
$expenses->whereBetween('expense_date', [$startDate, $endDate]);
}
$rows = $expenses->get();
$categories = ExpenseCategory::query()->where('company_id', $companyId)->whereIn('id', $rows->pluck('expense_category_id'))->get()->keyBy('id');
return $rows->map(fn ($row): array => ['expense_category_id' => (int) $row->expense_category_id, 'name' => $categories->get($row->expense_category_id)?->name, 'total_amount' => (float) $row->total_amount, 'expense_count' => (int) $row->expense_count])->all();
}
public function searchItems(int $companyId, ?string $query, int $limit): array
{
$items = Item::query()->where('company_id', $companyId)->orderBy('name')->limit($limit);
if ($query !== null && $query !== '') {
$items->where(fn ($builder) => $builder->where('name', 'like', "%{$query}%")->orWhere('description', 'like', "%{$query}%"));
}
return $items->get()->map(fn (Item $item): array => ['id' => $item->id, 'name' => $item->name, 'description' => $item->description, 'price' => $item->price])->all();
}
public function rankItems(int $companyId, string $metric, ?string $startDate, ?string $endDate, int $limit): array
{
$items = InvoiceItem::query()->join('invoices', 'invoice_items.invoice_id', '=', 'invoices.id')->where('invoices.company_id', $companyId)->whereNotNull('invoice_items.item_id')->selectRaw('invoice_items.item_id, SUM(invoice_items.quantity) as total_quantity, SUM(invoice_items.total) as total_revenue')->groupBy('invoice_items.item_id')->orderByDesc($metric === 'revenue' ? 'total_revenue' : 'total_quantity')->limit($limit);
if ($startDate !== null && $endDate !== null) {
$items->whereBetween('invoices.invoice_date', [$startDate, $endDate]);
}
$rows = $items->get();
$catalog = Item::query()->where('company_id', $companyId)->whereIn('id', $rows->pluck('item_id'))->get()->keyBy('id');
return $rows->map(fn ($row): array => ['item_id' => (int) $row->item_id, 'name' => $catalog->get($row->item_id)?->name, 'quantity_sold' => (float) $row->total_quantity, 'revenue' => (float) $row->total_revenue])->all();
}
private function customerInvoiceRanking(int $companyId, ?string $startDate, ?string $endDate, int $limit, string $aggregate): Collection
{
$invoices = Invoice::query()->where('company_id', $companyId)->whereNotNull('customer_id')->selectRaw("customer_id, {$aggregate} as metric_value, COUNT(*) as invoice_count")->groupBy('customer_id')->orderByDesc('metric_value')->limit($limit);
if ($startDate !== null && $endDate !== null) {
$invoices->whereBetween('invoice_date', [$startDate, $endDate]);
}
return $invoices->get();
}
private function customerPaymentRanking(int $companyId, ?string $startDate, ?string $endDate, int $limit): Collection
{
$payments = Payment::query()->where('company_id', $companyId)->whereNotNull('customer_id')->selectRaw('customer_id, SUM(amount) as metric_value')->groupBy('customer_id')->orderByDesc('metric_value')->limit($limit);
if ($startDate !== null && $endDate !== null) {
$payments->whereBetween('payment_date', [$startDate, $endDate]);
}
return $payments->get();
}
private function invoiceSummary(Invoice $invoice): array
{
return [
'id' => $invoice->id,
'invoice_number' => $invoice->invoice_number,
'customer_id' => $invoice->customer_id,
'customer_name' => $invoice->customer?->name,
'invoice_date' => $this->date($invoice->invoice_date),
'due_date' => $this->date($invoice->due_date),
'status' => $invoice->status,
'paid_status' => $invoice->paid_status,
'total' => $invoice->total,
'due_amount' => $invoice->due_amount,
'overdue' => (bool) $invoice->overdue,
];
}
private function address(?Address $address): ?array
{
if ($address === null) {
return null;
}
return $address->only(['id', 'name', 'address_street_1', 'address_street_2', 'city', 'state', 'country_id', 'zip', 'phone', 'fax', 'type']);
}
private function date(mixed $value): ?string
{
if ($value === null || $value === '') {
return null;
}
return $value instanceof Carbon ? $value->toDateString() : substr((string) $value, 0, 10);
}
}