mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-09-01 21:00:58 +00:00
* refactor: stabilize model identities for domain migration * refactor: extract module platform context * refactor: assign models to domain contexts * refactor: extract ai platform context * refactor: extract storage platform context * refactor: extract mail platform context * refactor: extract pdf platform context * refactor: extract operations platform context * refactor: move installation into operations platform * refactor: extract money domain context * refactor: extract taxation domain context * refactor: extract catalog domain context * refactor: extract metadata domain context * refactor: extract reporting domain context * refactor: extract purchases domain context * refactor: extract receivables domain context * refactor: extract accounts domain context * refactor: complete reporting statement boundary * refactor: extract contacts domain context * refactor: extract sales domain context * refactor: remove legacy application layers * fix: migrate legacy bouncer role identities
215 lines
7.5 KiB
PHP
215 lines
7.5 KiB
PHP
<?php
|
|
|
|
namespace App\Domains\Receivables\Application;
|
|
|
|
use App\Domains\Accounts\Models\Company;
|
|
use App\Domains\Accounts\Models\CompanySetting;
|
|
use App\Domains\Metadata\Contracts\CustomFieldValueWriter;
|
|
use App\Domains\Receivables\Contracts\PaymentEmailSender;
|
|
use App\Domains\Receivables\Contracts\PaymentExchangeRateRecorder;
|
|
use App\Domains\Receivables\Contracts\PaymentNumberAssigner;
|
|
use App\Domains\Receivables\Contracts\PaymentPdfDataProvider;
|
|
use App\Domains\Receivables\Models\Payment;
|
|
use App\Facades\Hashids;
|
|
use App\Platform\Mail\Contracts\MailConfigurator;
|
|
use App\Platform\Pdf\Facades\Pdf;
|
|
use App\Platform\Pdf\Rendering\PdfMetadata;
|
|
use App\Platform\Pdf\Rendering\PdfTemplateUtils;
|
|
use App\Support\Hashids\HashidConnection;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class PaymentService implements PaymentPdfDataProvider
|
|
{
|
|
public function __construct(
|
|
private readonly PaymentAllocationService $paymentAllocationService,
|
|
private readonly MailConfigurator $mailConfigurator,
|
|
private readonly CustomFieldValueWriter $customFieldValueWriter,
|
|
private readonly PaymentNumberAssigner $paymentNumberAssigner,
|
|
private readonly PaymentExchangeRateRecorder $paymentExchangeRateRecorder,
|
|
private readonly PaymentEmailSender $paymentEmailSender,
|
|
) {}
|
|
|
|
/**
|
|
* @param array<string, mixed> $attributes
|
|
* @param array<int, array{invoice_id: int, amount: int}> $allocations
|
|
*/
|
|
public function create(
|
|
array $attributes,
|
|
array $allocations = [],
|
|
?iterable $customFields = null,
|
|
): Payment {
|
|
$payment = DB::transaction(function () use ($attributes, $allocations, $customFields): Payment {
|
|
$payment = Payment::create($attributes);
|
|
$payment->unique_hash = Hashids::connection(HashidConnection::Payment->value)->encode($payment->id);
|
|
|
|
$numbering = $this->paymentNumberAssigner->next(
|
|
$payment,
|
|
(int) $payment->company_id,
|
|
(int) $payment->customer_id,
|
|
);
|
|
|
|
$payment->sequence_number = $numbering->sequence;
|
|
$payment->customer_sequence_number = $numbering->customerSequence;
|
|
$payment->save();
|
|
|
|
$this->paymentAllocationService->replace($payment, $allocations);
|
|
|
|
$companyCurrency = CompanySetting::getSetting('currency', $payment->company_id);
|
|
|
|
if ((string) $payment->currency_id !== $companyCurrency) {
|
|
$this->paymentExchangeRateRecorder->record($payment);
|
|
}
|
|
|
|
if ($customFields) {
|
|
$this->customFieldValueWriter->attach($payment, $customFields);
|
|
}
|
|
|
|
return $payment;
|
|
});
|
|
|
|
return $this->loadPayment($payment);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $attributes
|
|
* @param array<int, array{invoice_id: int, amount: int}> $allocations
|
|
*/
|
|
public function update(
|
|
Payment $payment,
|
|
array $attributes,
|
|
bool $replaceAllocations,
|
|
array $allocations = [],
|
|
?iterable $customFields = null,
|
|
): Payment {
|
|
$payment = DB::transaction(function () use ($payment, $attributes, $replaceAllocations, $allocations, $customFields): Payment {
|
|
$lockedPayment = Payment::query()->whereKey($payment->id)->lockForUpdate()->firstOrFail();
|
|
$targetAllocations = $replaceAllocations
|
|
? $allocations
|
|
: $lockedPayment->allocations()
|
|
->get(['invoice_id', 'amount'])
|
|
->map(fn ($allocation) => [
|
|
'invoice_id' => (int) $allocation->invoice_id,
|
|
'amount' => (int) $allocation->amount,
|
|
])
|
|
->all();
|
|
$customerChanged = (int) $lockedPayment->customer_id !== (int) $attributes['customer_id'];
|
|
|
|
if ($customerChanged && $targetAllocations !== []) {
|
|
throw ValidationException::withMessages([
|
|
'customer_id' => ['payment_customer_change_requires_unallocated_credit'],
|
|
]);
|
|
}
|
|
|
|
$numbering = $this->paymentNumberAssigner->next(
|
|
$lockedPayment,
|
|
(int) $lockedPayment->company_id,
|
|
(int) $attributes['customer_id'],
|
|
);
|
|
|
|
$attributes['customer_sequence_number'] = $numbering->customerSequence;
|
|
$lockedPayment->update($attributes);
|
|
$this->paymentAllocationService->replace($lockedPayment, $targetAllocations);
|
|
|
|
$companyCurrency = CompanySetting::getSetting('currency', $lockedPayment->company_id);
|
|
|
|
if ((string) $lockedPayment->currency_id !== $companyCurrency) {
|
|
$this->paymentExchangeRateRecorder->record($lockedPayment);
|
|
}
|
|
|
|
if ($customFields) {
|
|
$this->customFieldValueWriter->update($lockedPayment, $customFields);
|
|
}
|
|
|
|
return $lockedPayment;
|
|
});
|
|
|
|
return $this->loadPayment($payment);
|
|
}
|
|
|
|
public function delete(Collection $ids): bool
|
|
{
|
|
DB::transaction(function () use ($ids): void {
|
|
foreach ($ids->sort() as $id) {
|
|
$payment = Payment::query()->whereKey($id)->lockForUpdate()->first();
|
|
|
|
if (! $payment) {
|
|
continue;
|
|
}
|
|
|
|
$this->paymentAllocationService->replace($payment, []);
|
|
$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);
|
|
|
|
$this->mailConfigurator->applyCompanyConfig($payment->company_id);
|
|
|
|
$this->paymentEmailSender->send($data);
|
|
|
|
return [
|
|
'success' => true,
|
|
];
|
|
}
|
|
|
|
public function getPdfData(Payment $payment): mixed
|
|
{
|
|
$payment->loadMissing('allocations.invoice.currency');
|
|
|
|
$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, PdfMetadata::forDocument(
|
|
__('pdf_payment_label'),
|
|
$payment->payment_number,
|
|
$company,
|
|
));
|
|
}
|
|
|
|
private function loadPayment(Payment $payment): Payment
|
|
{
|
|
return Payment::with([
|
|
'customer',
|
|
'allocations.invoice',
|
|
'paymentMethod',
|
|
'fields',
|
|
])->findOrFail($payment->id);
|
|
}
|
|
}
|