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; } }