feat(payments): add customer ledgers and allocations (#744)

This commit is contained in:
Darko Gjorgjijoski
2026-08-05 03:28:35 +02:00
committed by GitHub
parent 8ae82ae91e
commit f322c2b74d
74 changed files with 5693 additions and 693 deletions
@@ -0,0 +1,36 @@
<?php
namespace App\Http\Controllers\Company\Customer;
use App\Http\Controllers\Controller;
use App\Http\Requests\CustomerStatementRequest;
use App\Http\Resources\CustomerStatementResource;
use App\Models\Customer;
use App\Services\CustomerStatementService;
use Carbon\Carbon;
class CustomerStatementController extends Controller
{
public function __construct(
private readonly CustomerStatementService $customerStatementService,
) {}
public function __invoke(CustomerStatementRequest $request, Customer $customer): CustomerStatementResource
{
$this->authorize('view', $customer);
$this->authorize('view report', $customer->company);
$type = $request->validated('type');
$statement = $this->customerStatementService->statement(
$customer,
$type,
Carbon::createFromFormat('Y-m-d', $request->validated('from_date')),
Carbon::createFromFormat('Y-m-d', $request->validated($type === CustomerStatementService::TYPE_OUTSTANDING ? 'as_of' : 'to_date')),
(int) $request->validated('per_page', 50),
(int) $request->validated('page', 1),
);
return new CustomerStatementResource($statement);
}
}
@@ -6,12 +6,14 @@ use App\Http\Controllers\Controller;
use App\Http\Resources\CustomerResource;
use App\Models\Customer;
use App\Services\CustomerService;
use App\Services\CustomerStatementService;
use Illuminate\Http\Request;
class CustomerStatsController extends Controller
{
public function __construct(
private readonly CustomerService $customerService,
private readonly CustomerStatementService $customerStatementService,
) {}
public function __invoke(Request $request, Customer $customer)
@@ -25,6 +27,7 @@ class CustomerStatsController extends Controller
);
$customer = Customer::find($customer->id);
$this->customerStatementService->hydrateAccountSummaries([$customer]);
return (new CustomerResource($customer))
->additional(['meta' => [
@@ -8,13 +8,16 @@ use App\Http\Requests\DeleteCustomersRequest;
use App\Http\Resources\CustomerResource;
use App\Models\Customer;
use App\Services\CustomerService;
use App\Services\CustomerStatementService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
class CustomersController extends Controller
{
public function __construct(
private readonly CustomerService $customerService,
private readonly CustomerStatementService $customerStatementService,
) {}
/**
@@ -31,10 +34,12 @@ class CustomersController extends Controller
$customers = Customer::with('creator')
->whereCompany()
->applyFilters($request->all())
->withSum('invoices as base_due_amount', 'base_due_amount')
->withSum('invoices as due_amount', 'due_amount')
->paginateData($limit);
$this->customerStatementService->hydrateAccountSummaries(
$customers instanceof LengthAwarePaginator ? $customers->getCollection() : $customers
);
return CustomerResource::collection($customers)
->additional(['meta' => [
'customer_total_count' => Customer::whereCompany()->count(),
@@ -52,6 +57,7 @@ class CustomersController extends Controller
$this->authorize('create', Customer::class);
$customer = $this->customerService->create($request);
$this->customerStatementService->hydrateAccountSummaries([$customer]);
return new CustomerResource($customer);
}
@@ -65,6 +71,8 @@ class CustomersController extends Controller
{
$this->authorize('view', $customer);
$this->customerStatementService->hydrateAccountSummaries([$customer]);
return new CustomerResource($customer);
}
@@ -79,6 +87,7 @@ class CustomersController extends Controller
$this->authorize('update', $customer);
$customer = $this->customerService->update($request, $customer);
$this->customerStatementService->hydrateAccountSummaries([$customer]);
return new CustomerResource($customer);
}
@@ -0,0 +1,60 @@
<?php
namespace App\Http\Controllers\Company\Customer;
use App\Http\Controllers\Controller;
use App\Http\Requests\SendCustomerStatementRequest;
use App\Mail\SendCustomerStatementMail;
use App\Models\Customer;
use App\Services\CustomerStatementPdfService;
use App\Services\CustomerStatementService;
use App\Services\Mail\CompanyMailConfigService;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Mail;
class SendCustomerStatementController extends Controller
{
public function __construct(
private readonly CustomerStatementService $customerStatementService,
private readonly CustomerStatementPdfService $customerStatementPdfService,
) {}
public function __invoke(SendCustomerStatementRequest $request, Customer $customer): JsonResponse
{
$this->authorize('view', $customer);
$this->authorize('view report', $customer->company);
$type = $request->validated('type');
$statement = $this->customerStatementService->statement(
$customer,
$type,
Carbon::createFromFormat('Y-m-d', $request->validated('from_date')),
Carbon::createFromFormat('Y-m-d', $request->validated($type === CustomerStatementService::TYPE_OUTSTANDING ? 'as_of' : 'to_date')),
PHP_INT_MAX,
);
$pdf = $this->customerStatementPdfService->render($statement);
CompanyMailConfigService::apply($customer->company_id);
$mail = Mail::to($request->validated('to'));
if ($request->filled('cc')) {
$mail->cc($request->validated('cc'));
}
if ($request->filled('bcc')) {
$mail->bcc($request->validated('bcc'));
}
$mail->send(new SendCustomerStatementMail([
...$request->safe()->only(['to', 'cc', 'bcc', 'subject', 'body']),
'from' => config('mail.from.address'),
'from_name' => config('mail.from.name'),
'customer' => $customer,
'pdf' => $pdf,
'filename' => __('Customer Statement').' '.$customer->name.'.pdf',
]));
return response()->json(['success' => true]);
}
}
@@ -93,6 +93,7 @@ class InvoicesController extends Controller
return new InvoiceResource($invoice->load([
'creditNotes:id,related_invoice_id,invoice_number,total',
'creditNotes.items:id,invoice_id,source_invoice_item_id,quantity',
'allocations.payment',
]));
}
@@ -0,0 +1,41 @@
<?php
namespace App\Http\Controllers\Company\Payment;
use App\Http\Controllers\Controller;
use App\Http\Requests\CreditAllocationRequest;
use App\Models\Customer;
use App\Models\Payment;
use App\Services\Document\PaymentAllocationService;
class CreditAllocationsController extends Controller
{
public function __construct(
private readonly PaymentAllocationService $paymentAllocationService,
) {}
public function store(CreditAllocationRequest $request, Customer $customer)
{
$this->authorize('view', $customer);
abort_unless((int) $customer->company_id === (int) $request->header('company'), 404);
$payments = Payment::query()
->where('company_id', $customer->company_id)
->where('customer_id', $customer->id)
->whereIn('id', collect($request->validated('allocations'))->pluck('payment_id')->unique())
->get();
foreach ($payments as $payment) {
$this->authorize('update', $payment);
}
$this->paymentAllocationService->applyCustomerCredits(
(int) $request->header('company'),
$customer->id,
$request->validated('allocations'),
);
return response()->json(['success' => true]);
}
}
@@ -5,9 +5,11 @@ namespace App\Http\Controllers\Company\Payment;
use App\Http\Controllers\Controller;
use App\Http\Requests\DeletePaymentsRequest;
use App\Http\Requests\PaymentRequest;
use App\Http\Requests\ReplacePaymentAllocationsRequest;
use App\Http\Requests\SendPaymentRequest;
use App\Http\Resources\PaymentResource;
use App\Models\Payment;
use App\Services\Document\PaymentAllocationService;
use App\Services\Document\PaymentService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
@@ -16,6 +18,7 @@ use Illuminate\Mail\Markdown;
class PaymentsController extends Controller
{
public function __construct(
private readonly PaymentAllocationService $paymentAllocationService,
private readonly PaymentService $paymentService,
) {}
@@ -30,12 +33,12 @@ class PaymentsController extends Controller
$limit = $request->has('limit') ? $request->limit : 10;
$payments = Payment::whereCompany()
$payments = Payment::with(['allocations.invoice'])
->whereCompany()
->join('customers', 'customers.id', '=', 'payments.customer_id')
->leftJoin('invoices', 'invoices.id', '=', 'payments.invoice_id')
->leftJoin('payment_methods', 'payment_methods.id', '=', 'payments.payment_method_id')
->applyFilters($request->all())
->select('payments.*', 'customers.name', 'invoices.invoice_number', 'payment_methods.name as payment_mode')
->select('payments.*', 'customers.name', 'payment_methods.name as payment_mode')
->latest()
->paginateData($limit);
@@ -64,7 +67,7 @@ class PaymentsController extends Controller
{
$this->authorize('view', $payment);
return new PaymentResource($payment);
return new PaymentResource($payment->load(['allocations.invoice']));
}
public function update(PaymentRequest $request, Payment $payment)
@@ -76,6 +79,17 @@ class PaymentsController extends Controller
return new PaymentResource($payment);
}
public function replaceAllocations(ReplacePaymentAllocationsRequest $request, Payment $payment)
{
$this->authorize('update', $payment);
abort_unless((int) $payment->company_id === (int) $request->header('company'), 404);
$payment = $this->paymentAllocationService->replace($payment, $request->validated('allocations'));
return new PaymentResource($payment->load(['allocations.invoice']));
}
public function delete(DeletePaymentsRequest $request)
{
$this->authorize('delete multiple payments');
@@ -0,0 +1,54 @@
<?php
namespace App\Http\Controllers\Company\Report;
use App\Http\Controllers\Controller;
use App\Http\Requests\CustomerStatementRequest;
use App\Models\Customer;
use App\Services\CustomerStatementPdfService;
use App\Services\CustomerStatementService;
use Carbon\Carbon;
use Silber\Bouncer\BouncerFacade;
class CustomerStatementReportController extends Controller
{
public function __construct(
private readonly CustomerStatementService $customerStatementService,
private readonly CustomerStatementPdfService $customerStatementPdfService,
) {}
public function __invoke(CustomerStatementRequest $request, Customer $customer)
{
BouncerFacade::scope()->to($customer->company_id);
$this->authorize('view', $customer);
$this->authorize('view report', $customer->company);
$type = $request->validated('type');
$statement = $this->customerStatementService->statement(
$customer,
$type,
Carbon::createFromFormat('Y-m-d', $request->validated('from_date')),
Carbon::createFromFormat('Y-m-d', $request->validated($type === CustomerStatementService::TYPE_OUTSTANDING ? 'as_of' : 'to_date')),
PHP_INT_MAX,
);
$pdf = $this->customerStatementPdfService->render($statement);
if ($request->boolean('preview')) {
return view('app.pdf.reports.customer-statement', [
'statement' => $statement,
'customer' => $customer,
'company' => $customer->company,
'currency' => $statement['currency'],
'logo' => $customer->company->logo_path,
]);
}
if ($request->boolean('download')) {
return $pdf->download(__('Customer Statement').' '.$customer->name.'.pdf');
}
return $pdf->stream();
}
}
@@ -21,16 +21,15 @@ class PaymentsController extends Controller
{
$limit = $request->has('limit') ? $request->limit : 10;
$payments = Payment::with(['customer', 'invoice', 'paymentMethod', 'creator'])
$payments = Payment::with(['customer', 'allocations.invoice', 'paymentMethod', 'creator'])
->whereCustomer(Auth::guard('customer')->id())
->leftJoin('invoices', 'invoices.id', '=', 'payments.invoice_id')
->applyFilters($request->only([
'payment_number',
'payment_method_id',
'orderByField',
'orderBy',
]))
->select('payments.*', 'invoices.invoice_number')
->select('payments.*')
->latest()
->paginateData($limit);
@@ -57,6 +56,6 @@ class PaymentsController extends Controller
return response()->json(['error' => 'payment_not_found'], 404);
}
return new PaymentResource($payment);
return new PaymentResource($payment->load(['allocations.invoice']));
}
}