From eeda6d980e60fe1f8f092b557b66a7d62066ade2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Wed, 11 Mar 2026 19:41:07 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[barobill]=20React=20=EC=97=B0=EB=8F=99?= =?UTF-8?q?=EC=9A=A9=20=EB=B0=94=EB=A1=9C=EB=B9=8C=20=EC=B9=B4=EB=93=9C/?= =?UTF-8?q?=EC=9D=80=ED=96=89/=ED=99=88=ED=83=9D=EC=8A=A4=20REST=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 바로빌 카드 거래 API (16 엔드포인트): 조회, 분할, 수동입력, 숨김/복원, 금액수정, 분개 - 바로빌 은행 거래 API (13 엔드포인트): 조회, 분할, 오버라이드, 수동입력, 잔액요약, 분개 - 홈택스 세금계산서 API (13 엔드포인트): 매출/매입 조회, 수동입력, 자체분개, 통합분개 - JournalEntry 소스 타입 상수 추가 (barobill_card, barobill_bank, hometax_invoice) --- .../V1/BarobillBankTransactionController.php | 287 +++++++++++++++ .../V1/BarobillCardTransactionController.php | 326 ++++++++++++++++++ .../Api/V1/HometaxInvoiceController.php | 278 +++++++++++++++ app/Models/Tenants/JournalEntry.php | 10 + .../BarobillBankTransactionService.php | 249 +++++++++++++ .../BarobillCardTransactionService.php | 308 +++++++++++++++++ app/Services/HometaxInvoiceService.php | 222 ++++++++++++ routes/api/v1/finance.php | 57 +++ 8 files changed, 1737 insertions(+) create mode 100644 app/Http/Controllers/Api/V1/BarobillBankTransactionController.php create mode 100644 app/Http/Controllers/Api/V1/BarobillCardTransactionController.php create mode 100644 app/Http/Controllers/Api/V1/HometaxInvoiceController.php create mode 100644 app/Services/BarobillBankTransactionService.php create mode 100644 app/Services/BarobillCardTransactionService.php create mode 100644 app/Services/HometaxInvoiceService.php diff --git a/app/Http/Controllers/Api/V1/BarobillBankTransactionController.php b/app/Http/Controllers/Api/V1/BarobillBankTransactionController.php new file mode 100644 index 0000000..2a8d14e --- /dev/null +++ b/app/Http/Controllers/Api/V1/BarobillBankTransactionController.php @@ -0,0 +1,287 @@ +validate([ + 'start_date' => 'nullable|date', + 'end_date' => 'nullable|date|after_or_equal:start_date', + 'bank_account_num' => 'nullable|string|max:50', + 'search' => 'nullable|string|max:100', + 'per_page' => 'nullable|integer|min:1|max:100', + 'page' => 'nullable|integer|min:1', + ]); + + return $this->service->index($params); + }, __('message.fetched')); + } + + /** + * 계좌 목록 (필터용) + */ + public function accounts(): JsonResponse + { + return ApiResponse::handle(function () { + return $this->service->accounts(); + }, __('message.fetched')); + } + + /** + * 잔액 요약 + */ + public function balanceSummary(Request $request): JsonResponse + { + return ApiResponse::handle(function () use ($request) { + $params = $request->validate([ + 'date' => 'nullable|date', + ]); + + return $this->service->balanceSummary($params); + }, __('message.fetched')); + } + + // ========================================================================= + // 분할 (Splits) + // ========================================================================= + + /** + * 거래 분할 조회 + */ + public function getSplits(Request $request): JsonResponse + { + return ApiResponse::handle(function () use ($request) { + $validated = $request->validate([ + 'unique_key' => 'required|string|max:500', + ]); + + return $this->service->getSplits($validated['unique_key']); + }, __('message.fetched')); + } + + /** + * 거래 분할 저장 + */ + public function saveSplits(Request $request): JsonResponse + { + return ApiResponse::handle(function () use ($request) { + $validated = $request->validate([ + 'unique_key' => 'required|string|max:500', + 'items' => 'required|array|min:1', + 'items.*.split_amount' => 'required|numeric', + 'items.*.account_code' => 'nullable|string|max:20', + 'items.*.account_name' => 'nullable|string|max:100', + 'items.*.deduction_type' => 'nullable|string|max:20', + 'items.*.evidence_name' => 'nullable|string|max:100', + 'items.*.description' => 'nullable|string|max:500', + 'items.*.memo' => 'nullable|string|max:500', + 'items.*.bank_account_num' => 'nullable|string|max:50', + 'items.*.trans_dt' => 'nullable|string|max:20', + 'items.*.trans_date' => 'nullable|date', + 'items.*.original_deposit' => 'nullable|numeric', + 'items.*.original_withdraw' => 'nullable|numeric', + 'items.*.summary' => 'nullable|string|max:500', + ]); + + return $this->service->saveSplits($validated['unique_key'], $validated['items']); + }, __('message.created')); + } + + /** + * 거래 분할 삭제 + */ + public function deleteSplits(Request $request): JsonResponse + { + return ApiResponse::handle(function () use ($request) { + $validated = $request->validate([ + 'unique_key' => 'required|string|max:500', + ]); + + return $this->service->deleteSplits($validated['unique_key']); + }, __('message.deleted')); + } + + // ========================================================================= + // 오버라이드 (Override) + // ========================================================================= + + /** + * 적요/분류 오버라이드 저장 + */ + public function saveOverride(Request $request): JsonResponse + { + return ApiResponse::handle(function () use ($request) { + $validated = $request->validate([ + 'unique_key' => 'required|string|max:500', + 'modified_summary' => 'nullable|string|max:500', + 'modified_cast' => 'nullable|string|max:100', + ]); + + return $this->service->saveOverride( + $validated['unique_key'], + $validated['modified_summary'] ?? null, + $validated['modified_cast'] ?? null + ); + }, __('message.updated')); + } + + // ========================================================================= + // 수동 입력 (Manual) + // ========================================================================= + + /** + * 수동 은행 거래 등록 + */ + public function storeManual(Request $request): JsonResponse + { + return ApiResponse::handle(function () use ($request) { + $validated = $request->validate([ + 'bank_account_num' => 'required|string|max:50', + 'bank_code' => 'nullable|string|max:10', + 'bank_name' => 'nullable|string|max:50', + 'trans_date' => 'required|date', + 'trans_time' => 'nullable|string|max:10', + 'trans_dt' => 'nullable|string|max:20', + 'deposit' => 'nullable|numeric|min:0', + 'withdraw' => 'nullable|numeric|min:0', + 'balance' => 'nullable|numeric', + 'summary' => 'nullable|string|max:500', + 'cast' => 'nullable|string|max:100', + 'memo' => 'nullable|string|max:500', + 'trans_office' => 'nullable|string|max:100', + 'account_code' => 'nullable|string|max:20', + 'account_name' => 'nullable|string|max:100', + 'client_code' => 'nullable|string|max:20', + 'client_name' => 'nullable|string|max:200', + ]); + + return $this->service->storeManual($validated); + }, __('message.created')); + } + + /** + * 수동 은행 거래 수정 + */ + public function updateManual(Request $request, int $id): JsonResponse + { + return ApiResponse::handle(function () use ($request, $id) { + $validated = $request->validate([ + 'deposit' => 'nullable|numeric|min:0', + 'withdraw' => 'nullable|numeric|min:0', + 'balance' => 'nullable|numeric', + 'summary' => 'nullable|string|max:500', + 'cast' => 'nullable|string|max:100', + 'memo' => 'nullable|string|max:500', + 'account_code' => 'nullable|string|max:20', + 'account_name' => 'nullable|string|max:100', + 'client_code' => 'nullable|string|max:20', + 'client_name' => 'nullable|string|max:200', + ]); + + return $this->service->updateManual($id, $validated); + }, __('message.updated')); + } + + /** + * 수동 은행 거래 삭제 + */ + public function destroyManual(int $id): JsonResponse + { + return ApiResponse::handle(function () use ($id) { + return $this->service->destroyManual($id); + }, __('message.deleted')); + } + + // ========================================================================= + // 분개 (Journal Entries) + // ========================================================================= + + /** + * 은행 거래 분개 조회 + */ + public function getJournalEntries(int $id): JsonResponse + { + return ApiResponse::handle(function () use ($id) { + $sourceKey = "barobill_bank_{$id}"; + + return $this->journalSyncService->getForSource( + JournalEntry::SOURCE_BAROBILL_BANK, + $sourceKey + ) ?? ['items' => []]; + }, __('message.fetched')); + } + + /** + * 은행 거래 분개 저장 + */ + public function storeJournalEntries(Request $request, int $id): JsonResponse + { + return ApiResponse::handle(function () use ($request, $id) { + $validated = $request->validate([ + 'items' => 'required|array|min:1', + 'items.*.side' => 'required|in:debit,credit', + 'items.*.account_code' => 'required|string|max:20', + 'items.*.account_name' => 'nullable|string|max:100', + 'items.*.debit_amount' => 'required|integer|min:0', + 'items.*.credit_amount' => 'required|integer|min:0', + 'items.*.vendor_name' => 'nullable|string|max:200', + 'items.*.memo' => 'nullable|string|max:500', + ]); + + $bankTx = \App\Models\Barobill\BarobillBankTransaction::find($id); + if (! $bankTx) { + throw new \Illuminate\Database\Eloquent\ModelNotFoundException; + } + + $entryDate = $bankTx->trans_date ?? now()->format('Y-m-d'); + $sourceKey = "barobill_bank_{$id}"; + + return $this->journalSyncService->saveForSource( + JournalEntry::SOURCE_BAROBILL_BANK, + $sourceKey, + $entryDate, + "바로빌 은행거래 분개 (#{$id})", + $validated['items'], + ); + }, __('message.created')); + } + + /** + * 은행 거래 분개 삭제 + */ + public function deleteJournalEntries(int $id): JsonResponse + { + return ApiResponse::handle(function () use ($id) { + $sourceKey = "barobill_bank_{$id}"; + + return $this->journalSyncService->deleteForSource( + JournalEntry::SOURCE_BAROBILL_BANK, + $sourceKey + ); + }, __('message.deleted')); + } +} diff --git a/app/Http/Controllers/Api/V1/BarobillCardTransactionController.php b/app/Http/Controllers/Api/V1/BarobillCardTransactionController.php new file mode 100644 index 0000000..e99648c --- /dev/null +++ b/app/Http/Controllers/Api/V1/BarobillCardTransactionController.php @@ -0,0 +1,326 @@ +validate([ + 'start_date' => 'nullable|date', + 'end_date' => 'nullable|date|after_or_equal:start_date', + 'card_num' => 'nullable|string|max:50', + 'search' => 'nullable|string|max:100', + 'include_hidden' => 'nullable|boolean', + 'per_page' => 'nullable|integer|min:1|max:100', + 'page' => 'nullable|integer|min:1', + ]); + + return $this->service->index($params); + }, __('message.fetched')); + } + + /** + * 단일 카드 거래 상세 + */ + public function show(int $id): JsonResponse + { + return ApiResponse::handle(function () use ($id) { + $tx = $this->service->show($id); + if (! $tx) { + throw new \Illuminate\Database\Eloquent\ModelNotFoundException; + } + + return $tx; + }, __('message.fetched')); + } + + /** + * 카드 번호 목록 (필터용) + */ + public function cardNumbers(): JsonResponse + { + return ApiResponse::handle(function () { + return $this->service->cardNumbers(); + }, __('message.fetched')); + } + + // ========================================================================= + // 분할 (Splits) + // ========================================================================= + + /** + * 카드 거래 분할 조회 + */ + public function getSplits(Request $request): JsonResponse + { + return ApiResponse::handle(function () use ($request) { + $validated = $request->validate([ + 'unique_key' => 'required|string|max:500', + ]); + + return $this->service->getSplits($validated['unique_key']); + }, __('message.fetched')); + } + + /** + * 카드 거래 분할 저장 + */ + public function saveSplits(Request $request): JsonResponse + { + return ApiResponse::handle(function () use ($request) { + $validated = $request->validate([ + 'unique_key' => 'required|string|max:500', + 'items' => 'required|array|min:1', + 'items.*.split_amount' => 'required|numeric', + 'items.*.split_supply_amount' => 'nullable|numeric', + 'items.*.split_tax' => 'nullable|numeric', + 'items.*.account_code' => 'nullable|string|max:20', + 'items.*.account_name' => 'nullable|string|max:100', + 'items.*.deduction_type' => 'nullable|string|max:20', + 'items.*.evidence_name' => 'nullable|string|max:100', + 'items.*.description' => 'nullable|string|max:500', + 'items.*.memo' => 'nullable|string|max:500', + 'items.*.card_num' => 'nullable|string|max:50', + 'items.*.use_dt' => 'nullable|string|max:20', + 'items.*.use_date' => 'nullable|date', + 'items.*.approval_num' => 'nullable|string|max:50', + 'items.*.original_amount' => 'nullable|numeric', + 'items.*.merchant_name' => 'nullable|string|max:200', + ]); + + return $this->service->saveSplits($validated['unique_key'], $validated['items']); + }, __('message.created')); + } + + /** + * 카드 거래 분할 삭제 + */ + public function deleteSplits(Request $request): JsonResponse + { + return ApiResponse::handle(function () use ($request) { + $validated = $request->validate([ + 'unique_key' => 'required|string|max:500', + ]); + + return $this->service->deleteSplits($validated['unique_key']); + }, __('message.deleted')); + } + + // ========================================================================= + // 수동 입력 (Manual) + // ========================================================================= + + /** + * 수동 카드 거래 등록 + */ + public function storeManual(Request $request): JsonResponse + { + return ApiResponse::handle(function () use ($request) { + $validated = $request->validate([ + 'card_num' => 'required|string|max:50', + 'card_company' => 'nullable|string|max:10', + 'card_company_name' => 'nullable|string|max:50', + 'use_dt' => 'required|string|max:20', + 'use_date' => 'required|date', + 'use_time' => 'nullable|string|max:10', + 'approval_num' => 'nullable|string|max:50', + 'approval_type' => 'nullable|string|max:10', + 'approval_amount' => 'required|numeric', + 'tax' => 'nullable|numeric', + 'service_charge' => 'nullable|numeric', + 'merchant_name' => 'required|string|max:200', + 'merchant_biz_num' => 'nullable|string|max:20', + 'account_code' => 'nullable|string|max:20', + 'account_name' => 'nullable|string|max:100', + 'deduction_type' => 'nullable|string|max:20', + 'evidence_name' => 'nullable|string|max:100', + 'description' => 'nullable|string|max:500', + 'memo' => 'nullable|string|max:500', + ]); + + return $this->service->storeManual($validated); + }, __('message.created')); + } + + /** + * 수동 카드 거래 수정 + */ + public function updateManual(Request $request, int $id): JsonResponse + { + return ApiResponse::handle(function () use ($request, $id) { + $validated = $request->validate([ + 'approval_amount' => 'nullable|numeric', + 'tax' => 'nullable|numeric', + 'merchant_name' => 'nullable|string|max:200', + 'account_code' => 'nullable|string|max:20', + 'account_name' => 'nullable|string|max:100', + 'deduction_type' => 'nullable|string|max:20', + 'description' => 'nullable|string|max:500', + 'memo' => 'nullable|string|max:500', + ]); + + return $this->service->updateManual($id, $validated); + }, __('message.updated')); + } + + /** + * 수동 카드 거래 삭제 + */ + public function destroyManual(int $id): JsonResponse + { + return ApiResponse::handle(function () use ($id) { + return $this->service->destroyManual($id); + }, __('message.deleted')); + } + + // ========================================================================= + // 숨김/복원 (Hide/Restore) + // ========================================================================= + + /** + * 카드 거래 숨김 + */ + public function hide(int $id): JsonResponse + { + return ApiResponse::handle(function () use ($id) { + return $this->service->hide($id); + }, __('message.updated')); + } + + /** + * 카드 거래 숨김 복원 + */ + public function restore(int $id): JsonResponse + { + return ApiResponse::handle(function () use ($id) { + return $this->service->restore($id); + }, __('message.updated')); + } + + /** + * 숨겨진 거래 목록 + */ + public function hiddenList(Request $request): JsonResponse + { + return ApiResponse::handle(function () use ($request) { + $params = $request->validate([ + 'start_date' => 'nullable|date', + 'end_date' => 'nullable|date|after_or_equal:start_date', + ]); + + return $this->service->hiddenList($params); + }, __('message.fetched')); + } + + // ========================================================================= + // 금액 수정 + // ========================================================================= + + /** + * 공급가액/세액 수정 + */ + public function updateAmount(Request $request, int $id): JsonResponse + { + return ApiResponse::handle(function () use ($request, $id) { + $validated = $request->validate([ + 'supply_amount' => 'required|numeric', + 'tax' => 'required|numeric', + 'modified_by_name' => 'nullable|string|max:50', + ]); + + return $this->service->updateAmount($id, $validated); + }, __('message.updated')); + } + + // ========================================================================= + // 분개 (Journal Entries) + // ========================================================================= + + /** + * 카드 거래 분개 조회 + */ + public function getJournalEntries(int $id): JsonResponse + { + return ApiResponse::handle(function () use ($id) { + $sourceKey = "barobill_card_{$id}"; + + return $this->journalSyncService->getForSource( + JournalEntry::SOURCE_BAROBILL_CARD, + $sourceKey + ) ?? ['items' => []]; + }, __('message.fetched')); + } + + /** + * 카드 거래 분개 저장 + */ + public function storeJournalEntries(Request $request, int $id): JsonResponse + { + return ApiResponse::handle(function () use ($request, $id) { + $validated = $request->validate([ + 'items' => 'required|array|min:1', + 'items.*.side' => 'required|in:debit,credit', + 'items.*.account_code' => 'required|string|max:20', + 'items.*.account_name' => 'nullable|string|max:100', + 'items.*.debit_amount' => 'required|integer|min:0', + 'items.*.credit_amount' => 'required|integer|min:0', + 'items.*.vendor_name' => 'nullable|string|max:200', + 'items.*.memo' => 'nullable|string|max:500', + ]); + + $tx = $this->service->show($id); + if (! $tx) { + throw new \Illuminate\Database\Eloquent\ModelNotFoundException; + } + + $entryDate = $tx->use_date ?? now()->format('Y-m-d'); + $sourceKey = "barobill_card_{$id}"; + + return $this->journalSyncService->saveForSource( + JournalEntry::SOURCE_BAROBILL_CARD, + $sourceKey, + $entryDate, + "바로빌 카드거래 분개 (#{$id})", + $validated['items'], + ); + }, __('message.created')); + } + + /** + * 카드 거래 분개 삭제 + */ + public function deleteJournalEntries(int $id): JsonResponse + { + return ApiResponse::handle(function () use ($id) { + $sourceKey = "barobill_card_{$id}"; + + return $this->journalSyncService->deleteForSource( + JournalEntry::SOURCE_BAROBILL_CARD, + $sourceKey + ); + }, __('message.deleted')); + } +} diff --git a/app/Http/Controllers/Api/V1/HometaxInvoiceController.php b/app/Http/Controllers/Api/V1/HometaxInvoiceController.php new file mode 100644 index 0000000..772d6b9 --- /dev/null +++ b/app/Http/Controllers/Api/V1/HometaxInvoiceController.php @@ -0,0 +1,278 @@ +validate([ + 'start_date' => 'nullable|date', + 'end_date' => 'nullable|date|after_or_equal:start_date', + 'search' => 'nullable|string|max:100', + 'per_page' => 'nullable|integer|min:1|max:100', + 'page' => 'nullable|integer|min:1', + ]); + + return $this->service->sales($params); + }, __('message.fetched')); + } + + /** + * 매입 세금계산서 목록 + */ + public function purchases(Request $request): JsonResponse + { + return ApiResponse::handle(function () use ($request) { + $params = $request->validate([ + 'start_date' => 'nullable|date', + 'end_date' => 'nullable|date|after_or_equal:start_date', + 'search' => 'nullable|string|max:100', + 'per_page' => 'nullable|integer|min:1|max:100', + 'page' => 'nullable|integer|min:1', + ]); + + return $this->service->purchases($params); + }, __('message.fetched')); + } + + /** + * 세금계산서 상세 조회 + */ + public function show(int $id): JsonResponse + { + return ApiResponse::handle(function () use ($id) { + $invoice = $this->service->show($id); + if (! $invoice) { + throw new \Illuminate\Database\Eloquent\ModelNotFoundException; + } + + return $invoice; + }, __('message.fetched')); + } + + /** + * 요약 통계 (매출/매입 합계) + */ + public function summary(Request $request): JsonResponse + { + return ApiResponse::handle(function () use ($request) { + $params = $request->validate([ + 'start_date' => 'nullable|date', + 'end_date' => 'nullable|date|after_or_equal:start_date', + ]); + + return $this->service->summary($params); + }, __('message.fetched')); + } + + // ========================================================================= + // 수동 입력 (Manual) + // ========================================================================= + + /** + * 수동 세금계산서 등록 + */ + public function store(Request $request): JsonResponse + { + return ApiResponse::handle(function () use ($request) { + $validated = $request->validate([ + 'invoice_type' => 'required|in:sales,purchase', + 'nts_confirm_num' => 'nullable|string|max:50', + 'write_date' => 'required|date', + 'issue_date' => 'nullable|date', + 'invoicer_corp_num' => 'nullable|string|max:20', + 'invoicer_corp_name' => 'nullable|string|max:200', + 'invoicer_ceo_name' => 'nullable|string|max:100', + 'invoicee_corp_num' => 'nullable|string|max:20', + 'invoicee_corp_name' => 'nullable|string|max:200', + 'invoicee_ceo_name' => 'nullable|string|max:100', + 'supply_amount' => 'required|integer', + 'tax_amount' => 'required|integer', + 'total_amount' => 'required|integer', + 'tax_type' => 'nullable|string|max:10', + 'purpose_type' => 'nullable|string|max:10', + 'issue_type' => 'nullable|string|max:10', + 'item_name' => 'nullable|string|max:200', + 'account_code' => 'nullable|string|max:20', + 'account_name' => 'nullable|string|max:100', + 'deduction_type' => 'nullable|string|max:20', + 'remark1' => 'nullable|string|max:500', + ]); + + return $this->service->storeManual($validated); + }, __('message.created')); + } + + /** + * 수동 세금계산서 수정 + */ + public function update(Request $request, int $id): JsonResponse + { + return ApiResponse::handle(function () use ($request, $id) { + $validated = $request->validate([ + 'write_date' => 'nullable|date', + 'issue_date' => 'nullable|date', + 'invoicer_corp_num' => 'nullable|string|max:20', + 'invoicer_corp_name' => 'nullable|string|max:200', + 'invoicee_corp_num' => 'nullable|string|max:20', + 'invoicee_corp_name' => 'nullable|string|max:200', + 'supply_amount' => 'nullable|integer', + 'tax_amount' => 'nullable|integer', + 'total_amount' => 'nullable|integer', + 'item_name' => 'nullable|string|max:200', + 'account_code' => 'nullable|string|max:20', + 'account_name' => 'nullable|string|max:100', + 'deduction_type' => 'nullable|string|max:20', + 'remark1' => 'nullable|string|max:500', + ]); + + return $this->service->updateManual($id, $validated); + }, __('message.updated')); + } + + /** + * 수동 세금계산서 삭제 + */ + public function destroy(int $id): JsonResponse + { + return ApiResponse::handle(function () use ($id) { + return $this->service->destroyManual($id); + }, __('message.deleted')); + } + + // ========================================================================= + // 분개 (자체 테이블: hometax_invoice_journals) + // ========================================================================= + + /** + * 분개 조회 + */ + public function getJournals(int $id): JsonResponse + { + return ApiResponse::handle(function () use ($id) { + return $this->service->getJournals($id); + }, __('message.fetched')); + } + + /** + * 분개 저장 + */ + public function saveJournals(Request $request, int $id): JsonResponse + { + return ApiResponse::handle(function () use ($request, $id) { + $validated = $request->validate([ + 'items' => 'required|array|min:1', + 'items.*.dc_type' => 'required|in:debit,credit', + 'items.*.account_code' => 'required|string|max:20', + 'items.*.account_name' => 'nullable|string|max:100', + 'items.*.debit_amount' => 'required|integer|min:0', + 'items.*.credit_amount' => 'required|integer|min:0', + 'items.*.description' => 'nullable|string|max:500', + ]); + + return $this->service->saveJournals($id, $validated['items']); + }, __('message.created')); + } + + /** + * 분개 삭제 + */ + public function deleteJournals(int $id): JsonResponse + { + return ApiResponse::handle(function () use ($id) { + return $this->service->deleteJournals($id); + }, __('message.deleted')); + } + + // ========================================================================= + // 통합 분개 (JournalSyncService - CEO 대시보드 연동) + // ========================================================================= + + /** + * 통합 분개 조회 + */ + public function getJournalEntries(int $id): JsonResponse + { + return ApiResponse::handle(function () use ($id) { + $sourceKey = "hometax_{$id}"; + + return $this->journalSyncService->getForSource( + JournalEntry::SOURCE_HOMETAX_INVOICE, + $sourceKey + ) ?? ['items' => []]; + }, __('message.fetched')); + } + + /** + * 통합 분개 저장 + */ + public function storeJournalEntries(Request $request, int $id): JsonResponse + { + return ApiResponse::handle(function () use ($request, $id) { + $validated = $request->validate([ + 'items' => 'required|array|min:1', + 'items.*.side' => 'required|in:debit,credit', + 'items.*.account_code' => 'required|string|max:20', + 'items.*.account_name' => 'nullable|string|max:100', + 'items.*.debit_amount' => 'required|integer|min:0', + 'items.*.credit_amount' => 'required|integer|min:0', + 'items.*.vendor_name' => 'nullable|string|max:200', + 'items.*.memo' => 'nullable|string|max:500', + ]); + + $invoice = $this->service->show($id); + if (! $invoice) { + throw new \Illuminate\Database\Eloquent\ModelNotFoundException; + } + + $entryDate = $invoice->write_date?->format('Y-m-d') ?? now()->format('Y-m-d'); + $sourceKey = "hometax_{$id}"; + + return $this->journalSyncService->saveForSource( + JournalEntry::SOURCE_HOMETAX_INVOICE, + $sourceKey, + $entryDate, + "홈택스 세금계산서 분개 (#{$id})", + $validated['items'], + ); + }, __('message.created')); + } + + /** + * 통합 분개 삭제 + */ + public function deleteJournalEntries(int $id): JsonResponse + { + return ApiResponse::handle(function () use ($id) { + $sourceKey = "hometax_{$id}"; + + return $this->journalSyncService->deleteForSource( + JournalEntry::SOURCE_HOMETAX_INVOICE, + $sourceKey + ); + }, __('message.deleted')); + } +} diff --git a/app/Models/Tenants/JournalEntry.php b/app/Models/Tenants/JournalEntry.php index 6a0970a..6b2b415 100644 --- a/app/Models/Tenants/JournalEntry.php +++ b/app/Models/Tenants/JournalEntry.php @@ -34,14 +34,24 @@ class JournalEntry extends Model // Status public const STATUS_DRAFT = 'draft'; + public const STATUS_CONFIRMED = 'confirmed'; // Source type public const SOURCE_MANUAL = 'manual'; + public const SOURCE_BANK_TRANSACTION = 'bank_transaction'; + public const SOURCE_TAX_INVOICE = 'tax_invoice'; + public const SOURCE_CARD_TRANSACTION = 'card_transaction'; + public const SOURCE_BAROBILL_CARD = 'barobill_card'; + + public const SOURCE_BAROBILL_BANK = 'barobill_bank'; + + public const SOURCE_HOMETAX_INVOICE = 'hometax_invoice'; + // Entry type public const TYPE_GENERAL = 'general'; diff --git a/app/Services/BarobillBankTransactionService.php b/app/Services/BarobillBankTransactionService.php new file mode 100644 index 0000000..3af3ca5 --- /dev/null +++ b/app/Services/BarobillBankTransactionService.php @@ -0,0 +1,249 @@ +tenantId(); + $startDate = $params['start_date'] ?? now()->startOfMonth()->format('Y-m-d'); + $endDate = $params['end_date'] ?? now()->format('Y-m-d'); + $accountNum = $params['bank_account_num'] ?? null; + $search = $params['search'] ?? null; + $perPage = $params['per_page'] ?? 50; + + $query = BarobillBankTransaction::where('tenant_id', $tenantId) + ->whereBetween('trans_date', [$startDate, $endDate]); + + if ($accountNum) { + $query->where('bank_account_num', $accountNum); + } + + if ($search) { + $query->where(function ($q) use ($search) { + $q->where('summary', 'like', "%{$search}%") + ->orWhere('memo', 'like', "%{$search}%") + ->orWhere('client_name', 'like', "%{$search}%"); + }); + } + + $query->orderByDesc('trans_date')->orderByDesc('trans_dt'); + + $transactions = $query->paginate($perPage); + + // 분할/오버라이드 정보 로드 + $uniqueKeys = $transactions->getCollection()->map->unique_key->toArray(); + + $splits = BarobillBankTransactionSplit::where('tenant_id', $tenantId) + ->whereIn('original_unique_key', $uniqueKeys) + ->orderBy('sort_order') + ->get() + ->groupBy('original_unique_key'); + + $overrides = BarobillBankTransactionOverride::getByUniqueKeys($tenantId, $uniqueKeys); + + $transactions->getCollection()->transform(function ($tx) use ($splits, $overrides) { + $tx->splits = $splits->get($tx->unique_key, collect()); + $tx->has_splits = $tx->splits->isNotEmpty(); + $tx->override = $overrides->get($tx->unique_key); + + return $tx; + }); + + return [ + 'data' => $transactions, + ]; + } + + /** + * 계좌 목록 (필터용) + */ + public function accounts(): array + { + $tenantId = $this->tenantId(); + + $accounts = BarobillBankTransaction::where('tenant_id', $tenantId) + ->select('bank_account_num', 'bank_name') + ->distinct() + ->orderBy('bank_account_num') + ->get(); + + return ['items' => $accounts]; + } + + /** + * 거래 분할 조회 + */ + public function getSplits(string $uniqueKey): array + { + $tenantId = $this->tenantId(); + $splits = BarobillBankTransactionSplit::getByUniqueKey($tenantId, $uniqueKey); + + return ['items' => $splits]; + } + + /** + * 거래 분할 저장 + */ + public function saveSplits(string $uniqueKey, array $items): array + { + $tenantId = $this->tenantId(); + + return DB::transaction(function () use ($tenantId, $uniqueKey, $items) { + BarobillBankTransactionSplit::where('tenant_id', $tenantId) + ->where('original_unique_key', $uniqueKey) + ->delete(); + + $created = []; + foreach ($items as $index => $item) { + $created[] = BarobillBankTransactionSplit::create([ + 'tenant_id' => $tenantId, + 'original_unique_key' => $uniqueKey, + 'split_amount' => $item['split_amount'], + 'account_code' => $item['account_code'] ?? null, + 'account_name' => $item['account_name'] ?? null, + 'deduction_type' => $item['deduction_type'] ?? null, + 'evidence_name' => $item['evidence_name'] ?? null, + 'description' => $item['description'] ?? null, + 'memo' => $item['memo'] ?? null, + 'sort_order' => $index + 1, + 'bank_account_num' => $item['bank_account_num'] ?? null, + 'trans_dt' => $item['trans_dt'] ?? null, + 'trans_date' => $item['trans_date'] ?? null, + 'original_deposit' => $item['original_deposit'] ?? 0, + 'original_withdraw' => $item['original_withdraw'] ?? 0, + 'summary' => $item['summary'] ?? null, + ]); + } + + return ['items' => $created, 'count' => count($created)]; + }); + } + + /** + * 거래 분할 삭제 + */ + public function deleteSplits(string $uniqueKey): array + { + $tenantId = $this->tenantId(); + $deleted = BarobillBankTransactionSplit::where('tenant_id', $tenantId) + ->where('original_unique_key', $uniqueKey) + ->delete(); + + return ['deleted_count' => $deleted]; + } + + /** + * 적요/분류 오버라이드 저장 + */ + public function saveOverride(string $uniqueKey, ?string $modifiedSummary, ?string $modifiedCast): BarobillBankTransactionOverride + { + $tenantId = $this->tenantId(); + + return BarobillBankTransactionOverride::saveOverride($tenantId, $uniqueKey, $modifiedSummary, $modifiedCast); + } + + /** + * 수동 은행 거래 등록 + */ + public function storeManual(array $data): BarobillBankTransaction + { + $tenantId = $this->tenantId(); + + return BarobillBankTransaction::create([ + 'tenant_id' => $tenantId, + 'bank_account_num' => $data['bank_account_num'], + 'bank_code' => $data['bank_code'] ?? null, + 'bank_name' => $data['bank_name'] ?? null, + 'trans_date' => $data['trans_date'], + 'trans_time' => $data['trans_time'] ?? null, + 'trans_dt' => $data['trans_dt'] ?? $data['trans_date'].($data['trans_time'] ?? '000000'), + 'deposit' => $data['deposit'] ?? 0, + 'withdraw' => $data['withdraw'] ?? 0, + 'balance' => $data['balance'] ?? 0, + 'summary' => $data['summary'] ?? null, + 'cast' => $data['cast'] ?? null, + 'memo' => $data['memo'] ?? null, + 'trans_office' => $data['trans_office'] ?? null, + 'account_code' => $data['account_code'] ?? null, + 'account_name' => $data['account_name'] ?? null, + 'client_code' => $data['client_code'] ?? null, + 'client_name' => $data['client_name'] ?? null, + 'is_manual' => true, + ]); + } + + /** + * 수동 은행 거래 수정 + */ + public function updateManual(int $id, array $data): BarobillBankTransaction + { + $tx = BarobillBankTransaction::where('tenant_id', $this->tenantId()) + ->where('is_manual', true) + ->findOrFail($id); + + $tx->update($data); + + return $tx->fresh(); + } + + /** + * 수동 은행 거래 삭제 + */ + public function destroyManual(int $id): bool + { + $tx = BarobillBankTransaction::where('tenant_id', $this->tenantId()) + ->where('is_manual', true) + ->findOrFail($id); + + return $tx->delete(); + } + + /** + * 잔액 요약 + */ + public function balanceSummary(array $params): array + { + $tenantId = $this->tenantId(); + $date = $params['date'] ?? now()->format('Y-m-d'); + + $accounts = BarobillBankTransaction::where('tenant_id', $tenantId) + ->select('bank_account_num', 'bank_name') + ->distinct() + ->get(); + + $summary = []; + foreach ($accounts as $account) { + $lastTx = BarobillBankTransaction::where('tenant_id', $tenantId) + ->where('bank_account_num', $account->bank_account_num) + ->where('trans_date', '<=', $date) + ->orderByDesc('trans_date') + ->orderByDesc('trans_dt') + ->first(); + + $summary[] = [ + 'bank_account_num' => $account->bank_account_num, + 'bank_name' => $account->bank_name, + 'balance' => $lastTx ? $lastTx->balance : 0, + 'last_trans_date' => $lastTx?->trans_date, + ]; + } + + return ['items' => $summary]; + } +} diff --git a/app/Services/BarobillCardTransactionService.php b/app/Services/BarobillCardTransactionService.php new file mode 100644 index 0000000..688c433 --- /dev/null +++ b/app/Services/BarobillCardTransactionService.php @@ -0,0 +1,308 @@ +tenantId(); + $startDate = $params['start_date'] ?? now()->startOfMonth()->format('Y-m-d'); + $endDate = $params['end_date'] ?? now()->format('Y-m-d'); + $cardNum = $params['card_num'] ?? null; + $search = $params['search'] ?? null; + $includeHidden = $params['include_hidden'] ?? false; + $perPage = $params['per_page'] ?? 50; + + $query = BarobillCardTransaction::where('tenant_id', $tenantId) + ->whereBetween('use_date', [$startDate, $endDate]); + + if ($cardNum) { + $query->where('card_num', $cardNum); + } + + if ($search) { + $query->where(function ($q) use ($search) { + $q->where('merchant_name', 'like', "%{$search}%") + ->orWhere('memo', 'like', "%{$search}%") + ->orWhere('approval_num', 'like', "%{$search}%"); + }); + } + + // 숨김 거래 필터링 + if (! $includeHidden) { + $hiddenKeys = BarobillCardTransactionHide::getHiddenKeys($tenantId, $startDate, $endDate); + if (! empty($hiddenKeys)) { + $query->whereNotIn( + DB::raw("CONCAT(card_num, '|', use_dt, '|', approval_num, '|', approval_amount)"), + $hiddenKeys + ); + } + } + + $query->orderByDesc('use_date')->orderByDesc('use_dt'); + + $transactions = $query->paginate($perPage); + + // 분할 거래 정보 로드 + $uniqueKeys = $transactions->getCollection()->map->unique_key->toArray(); + $splits = BarobillCardTransactionSplit::where('tenant_id', $tenantId) + ->whereIn('original_unique_key', $uniqueKeys) + ->orderBy('sort_order') + ->get() + ->groupBy('original_unique_key'); + + $transactions->getCollection()->transform(function ($tx) use ($splits) { + $tx->splits = $splits->get($tx->unique_key, collect()); + $tx->has_splits = $tx->splits->isNotEmpty(); + + return $tx; + }); + + return [ + 'data' => $transactions, + ]; + } + + /** + * 단일 카드 거래 상세 + */ + public function show(int $id): ?BarobillCardTransaction + { + return BarobillCardTransaction::where('tenant_id', $this->tenantId()) + ->find($id); + } + + /** + * 카드 거래 분할 조회 + */ + public function getSplits(string $uniqueKey): array + { + $tenantId = $this->tenantId(); + $splits = BarobillCardTransactionSplit::getByUniqueKey($tenantId, $uniqueKey); + + return ['items' => $splits]; + } + + /** + * 카드 거래 분할 저장 + */ + public function saveSplits(string $uniqueKey, array $items): array + { + $tenantId = $this->tenantId(); + + return DB::transaction(function () use ($tenantId, $uniqueKey, $items) { + // 기존 분할 삭제 + BarobillCardTransactionSplit::where('tenant_id', $tenantId) + ->where('original_unique_key', $uniqueKey) + ->delete(); + + $created = []; + foreach ($items as $index => $item) { + $created[] = BarobillCardTransactionSplit::create([ + 'tenant_id' => $tenantId, + 'original_unique_key' => $uniqueKey, + 'split_amount' => $item['split_amount'], + 'split_supply_amount' => $item['split_supply_amount'] ?? 0, + 'split_tax' => $item['split_tax'] ?? 0, + 'account_code' => $item['account_code'] ?? null, + 'account_name' => $item['account_name'] ?? null, + 'deduction_type' => $item['deduction_type'] ?? null, + 'evidence_name' => $item['evidence_name'] ?? null, + 'description' => $item['description'] ?? null, + 'memo' => $item['memo'] ?? null, + 'sort_order' => $index + 1, + 'card_num' => $item['card_num'] ?? null, + 'use_dt' => $item['use_dt'] ?? null, + 'use_date' => $item['use_date'] ?? null, + 'approval_num' => $item['approval_num'] ?? null, + 'original_amount' => $item['original_amount'] ?? 0, + 'merchant_name' => $item['merchant_name'] ?? null, + ]); + } + + return ['items' => $created, 'count' => count($created)]; + }); + } + + /** + * 카드 거래 분할 삭제 + */ + public function deleteSplits(string $uniqueKey): array + { + $tenantId = $this->tenantId(); + $deleted = BarobillCardTransactionSplit::where('tenant_id', $tenantId) + ->where('original_unique_key', $uniqueKey) + ->delete(); + + return ['deleted_count' => $deleted]; + } + + /** + * 수동 카드 거래 등록 + */ + public function storeManual(array $data): BarobillCardTransaction + { + $tenantId = $this->tenantId(); + + return BarobillCardTransaction::create([ + 'tenant_id' => $tenantId, + 'card_num' => $data['card_num'], + 'card_company' => $data['card_company'] ?? null, + 'card_company_name' => $data['card_company_name'] ?? null, + 'use_dt' => $data['use_dt'], + 'use_date' => $data['use_date'], + 'use_time' => $data['use_time'] ?? null, + 'approval_num' => $data['approval_num'] ?? 'MANUAL-'.now()->format('YmdHis'), + 'approval_type' => $data['approval_type'] ?? '1', + 'approval_amount' => $data['approval_amount'], + 'tax' => $data['tax'] ?? 0, + 'service_charge' => $data['service_charge'] ?? 0, + 'payment_plan' => $data['payment_plan'] ?? null, + 'merchant_name' => $data['merchant_name'], + 'merchant_biz_num' => $data['merchant_biz_num'] ?? null, + 'account_code' => $data['account_code'] ?? null, + 'account_name' => $data['account_name'] ?? null, + 'deduction_type' => $data['deduction_type'] ?? null, + 'evidence_name' => $data['evidence_name'] ?? null, + 'description' => $data['description'] ?? null, + 'memo' => $data['memo'] ?? null, + 'is_manual' => true, + ]); + } + + /** + * 수동 카드 거래 수정 + */ + public function updateManual(int $id, array $data): BarobillCardTransaction + { + $tx = BarobillCardTransaction::where('tenant_id', $this->tenantId()) + ->where('is_manual', true) + ->findOrFail($id); + + $tx->update($data); + + return $tx->fresh(); + } + + /** + * 수동 카드 거래 삭제 + */ + public function destroyManual(int $id): bool + { + $tx = BarobillCardTransaction::where('tenant_id', $this->tenantId()) + ->where('is_manual', true) + ->findOrFail($id); + + return $tx->delete(); + } + + /** + * 카드 거래 숨김 + */ + public function hide(int $id): BarobillCardTransactionHide + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + $tx = BarobillCardTransaction::where('tenant_id', $tenantId)->findOrFail($id); + + return BarobillCardTransactionHide::hideTransaction($tenantId, $tx->unique_key, [ + 'card_num' => $tx->card_num, + 'use_date' => $tx->use_date, + 'approval_num' => $tx->approval_num, + 'approval_amount' => $tx->approval_amount, + 'merchant_name' => $tx->merchant_name, + ], $userId); + } + + /** + * 카드 거래 숨김 복원 + */ + public function restore(int $id): bool + { + $tenantId = $this->tenantId(); + $tx = BarobillCardTransaction::where('tenant_id', $tenantId)->findOrFail($id); + + return BarobillCardTransactionHide::restoreTransaction($tenantId, $tx->unique_key); + } + + /** + * 숨겨진 거래 목록 + */ + public function hiddenList(array $params): array + { + $tenantId = $this->tenantId(); + $startDate = $params['start_date'] ?? now()->startOfMonth()->format('Y-m-d'); + $endDate = $params['end_date'] ?? now()->format('Y-m-d'); + + $hiddenItems = BarobillCardTransactionHide::where('tenant_id', $tenantId) + ->whereBetween('use_date', [$startDate, $endDate]) + ->orderByDesc('created_at') + ->get(); + + return ['items' => $hiddenItems]; + } + + /** + * 금액 수정 (공급가액/세액 수정) + */ + public function updateAmount(int $id, array $data): BarobillCardTransaction + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + $tx = BarobillCardTransaction::where('tenant_id', $tenantId)->findOrFail($id); + + // 변경 이력 기록 + BarobillCardTransactionAmountLog::create([ + 'card_transaction_id' => $tx->id, + 'original_unique_key' => $tx->unique_key, + 'before_supply_amount' => $tx->modified_supply_amount ?? $tx->approval_amount, + 'before_tax' => $tx->modified_tax ?? $tx->tax, + 'after_supply_amount' => $data['supply_amount'], + 'after_tax' => $data['tax'], + 'modified_by' => $userId, + 'modified_by_name' => $data['modified_by_name'] ?? '', + 'ip_address' => request()->ip(), + ]); + + $tx->update([ + 'modified_supply_amount' => $data['supply_amount'], + 'modified_tax' => $data['tax'], + ]); + + return $tx->fresh(); + } + + /** + * 카드 번호 목록 (필터용) + */ + public function cardNumbers(): array + { + $tenantId = $this->tenantId(); + + $cards = BarobillCardTransaction::where('tenant_id', $tenantId) + ->select('card_num', 'card_company_name') + ->distinct() + ->orderBy('card_num') + ->get(); + + return ['items' => $cards]; + } +} diff --git a/app/Services/HometaxInvoiceService.php b/app/Services/HometaxInvoiceService.php new file mode 100644 index 0000000..75a7bf9 --- /dev/null +++ b/app/Services/HometaxInvoiceService.php @@ -0,0 +1,222 @@ +listByType('sales', $params); + } + + /** + * 매입 세금계산서 목록 + */ + public function purchases(array $params): array + { + return $this->listByType('purchase', $params); + } + + /** + * 세금계산서 상세 조회 + */ + public function show(int $id): ?HometaxInvoice + { + return HometaxInvoice::where('tenant_id', $this->tenantId()) + ->with('journals') + ->find($id); + } + + /** + * 분개 저장 (홈택스 자체 분개 테이블 사용) + */ + public function saveJournals(int $invoiceId, array $items): array + { + $tenantId = $this->tenantId(); + $invoice = HometaxInvoice::where('tenant_id', $tenantId)->findOrFail($invoiceId); + + return DB::transaction(function () use ($tenantId, $invoice, $items) { + // 기존 분개 삭제 + HometaxInvoiceJournal::where('tenant_id', $tenantId) + ->where('hometax_invoice_id', $invoice->id) + ->delete(); + + $created = []; + foreach ($items as $index => $item) { + $created[] = HometaxInvoiceJournal::create([ + 'tenant_id' => $tenantId, + 'hometax_invoice_id' => $invoice->id, + 'nts_confirm_num' => $invoice->nts_confirm_num, + 'dc_type' => $item['dc_type'], + 'account_code' => $item['account_code'], + 'account_name' => $item['account_name'] ?? null, + 'debit_amount' => $item['debit_amount'] ?? 0, + 'credit_amount' => $item['credit_amount'] ?? 0, + 'description' => $item['description'] ?? null, + 'sort_order' => $index + 1, + 'invoice_type' => $invoice->invoice_type, + 'write_date' => $invoice->write_date, + 'supply_amount' => $invoice->supply_amount, + 'tax_amount' => $invoice->tax_amount, + 'total_amount' => $invoice->total_amount, + 'trading_partner_name' => $invoice->invoice_type === 'sales' + ? $invoice->invoicee_corp_name + : $invoice->invoicer_corp_name, + ]); + } + + return ['items' => $created, 'count' => count($created)]; + }); + } + + /** + * 분개 조회 + */ + public function getJournals(int $invoiceId): array + { + $tenantId = $this->tenantId(); + $journals = HometaxInvoiceJournal::getByInvoiceId($tenantId, $invoiceId); + + return ['items' => $journals]; + } + + /** + * 분개 삭제 + */ + public function deleteJournals(int $invoiceId): array + { + $tenantId = $this->tenantId(); + $deleted = HometaxInvoiceJournal::where('tenant_id', $tenantId) + ->where('hometax_invoice_id', $invoiceId) + ->delete(); + + return ['deleted_count' => $deleted]; + } + + /** + * 수동 세금계산서 등록 + */ + public function storeManual(array $data): HometaxInvoice + { + $tenantId = $this->tenantId(); + + return HometaxInvoice::create(array_merge($data, [ + 'tenant_id' => $tenantId, + ])); + } + + /** + * 수동 세금계산서 수정 + */ + public function updateManual(int $id, array $data): HometaxInvoice + { + $invoice = HometaxInvoice::where('tenant_id', $this->tenantId())->findOrFail($id); + $invoice->update($data); + + return $invoice->fresh(); + } + + /** + * 수동 세금계산서 삭제 (soft delete) + */ + public function destroyManual(int $id): bool + { + $invoice = HometaxInvoice::where('tenant_id', $this->tenantId())->findOrFail($id); + + return $invoice->delete(); + } + + /** + * 요약 통계 + */ + public function summary(array $params): array + { + $tenantId = $this->tenantId(); + $startDate = $params['start_date'] ?? now()->startOfMonth()->format('Y-m-d'); + $endDate = $params['end_date'] ?? now()->format('Y-m-d'); + + $salesQuery = HometaxInvoice::where('tenant_id', $tenantId) + ->sales() + ->period($startDate, $endDate); + + $purchaseQuery = HometaxInvoice::where('tenant_id', $tenantId) + ->purchase() + ->period($startDate, $endDate); + + return [ + 'sales' => [ + 'count' => (clone $salesQuery)->count(), + 'supply_amount' => (int) (clone $salesQuery)->sum('supply_amount'), + 'tax_amount' => (int) (clone $salesQuery)->sum('tax_amount'), + 'total_amount' => (int) (clone $salesQuery)->sum('total_amount'), + ], + 'purchase' => [ + 'count' => (clone $purchaseQuery)->count(), + 'supply_amount' => (int) (clone $purchaseQuery)->sum('supply_amount'), + 'tax_amount' => (int) (clone $purchaseQuery)->sum('tax_amount'), + 'total_amount' => (int) (clone $purchaseQuery)->sum('total_amount'), + ], + ]; + } + + /** + * 타입별 목록 조회 (공통) + */ + private function listByType(string $invoiceType, array $params): array + { + $tenantId = $this->tenantId(); + $startDate = $params['start_date'] ?? now()->startOfMonth()->format('Y-m-d'); + $endDate = $params['end_date'] ?? now()->format('Y-m-d'); + $search = $params['search'] ?? null; + $perPage = $params['per_page'] ?? 50; + + $query = HometaxInvoice::where('tenant_id', $tenantId) + ->where('invoice_type', $invoiceType) + ->period($startDate, $endDate); + + if ($search) { + $query->where(function ($q) use ($search, $invoiceType) { + if ($invoiceType === 'sales') { + $q->where('invoicee_corp_name', 'like', "%{$search}%") + ->orWhere('invoicee_corp_num', 'like', "%{$search}%"); + } else { + $q->where('invoicer_corp_name', 'like', "%{$search}%") + ->orWhere('invoicer_corp_num', 'like', "%{$search}%"); + } + $q->orWhere('nts_confirm_num', 'like', "%{$search}%") + ->orWhere('item_name', 'like', "%{$search}%"); + }); + } + + $query->orderByDesc('write_date')->orderByDesc('issue_date'); + + $invoices = $query->paginate($perPage); + + // 분개 존재 여부 로드 + $invoiceIds = $invoices->getCollection()->pluck('id')->toArray(); + $journaledIds = HometaxInvoiceJournal::getJournaledInvoiceIds($tenantId, $invoiceIds); + + $invoices->getCollection()->transform(function ($invoice) use ($journaledIds) { + $invoice->has_journal = in_array($invoice->id, $journaledIds); + + return $invoice; + }); + + return [ + 'data' => $invoices, + ]; + } +} diff --git a/routes/api/v1/finance.php b/routes/api/v1/finance.php index 77f1080..d4a580a 100644 --- a/routes/api/v1/finance.php +++ b/routes/api/v1/finance.php @@ -16,6 +16,8 @@ use App\Http\Controllers\Api\V1\BadDebtController; use App\Http\Controllers\Api\V1\BankAccountController; use App\Http\Controllers\Api\V1\BankTransactionController; +use App\Http\Controllers\Api\V1\BarobillBankTransactionController; +use App\Http\Controllers\Api\V1\BarobillCardTransactionController; use App\Http\Controllers\Api\V1\BarobillController; use App\Http\Controllers\Api\V1\BarobillSettingController; use App\Http\Controllers\Api\V1\BillController; @@ -28,6 +30,7 @@ use App\Http\Controllers\Api\V1\EntertainmentController; use App\Http\Controllers\Api\V1\ExpectedExpenseController; use App\Http\Controllers\Api\V1\GeneralJournalEntryController; +use App\Http\Controllers\Api\V1\HometaxInvoiceController; use App\Http\Controllers\Api\V1\LoanController; use App\Http\Controllers\Api\V1\PaymentController; use App\Http\Controllers\Api\V1\PayrollController; @@ -282,6 +285,60 @@ Route::get('/certificate-url', [BarobillController::class, 'certificateUrl'])->name('v1.barobill.certificate-url'); }); +// Barobill Card Transaction API (바로빌 카드 거래 - React 연동) +Route::prefix('barobill-card-transactions')->group(function () { + Route::get('', [BarobillCardTransactionController::class, 'index'])->name('v1.barobill-card-transactions.index'); + Route::get('/card-numbers', [BarobillCardTransactionController::class, 'cardNumbers'])->name('v1.barobill-card-transactions.card-numbers'); + Route::get('/hidden', [BarobillCardTransactionController::class, 'hiddenList'])->name('v1.barobill-card-transactions.hidden'); + Route::get('/splits', [BarobillCardTransactionController::class, 'getSplits'])->name('v1.barobill-card-transactions.splits.show'); + Route::post('/splits', [BarobillCardTransactionController::class, 'saveSplits'])->name('v1.barobill-card-transactions.splits.store'); + Route::delete('/splits', [BarobillCardTransactionController::class, 'deleteSplits'])->name('v1.barobill-card-transactions.splits.destroy'); + Route::post('/manual', [BarobillCardTransactionController::class, 'storeManual'])->name('v1.barobill-card-transactions.manual.store'); + Route::put('/manual/{id}', [BarobillCardTransactionController::class, 'updateManual'])->whereNumber('id')->name('v1.barobill-card-transactions.manual.update'); + Route::delete('/manual/{id}', [BarobillCardTransactionController::class, 'destroyManual'])->whereNumber('id')->name('v1.barobill-card-transactions.manual.destroy'); + Route::get('/{id}', [BarobillCardTransactionController::class, 'show'])->whereNumber('id')->name('v1.barobill-card-transactions.show'); + Route::post('/{id}/hide', [BarobillCardTransactionController::class, 'hide'])->whereNumber('id')->name('v1.barobill-card-transactions.hide'); + Route::post('/{id}/restore', [BarobillCardTransactionController::class, 'restore'])->whereNumber('id')->name('v1.barobill-card-transactions.restore'); + Route::put('/{id}/amount', [BarobillCardTransactionController::class, 'updateAmount'])->whereNumber('id')->name('v1.barobill-card-transactions.update-amount'); + Route::get('/{id}/journal-entries', [BarobillCardTransactionController::class, 'getJournalEntries'])->whereNumber('id')->name('v1.barobill-card-transactions.journal-entries.show'); + Route::post('/{id}/journal-entries', [BarobillCardTransactionController::class, 'storeJournalEntries'])->whereNumber('id')->name('v1.barobill-card-transactions.journal-entries.store'); + Route::delete('/{id}/journal-entries', [BarobillCardTransactionController::class, 'deleteJournalEntries'])->whereNumber('id')->name('v1.barobill-card-transactions.journal-entries.destroy'); +}); + +// Barobill Bank Transaction API (바로빌 은행 거래 - React 연동) +Route::prefix('barobill-bank-transactions')->group(function () { + Route::get('', [BarobillBankTransactionController::class, 'index'])->name('v1.barobill-bank-transactions.index'); + Route::get('/accounts', [BarobillBankTransactionController::class, 'accounts'])->name('v1.barobill-bank-transactions.accounts'); + Route::get('/balance-summary', [BarobillBankTransactionController::class, 'balanceSummary'])->name('v1.barobill-bank-transactions.balance-summary'); + Route::get('/splits', [BarobillBankTransactionController::class, 'getSplits'])->name('v1.barobill-bank-transactions.splits.show'); + Route::post('/splits', [BarobillBankTransactionController::class, 'saveSplits'])->name('v1.barobill-bank-transactions.splits.store'); + Route::delete('/splits', [BarobillBankTransactionController::class, 'deleteSplits'])->name('v1.barobill-bank-transactions.splits.destroy'); + Route::post('/override', [BarobillBankTransactionController::class, 'saveOverride'])->name('v1.barobill-bank-transactions.override'); + Route::post('/manual', [BarobillBankTransactionController::class, 'storeManual'])->name('v1.barobill-bank-transactions.manual.store'); + Route::put('/manual/{id}', [BarobillBankTransactionController::class, 'updateManual'])->whereNumber('id')->name('v1.barobill-bank-transactions.manual.update'); + Route::delete('/manual/{id}', [BarobillBankTransactionController::class, 'destroyManual'])->whereNumber('id')->name('v1.barobill-bank-transactions.manual.destroy'); + Route::get('/{id}/journal-entries', [BarobillBankTransactionController::class, 'getJournalEntries'])->whereNumber('id')->name('v1.barobill-bank-transactions.journal-entries.show'); + Route::post('/{id}/journal-entries', [BarobillBankTransactionController::class, 'storeJournalEntries'])->whereNumber('id')->name('v1.barobill-bank-transactions.journal-entries.store'); + Route::delete('/{id}/journal-entries', [BarobillBankTransactionController::class, 'deleteJournalEntries'])->whereNumber('id')->name('v1.barobill-bank-transactions.journal-entries.destroy'); +}); + +// Hometax Invoice API (홈택스 세금계산서 - React 연동) +Route::prefix('hometax-invoices')->group(function () { + Route::get('/sales', [HometaxInvoiceController::class, 'sales'])->name('v1.hometax-invoices.sales'); + Route::get('/purchases', [HometaxInvoiceController::class, 'purchases'])->name('v1.hometax-invoices.purchases'); + Route::get('/summary', [HometaxInvoiceController::class, 'summary'])->name('v1.hometax-invoices.summary'); + Route::post('', [HometaxInvoiceController::class, 'store'])->name('v1.hometax-invoices.store'); + Route::get('/{id}', [HometaxInvoiceController::class, 'show'])->whereNumber('id')->name('v1.hometax-invoices.show'); + Route::put('/{id}', [HometaxInvoiceController::class, 'update'])->whereNumber('id')->name('v1.hometax-invoices.update'); + Route::delete('/{id}', [HometaxInvoiceController::class, 'destroy'])->whereNumber('id')->name('v1.hometax-invoices.destroy'); + Route::get('/{id}/journals', [HometaxInvoiceController::class, 'getJournals'])->whereNumber('id')->name('v1.hometax-invoices.journals.show'); + Route::post('/{id}/journals', [HometaxInvoiceController::class, 'saveJournals'])->whereNumber('id')->name('v1.hometax-invoices.journals.store'); + Route::delete('/{id}/journals', [HometaxInvoiceController::class, 'deleteJournals'])->whereNumber('id')->name('v1.hometax-invoices.journals.destroy'); + Route::get('/{id}/journal-entries', [HometaxInvoiceController::class, 'getJournalEntries'])->whereNumber('id')->name('v1.hometax-invoices.journal-entries.show'); + Route::post('/{id}/journal-entries', [HometaxInvoiceController::class, 'storeJournalEntries'])->whereNumber('id')->name('v1.hometax-invoices.journal-entries.store'); + Route::delete('/{id}/journal-entries', [HometaxInvoiceController::class, 'deleteJournalEntries'])->whereNumber('id')->name('v1.hometax-invoices.journal-entries.destroy'); +}); + // Tax Invoice API (세금계산서) Route::prefix('tax-invoices')->group(function () { Route::get('', [TaxInvoiceController::class, 'index'])->name('v1.tax-invoices.index');