175 lines
7.2 KiB
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();
|
|
}
|
|
}
|