Files
InvoiceShelf/app/Services/Document/PaymentService.php
Darko Gjorgjijoski 05e8acc3b1 feat(pdf): let payment receipts and reports be overridden too (#731)
Only invoices and estimates could be customised. Payment receipts and all five
reports were hardcoded to app.pdf.*, so changing them meant editing files inside
the image -- and losing the edit on the next upgrade.

Those documents have no template picker and no design to choose between, so
overriding one is not a selection: it is a same-named file in
storage/app/templates/pdf/{type}/ winning over the built-in. PdfTemplateUtils::
resolveView() is that rule, and it needs no setting, no column and no UI.

resolveView asks View::exists rather than checking the storage disk. The disk and
the view namespace are registered separately and could disagree about where
custom templates live; asking the thing that will actually render removes that
possibility.

make:template covers the new types. Their names are not free, since an override
replaces one specific document, so it validates against the real list -- 'payment'
for payments, and the five report names -- and reports what is available when the
name is wrong. Neither type gets a preview image written, having no picker to
show one in.

The payment preview route also went through the built-in view directly rather
than the service, so ?preview ignored an override and rendered with none of the
shared data. It goes through the service now, like invoices and estimates.

Claude-Session: https://claude.ai/code/session_01QmECndmNZwzN65Zz9P87dF
2026-08-01 13:01:15 +02:00

227 lines
7.3 KiB
PHP

<?php
namespace App\Services\Document;
use App\Facades\Hashids;
use App\Facades\Pdf;
use App\Mail\SendPaymentMail;
use App\Models\Company;
use App\Models\CompanySetting;
use App\Models\ExchangeRateLog;
use App\Models\Invoice;
use App\Models\Payment;
use App\Services\Mail\CompanyMailConfigService;
use App\Support\Pdf\PdfTemplateUtils;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
class PaymentService
{
public function create(Request $request): Payment
{
$data = $request->getPaymentPayload();
if ($request->invoice_id) {
$invoice = Invoice::find($request->invoice_id);
$invoice->subtractInvoicePayment($request->amount);
}
$payment = Payment::create($data);
$payment->unique_hash = Hashids::connection(Payment::class)->encode($payment->id);
$serial = (new SerialNumberService)
->setModel($payment)
->setCompany($payment->company_id)
->setCustomer($payment->customer_id)
->setNextNumbers();
$payment->sequence_number = $serial->nextSequenceNumber;
$payment->customer_sequence_number = $serial->nextCustomerSequenceNumber;
$payment->save();
$companyCurrency = CompanySetting::getSetting('currency', $request->header('company'));
if ((string) $payment['currency_id'] !== $companyCurrency) {
ExchangeRateLog::addExchangeRateLog($payment);
}
$customFields = $request->customFields;
if ($customFields) {
$payment->addCustomFields($customFields);
}
return Payment::with([
'customer',
'invoice',
'paymentMethod',
'fields',
])->find($payment->id);
}
public function update(Payment $payment, Request $request): Payment
{
$data = $request->getPaymentPayload();
if ($request->invoice_id && (! $payment->invoice_id || $payment->invoice_id !== $request->invoice_id)) {
$invoice = Invoice::find($request->invoice_id);
$invoice->subtractInvoicePayment($request->amount);
}
if ($payment->invoice_id && (! $request->invoice_id || $payment->invoice_id !== $request->invoice_id)) {
$invoice = Invoice::find($payment->invoice_id);
$invoice->addInvoicePayment($payment->amount);
}
if ($payment->invoice_id && $payment->invoice_id === $request->invoice_id && $request->amount !== $payment->amount) {
$invoice = Invoice::find($payment->invoice_id);
$invoice->addInvoicePayment($payment->amount);
$invoice->subtractInvoicePayment($request->amount);
}
$serial = (new SerialNumberService)
->setModel($payment)
->setCompany($payment->company_id)
->setCustomer($request->customer_id)
->setModelObject($payment->id)
->setNextNumbers();
$data['customer_sequence_number'] = $serial->nextCustomerSequenceNumber;
$payment->update($data);
$companyCurrency = CompanySetting::getSetting('currency', $request->header('company'));
if ((string) $data['currency_id'] !== $companyCurrency) {
ExchangeRateLog::addExchangeRateLog($payment);
}
$customFields = $request->customFields;
if ($customFields) {
$payment->updateCustomFields($customFields);
}
return Payment::with([
'customer',
'invoice',
'paymentMethod',
])->find($payment->id);
}
public function delete(Collection $ids): bool
{
foreach ($ids as $id) {
$payment = Payment::find($id);
if ($payment->invoice_id != null) {
$invoice = Invoice::find($payment->invoice_id);
$invoice->due_amount = ((int) $invoice->due_amount + (int) $payment->amount);
if ($invoice->due_amount == $invoice->total) {
$invoice->paid_status = Invoice::STATUS_UNPAID;
} else {
$invoice->paid_status = Invoice::STATUS_PARTIALLY_PAID;
}
$invoice->status = $invoice->getPreviousStatus();
$invoice->save();
}
$payment->delete();
}
return true;
}
public function sendPaymentData(Payment $payment, array $data): array
{
$data['payment'] = $payment->toArray();
$data['user'] = $payment->customer->toArray();
$data['company'] = Company::find($payment->company_id);
$data['body'] = $payment->getEmailBody($data['body']);
$data['attach']['data'] = ($payment->getEmailAttachmentSetting()) ? $this->getPdfData($payment) : null;
return $data;
}
public function send(Payment $payment, array $data): array
{
$data = $this->sendPaymentData($payment, $data);
CompanyMailConfigService::apply($payment->company_id);
$mail = \Mail::to($data['to']);
if (! empty($data['cc'])) {
$mail->cc($data['cc']);
}
if (! empty($data['bcc'])) {
$mail->bcc($data['bcc']);
}
$mail->send(new SendPaymentMail($data));
return [
'success' => true,
];
}
public function getPdfData(Payment $payment)
{
$company = Company::find($payment->company_id);
$locale = CompanySetting::getSetting('language', $company->id);
\App::setLocale($locale);
$logo = $company->logo_path;
view()->share([
'payment' => $payment,
'company_address' => $payment->getCompanyAddress(),
'billing_address' => $payment->getCustomerBillingAddress(),
'notes' => $payment->getNotes(),
'logo' => $logo ?? null,
]);
$templatePath = PdfTemplateUtils::resolveView('payment', 'payment');
if (request()->has('preview')) {
return view($templatePath);
}
return Pdf::loadView($templatePath);
}
public function generateFromTransaction($transaction): Payment
{
$invoice = Invoice::find($transaction->invoice_id);
$serial = (new SerialNumberService)
->setModel(new Payment)
->setCompany($invoice->company_id)
->setCustomer($invoice->customer_id)
->setNextNumbers();
$data['payment_number'] = $serial->getNextNumber();
$data['payment_date'] = Carbon::now();
$data['amount'] = $invoice->total;
$data['invoice_id'] = $invoice->id;
$data['payment_method_id'] = request()->payment_method_id;
$data['customer_id'] = $invoice->customer_id;
$data['exchange_rate'] = $invoice->exchange_rate;
$data['base_amount'] = $data['amount'] * $data['exchange_rate'];
$data['currency_id'] = $invoice->currency_id;
$data['company_id'] = $invoice->company_id;
$data['transaction_id'] = $transaction->id;
$payment = Payment::create($data);
$payment->unique_hash = Hashids::connection(Payment::class)->encode($payment->id);
$payment->sequence_number = $serial->nextSequenceNumber;
$payment->customer_sequence_number = $serial->nextCustomerSequenceNumber;
$payment->save();
$invoice->subtractInvoicePayment($invoice->total);
return $payment;
}
}