ultimatepos/Modules/Accounting/Services/OperationalTransactionPostingService.php

175 lines
7.2 KiB
PHP

<?php
namespace Modules\Accounting\Services;
use App\Transaction;
use App\TransactionPayment;
use Modules\Accounting\Domain\FinancialPostingRequest;
use Modules\Accounting\Utils\AccountingUtil;
class OperationalTransactionPostingService
{
public function __construct(
protected FinancialPostingService $postingService,
protected AccountingUtil $accountingUtil,
protected HoldingEntityResolutionService $entityResolution
) {}
public function usesUnifiedPosting(): bool
{
return (bool) config('accounting.rollout.unified_operational_posting', false);
}
public function post(
string $type,
int $id,
int $userId,
int $businessId,
int $depositTo,
int $paymentAccount,
?int $locationId = null
): void {
if (! $this->usesUnifiedPosting()) {
$this->accountingUtil->saveMap($type, $id, $userId, $businessId, $depositTo, $paymentAccount);
return;
}
$context = $this->resolveContext($type, $id, $businessId);
$holdingEntityId = $this->entityResolution->resolveFromLocation($businessId, $locationId ?? $context['location_id']);
$this->postingService->post(new FinancialPostingRequest(
businessId: $businessId,
userId: $userId,
sourceType: $type,
sourceId: $id,
note: $this->noteForType($type, $context),
lines: [
[
'accounting_account_id' => $depositTo,
'amount' => $context['amount'],
'type' => 'debit',
'map_type' => 'deposit_to',
'transaction_id' => $context['transaction_id'],
'transaction_payment_id' => $context['transaction_payment_id'],
'holding_entity_id' => $holdingEntityId,
],
[
'accounting_account_id' => $paymentAccount,
'amount' => $context['amount'],
'type' => 'credit',
'map_type' => 'payment_account',
'transaction_id' => $context['transaction_id'],
'transaction_payment_id' => $context['transaction_payment_id'],
'holding_entity_id' => $holdingEntityId,
],
],
operationDate: $context['operation_date'],
holdingEntityId: $holdingEntityId,
transactionId: $context['transaction_id'],
transactionPaymentId: $context['transaction_payment_id'],
mappingType: $type,
), true);
}
public function delete(string $type, int $transactionId, ?int $paymentId = null, ?int $businessId = null): void
{
if ($this->usesUnifiedPosting() && $businessId) {
$sourceId = in_array($type, ['sell_payment', 'purchase_payment'], true) ? (int) $paymentId : $transactionId;
if ($sourceId) {
$postingKey = $businessId.':'.$type.':'.$sourceId;
$userId = (int) (request()->session()->get('user.id') ?: 1);
$this->postingService->reverseByPostingKey($businessId, $postingKey, $userId);
}
}
$this->accountingUtil->deleteMap($transactionId, $paymentId);
}
public function assessGaps(int $businessId, int $days = 90): array
{
$since = now()->subDays($days)->toDateString();
$unmappedSells = $this->countUnmappedTransactions($businessId, 'sell', $since);
$unmappedPurchases = $this->countUnmappedTransactions($businessId, 'purchase', $since);
$unmappedExpenses = $this->countUnmappedTransactions($businessId, 'expense', $since);
$unmappedPayments = $this->countUnmappedPayments($businessId, $since);
$total = $unmappedSells + $unmappedPurchases + $unmappedExpenses + $unmappedPayments;
return [
'unmapped_sells' => $unmappedSells,
'unmapped_purchases' => $unmappedPurchases,
'unmapped_expenses' => $unmappedExpenses,
'unmapped_payments' => $unmappedPayments,
'total_unmapped' => $total,
'ready' => $total === 0,
'unified_posting' => $this->usesUnifiedPosting(),
];
}
protected function resolveContext(string $type, int $id, int $businessId): array
{
if (in_array($type, ['sell_payment', 'purchase_payment'], true)) {
$payment = TransactionPayment::where('id', $id)->where('business_id', $businessId)->firstOrFail();
$transaction = Transaction::find($payment->transaction_id);
return [
'amount' => (float) $payment->amount,
'operation_date' => optional($payment->paid_on)->format('Y-m-d H:i:s') ?? now()->toDateTimeString(),
'transaction_id' => null,
'transaction_payment_id' => $payment->id,
'location_id' => $transaction?->location_id,
'reference' => $payment->payment_ref_no ?? (string) $payment->id,
];
}
$transaction = Transaction::where('business_id', $businessId)->where('id', $id)->firstOrFail();
return [
'amount' => (float) $transaction->final_total,
'operation_date' => optional($transaction->transaction_date)->format('Y-m-d H:i:s') ?? now()->toDateTimeString(),
'transaction_id' => $transaction->id,
'transaction_payment_id' => null,
'location_id' => $transaction->location_id,
'reference' => $transaction->invoice_no ?? (string) $transaction->id,
];
}
protected function noteForType(string $type, array $context): string
{
return match ($type) {
'sell' => 'Sell invoice '.$context['reference'],
'purchase' => 'Purchase '.$context['reference'],
'expense' => 'Expense '.$context['reference'],
'sell_payment' => 'Sell payment '.$context['reference'],
'purchase_payment' => 'Purchase payment '.$context['reference'],
default => ucfirst(str_replace('_', ' ', $type)).' '.$context['reference'],
};
}
protected function countUnmappedTransactions(int $businessId, string $type, string $since): int
{
return (int) Transaction::where('transactions.business_id', $businessId)
->where('transactions.type', $type)
->where('transactions.status', 'final')
->whereDate('transactions.transaction_date', '>=', $since)
->whereNotExists(function ($q) {
$q->selectRaw('1')
->from('accounting_accounts_transactions as aat')
->whereColumn('aat.transaction_id', 'transactions.id');
})
->count();
}
protected function countUnmappedPayments(int $businessId, string $since): int
{
return (int) TransactionPayment::where('transaction_payments.business_id', $businessId)
->whereDate('transaction_payments.paid_on', '>=', $since)
->whereNotExists(function ($q) {
$q->selectRaw('1')
->from('accounting_accounts_transactions as aat')
->whereColumn('aat.transaction_payment_id', 'transaction_payments.id');
})
->count();
}
}