feat: [loan] 상품권 접대비 자동 연동 기능 추가

- ExpenseAccount: loan_id 필드 + SUB_TYPE_GIFT_CERTIFICATE 상수 추가
- LoanService: 상품권 used+접대비해당 시 expense_accounts 자동 upsert/삭제
- 마이그레이션: expense_accounts에 loan_id 컬럼 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-03-05 21:22:44 +09:00
parent 8c9f2fcfb5
commit 31d2f08dd8
3 changed files with 92 additions and 0 deletions

View File

@@ -34,6 +34,7 @@ class ExpenseAccount extends Model
'vendor_name',
'payment_method',
'card_no',
'loan_id',
'created_by',
'updated_by',
'deleted_by',
@@ -53,6 +54,9 @@ class ExpenseAccount extends Model
public const TYPE_OFFICE = 'office';
// 세부 유형 상수 (접대비)
public const SUB_TYPE_GIFT_CERTIFICATE = 'gift_certificate';
// 세부 유형 상수 (복리후생)
public const SUB_TYPE_MEAL = 'meal';

View File

@@ -2,6 +2,7 @@
namespace App\Services;
use App\Models\Tenants\ExpenseAccount;
use App\Models\Tenants\Loan;
use App\Models\Tenants\Withdrawal;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
@@ -261,9 +262,56 @@ public function update(int $id, array $data): Loan
$loan->save();
// 상품권 → 접대비 자동 연동
if ($loan->category === Loan::CATEGORY_GIFT_CERTIFICATE) {
$this->syncGiftCertificateExpense($loan);
}
return $loan->fresh(['user:id,name,email', 'creator:id,name']);
}
/**
* 상품권 → 접대비 자동 연동
*
* 상태가 'used' + entertainment_expense='applicable' → expense_accounts에 INSERT
* 그 외 → 기존 연결된 expense_accounts 삭제
*/
private function syncGiftCertificateExpense(Loan $loan): void
{
$metadata = $loan->metadata ?? [];
$isEntertainment = ($loan->status === Loan::STATUS_USED)
&& ($metadata['entertainment_expense'] ?? '') === 'applicable';
if ($isEntertainment) {
// upsert: loan_id 기준으로 있으면 업데이트, 없으면 생성
ExpenseAccount::query()
->updateOrCreate(
[
'tenant_id' => $loan->tenant_id,
'loan_id' => $loan->id,
],
[
'account_type' => ExpenseAccount::TYPE_ENTERTAINMENT,
'sub_type' => ExpenseAccount::SUB_TYPE_GIFT_CERTIFICATE,
'expense_date' => $loan->settlement_date ?? $loan->loan_date,
'amount' => $loan->amount,
'description' => ($metadata['cert_name'] ?? '상품권') . ' 접대비 전환',
'vendor_name' => $metadata['vendor_name'] ?? null,
'vendor_id' => ! empty($metadata['vendor_id']) ? (int) $metadata['vendor_id'] : null,
'payment_method' => ExpenseAccount::PAYMENT_CASH,
'created_by' => $loan->updated_by ?? $loan->created_by,
'updated_by' => $loan->updated_by ?? $loan->created_by,
]
);
} else {
// 접대비 해당이 아니면 연결된 레코드 삭제
ExpenseAccount::query()
->where('tenant_id', $loan->tenant_id)
->where('loan_id', $loan->id)
->delete();
}
}
/**
* 가지급금 삭제
*/
@@ -280,6 +328,14 @@ public function destroy(int $id): bool
throw new BadRequestHttpException(__('error.loan.not_deletable'));
}
// 상품권 연결 접대비 레코드도 삭제
if ($loan->category === Loan::CATEGORY_GIFT_CERTIFICATE) {
ExpenseAccount::query()
->where('tenant_id', $tenantId)
->where('loan_id', $loan->id)
->delete();
}
$loan->deleted_by = $userId;
$loan->save();
$loan->delete();

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('expense_accounts', function (Blueprint $table) {
$table->unsignedBigInteger('loan_id')->nullable()->after('card_no')
->comment('연결된 가지급금 ID (상품권→접대비 전환 시)');
$table->index('loan_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('expense_accounts', function (Blueprint $table) {
$table->dropIndex(['loan_id']);
$table->dropColumn('loan_id');
});
}
};