ultimatepos/Modules/Accounting/Services/PeriodCloseService.php

141 lines
5.0 KiB
PHP

<?php
namespace Modules\Accounting\Services;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Modules\Accounting\Domain\FinancialPostingRequest;
use Modules\Accounting\Entities\AccountingAccount;
use Modules\Accounting\Entities\AccountingFiscalPeriod;
use Modules\Accounting\Utils\AccountingUtil;
class PeriodCloseService
{
public function __construct(
protected FinancialPostingService $postingService,
protected AccountingUtil $accountingUtil
) {}
public 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();
}
public function createPeriod(int $businessId, string $name, string $startDate, string $endDate): AccountingFiscalPeriod
{
return AccountingFiscalPeriod::create([
'business_id' => $businessId,
'name' => $name,
'start_date' => $startDate,
'end_date' => $endDate,
'is_closed' => false,
]);
}
public function closePeriod(int $businessId, int $periodId, int $userId, ?int $retainedEarningsAccountId = null): AccountingFiscalPeriod
{
$period = AccountingFiscalPeriod::where('business_id', $businessId)->findOrFail($periodId);
if ($period->is_closed) {
throw new \RuntimeException('Period already closed.');
}
$balanceFormula = $this->accountingUtil->balanceFormula();
$incomeExpense = AccountingAccount::join('accounting_accounts_transactions as AAT', 'AAT.accounting_account_id', '=', 'accounting_accounts.id')
->where('accounting_accounts.business_id', $businessId)
->whereIn('accounting_accounts.account_primary_type', ['income', 'expense', 'expenses'])
->whereDate('AAT.operation_date', '>=', $period->start_date)
->whereDate('AAT.operation_date', '<=', $period->end_date)
->select(
'accounting_accounts.id',
'accounting_accounts.account_primary_type',
DB::raw($balanceFormula)
)
->groupBy('accounting_accounts.id', 'accounting_accounts.account_primary_type')
->get();
$lines = [];
$netIncome = 0.0;
foreach ($incomeExpense as $row) {
$balance = (float) $row->balance;
if (abs($balance) < 0.01) {
continue;
}
if ($row->account_primary_type === 'income') {
$netIncome += $balance;
$lines[] = [
'accounting_account_id' => $row->id,
'amount' => abs($balance),
'type' => $balance > 0 ? 'debit' : 'credit',
];
} elseif (in_array($row->account_primary_type, ['expense', 'expenses'], true)) {
$netIncome -= $balance;
$lines[] = [
'accounting_account_id' => $row->id,
'amount' => abs($balance),
'type' => $balance > 0 ? 'credit' : 'debit',
];
}
}
if ($retainedEarningsAccountId && abs($netIncome) >= 0.01) {
$lines[] = [
'accounting_account_id' => $retainedEarningsAccountId,
'amount' => abs($netIncome),
'type' => $netIncome >= 0 ? 'credit' : 'debit',
];
}
if (! empty($lines)) {
$mapping = $this->postingService->post(new FinancialPostingRequest(
businessId: $businessId,
userId: $userId,
sourceType: 'period_close',
sourceId: $period->id,
note: 'Period close: '.$period->name,
lines: $lines,
operationDate: Carbon::parse($period->end_date)->endOfDay()->toDateTimeString(),
mappingType: 'journal_entry',
));
$period->closing_journal_mapping_id = $mapping?->id;
}
$period->is_closed = true;
$period->closed_at = now();
$period->closed_by = $userId;
$period->save();
return $period;
}
public function closeAndRollForward(
int $businessId,
int $periodId,
int $userId,
?int $retainedEarningsAccountId = null,
bool $createNextPeriod = false
): AccountingFiscalPeriod {
$period = $this->closePeriod($businessId, $periodId, $userId, $retainedEarningsAccountId);
if ($createNextPeriod) {
$start = Carbon::parse($period->end_date)->addDay();
$end = $start->copy()->addYear()->subDay();
$this->createPeriod(
$businessId,
(string) $start->year.' - '.($start->year + 1),
$start->toDateString(),
$end->toDateString()
);
}
return $period;
}
}