session()->get('user.business_id'); if (! (auth()->user()->can('superadmin') || $this->moduleUtil->hasThePermissionInSubscription($business_id, 'accounting_module')) || ! auth()->user()->can($permission)) { abort(403, 'Unauthorized action.'); } } // --- Health --- public function health(AccountingHealthService $healthService) { $this->authorizeAccounting('accounting.view_reports'); $business_id = request()->session()->get('user.business_id'); $health = $healthService->assess($business_id); return view('accounting::holding.health', compact('health')); } // --- Fiscal periods --- public function fiscalPeriods() { $this->authorizeAccounting('accounting.manage_accounts'); $business_id = request()->session()->get('user.business_id'); $periods = AccountingFiscalPeriod::where('business_id', $business_id)->orderByDesc('start_date')->get(); $accounts = AccountingAccount::forDropdown($business_id, false); return view('accounting::holding.fiscal_periods', compact('periods', 'accounts')); } public function storeFiscalPeriod(Request $request, PeriodCloseService $periodCloseService) { $this->authorizeAccounting('accounting.manage_accounts'); $business_id = request()->session()->get('user.business_id'); $request->validate([ 'name' => 'required|string|max:191', 'start_date' => 'required|date', 'end_date' => 'required|date|after_or_equal:start_date', ]); $periodCloseService->createPeriod( $business_id, $request->name, $request->start_date, $request->end_date ); return redirect()->back()->with('status', ['success' => 1, 'msg' => __('lang_v1.added_success')]); } public function closeFiscalPeriod($id, Request $request, PeriodCloseService $periodCloseService) { $this->authorizeAccounting('accounting.manage_accounts'); $business_id = request()->session()->get('user.business_id'); $user_id = request()->session()->get('user.id'); $periodCloseService->closeAndRollForward( $business_id, (int) $id, $user_id, $request->input('retained_earnings_account_id'), (bool) $request->input('create_next_period') ); return redirect()->back()->with('status', ['success' => 1, 'msg' => __('accounting::lang.period_closed_success')]); } // --- Cheques --- public function cheques() { $this->authorizeAccounting('accounting.map_transactions'); $business_id = request()->session()->get('user.business_id'); if (request()->ajax()) { $accounts = AccountingAccount::forDropdown($business_id, false); $cheques = AccountingCheque::where('business_id', $business_id)->orderByDesc('id'); return DataTables::of($cheques) ->addColumn('action', function ($row) use ($accounts) { return view('accounting::holding.partials.cheque_actions', [ 'row' => $row, 'accounts' => $accounts, ])->render(); }) ->editColumn('amount', function ($row) { return $this->util->num_f($row->amount, true); }) ->rawColumns(['action']) ->make(true); } $accounts = AccountingAccount::forDropdown($business_id, false); return view('accounting::holding.cheques', compact('accounts')); } public function storeCheque(Request $request, ChequeService $chequeService) { $this->authorizeAccounting('accounting.map_transactions'); $business_id = request()->session()->get('user.business_id'); $user_id = request()->session()->get('user.id'); $request->validate([ 'direction' => 'required|in:received,paid', 'cheque_number' => 'required|string|max:100', 'amount' => 'required|numeric|min:0.01', 'issue_date' => 'required|date', ]); $chequeService->register($request->only([ 'direction', 'cheque_number', 'bank_name', 'amount', 'issue_date', 'due_date', 'contact_id', 'accounting_account_id', 'counter_account_id', 'note', ]), $business_id, $user_id); return redirect()->back()->with('status', ['success' => 1, 'msg' => __('lang_v1.added_success')]); } public function transitionCheque($id, Request $request, ChequeService $chequeService) { $this->authorizeAccounting('accounting.map_transactions'); $business_id = request()->session()->get('user.business_id'); $user_id = request()->session()->get('user.id'); $cheque = AccountingCheque::where('business_id', $business_id)->findOrFail($id); $request->validate([ 'status' => 'required|in:deposited,cleared,bounced,endorsed,cancelled,pending', 'counter_account_id' => 'nullable|integer', ]); try { $chequeService->transition( $cheque, $request->input('status'), $user_id, $request->input('counter_account_id') ? (int) $request->input('counter_account_id') : null ); } catch (\RuntimeException $e) { return redirect()->back()->with('status', ['success' => 0, 'msg' => $e->getMessage()]); } return redirect()->back()->with('status', ['success' => 1, 'msg' => __('lang_v1.updated_success')]); } // --- Installments --- public function installments(InstallmentBridgeService $bridgeService, InstallmentService $installmentService) { $this->authorizeAccounting('accounting.map_transactions'); $business_id = request()->session()->get('user.business_id'); $plans = AccountingInstallmentPlan::where('business_id', $business_id) ->with('schedule') ->orderByDesc('id') ->paginate(20); $eligibleSells = $bridgeService->eligibleSells($business_id); $accounts = AccountingAccount::forDropdown($business_id, false); return view('accounting::holding.installments', compact('plans', 'eligibleSells', 'accounts', 'installmentService')); } public function createInstallmentFromSell($transactionId, InstallmentBridgeService $bridgeService) { $this->authorizeAccounting('accounting.map_transactions'); $business_id = request()->session()->get('user.business_id'); $user_id = request()->session()->get('user.id'); $transaction = Transaction::where('business_id', $business_id) ->where('type', 'sell') ->findOrFail($transactionId); $installmentCount = (int) request()->input('installment_count', $transaction->pay_term_number ?: 3); $plan = $bridgeService->createFromSell($transaction, $user_id, $installmentCount); if (! $plan) { return redirect()->back()->with('status', ['success' => 0, 'msg' => __('accounting::lang.installment_create_failed')]); } return redirect()->back()->with('status', ['success' => 1, 'msg' => __('accounting::lang.installment_created_from_sell')]); } public function storeInstallmentPlan(Request $request, InstallmentService $installmentService) { $this->authorizeAccounting('accounting.map_transactions'); $business_id = request()->session()->get('user.business_id'); $user_id = request()->session()->get('user.id'); $request->validate([ 'transaction_id' => 'required|integer', 'principal_amount' => 'required|numeric|min:0.01', 'installment_count' => 'required|integer|min:1', 'start_date' => 'required|date', ]); $installmentService->createPlan($request->all(), $business_id, $user_id); return redirect()->back()->with('status', ['success' => 1, 'msg' => __('lang_v1.added_success')]); } public function payInstallment($id, Request $request, InstallmentService $installmentService) { $this->authorizeAccounting('accounting.map_transactions'); $schedule = AccountingInstallmentSchedule::with('plan')->findOrFail($id); $installmentService->recordPayment( $schedule, (float) $request->input('amount'), request()->session()->get('user.id'), (int) $request->input('debit_account_id'), (int) $request->input('credit_account_id') ); return redirect()->back()->with('status', ['success' => 1, 'msg' => __('lang_v1.updated_success')]); } // --- Depreciation --- public function depreciation() { $this->authorizeAccounting('accounting.add_journal'); $business_id = request()->session()->get('user.business_id'); $runs = AccountingDepreciationRun::where('business_id', $business_id)->orderByDesc('id')->paginate(20); $accounts = AccountingAccount::forDropdown($business_id, false); return view('accounting::holding.depreciation', compact('runs', 'accounts')); } public function storeDepreciation(Request $request, DepreciationService $depreciationService) { $this->authorizeAccounting('accounting.add_journal'); $business_id = request()->session()->get('user.business_id'); $user_id = request()->session()->get('user.id'); $depreciationService->createRun($request->all(), $business_id, $user_id); return redirect()->back()->with('status', ['success' => 1, 'msg' => __('lang_v1.added_success')]); } public function postDepreciation($id, DepreciationService $depreciationService) { $this->authorizeAccounting('accounting.add_journal'); $business_id = request()->session()->get('user.business_id'); $user_id = request()->session()->get('user.id'); $run = AccountingDepreciationRun::where('business_id', $business_id)->findOrFail($id); $depreciationService->postRun($run, $user_id); return redirect()->back()->with('status', ['success' => 1, 'msg' => __('accounting::lang.depreciation_posted')]); } public function batchDepreciation(Request $request, DepreciationService $depreciationService) { $this->authorizeAccounting('accounting.add_journal'); $business_id = request()->session()->get('user.business_id'); $user_id = request()->session()->get('user.id'); $request->validate([ 'period_date' => 'required|date', 'expense_account_id' => 'required|integer', 'accumulated_account_id' => 'required|integer', ]); $result = $depreciationService->batchFromAssets( $business_id, $user_id, $request->period_date, (int) $request->expense_account_id, (int) $request->accumulated_account_id ); return redirect()->back()->with('status', [ 'success' => 1, 'msg' => __('accounting::lang.depreciation_batch_done', ['created' => $result['created'], 'posted' => $result['posted']]), ]); } // --- Bank reconciliation --- public function reconciliations() { $this->authorizeAccounting('accounting.view_reports'); $business_id = request()->session()->get('user.business_id'); $reconciliations = AccountingBankReconciliation::where('business_id', $business_id)->orderByDesc('id')->paginate(20); $accounts = AccountingAccount::forDropdown($business_id, false); return view('accounting::holding.reconciliations', compact('reconciliations', 'accounts')); } public function storeReconciliation(Request $request, BankReconciliationService $bankReconciliationService) { $this->authorizeAccounting('accounting.view_reports'); $business_id = request()->session()->get('user.business_id'); $user_id = request()->session()->get('user.id'); $request->validate([ 'accounting_account_id' => 'required|integer', 'statement_date' => 'required|date', 'statement_balance' => 'required|numeric', ]); $bankReconciliationService->create($business_id, $request->all(), $user_id); return redirect()->back()->with('status', ['success' => 1, 'msg' => __('lang_v1.added_success')]); } public function completeReconciliation($id, Request $request, BankReconciliationService $bankReconciliationService) { $this->authorizeAccounting('accounting.view_reports'); $business_id = request()->session()->get('user.business_id'); $reconciliation = AccountingBankReconciliation::where('business_id', $business_id)->findOrFail($id); $bankReconciliationService->complete($reconciliation, $request->input('matched_items', [])); return redirect()->back()->with('status', ['success' => 1, 'msg' => __('accounting::lang.reconciliation_completed')]); } public function syncEntityTagging(HoldingEntityResolutionService $entityResolution) { $this->authorizeAccounting('accounting.manage_accounts'); $business_id = request()->session()->get('user.business_id'); $updated = $entityResolution->backfillUntagged($business_id); return redirect()->back()->with('status', [ 'success' => 1, 'msg' => __('accounting::lang.entity_tagging_synced', ['count' => $updated]), ]); } // --- Consolidation --- public function consolidation(ConsolidationService $consolidationService) { $this->authorizeAccounting('accounting.view_reports'); $business_id = request()->session()->get('user.business_id'); $runs = AccountingConsolidationRun::where('business_id', $business_id)->orderByDesc('id')->paginate(20); $entities = HoldingEntity::where('business_id', $business_id) ->where('is_active', true) ->orderBy('name') ->get(['id', 'name', 'meta']); $defaults = $this->buildConsolidationDefaults($business_id, $entities, null); $readiness = $consolidationService->readiness($business_id); return view('accounting::holding.consolidation', compact('runs', 'entities', 'defaults', 'readiness')); } public function exchangeRates(ExchangeRateService $exchangeRateService, AccountingUtil $accountingUtil) { $this->authorizeAccounting('accounting.manage_accounts'); $business_id = request()->session()->get('user.business_id'); $rates = $exchangeRateService->listRates($business_id); $business = Business::with('currency')->find($business_id); $base_currency_code = strtoupper((string) optional($business->currency)->code ?: 'IRR'); $accounting_settings = $accountingUtil->getAccountingSettings($business_id); $fx_providers = [ 'open_er_api' => 'open.er-api.com (free)', 'iran_market' => __('accounting::lang.iran_market_provider'), 'exchangerate_api_v4' => 'ExchangeRate-API v4 (free)', 'exchangerate_api_v6' => 'ExchangeRate-API v6 (API key)', ]; return view('accounting::holding.exchange_rates', compact('rates', 'base_currency_code', 'accounting_settings', 'fx_providers')); } public function storeExchangeRate(Request $request, ExchangeRateService $exchangeRateService) { $this->authorizeAccounting('accounting.manage_accounts'); $business_id = request()->session()->get('user.business_id'); $request->validate([ 'from_currency' => 'required|string|max:10', 'to_currency' => 'required|string|max:10', 'rate' => 'required|numeric|min:0.000001', 'rate_date' => 'required|date', ]); $exchangeRateService->upsertRate( $business_id, $request->from_currency, $request->to_currency, (float) $request->rate, $request->rate_date, 'manual' ); return redirect()->back()->with('status', ['success' => 1, 'msg' => __('lang_v1.added_success')]); } public function syncExchangeRates(Request $request, ExchangeRateService $exchangeRateService) { $this->authorizeAccounting('accounting.manage_accounts'); $business_id = request()->session()->get('user.business_id'); $result = $exchangeRateService->syncForBusiness( $business_id, $request->input('rate_date', now()->toDateString()), true ); return redirect()->back()->with('status', [ 'success' => 1, 'msg' => __('accounting::lang.exchange_rates_synced', [ 'upserted' => $result['upserted'], 'entities' => $result['entities_updated'], 'api' => $result['api_fetched'] ?? 0, ]), ]); } public function saveExchangeRateSettings(Request $request, AccountingUtil $accountingUtil) { $this->authorizeAccounting('accounting.manage_accounts'); $business_id = request()->session()->get('user.business_id'); $existing = $accountingUtil->getAccountingSettings($business_id); $merged = array_merge($existing, [ 'fx_api_enabled' => $request->has('fx_api_enabled') ? 1 : 0, 'fx_api_provider' => $request->input('fx_api_provider', config('accounting.fx_api.default_provider', 'open_er_api')), 'fx_api_key' => $request->input('fx_api_key', ''), ]); Business::where('id', $business_id)->update(['accounting_settings' => json_encode($merged)]); return redirect()->back()->with('status', ['success' => 1, 'msg' => __('lang_v1.updated_success')]); } public function runConsolidation(Request $request, ConsolidationService $consolidationService) { $this->authorizeAccounting('accounting.view_reports'); $business_id = request()->session()->get('user.business_id'); $user_id = request()->session()->get('user.id'); $request->validate([ 'period_start' => 'required|date', 'period_end' => 'required|date|after_or_equal:period_start', 'consolidation_currency_code' => 'nullable|string|max:10', 'entity_rate' => 'nullable|array', ]); $entities = HoldingEntity::where('business_id', $business_id) ->where('is_active', true) ->orderBy('name') ->get(['id', 'name', 'meta']); $defaults = $this->buildConsolidationDefaults( $business_id, $entities, (string) $request->input('consolidation_currency_code', 'IRR') ); $rawRates = (array) $request->input('entity_rate', $defaults['entity_rates']); $entityRates = []; foreach ($rawRates as $entityId => $rate) { $entityRates[(int) $entityId] = max(1.0E-10, (float) $rate); } $rawOfficial = (array) $request->input('entity_official_rate', $defaults['official_entity_rates']); $officialRates = []; foreach ($rawOfficial as $entityId => $rate) { $officialRates[(int) $entityId] = max(1.0E-10, (float) $rate); } $consolidationService->run( $business_id, $request->period_start, $request->period_end, $user_id, $request->input('parent_entity_id'), (string) $request->input('consolidation_currency_code', $defaults['target_currency_code']), $entityRates, (string) $request->input('rate_mode', 'market'), $officialRates ); return redirect()->back()->with('status', ['success' => 1, 'msg' => __('accounting::lang.consolidation_run_created')]); } public function finalizeConsolidation($id, ConsolidationService $consolidationService) { $this->authorizeAccounting('accounting.view_reports'); $business_id = request()->session()->get('user.business_id'); $run = AccountingConsolidationRun::where('business_id', $business_id)->findOrFail($id); $consolidationService->finalize($run); return redirect()->back()->with('status', ['success' => 1, 'msg' => __('accounting::lang.consolidation_finalized')]); } // --- Payment account GL link --- public function linkPaymentAccount(Request $request, FinancialPostingService $postingService) { $this->authorizeAccounting('accounting.manage_accounts'); $business_id = request()->session()->get('user.business_id'); $request->validate([ 'payment_account_id' => 'required|integer', 'accounting_account_id' => 'required|integer', ]); $postingService->linkPaymentAccountToGl( (int) $business_id, (int) $request->payment_account_id, (int) $request->accounting_account_id ); return redirect()->back()->with('status', ['success' => 1, 'msg' => __('accounting::lang.payment_account_linked')]); } public function autoLinkPaymentAccounts(PaymentAccountGlLinkService $linkService) { $this->authorizeAccounting('accounting.manage_accounts'); $business_id = (int) request()->session()->get('user.business_id'); $linked = $linkService->linkByNameMatch($business_id); return redirect()->back()->with('status', [ 'success' => 1, 'msg' => __('accounting::lang.payment_accounts_auto_linked', ['count' => $linked]), ]); } public function postFinancialContract(Request $request, FinancialPostingService $postingService) { $this->authorizeAccounting('accounting.add_journal'); $business_id = request()->session()->get('user.business_id'); $user_id = request()->session()->get('user.id'); $payload = $request->validate([ 'source_type' => 'required|string|max:100', 'source_id' => 'required', 'note' => 'nullable|string|max:5000', 'operation_date' => 'nullable|date', 'mapping_type' => 'nullable|string|max:100', 'ref_no' => 'nullable|string|max:191', 'cost_center_id' => 'nullable|integer', 'project_id' => 'nullable|integer', 'holding_entity_id' => 'nullable|integer', 'allow_duplicate' => 'nullable|boolean', 'lines' => 'required|array|min:2', 'lines.*.accounting_account_id' => 'required|integer', 'lines.*.amount' => 'required|numeric|min:0.000001', 'lines.*.type' => 'required|in:debit,credit', 'lines.*.sub_type' => 'nullable|string|max:100', 'lines.*.note' => 'nullable|string|max:500', 'lines.*.cost_center_id' => 'nullable|integer', 'lines.*.project_id' => 'nullable|integer', 'lines.*.holding_entity_id' => 'nullable|integer', ]); $postingRequest = new FinancialPostingRequest( businessId: (int) $business_id, userId: (int) $user_id, sourceType: (string) $payload['source_type'], sourceId: $payload['source_id'], note: (string) ($payload['note'] ?? ''), lines: $payload['lines'], operationDate: $payload['operation_date'] ?? null, costCenterId: isset($payload['cost_center_id']) ? (int) $payload['cost_center_id'] : null, projectId: isset($payload['project_id']) ? (int) $payload['project_id'] : null, holdingEntityId: isset($payload['holding_entity_id']) ? (int) $payload['holding_entity_id'] : null, mappingType: (string) ($payload['mapping_type'] ?? 'journal_entry'), refNo: $payload['ref_no'] ?? null, ); $mapping = $postingService->post($postingRequest, (bool) ($payload['allow_duplicate'] ?? false)); return response()->json([ 'success' => true, 'posting_key' => $postingRequest->postingKey(), 'mapping_id' => $mapping?->id, 'message' => __('accounting::lang.financial_contract_posted'), ]); } public function mappingGaps(LocationDefaultMappingService $mappingService) { $this->authorizeAccounting('accounting.manage_accounts'); $business_id = (int) request()->session()->get('user.business_id'); $report = $mappingService->assess($business_id); if (request()->expectsJson() || request()->ajax()) { return response()->json($report); } return view('accounting::holding.mapping_gaps', compact('report')); } public function syncLocationMappings(Request $request, LocationDefaultMappingService $mappingService) { $this->authorizeAccounting('accounting.manage_accounts'); $business_id = (int) request()->session()->get('user.business_id'); $overwrite = (bool) $request->input('overwrite', false); $result = $mappingService->sync($business_id, $overwrite); if ($request->expectsJson() || $request->ajax()) { return response()->json([ 'success' => true, 'message' => __('accounting::lang.location_mapping_synced', [ 'locations' => $result['updated_locations'], 'fields' => $result['filled_fields'], 'payment_links' => $result['payment_accounts_linked'], ]), 'result' => $result, ]); } return redirect()->back()->with('status', [ 'success' => 1, 'msg' => __('accounting::lang.location_mapping_synced', [ 'locations' => $result['updated_locations'], 'fields' => $result['filled_fields'], 'payment_links' => $result['payment_accounts_linked'], ]), ]); } public function analyticalDimensions() { $this->authorizeAccounting('accounting.manage_accounts'); $business_id = request()->session()->get('user.business_id'); $dimensions = AccountingAnalyticalDimension::where('business_id', $business_id)->orderBy('dimension_type')->get(); return view('accounting::holding.analytical_dimensions', compact('dimensions')); } public function storeAnalyticalDimension(Request $request) { $this->authorizeAccounting('accounting.manage_accounts'); $business_id = request()->session()->get('user.business_id'); $request->validate([ 'dimension_type' => 'required|in:cost_center,project,department', 'code' => 'required|string|max:50', 'name' => 'required|string|max:191', ]); AccountingAnalyticalDimension::create([ 'business_id' => $business_id, 'dimension_type' => $request->dimension_type, 'code' => $request->code, 'name' => $request->name, 'holding_entity_id' => $request->holding_entity_id, 'is_active' => true, ]); return redirect()->back()->with('status', ['success' => 1, 'msg' => __('lang_v1.added_success')]); } public function journalApprovals() { $this->authorizeAccounting('accounting.view_journal'); $business_id = request()->session()->get('user.business_id'); $journals = AccountingAccTransMapping::where('business_id', $business_id) ->where('type', 'journal_entry') ->where('approval_status', 'pending') ->orderByDesc('id') ->paginate(20); return view('accounting::holding.journal_approvals', compact('journals')); } public function approveJournal($id, JournalApprovalService $approvalService) { $this->authorizeAccounting('accounting.edit_journal'); $business_id = request()->session()->get('user.business_id'); $user_id = request()->session()->get('user.id'); $journal = AccountingAccTransMapping::where('business_id', $business_id)->findOrFail($id); $approvalService->approve($journal, $user_id); return redirect()->back()->with('status', ['success' => 1, 'msg' => __('accounting::lang.journal_approved')]); } public function rejectJournal($id, JournalApprovalService $approvalService) { $this->authorizeAccounting('accounting.edit_journal'); $business_id = request()->session()->get('user.business_id'); $user_id = request()->session()->get('user.id'); $journal = AccountingAccTransMapping::where('business_id', $business_id)->findOrFail($id); $approvalService->reject($journal, $user_id); return redirect()->back()->with('status', ['success' => 1, 'msg' => __('accounting::lang.journal_rejected')]); } public function openingBalance(OpeningBalanceService $openingBalanceService) { $this->authorizeAccounting('accounting.add_journal'); $business_id = request()->session()->get('user.business_id'); $accounts = AccountingAccount::forDropdown($business_id, false); $inactiveAccounts = $openingBalanceService->accountsWithoutActivity($business_id); return view('accounting::holding.opening_balance', compact('accounts', 'inactiveAccounts')); } public function storeOpeningBalance(Request $request, OpeningBalanceService $openingBalanceService) { $this->authorizeAccounting('accounting.add_journal'); $business_id = request()->session()->get('user.business_id'); $user_id = request()->session()->get('user.id'); $lines = []; foreach ($request->input('account_id', []) as $index => $accountId) { $debit = $request->input('debit.'.$index); $credit = $request->input('credit.'.$index); if (! empty($debit)) { $lines[] = ['accounting_account_id' => $accountId, 'amount' => $debit, 'type' => 'debit']; } if (! empty($credit)) { $lines[] = ['accounting_account_id' => $accountId, 'amount' => $credit, 'type' => 'credit']; } } $openingBalanceService->post( $business_id, $user_id, $request->input('operation_date', now()->toDateString()), $lines, $request->input('note') ); return redirect()->route('accounting.openingBalance')->with('status', ['success' => 1, 'msg' => __('accounting::lang.opening_balance_posted')]); } // --- Bank loans --- public function bankLoans() { $this->authorizeAccounting('accounting.map_transactions'); $business_id = request()->session()->get('user.business_id'); $loans = AccountingBankLoan::where('business_id', $business_id) ->with('payments') ->orderByDesc('id') ->paginate(20); $accounts = AccountingAccount::forDropdown($business_id, false); return view('accounting::holding.bank_loans', compact('loans', 'accounts')); } public function storeBankLoan(Request $request, BankLoanService $bankLoanService) { $this->authorizeAccounting('accounting.map_transactions'); $business_id = request()->session()->get('user.business_id'); $user_id = request()->session()->get('user.id'); $request->validate([ 'loan_number' => 'required|string|max:100', 'lender_name' => 'required|string|max:191', 'principal_amount' => 'required|numeric|min:0.01', 'disbursement_date' => 'required|date', ]); $bankLoanService->disburse($request->only([ 'loan_number', 'lender_name', 'principal_amount', 'interest_rate', 'term_months', 'disbursement_date', 'maturity_date', 'loan_account_id', 'bank_account_id', 'note', ]), $business_id, $user_id); return redirect()->back()->with('status', ['success' => 1, 'msg' => __('lang_v1.added_success')]); } public function payBankLoan($id, Request $request, BankLoanService $bankLoanService) { $this->authorizeAccounting('accounting.map_transactions'); $business_id = request()->session()->get('user.business_id'); $user_id = request()->session()->get('user.id'); $loan = AccountingBankLoan::where('business_id', $business_id)->findOrFail($id); $request->validate([ 'payment_date' => 'required|date', 'principal_amount' => 'nullable|numeric|min:0', 'interest_amount' => 'nullable|numeric|min:0', ]); $bankLoanService->recordPayment($loan, $request->only([ 'payment_date', 'principal_amount', 'interest_amount', 'bank_account_id', 'interest_account_id', 'note', ]), $user_id); return redirect()->back()->with('status', ['success' => 1, 'msg' => __('accounting::lang.loan_payment_recorded')]); } // --- Insurance --- public function insurancePremiums() { $this->authorizeAccounting('accounting.map_transactions'); $business_id = request()->session()->get('user.business_id'); $premiums = AccountingInsurancePremium::where('business_id', $business_id) ->orderByDesc('id') ->paginate(20); $accounts = AccountingAccount::forDropdown($business_id, false); return view('accounting::holding.insurance', compact('premiums', 'accounts')); } public function storeInsurancePremium(Request $request, InsurancePremiumService $insuranceService) { $this->authorizeAccounting('accounting.map_transactions'); $business_id = request()->session()->get('user.business_id'); $user_id = request()->session()->get('user.id'); $request->validate([ 'provider_name' => 'required|string|max:191', 'amount' => 'required|numeric|min:0.01', ]); $insuranceService->register($request->only([ 'policy_number', 'provider_name', 'premium_type', 'amount', 'due_date', 'note', ]), $business_id, $user_id); return redirect()->back()->with('status', ['success' => 1, 'msg' => __('lang_v1.added_success')]); } public function payInsurancePremium($id, Request $request, InsurancePremiumService $insuranceService) { $this->authorizeAccounting('accounting.map_transactions'); $business_id = request()->session()->get('user.business_id'); $user_id = request()->session()->get('user.id'); $premium = AccountingInsurancePremium::where('business_id', $business_id)->findOrFail($id); $request->validate([ 'paid_date' => 'required|date', 'expense_account_id' => 'required|integer', 'bank_account_id' => 'required|integer', ]); $insuranceService->markPaid($premium, $request->only([ 'paid_date', 'expense_account_id', 'bank_account_id', ]), $user_id); return redirect()->back()->with('status', ['success' => 1, 'msg' => __('accounting::lang.insurance_premium_paid')]); } // --- Payroll tax table --- public function payrollTaxTable() { $this->authorizeAccounting('accounting.manage_accounts'); $business_id = request()->session()->get('user.business_id'); $brackets = AccountingPayrollTaxBracket::where('business_id', $business_id) ->orderBy('sort_order') ->orderBy('min_amount') ->get(); return view('accounting::holding.payroll_tax_table', compact('brackets')); } public function storePayrollTaxBracket(Request $request, PayrollTaxTableService $taxTableService) { $this->authorizeAccounting('accounting.manage_accounts'); $business_id = request()->session()->get('user.business_id'); $request->validate([ 'name' => 'required|string|max:191', 'min_amount' => 'required|numeric|min:0', 'rate_percent' => 'required|numeric|min:0', ]); $taxTableService->store($request->only([ 'name', 'min_amount', 'max_amount', 'rate_percent', 'fixed_deduction', 'sort_order', 'is_active', ]), $business_id); return redirect()->back()->with('status', ['success' => 1, 'msg' => __('lang_v1.added_success')]); } public function updatePayrollTaxBracket($id, Request $request, PayrollTaxTableService $taxTableService) { $this->authorizeAccounting('accounting.manage_accounts'); $business_id = request()->session()->get('user.business_id'); $bracket = AccountingPayrollTaxBracket::where('business_id', $business_id)->findOrFail($id); $request->validate([ 'name' => 'required|string|max:191', 'min_amount' => 'required|numeric|min:0', 'rate_percent' => 'required|numeric|min:0', ]); $taxTableService->update($bracket, $request->only([ 'name', 'min_amount', 'max_amount', 'rate_percent', 'fixed_deduction', 'sort_order', 'is_active', ])); return redirect()->back()->with('status', ['success' => 1, 'msg' => __('lang_v1.updated_success')]); } public function deferredDocuments(DeferredDocumentsService $deferredService) { $this->authorizeAccounting('accounting.view_reports'); $business_id = request()->session()->get('user.business_id'); $summary = $deferredService->summary($business_id); $items = $deferredService->items($business_id); return view('accounting::holding.deferred_documents', compact('summary', 'items')); } public function chequeReport() { $this->authorizeAccounting('accounting.view_reports'); $business_id = request()->session()->get('user.business_id'); $query = AccountingCheque::where('business_id', $business_id); if (request()->filled('direction')) { $query->where('direction', request('direction')); } if (request()->filled('status')) { $query->where('status', request('status')); } $all = AccountingCheque::where('business_id', $business_id)->get(); $summary = [ 'received_pending' => [ 'label' => __('accounting::lang.received_pending_cheques'), 'count' => $all->where('direction', 'received')->whereIn('status', ['pending', 'deposited'])->count(), 'amount' => $all->where('direction', 'received')->whereIn('status', ['pending', 'deposited'])->sum('amount'), ], 'paid_pending' => [ 'label' => __('accounting::lang.paid_pending_cheques'), 'count' => $all->where('direction', 'paid')->whereIn('status', ['pending', 'deposited'])->count(), 'amount' => $all->where('direction', 'paid')->whereIn('status', ['pending', 'deposited'])->sum('amount'), ], 'cleared' => [ 'label' => __('accounting::lang.cleared_cheques'), 'count' => $all->where('status', 'cleared')->count(), 'amount' => $all->where('status', 'cleared')->sum('amount'), ], 'bounced' => [ 'label' => __('accounting::lang.bounced_cheques'), 'count' => $all->where('status', 'bounced')->count(), 'amount' => $all->where('status', 'bounced')->sum('amount'), ], ]; $cheques = $query->orderByDesc('due_date')->paginate(50); return view('accounting::holding.cheque_report', compact('summary', 'cheques')); } private function buildConsolidationDefaults(int $businessId, $entities, ?string $targetCurrencyCode): array { $exchangeRateService = app(ExchangeRateService::class); $business = Business::with('currency')->find($businessId); $baseCurrencyCode = (string) optional($business->currency)->code ?: 'IRR'; $target = strtoupper($targetCurrencyCode ?: $baseCurrencyCode); $entityRates = []; $officialEntityRates = []; foreach ($entities as $entity) { $entityCurrency = strtoupper((string) data_get($entity->meta, 'currency_code', $baseCurrencyCode)); $entityRates[(int) $entity->id] = $exchangeRateService->resolveEntityRate( $businessId, $entityCurrency, $target, (array) $entity->meta ); $officialEntityRates[(int) $entity->id] = $exchangeRateService->getRateByType( $businessId, $entityCurrency, $target, 'official' ) ?? $entityRates[(int) $entity->id]; } return [ 'base_currency_code' => $baseCurrencyCode, 'target_currency_code' => $target, 'entity_rates' => $entityRates, 'official_entity_rates' => $officialEntityRates, ]; } }