feat: [barobill] React 연동용 바로빌 카드/은행/홈택스 REST API 구현

- 바로빌 카드 거래 API (16 엔드포인트): 조회, 분할, 수동입력, 숨김/복원, 금액수정, 분개
- 바로빌 은행 거래 API (13 엔드포인트): 조회, 분할, 오버라이드, 수동입력, 잔액요약, 분개
- 홈택스 세금계산서 API (13 엔드포인트): 매출/매입 조회, 수동입력, 자체분개, 통합분개
- JournalEntry 소스 타입 상수 추가 (barobill_card, barobill_bank, hometax_invoice)
This commit is contained in:
김보곤
2026-03-11 19:41:07 +09:00
parent 82621a6045
commit eeda6d980e
8 changed files with 1737 additions and 0 deletions

View File

@@ -0,0 +1,287 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Helpers\ApiResponse;
use App\Http\Controllers\Controller;
use App\Models\Tenants\JournalEntry;
use App\Services\BarobillBankTransactionService;
use App\Services\JournalSyncService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/**
* 바로빌 은행 거래 API 컨트롤러 (React 연동용)
*
* MNG에서 동기화된 은행 거래 데이터를 React에서 조회/관리
*/
class BarobillBankTransactionController extends Controller
{
public function __construct(
protected BarobillBankTransactionService $service,
protected JournalSyncService $journalSyncService,
) {}
/**
* 은행 거래 목록 조회
*/
public function index(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',
'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'));
}
}

View File

@@ -0,0 +1,326 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Helpers\ApiResponse;
use App\Http\Controllers\Controller;
use App\Models\Tenants\JournalEntry;
use App\Services\BarobillCardTransactionService;
use App\Services\JournalSyncService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/**
* 바로빌 카드 거래 API 컨트롤러 (React 연동용)
*
* MNG에서 동기화된 카드 거래 데이터를 React에서 조회/관리
*/
class BarobillCardTransactionController extends Controller
{
public function __construct(
protected BarobillCardTransactionService $service,
protected JournalSyncService $journalSyncService,
) {}
/**
* 카드 거래 목록 조회
*/
public function index(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',
'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'));
}
}

View File

@@ -0,0 +1,278 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Helpers\ApiResponse;
use App\Http\Controllers\Controller;
use App\Models\Tenants\JournalEntry;
use App\Services\HometaxInvoiceService;
use App\Services\JournalSyncService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
/**
* 홈택스 세금계산서 API 컨트롤러 (React 연동용)
*
* MNG에서 동기화된 홈택스 세금계산서를 React에서 조회/관리
*/
class HometaxInvoiceController extends Controller
{
public function __construct(
protected HometaxInvoiceService $service,
protected JournalSyncService $journalSyncService,
) {}
/**
* 매출 세금계산서 목록
*/
public function sales(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->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'));
}
}

View File

@@ -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';

View File

@@ -0,0 +1,249 @@
<?php
namespace App\Services;
use App\Models\Barobill\BarobillBankTransaction;
use App\Models\Barobill\BarobillBankTransactionOverride;
use App\Models\Barobill\BarobillBankTransactionSplit;
use Illuminate\Support\Facades\DB;
/**
* 바로빌 은행 거래 서비스 (React 연동용)
*
* MNG에서 동기화된 barobill_bank_transactions 데이터를
* React 프론트엔드에서 조회/분개/수정 등 처리
*/
class BarobillBankTransactionService extends Service
{
/**
* 은행 거래 목록 조회 (기간별, 계좌번호별)
*/
public function index(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');
$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];
}
}

View File

@@ -0,0 +1,308 @@
<?php
namespace App\Services;
use App\Models\Barobill\BarobillCardTransaction;
use App\Models\Barobill\BarobillCardTransactionAmountLog;
use App\Models\Barobill\BarobillCardTransactionHide;
use App\Models\Barobill\BarobillCardTransactionSplit;
use Illuminate\Support\Facades\DB;
/**
* 바로빌 카드 거래 서비스 (React 연동용)
*
* MNG에서 동기화된 barobill_card_transactions 데이터를
* React 프론트엔드에서 조회/분개/숨김 등 처리
*/
class BarobillCardTransactionService extends Service
{
/**
* 카드 거래 목록 조회 (기간별, 카드번호별)
*/
public function index(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');
$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];
}
}

View File

@@ -0,0 +1,222 @@
<?php
namespace App\Services;
use App\Models\Barobill\HometaxInvoice;
use App\Models\Barobill\HometaxInvoiceJournal;
use Illuminate\Support\Facades\DB;
/**
* 홈택스 세금계산서 서비스 (React 연동용)
*
* MNG에서 동기화된 hometax_invoices 데이터를
* React 프론트엔드에서 조회/분개 등 처리
*/
class HometaxInvoiceService extends Service
{
/**
* 매출 세금계산서 목록
*/
public function sales(array $params): array
{
return $this->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,
];
}
}

View File

@@ -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');