229 lines
9.5 KiB
PHP
229 lines
9.5 KiB
PHP
<?php
|
|
|
|
namespace Modules\Accounting\Services;
|
|
|
|
use App\Utils\Util;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Modules\Accounting\Domain\FinancialPostingRequest;
|
|
use Modules\Accounting\Entities\AccountingAccountsTransaction;
|
|
use Modules\Accounting\Entities\AccountingAccTransMapping;
|
|
use Modules\Accounting\Entities\AccountingFiscalPeriod;
|
|
use Modules\Accounting\Entities\AccountingPostingLog;
|
|
use Modules\Accounting\Services\JournalApprovalService;
|
|
use Modules\Accounting\Utils\AccountingUtil;
|
|
|
|
class FinancialPostingService
|
|
{
|
|
public function __construct(
|
|
protected Util $util,
|
|
protected AccountingUtil $accountingUtil
|
|
) {}
|
|
|
|
public function post(FinancialPostingRequest $request, bool $allowDuplicate = false): ?AccountingAccTransMapping
|
|
{
|
|
if (! $request->isBalanced()) {
|
|
throw new \InvalidArgumentException('Journal lines are not balanced.');
|
|
}
|
|
|
|
if ($request->operationDate && $this->isDateLocked($request->businessId, $request->operationDate)) {
|
|
throw new \RuntimeException('Accounting period is closed for the given date.');
|
|
}
|
|
|
|
$existing = AccountingPostingLog::where('business_id', $request->businessId)
|
|
->where('posting_key', $request->postingKey())
|
|
->where('status', 'posted')
|
|
->first();
|
|
|
|
if ($existing && ! $allowDuplicate) {
|
|
return AccountingAccTransMapping::find($existing->acc_trans_mapping_id);
|
|
}
|
|
|
|
return DB::transaction(function () use ($request, $existing, $allowDuplicate) {
|
|
if ($existing && $allowDuplicate) {
|
|
$this->reverseByPostingKey($request->businessId, $request->postingKey(), $request->userId);
|
|
}
|
|
|
|
$settings = $this->accountingUtil->getAccountingSettings($request->businessId);
|
|
$refNo = $request->refNo;
|
|
if (empty($refNo)) {
|
|
$refCount = $this->util->setAndGetReferenceCount('journal_entry');
|
|
$prefix = $settings['journal_entry_prefix'] ?? 'JE-';
|
|
$refNo = $this->util->generateReferenceNumber('journal_entry', $refCount, $request->businessId, $prefix);
|
|
}
|
|
|
|
$mapping = new AccountingAccTransMapping();
|
|
$mapping->business_id = $request->businessId;
|
|
$mapping->ref_no = $refNo;
|
|
$mapping->note = $request->note;
|
|
$mapping->type = $request->mappingType;
|
|
$mapping->created_by = $request->userId;
|
|
$mapping->operation_date = $request->operationDate ?? now();
|
|
if (\Illuminate\Support\Facades\Schema::hasColumn('accounting_acc_trans_mappings', 'approval_status')) {
|
|
$approvalService = app(JournalApprovalService::class);
|
|
$mapping->approval_status = $approvalService->initialStatus($request->businessId);
|
|
if ($mapping->approval_status === 'approved') {
|
|
$mapping->approved_by = $request->userId;
|
|
$mapping->approved_at = now();
|
|
}
|
|
}
|
|
$mapping->save();
|
|
|
|
$approvalService = app(JournalApprovalService::class);
|
|
$postable = $approvalService->isPostable($mapping);
|
|
$payload = $this->serializePostingPayload($request);
|
|
|
|
if ($postable) {
|
|
$this->createJournalLines($mapping, $request);
|
|
}
|
|
|
|
AccountingPostingLog::updateOrCreate(
|
|
[
|
|
'business_id' => $request->businessId,
|
|
'posting_key' => $request->postingKey(),
|
|
],
|
|
[
|
|
'source_type' => $request->sourceType,
|
|
'source_id' => is_numeric($request->sourceId) ? (int) $request->sourceId : null,
|
|
'acc_trans_mapping_id' => $mapping->id,
|
|
'status' => $postable ? 'posted' : 'pending',
|
|
'payload' => json_encode($payload),
|
|
'error_message' => null,
|
|
'created_by' => $request->userId,
|
|
]
|
|
);
|
|
|
|
return $mapping;
|
|
});
|
|
}
|
|
|
|
public function postPendingJournalLines(AccountingAccTransMapping $mapping): void
|
|
{
|
|
if (! app(JournalApprovalService::class)->isPostable($mapping)) {
|
|
return;
|
|
}
|
|
|
|
$log = AccountingPostingLog::where('acc_trans_mapping_id', $mapping->id)
|
|
->where('status', 'pending')
|
|
->first();
|
|
|
|
if (! $log || AccountingAccountsTransaction::where('acc_trans_mapping_id', $mapping->id)->exists()) {
|
|
return;
|
|
}
|
|
|
|
$payload = json_decode($log->payload, true);
|
|
if (empty($payload['lines'])) {
|
|
return;
|
|
}
|
|
|
|
$request = new FinancialPostingRequest(
|
|
businessId: (int) $mapping->business_id,
|
|
userId: (int) ($mapping->approved_by ?? $mapping->created_by),
|
|
sourceType: (string) ($log->source_type ?? 'journal_entry'),
|
|
sourceId: $log->source_id ?? $mapping->id,
|
|
note: (string) ($mapping->note ?? ''),
|
|
lines: $payload['lines'],
|
|
operationDate: $mapping->operation_date ? (string) $mapping->operation_date : null,
|
|
costCenterId: $payload['cost_center_id'] ?? null,
|
|
projectId: $payload['project_id'] ?? null,
|
|
holdingEntityId: $payload['holding_entity_id'] ?? null,
|
|
transactionId: $payload['transaction_id'] ?? null,
|
|
transactionPaymentId: $payload['transaction_payment_id'] ?? null,
|
|
mappingType: (string) ($mapping->type ?? 'journal_entry'),
|
|
refNo: $mapping->ref_no,
|
|
);
|
|
|
|
DB::transaction(function () use ($mapping, $request, $log) {
|
|
$this->createJournalLines($mapping, $request);
|
|
$log->update(['status' => 'posted', 'error_message' => null]);
|
|
});
|
|
}
|
|
|
|
protected function createJournalLines(AccountingAccTransMapping $mapping, FinancialPostingRequest $request): void
|
|
{
|
|
foreach ($request->lines as $line) {
|
|
AccountingAccountsTransaction::create([
|
|
'accounting_account_id' => $line['accounting_account_id'],
|
|
'amount' => $line['amount'],
|
|
'type' => $line['type'],
|
|
'sub_type' => $line['sub_type'] ?? $request->mappingType,
|
|
'created_by' => $request->userId,
|
|
'operation_date' => $mapping->operation_date,
|
|
'acc_trans_mapping_id' => $mapping->id,
|
|
'note' => $line['note'] ?? null,
|
|
'cost_center_id' => $line['cost_center_id'] ?? $request->costCenterId,
|
|
'project_id' => $line['project_id'] ?? $request->projectId,
|
|
'holding_entity_id' => $line['holding_entity_id'] ?? $request->holdingEntityId,
|
|
'transaction_id' => $line['transaction_id'] ?? $request->transactionId,
|
|
'transaction_payment_id' => $line['transaction_payment_id'] ?? $request->transactionPaymentId,
|
|
'map_type' => $line['map_type'] ?? null,
|
|
'source_type' => $request->sourceType,
|
|
'source_id' => is_numeric($request->sourceId) ? (int) $request->sourceId : null,
|
|
'posting_key' => $request->postingKey(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
protected function serializePostingPayload(FinancialPostingRequest $request): array
|
|
{
|
|
return [
|
|
'lines' => $request->lines,
|
|
'mapping_type' => $request->mappingType,
|
|
'cost_center_id' => $request->costCenterId,
|
|
'project_id' => $request->projectId,
|
|
'holding_entity_id' => $request->holdingEntityId,
|
|
'transaction_id' => $request->transactionId,
|
|
'transaction_payment_id' => $request->transactionPaymentId,
|
|
];
|
|
}
|
|
|
|
public function reverseByPostingKey(int $businessId, string $postingKey, int $userId): void
|
|
{
|
|
$log = AccountingPostingLog::where('business_id', $businessId)
|
|
->where('posting_key', $postingKey)
|
|
->where('status', 'posted')
|
|
->first();
|
|
|
|
if (! $log || ! $log->acc_trans_mapping_id) {
|
|
return;
|
|
}
|
|
|
|
AccountingAccountsTransaction::where('acc_trans_mapping_id', $log->acc_trans_mapping_id)->delete();
|
|
AccountingAccTransMapping::where('id', $log->acc_trans_mapping_id)->delete();
|
|
|
|
$log->update(['status' => 'reversed', 'error_message' => 'Reversed by user '.$userId]);
|
|
}
|
|
|
|
public function linkPaymentAccountToGl(int $businessId, int $paymentAccountId, int $glAccountId): void
|
|
{
|
|
$paymentAccount = DB::table('accounts')
|
|
->where('id', $paymentAccountId)
|
|
->where('business_id', $businessId)
|
|
->first(['id']);
|
|
if (! $paymentAccount) {
|
|
throw new \InvalidArgumentException('Payment account not found for business.');
|
|
}
|
|
|
|
$glAccount = DB::table('accounting_accounts')
|
|
->where('id', $glAccountId)
|
|
->where('business_id', $businessId)
|
|
->first(['id']);
|
|
if (! $glAccount) {
|
|
throw new \InvalidArgumentException('GL account not found for business.');
|
|
}
|
|
|
|
DB::table('accounts')
|
|
->where('id', $paymentAccountId)
|
|
->where('business_id', $businessId)
|
|
->update(['accounting_account_id' => $glAccountId]);
|
|
}
|
|
|
|
protected function isDateLocked(int $businessId, string $date): bool
|
|
{
|
|
return AccountingFiscalPeriod::where('business_id', $businessId)
|
|
->where('is_closed', true)
|
|
->whereDate('start_date', '<=', $date)
|
|
->whereDate('end_date', '>=', $date)
|
|
->exists();
|
|
}
|
|
}
|