feat: [bill,loan] 어음 V8 확장 필드 및 가지급금 상품권 카테고리 지원
- Bill 모델: V8 확장 필드 54개 추가 (증권종류, 할인, 배서, 추심, 개서, 부도 등) - Bill 상태: 수취/발행 어음·수표별 세분화된 상태 체계 - BillService: assignV8Fields/syncInstallments 헬퍼 추출, instrument_type/medium 필터 - BillInstallment: type/counterparty 필드 추가 - Loan 모델: holding/used/disposed 상태 + metadata(JSON) 필드 추가 - LoanService: 상품권 카테고리 지원 (summary 상태별 집계, store 기본상태 holding) - FormRequest: V8 확장 필드 검증 규칙 추가 - 마이그레이션: bills V8 필드 + loans metadata 컬럼 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -34,8 +34,10 @@ public function index(LoanIndexRequest $request): JsonResponse
|
||||
*/
|
||||
public function summary(LoanIndexRequest $request): JsonResponse
|
||||
{
|
||||
$userId = $request->validated()['user_id'] ?? null;
|
||||
$result = $this->loanService->summary($userId);
|
||||
$validated = $request->validated();
|
||||
$userId = $validated['user_id'] ?? null;
|
||||
$category = $validated['category'] ?? null;
|
||||
$result = $this->loanService->summary($userId, $category);
|
||||
|
||||
return ApiResponse::success($result, __('message.fetched'));
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ public function rules(): array
|
||||
return [
|
||||
'user_id' => ['nullable', 'integer', 'exists:users,id'],
|
||||
'status' => ['nullable', 'string', Rule::in(Loan::STATUSES)],
|
||||
'category' => ['nullable', 'string', Rule::in(Loan::CATEGORIES)],
|
||||
'start_date' => ['nullable', 'date', 'date_format:Y-m-d'],
|
||||
'end_date' => ['nullable', 'date', 'date_format:Y-m-d', 'after_or_equal:start_date'],
|
||||
'search' => ['nullable', 'string', 'max:100'],
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
namespace App\Http\Requests\Loan;
|
||||
|
||||
use App\Models\Tenants\Loan;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class LoanStoreRequest extends FormRequest
|
||||
{
|
||||
@@ -21,12 +23,27 @@ public function authorize(): bool
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$isGiftCertificate = $this->input('category') === Loan::CATEGORY_GIFT_CERTIFICATE;
|
||||
|
||||
return [
|
||||
'user_id' => ['required', 'integer', 'exists:users,id'],
|
||||
'user_id' => [$isGiftCertificate ? 'nullable' : 'required', 'integer', 'exists:users,id'],
|
||||
'loan_date' => ['required', 'date', 'date_format:Y-m-d'],
|
||||
'amount' => ['required', 'numeric', 'min:0', 'max:999999999999.99'],
|
||||
'purpose' => ['nullable', 'string', 'max:1000'],
|
||||
'withdrawal_id' => ['nullable', 'integer', 'exists:withdrawals,id'],
|
||||
'category' => ['nullable', 'string', Rule::in(Loan::CATEGORIES)],
|
||||
'status' => ['nullable', 'string', Rule::in(Loan::STATUSES)],
|
||||
'metadata' => ['nullable', 'array'],
|
||||
'metadata.serial_number' => ['nullable', 'string', 'max:100'],
|
||||
'metadata.cert_name' => ['nullable', 'string', 'max:200'],
|
||||
'metadata.vendor_id' => ['nullable', 'string', 'max:50'],
|
||||
'metadata.vendor_name' => ['nullable', 'string', 'max:200'],
|
||||
'metadata.purchase_purpose' => ['nullable', 'string', 'max:50'],
|
||||
'metadata.entertainment_expense' => ['nullable', 'string', 'max:50'],
|
||||
'metadata.recipient_name' => ['nullable', 'string', 'max:100'],
|
||||
'metadata.recipient_organization' => ['nullable', 'string', 'max:200'],
|
||||
'metadata.usage_description' => ['nullable', 'string', 'max:1000'],
|
||||
'metadata.memo' => ['nullable', 'string', 'max:2000'],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
namespace App\Http\Requests\Loan;
|
||||
|
||||
use App\Models\Tenants\Loan;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class LoanUpdateRequest extends FormRequest
|
||||
{
|
||||
@@ -27,6 +29,20 @@ public function rules(): array
|
||||
'amount' => ['sometimes', 'numeric', 'min:0', 'max:999999999999.99'],
|
||||
'purpose' => ['nullable', 'string', 'max:1000'],
|
||||
'withdrawal_id' => ['nullable', 'integer', 'exists:withdrawals,id'],
|
||||
'category' => ['sometimes', 'string', Rule::in(Loan::CATEGORIES)],
|
||||
'status' => ['sometimes', 'string', Rule::in(Loan::STATUSES)],
|
||||
'settlement_date' => ['nullable', 'date', 'date_format:Y-m-d'],
|
||||
'metadata' => ['nullable', 'array'],
|
||||
'metadata.serial_number' => ['nullable', 'string', 'max:100'],
|
||||
'metadata.cert_name' => ['nullable', 'string', 'max:200'],
|
||||
'metadata.vendor_id' => ['nullable', 'string', 'max:50'],
|
||||
'metadata.vendor_name' => ['nullable', 'string', 'max:200'],
|
||||
'metadata.purchase_purpose' => ['nullable', 'string', 'max:50'],
|
||||
'metadata.entertainment_expense' => ['nullable', 'string', 'max:50'],
|
||||
'metadata.recipient_name' => ['nullable', 'string', 'max:100'],
|
||||
'metadata.recipient_organization' => ['nullable', 'string', 'max:200'],
|
||||
'metadata.usage_description' => ['nullable', 'string', 'max:1000'],
|
||||
'metadata.memo' => ['nullable', 'string', 'max:2000'],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ public function rules(): array
|
||||
$tenantId = app('tenant_id') ?? 0;
|
||||
|
||||
return [
|
||||
// === 기존 필드 ===
|
||||
'bill_number' => [
|
||||
'nullable',
|
||||
'string',
|
||||
@@ -30,16 +31,99 @@ public function rules(): array
|
||||
'client_name' => ['nullable', 'string', 'max:100'],
|
||||
'amount' => ['required', 'numeric', 'min:0'],
|
||||
'issue_date' => ['required', 'date'],
|
||||
'maturity_date' => ['required', 'date', 'after_or_equal:issue_date'],
|
||||
'status' => ['nullable', 'string', 'in:stored,maturityAlert,maturityResult,paymentComplete,dishonored,collectionRequest,collectionComplete,suing'],
|
||||
'maturity_date' => ['nullable', 'date', 'after_or_equal:issue_date'],
|
||||
'status' => ['nullable', 'string', 'max:30'],
|
||||
'reason' => ['nullable', 'string', 'max:255'],
|
||||
'installment_count' => ['nullable', 'integer', 'min:0'],
|
||||
'note' => ['nullable', 'string', 'max:1000'],
|
||||
'is_electronic' => ['nullable', 'boolean'],
|
||||
'bank_account_id' => ['nullable', 'integer', 'exists:bank_accounts,id'],
|
||||
|
||||
// === V8 증권종류/매체/구분 ===
|
||||
'instrument_type' => ['nullable', 'string', 'in:promissory,exchange,cashierCheck,currentCheck'],
|
||||
'medium' => ['nullable', 'string', 'in:electronic,paper'],
|
||||
'bill_category' => ['nullable', 'string', 'in:commercial,other'],
|
||||
|
||||
// === 전자어음 ===
|
||||
'electronic_bill_no' => ['nullable', 'string', 'max:100'],
|
||||
'registration_org' => ['nullable', 'string', 'in:kftc,bank'],
|
||||
|
||||
// === 환어음 ===
|
||||
'drawee' => ['nullable', 'string', 'max:100'],
|
||||
'acceptance_status' => ['nullable', 'string', 'in:accepted,pending,refused'],
|
||||
'acceptance_date' => ['nullable', 'date'],
|
||||
'acceptance_refusal_date' => ['nullable', 'date'],
|
||||
'acceptance_refusal_reason' => ['nullable', 'string', 'max:50'],
|
||||
|
||||
// === 받을어음 전용 ===
|
||||
'endorsement' => ['nullable', 'string', 'in:endorsable,nonEndorsable'],
|
||||
'endorsement_order' => ['nullable', 'string', 'max:5'],
|
||||
'storage_place' => ['nullable', 'string', 'in:safe,bank,other'],
|
||||
'issuer_bank' => ['nullable', 'string', 'max:100'],
|
||||
|
||||
// 할인
|
||||
'is_discounted' => ['nullable', 'boolean'],
|
||||
'discount_date' => ['nullable', 'date'],
|
||||
'discount_bank' => ['nullable', 'string', 'max:100'],
|
||||
'discount_rate' => ['nullable', 'numeric', 'min:0', 'max:100'],
|
||||
'discount_amount' => ['nullable', 'numeric', 'min:0'],
|
||||
|
||||
// 배서양도
|
||||
'endorsement_date' => ['nullable', 'date'],
|
||||
'endorsee' => ['nullable', 'string', 'max:100'],
|
||||
'endorsement_reason' => ['nullable', 'string', 'in:payment,guarantee,collection,other'],
|
||||
|
||||
// 추심
|
||||
'collection_bank' => ['nullable', 'string', 'max:100'],
|
||||
'collection_request_date' => ['nullable', 'date'],
|
||||
'collection_fee' => ['nullable', 'numeric', 'min:0'],
|
||||
'collection_complete_date' => ['nullable', 'date'],
|
||||
'collection_result' => ['nullable', 'string', 'in:success,partial,failed,pending'],
|
||||
'collection_deposit_date' => ['nullable', 'date'],
|
||||
'collection_deposit_amount' => ['nullable', 'numeric', 'min:0'],
|
||||
|
||||
// === 지급어음 전용 ===
|
||||
'settlement_bank' => ['nullable', 'string', 'max:100'],
|
||||
'payment_method' => ['nullable', 'string', 'in:autoTransfer,currentAccount,other'],
|
||||
'actual_payment_date' => ['nullable', 'date'],
|
||||
|
||||
// === 공통 ===
|
||||
'payment_place' => ['nullable', 'string', 'max:30'],
|
||||
'payment_place_detail' => ['nullable', 'string', 'max:200'],
|
||||
|
||||
// 개서
|
||||
'renewal_date' => ['nullable', 'date'],
|
||||
'renewal_new_bill_no' => ['nullable', 'string', 'max:50'],
|
||||
'renewal_reason' => ['nullable', 'string', 'in:maturityExtension,amountChange,conditionChange,other'],
|
||||
|
||||
// 소구
|
||||
'recourse_date' => ['nullable', 'date'],
|
||||
'recourse_amount' => ['nullable', 'numeric', 'min:0'],
|
||||
'recourse_target' => ['nullable', 'string', 'max:100'],
|
||||
'recourse_reason' => ['nullable', 'string', 'in:endorsedDishonor,discountDishonor,other'],
|
||||
|
||||
// 환매
|
||||
'buyback_date' => ['nullable', 'date'],
|
||||
'buyback_amount' => ['nullable', 'numeric', 'min:0'],
|
||||
'buyback_bank' => ['nullable', 'string', 'max:100'],
|
||||
|
||||
// 부도/법적절차
|
||||
'dishonored_date' => ['nullable', 'date'],
|
||||
'dishonored_reason' => ['nullable', 'string', 'max:30'],
|
||||
'has_protest' => ['nullable', 'boolean'],
|
||||
'protest_date' => ['nullable', 'date'],
|
||||
'recourse_notice_date' => ['nullable', 'date'],
|
||||
'recourse_notice_deadline' => ['nullable', 'date'],
|
||||
|
||||
// 분할배서
|
||||
'is_split' => ['nullable', 'boolean'],
|
||||
|
||||
// === 차수 관리 ===
|
||||
'installments' => ['nullable', 'array'],
|
||||
'installments.*.date' => ['required_with:installments', 'date'],
|
||||
'installments.*.amount' => ['required_with:installments', 'numeric', 'min:0'],
|
||||
'installments.*.type' => ['nullable', 'string', 'max:30'],
|
||||
'installments.*.counterparty' => ['nullable', 'string', 'max:100'],
|
||||
'installments.*.note' => ['nullable', 'string', 'max:255'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ public function authorize(): bool
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
// === 기존 필드 ===
|
||||
'bill_number' => ['nullable', 'string', 'max:50'],
|
||||
'bill_type' => ['nullable', 'string', 'in:received,issued'],
|
||||
'client_id' => ['nullable', 'integer', 'exists:clients,id'],
|
||||
@@ -21,15 +22,72 @@ public function rules(): array
|
||||
'amount' => ['nullable', 'numeric', 'min:0'],
|
||||
'issue_date' => ['nullable', 'date'],
|
||||
'maturity_date' => ['nullable', 'date', 'after_or_equal:issue_date'],
|
||||
'status' => ['nullable', 'string', 'in:stored,maturityAlert,maturityResult,paymentComplete,dishonored,collectionRequest,collectionComplete,suing'],
|
||||
'status' => ['nullable', 'string', 'max:30'],
|
||||
'reason' => ['nullable', 'string', 'max:255'],
|
||||
'installment_count' => ['nullable', 'integer', 'min:0'],
|
||||
'note' => ['nullable', 'string', 'max:1000'],
|
||||
'is_electronic' => ['nullable', 'boolean'],
|
||||
'bank_account_id' => ['nullable', 'integer', 'exists:bank_accounts,id'],
|
||||
|
||||
// === V8 확장 ===
|
||||
'instrument_type' => ['nullable', 'string', 'in:promissory,exchange,cashierCheck,currentCheck'],
|
||||
'medium' => ['nullable', 'string', 'in:electronic,paper'],
|
||||
'bill_category' => ['nullable', 'string', 'in:commercial,other'],
|
||||
'electronic_bill_no' => ['nullable', 'string', 'max:100'],
|
||||
'registration_org' => ['nullable', 'string', 'in:kftc,bank'],
|
||||
'drawee' => ['nullable', 'string', 'max:100'],
|
||||
'acceptance_status' => ['nullable', 'string', 'in:accepted,pending,refused'],
|
||||
'acceptance_date' => ['nullable', 'date'],
|
||||
'acceptance_refusal_date' => ['nullable', 'date'],
|
||||
'acceptance_refusal_reason' => ['nullable', 'string', 'max:50'],
|
||||
'endorsement' => ['nullable', 'string', 'in:endorsable,nonEndorsable'],
|
||||
'endorsement_order' => ['nullable', 'string', 'max:5'],
|
||||
'storage_place' => ['nullable', 'string', 'in:safe,bank,other'],
|
||||
'issuer_bank' => ['nullable', 'string', 'max:100'],
|
||||
'is_discounted' => ['nullable', 'boolean'],
|
||||
'discount_date' => ['nullable', 'date'],
|
||||
'discount_bank' => ['nullable', 'string', 'max:100'],
|
||||
'discount_rate' => ['nullable', 'numeric', 'min:0', 'max:100'],
|
||||
'discount_amount' => ['nullable', 'numeric', 'min:0'],
|
||||
'endorsement_date' => ['nullable', 'date'],
|
||||
'endorsee' => ['nullable', 'string', 'max:100'],
|
||||
'endorsement_reason' => ['nullable', 'string', 'in:payment,guarantee,collection,other'],
|
||||
'collection_bank' => ['nullable', 'string', 'max:100'],
|
||||
'collection_request_date' => ['nullable', 'date'],
|
||||
'collection_fee' => ['nullable', 'numeric', 'min:0'],
|
||||
'collection_complete_date' => ['nullable', 'date'],
|
||||
'collection_result' => ['nullable', 'string', 'in:success,partial,failed,pending'],
|
||||
'collection_deposit_date' => ['nullable', 'date'],
|
||||
'collection_deposit_amount' => ['nullable', 'numeric', 'min:0'],
|
||||
'settlement_bank' => ['nullable', 'string', 'max:100'],
|
||||
'payment_method' => ['nullable', 'string', 'in:autoTransfer,currentAccount,other'],
|
||||
'actual_payment_date' => ['nullable', 'date'],
|
||||
'payment_place' => ['nullable', 'string', 'max:30'],
|
||||
'payment_place_detail' => ['nullable', 'string', 'max:200'],
|
||||
'renewal_date' => ['nullable', 'date'],
|
||||
'renewal_new_bill_no' => ['nullable', 'string', 'max:50'],
|
||||
'renewal_reason' => ['nullable', 'string', 'in:maturityExtension,amountChange,conditionChange,other'],
|
||||
'recourse_date' => ['nullable', 'date'],
|
||||
'recourse_amount' => ['nullable', 'numeric', 'min:0'],
|
||||
'recourse_target' => ['nullable', 'string', 'max:100'],
|
||||
'recourse_reason' => ['nullable', 'string', 'in:endorsedDishonor,discountDishonor,other'],
|
||||
'buyback_date' => ['nullable', 'date'],
|
||||
'buyback_amount' => ['nullable', 'numeric', 'min:0'],
|
||||
'buyback_bank' => ['nullable', 'string', 'max:100'],
|
||||
'dishonored_date' => ['nullable', 'date'],
|
||||
'dishonored_reason' => ['nullable', 'string', 'max:30'],
|
||||
'has_protest' => ['nullable', 'boolean'],
|
||||
'protest_date' => ['nullable', 'date'],
|
||||
'recourse_notice_date' => ['nullable', 'date'],
|
||||
'recourse_notice_deadline' => ['nullable', 'date'],
|
||||
'is_split' => ['nullable', 'boolean'],
|
||||
|
||||
// === 차수 관리 ===
|
||||
'installments' => ['nullable', 'array'],
|
||||
'installments.*.date' => ['required_with:installments', 'date'],
|
||||
'installments.*.amount' => ['required_with:installments', 'numeric', 'min:0'],
|
||||
'installments.*.type' => ['nullable', 'string', 'max:30'],
|
||||
'installments.*.counterparty' => ['nullable', 'string', 'max:100'],
|
||||
'installments.*.note' => ['nullable', 'string', 'max:255'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -31,6 +31,58 @@ class Bill extends Model
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
// V8 확장 필드
|
||||
'instrument_type',
|
||||
'medium',
|
||||
'bill_category',
|
||||
'electronic_bill_no',
|
||||
'registration_org',
|
||||
'drawee',
|
||||
'acceptance_status',
|
||||
'acceptance_date',
|
||||
'acceptance_refusal_date',
|
||||
'acceptance_refusal_reason',
|
||||
'endorsement',
|
||||
'endorsement_order',
|
||||
'storage_place',
|
||||
'issuer_bank',
|
||||
'is_discounted',
|
||||
'discount_date',
|
||||
'discount_bank',
|
||||
'discount_rate',
|
||||
'discount_amount',
|
||||
'endorsement_date',
|
||||
'endorsee',
|
||||
'endorsement_reason',
|
||||
'collection_bank',
|
||||
'collection_request_date',
|
||||
'collection_fee',
|
||||
'collection_complete_date',
|
||||
'collection_result',
|
||||
'collection_deposit_date',
|
||||
'collection_deposit_amount',
|
||||
'settlement_bank',
|
||||
'payment_method',
|
||||
'actual_payment_date',
|
||||
'payment_place',
|
||||
'payment_place_detail',
|
||||
'renewal_date',
|
||||
'renewal_new_bill_no',
|
||||
'renewal_reason',
|
||||
'recourse_date',
|
||||
'recourse_amount',
|
||||
'recourse_target',
|
||||
'recourse_reason',
|
||||
'buyback_date',
|
||||
'buyback_amount',
|
||||
'buyback_bank',
|
||||
'dishonored_date',
|
||||
'dishonored_reason',
|
||||
'has_protest',
|
||||
'protest_date',
|
||||
'recourse_notice_date',
|
||||
'recourse_notice_deadline',
|
||||
'is_split',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
@@ -41,21 +93,57 @@ class Bill extends Model
|
||||
'bank_account_id' => 'integer',
|
||||
'installment_count' => 'integer',
|
||||
'is_electronic' => 'boolean',
|
||||
// V8 확장 casts
|
||||
'acceptance_date' => 'date',
|
||||
'acceptance_refusal_date' => 'date',
|
||||
'discount_date' => 'date',
|
||||
'discount_rate' => 'decimal:2',
|
||||
'discount_amount' => 'decimal:2',
|
||||
'endorsement_date' => 'date',
|
||||
'collection_request_date' => 'date',
|
||||
'collection_fee' => 'decimal:2',
|
||||
'collection_complete_date' => 'date',
|
||||
'collection_deposit_date' => 'date',
|
||||
'collection_deposit_amount' => 'decimal:2',
|
||||
'actual_payment_date' => 'date',
|
||||
'renewal_date' => 'date',
|
||||
'recourse_date' => 'date',
|
||||
'recourse_amount' => 'decimal:2',
|
||||
'buyback_date' => 'date',
|
||||
'buyback_amount' => 'decimal:2',
|
||||
'dishonored_date' => 'date',
|
||||
'protest_date' => 'date',
|
||||
'recourse_notice_date' => 'date',
|
||||
'recourse_notice_deadline' => 'date',
|
||||
'is_discounted' => 'boolean',
|
||||
'has_protest' => 'boolean',
|
||||
'is_split' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* 배열/JSON 변환 시 날짜 형식 지정
|
||||
*/
|
||||
/**
|
||||
* 날짜 cast 필드 목록 (toArray에서 Y-m-d 형식 변환용)
|
||||
*/
|
||||
private const DATE_FIELDS = [
|
||||
'issue_date', 'maturity_date',
|
||||
'acceptance_date', 'acceptance_refusal_date',
|
||||
'discount_date', 'endorsement_date',
|
||||
'collection_request_date', 'collection_complete_date', 'collection_deposit_date',
|
||||
'actual_payment_date',
|
||||
'renewal_date', 'recourse_date', 'buyback_date',
|
||||
'dishonored_date', 'protest_date', 'recourse_notice_date', 'recourse_notice_deadline',
|
||||
];
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
$array = parent::toArray();
|
||||
|
||||
// 날짜 필드를 Y-m-d 형식으로 변환
|
||||
if (isset($array['issue_date']) && $this->issue_date) {
|
||||
$array['issue_date'] = $this->issue_date->format('Y-m-d');
|
||||
}
|
||||
if (isset($array['maturity_date']) && $this->maturity_date) {
|
||||
$array['maturity_date'] = $this->maturity_date->format('Y-m-d');
|
||||
foreach (self::DATE_FIELDS as $field) {
|
||||
if (isset($array[$field]) && $this->{$field}) {
|
||||
$array[$field] = $this->{$field}->format('Y-m-d');
|
||||
}
|
||||
}
|
||||
|
||||
return $array;
|
||||
@@ -69,14 +157,42 @@ public function toArray(): array
|
||||
'issued' => '발행',
|
||||
];
|
||||
|
||||
/**
|
||||
* 증권종류
|
||||
*/
|
||||
public const INSTRUMENT_TYPES = [
|
||||
'promissory' => '약속어음',
|
||||
'exchange' => '환어음',
|
||||
'cashierCheck' => '자기앞수표',
|
||||
'currentCheck' => '당좌수표',
|
||||
];
|
||||
|
||||
/**
|
||||
* 수취 어음 상태 목록
|
||||
*/
|
||||
public const RECEIVED_STATUSES = [
|
||||
'stored' => '보관중',
|
||||
'endorsed' => '배서양도',
|
||||
'discounted' => '할인',
|
||||
'collectionRequest' => '추심의뢰',
|
||||
'collectionComplete' => '추심완료',
|
||||
'maturityDeposit' => '만기입금',
|
||||
'paymentComplete' => '결제완료',
|
||||
'dishonored' => '부도',
|
||||
'renewed' => '개서',
|
||||
'buyback' => '환매',
|
||||
// 하위호환
|
||||
'maturityAlert' => '만기입금(7일전)',
|
||||
'maturityResult' => '만기결과',
|
||||
'paymentComplete' => '결제완료',
|
||||
];
|
||||
|
||||
/**
|
||||
* 수취 수표 상태 목록
|
||||
*/
|
||||
public const RECEIVED_CHECK_STATUSES = [
|
||||
'stored' => '보관중',
|
||||
'endorsed' => '배서양도',
|
||||
'deposited' => '입금',
|
||||
'dishonored' => '부도',
|
||||
];
|
||||
|
||||
@@ -85,10 +201,25 @@ public function toArray(): array
|
||||
*/
|
||||
public const ISSUED_STATUSES = [
|
||||
'stored' => '보관중',
|
||||
'issued' => '지급대기',
|
||||
'maturityPayment' => '만기결제',
|
||||
'paymentComplete' => '결제완료',
|
||||
'dishonored' => '부도',
|
||||
'renewed' => '개서',
|
||||
// 하위호환
|
||||
'maturityAlert' => '만기입금(7일전)',
|
||||
'collectionRequest' => '추심의뢰',
|
||||
'collectionComplete' => '추심완료',
|
||||
'suing' => '추소중',
|
||||
];
|
||||
|
||||
/**
|
||||
* 발행 수표 상태 목록
|
||||
*/
|
||||
public const ISSUED_CHECK_STATUSES = [
|
||||
'stored' => '보관중',
|
||||
'issued' => '지급대기',
|
||||
'cashed' => '현금화',
|
||||
'dishonored' => '부도',
|
||||
];
|
||||
|
||||
@@ -149,11 +280,25 @@ public function getBillTypeLabelAttribute(): string
|
||||
*/
|
||||
public function getStatusLabelAttribute(): string
|
||||
{
|
||||
$isCheck = in_array($this->instrument_type, ['cashierCheck', 'currentCheck']);
|
||||
|
||||
if ($this->bill_type === 'received') {
|
||||
return self::RECEIVED_STATUSES[$this->status] ?? $this->status;
|
||||
$statuses = $isCheck ? self::RECEIVED_CHECK_STATUSES : self::RECEIVED_STATUSES;
|
||||
|
||||
return $statuses[$this->status] ?? self::RECEIVED_STATUSES[$this->status] ?? $this->status;
|
||||
}
|
||||
|
||||
return self::ISSUED_STATUSES[$this->status] ?? $this->status;
|
||||
$statuses = $isCheck ? self::ISSUED_CHECK_STATUSES : self::ISSUED_STATUSES;
|
||||
|
||||
return $statuses[$this->status] ?? self::ISSUED_STATUSES[$this->status] ?? $this->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 증권종류 라벨
|
||||
*/
|
||||
public function getInstrumentTypeLabelAttribute(): string
|
||||
{
|
||||
return self::INSTRUMENT_TYPES[$this->instrument_type] ?? $this->instrument_type ?? '약속어음';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,8 +12,10 @@ class BillInstallment extends Model
|
||||
|
||||
protected $fillable = [
|
||||
'bill_id',
|
||||
'type',
|
||||
'installment_date',
|
||||
'amount',
|
||||
'counterparty',
|
||||
'note',
|
||||
'created_by',
|
||||
];
|
||||
|
||||
@@ -28,6 +28,12 @@ class Loan extends Model
|
||||
|
||||
public const STATUS_PARTIAL = 'partial'; // 부분정산
|
||||
|
||||
public const STATUS_HOLDING = 'holding'; // 보유 (상품권)
|
||||
|
||||
public const STATUS_USED = 'used'; // 사용 (상품권)
|
||||
|
||||
public const STATUS_DISPOSED = 'disposed'; // 폐기 (상품권)
|
||||
|
||||
/**
|
||||
* 상태 목록
|
||||
*/
|
||||
@@ -35,6 +41,9 @@ class Loan extends Model
|
||||
self::STATUS_OUTSTANDING,
|
||||
self::STATUS_SETTLED,
|
||||
self::STATUS_PARTIAL,
|
||||
self::STATUS_HOLDING,
|
||||
self::STATUS_USED,
|
||||
self::STATUS_DISPOSED,
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -104,6 +113,7 @@ class Loan extends Model
|
||||
'settlement_amount',
|
||||
'status',
|
||||
'category',
|
||||
'metadata',
|
||||
'withdrawal_id',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
@@ -115,6 +125,7 @@ class Loan extends Model
|
||||
'settlement_date' => 'date',
|
||||
'amount' => 'decimal:2',
|
||||
'settlement_amount' => 'decimal:2',
|
||||
'metadata' => 'array',
|
||||
];
|
||||
|
||||
// =========================================================================
|
||||
@@ -166,6 +177,9 @@ public function getStatusLabelAttribute(): string
|
||||
self::STATUS_OUTSTANDING => '미정산',
|
||||
self::STATUS_SETTLED => '정산완료',
|
||||
self::STATUS_PARTIAL => '부분정산',
|
||||
self::STATUS_HOLDING => '보유',
|
||||
self::STATUS_USED => '사용',
|
||||
self::STATUS_DISPOSED => '폐기',
|
||||
default => $this->status,
|
||||
};
|
||||
}
|
||||
@@ -209,15 +223,21 @@ public function getElapsedDaysAttribute(): int
|
||||
*/
|
||||
public function isEditable(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_OUTSTANDING;
|
||||
return in_array($this->status, [
|
||||
self::STATUS_OUTSTANDING,
|
||||
self::STATUS_HOLDING,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 삭제 가능 여부 (미정산 상태만)
|
||||
* 삭제 가능 여부 (미정산/보유 상태만)
|
||||
*/
|
||||
public function isDeletable(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_OUTSTANDING;
|
||||
return in_array($this->status, [
|
||||
self::STATUS_OUTSTANDING,
|
||||
self::STATUS_HOLDING,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -48,6 +48,16 @@ public function index(array $params): LengthAwarePaginator
|
||||
$query->where('client_id', $params['client_id']);
|
||||
}
|
||||
|
||||
// 증권종류 필터
|
||||
if (! empty($params['instrument_type'])) {
|
||||
$query->where('instrument_type', $params['instrument_type']);
|
||||
}
|
||||
|
||||
// 매체 필터
|
||||
if (! empty($params['medium'])) {
|
||||
$query->where('medium', $params['medium']);
|
||||
}
|
||||
|
||||
// 전자어음 필터
|
||||
if (isset($params['is_electronic']) && $params['is_electronic'] !== '') {
|
||||
$query->where('is_electronic', (bool) $params['is_electronic']);
|
||||
@@ -113,32 +123,23 @@ public function store(array $data): Bill
|
||||
$bill->client_name = $data['client_name'] ?? null;
|
||||
$bill->amount = $data['amount'];
|
||||
$bill->issue_date = $data['issue_date'];
|
||||
$bill->maturity_date = $data['maturity_date'];
|
||||
$bill->maturity_date = $data['maturity_date'] ?? null;
|
||||
$bill->status = $data['status'] ?? 'stored';
|
||||
$bill->reason = $data['reason'] ?? null;
|
||||
$bill->installment_count = $data['installment_count'] ?? 0;
|
||||
$bill->note = $data['note'] ?? null;
|
||||
$bill->is_electronic = $data['is_electronic'] ?? false;
|
||||
$bill->bank_account_id = $data['bank_account_id'] ?? null;
|
||||
|
||||
// V8 확장 필드
|
||||
$this->assignV8Fields($bill, $data);
|
||||
|
||||
$bill->created_by = $userId;
|
||||
$bill->updated_by = $userId;
|
||||
$bill->save();
|
||||
|
||||
// 차수 관리 저장
|
||||
if (! empty($data['installments'])) {
|
||||
foreach ($data['installments'] as $installment) {
|
||||
BillInstallment::create([
|
||||
'bill_id' => $bill->id,
|
||||
'installment_date' => $installment['date'],
|
||||
'amount' => $installment['amount'],
|
||||
'note' => $installment['note'] ?? null,
|
||||
'created_by' => $userId,
|
||||
]);
|
||||
}
|
||||
// 차수 카운트 업데이트
|
||||
$bill->installment_count = count($data['installments']);
|
||||
$bill->save();
|
||||
}
|
||||
$this->syncInstallments($bill, $data['installments'] ?? [], $userId);
|
||||
|
||||
return $bill->load(['client:id,name', 'bankAccount:id,bank_name,account_name', 'installments']);
|
||||
});
|
||||
@@ -157,6 +158,7 @@ public function update(int $id, array $data): Bill
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
// 기존 필드
|
||||
if (isset($data['bill_number'])) {
|
||||
$bill->bill_number = $data['bill_number'];
|
||||
}
|
||||
@@ -175,7 +177,7 @@ public function update(int $id, array $data): Bill
|
||||
if (isset($data['issue_date'])) {
|
||||
$bill->issue_date = $data['issue_date'];
|
||||
}
|
||||
if (isset($data['maturity_date'])) {
|
||||
if (array_key_exists('maturity_date', $data)) {
|
||||
$bill->maturity_date = $data['maturity_date'];
|
||||
}
|
||||
if (isset($data['status'])) {
|
||||
@@ -194,27 +196,15 @@ public function update(int $id, array $data): Bill
|
||||
$bill->bank_account_id = $data['bank_account_id'];
|
||||
}
|
||||
|
||||
// V8 확장 필드
|
||||
$this->assignV8Fields($bill, $data);
|
||||
|
||||
$bill->updated_by = $userId;
|
||||
$bill->save();
|
||||
|
||||
// 차수 관리 업데이트 (전체 교체)
|
||||
if (isset($data['installments'])) {
|
||||
// 기존 차수 삭제
|
||||
$bill->installments()->delete();
|
||||
|
||||
// 새 차수 추가
|
||||
foreach ($data['installments'] as $installment) {
|
||||
BillInstallment::create([
|
||||
'bill_id' => $bill->id,
|
||||
'installment_date' => $installment['date'],
|
||||
'amount' => $installment['amount'],
|
||||
'note' => $installment['note'] ?? null,
|
||||
'created_by' => $userId,
|
||||
]);
|
||||
}
|
||||
// 차수 카운트 업데이트
|
||||
$bill->installment_count = count($data['installments']);
|
||||
$bill->save();
|
||||
$this->syncInstallments($bill, $data['installments'], $userId);
|
||||
}
|
||||
|
||||
return $bill->fresh(['client:id,name', 'bankAccount:id,bank_name,account_name', 'installments']);
|
||||
@@ -440,6 +430,68 @@ public function dashboardDetail(): array
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* V8 확장 필드를 Bill 모델에 할당
|
||||
*/
|
||||
private function assignV8Fields(Bill $bill, array $data): void
|
||||
{
|
||||
$v8Fields = [
|
||||
'instrument_type', 'medium', 'bill_category',
|
||||
'electronic_bill_no', 'registration_org',
|
||||
'drawee', 'acceptance_status', 'acceptance_date',
|
||||
'acceptance_refusal_date', 'acceptance_refusal_reason',
|
||||
'endorsement', 'endorsement_order', 'storage_place', 'issuer_bank',
|
||||
'is_discounted', 'discount_date', 'discount_bank', 'discount_rate', 'discount_amount',
|
||||
'endorsement_date', 'endorsee', 'endorsement_reason',
|
||||
'collection_bank', 'collection_request_date', 'collection_fee',
|
||||
'collection_complete_date', 'collection_result', 'collection_deposit_date', 'collection_deposit_amount',
|
||||
'settlement_bank', 'payment_method', 'actual_payment_date',
|
||||
'payment_place', 'payment_place_detail',
|
||||
'renewal_date', 'renewal_new_bill_no', 'renewal_reason',
|
||||
'recourse_date', 'recourse_amount', 'recourse_target', 'recourse_reason',
|
||||
'buyback_date', 'buyback_amount', 'buyback_bank',
|
||||
'dishonored_date', 'dishonored_reason', 'has_protest', 'protest_date',
|
||||
'recourse_notice_date', 'recourse_notice_deadline',
|
||||
'is_split',
|
||||
];
|
||||
|
||||
foreach ($v8Fields as $field) {
|
||||
if (array_key_exists($field, $data)) {
|
||||
$bill->{$field} = $data[$field];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 차수(이력) 동기화 — 기존 삭제 후 새로 생성
|
||||
*/
|
||||
private function syncInstallments(Bill $bill, array $installments, int $userId): void
|
||||
{
|
||||
if (empty($installments)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 기존 차수 삭제
|
||||
$bill->installments()->delete();
|
||||
|
||||
// 새 차수 추가
|
||||
foreach ($installments as $installment) {
|
||||
BillInstallment::create([
|
||||
'bill_id' => $bill->id,
|
||||
'type' => $installment['type'] ?? 'other',
|
||||
'installment_date' => $installment['date'],
|
||||
'amount' => $installment['amount'],
|
||||
'counterparty' => $installment['counterparty'] ?? null,
|
||||
'note' => $installment['note'] ?? null,
|
||||
'created_by' => $userId,
|
||||
]);
|
||||
}
|
||||
|
||||
// 차수 카운트 업데이트
|
||||
$bill->installment_count = count($installments);
|
||||
$bill->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 어음번호 자동 생성
|
||||
*/
|
||||
|
||||
@@ -25,6 +25,11 @@ public function index(array $params): LengthAwarePaginator
|
||||
->where('tenant_id', $tenantId)
|
||||
->with(['user:id,name,email', 'creator:id,name']);
|
||||
|
||||
// 카테고리 필터
|
||||
if (! empty($params['category'])) {
|
||||
$query->where('category', $params['category']);
|
||||
}
|
||||
|
||||
// 사용자 필터
|
||||
if (! empty($params['user_id'])) {
|
||||
$query->where('user_id', $params['user_id']);
|
||||
@@ -84,7 +89,7 @@ public function show(int $id): Loan
|
||||
/**
|
||||
* 가지급금 요약 (특정 사용자 또는 전체)
|
||||
*/
|
||||
public function summary(?int $userId = null): array
|
||||
public function summary(?int $userId = null, ?string $category = null): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
@@ -95,7 +100,14 @@ public function summary(?int $userId = null): array
|
||||
$query->where('user_id', $userId);
|
||||
}
|
||||
|
||||
$stats = $query->selectRaw('
|
||||
if ($category) {
|
||||
$query->where('category', $category);
|
||||
}
|
||||
|
||||
// 상품권 카테고리: holding/used/disposed 상태별 집계 추가
|
||||
$isGiftCertificate = $category === Loan::CATEGORY_GIFT_CERTIFICATE;
|
||||
|
||||
$selectRaw = '
|
||||
COUNT(*) as total_count,
|
||||
SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as outstanding_count,
|
||||
SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as settled_count,
|
||||
@@ -103,10 +115,27 @@ public function summary(?int $userId = null): array
|
||||
SUM(amount) as total_amount,
|
||||
SUM(COALESCE(settlement_amount, 0)) as total_settled,
|
||||
SUM(amount - COALESCE(settlement_amount, 0)) as total_outstanding
|
||||
', [Loan::STATUS_OUTSTANDING, Loan::STATUS_SETTLED, Loan::STATUS_PARTIAL])
|
||||
->first();
|
||||
';
|
||||
$bindings = [Loan::STATUS_OUTSTANDING, Loan::STATUS_SETTLED, Loan::STATUS_PARTIAL];
|
||||
|
||||
return [
|
||||
if ($isGiftCertificate) {
|
||||
$selectRaw .= ',
|
||||
SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as holding_count,
|
||||
SUM(CASE WHEN status = ? THEN amount ELSE 0 END) as holding_amount,
|
||||
SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as used_count,
|
||||
SUM(CASE WHEN status = ? THEN amount ELSE 0 END) as used_amount,
|
||||
SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as disposed_count
|
||||
';
|
||||
$bindings = array_merge($bindings, [
|
||||
Loan::STATUS_HOLDING, Loan::STATUS_HOLDING,
|
||||
Loan::STATUS_USED, Loan::STATUS_USED,
|
||||
Loan::STATUS_DISPOSED,
|
||||
]);
|
||||
}
|
||||
|
||||
$stats = $query->selectRaw($selectRaw, $bindings)->first();
|
||||
|
||||
$result = [
|
||||
'total_count' => (int) $stats->total_count,
|
||||
'outstanding_count' => (int) $stats->outstanding_count,
|
||||
'settled_count' => (int) $stats->settled_count,
|
||||
@@ -115,6 +144,16 @@ public function summary(?int $userId = null): array
|
||||
'total_settled' => (float) $stats->total_settled,
|
||||
'total_outstanding' => (float) $stats->total_outstanding,
|
||||
];
|
||||
|
||||
if ($isGiftCertificate) {
|
||||
$result['holding_count'] = (int) $stats->holding_count;
|
||||
$result['holding_amount'] = (float) $stats->holding_amount;
|
||||
$result['used_count'] = (int) $stats->used_count;
|
||||
$result['used_amount'] = (float) $stats->used_amount;
|
||||
$result['disposed_count'] = (int) $stats->disposed_count;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -144,13 +183,23 @@ public function store(array $data): Loan
|
||||
$withdrawalId = $withdrawal->id;
|
||||
}
|
||||
|
||||
// 상품권: user_id 미지정 시 현재 사용자로 대체
|
||||
$loanUserId = $data['user_id'] ?? $userId;
|
||||
|
||||
// 상태 결정: 상품권은 holding, 그 외는 outstanding
|
||||
$category = $data['category'] ?? null;
|
||||
$status = $data['status']
|
||||
?? ($category === Loan::CATEGORY_GIFT_CERTIFICATE ? Loan::STATUS_HOLDING : Loan::STATUS_OUTSTANDING);
|
||||
|
||||
return Loan::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'user_id' => $data['user_id'],
|
||||
'user_id' => $loanUserId,
|
||||
'loan_date' => $data['loan_date'],
|
||||
'amount' => $data['amount'],
|
||||
'purpose' => $data['purpose'] ?? null,
|
||||
'status' => Loan::STATUS_OUTSTANDING,
|
||||
'status' => $status,
|
||||
'category' => $category,
|
||||
'metadata' => $data['metadata'] ?? null,
|
||||
'withdrawal_id' => $withdrawalId,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
@@ -186,14 +235,29 @@ public function update(int $id, array $data): Loan
|
||||
}
|
||||
}
|
||||
|
||||
$loan->fill([
|
||||
$fillData = [
|
||||
'user_id' => $data['user_id'] ?? $loan->user_id,
|
||||
'loan_date' => $data['loan_date'] ?? $loan->loan_date,
|
||||
'amount' => $data['amount'] ?? $loan->amount,
|
||||
'purpose' => $data['purpose'] ?? $loan->purpose,
|
||||
'withdrawal_id' => $data['withdrawal_id'] ?? $loan->withdrawal_id,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
];
|
||||
|
||||
if (isset($data['category'])) {
|
||||
$fillData['category'] = $data['category'];
|
||||
}
|
||||
if (array_key_exists('metadata', $data)) {
|
||||
$fillData['metadata'] = $data['metadata'];
|
||||
}
|
||||
if (isset($data['status'])) {
|
||||
$fillData['status'] = $data['status'];
|
||||
}
|
||||
if (array_key_exists('settlement_date', $data)) {
|
||||
$fillData['settlement_date'] = $data['settlement_date'];
|
||||
}
|
||||
|
||||
$loan->fill($fillData);
|
||||
|
||||
$loan->save();
|
||||
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* 어음/수표 V8 확장 마이그레이션
|
||||
*
|
||||
* 프로토타입 v8의 ~45개 필드 지원을 위해 bills 테이블에 신규 컬럼 추가.
|
||||
* 기존 컬럼(bill_number, bill_type, client_id, amount 등)은 그대로 유지.
|
||||
*
|
||||
* 주요 변경:
|
||||
* - 증권종류(instrument_type), 매체(medium), 어음구분(bill_category) 추가
|
||||
* - 전자어음 관리번호, 등록기관 추가
|
||||
* - 환어음 정보(지급인, 인수여부) 추가
|
||||
* - 받을어음: 배서, 보관장소, 할인, 추심 관련 필드 추가
|
||||
* - 지급어음: 결제방법, 실제결제일 추가
|
||||
* - 지급장소, 개서, 소구, 환매, 부도 법적절차 필드 추가
|
||||
* - 분할배서, 이력관리 확장 (bill_installments에 type, counterparty 추가)
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('bills', function (Blueprint $table) {
|
||||
// === 증권종류/매체/구분 ===
|
||||
$table->string('instrument_type', 30)->default('promissory')->after('bill_type')
|
||||
->comment('증권종류: promissory/exchange/cashierCheck/currentCheck');
|
||||
$table->string('medium', 20)->default('paper')->after('instrument_type')
|
||||
->comment('매체: electronic/paper');
|
||||
$table->string('bill_category', 30)->nullable()->after('medium')
|
||||
->comment('어음구분: commercial/other');
|
||||
|
||||
// === 전자어음 정보 ===
|
||||
$table->string('electronic_bill_no', 100)->nullable()->after('is_electronic')
|
||||
->comment('전자어음 관리번호');
|
||||
$table->string('registration_org', 30)->nullable()->after('electronic_bill_no')
|
||||
->comment('등록기관: kftc/bank');
|
||||
|
||||
// === 환어음 정보 ===
|
||||
$table->string('drawee', 100)->nullable()->after('registration_org')
|
||||
->comment('환어음 지급인 (Drawee)');
|
||||
$table->string('acceptance_status', 20)->nullable()->after('drawee')
|
||||
->comment('인수여부: accepted/pending/refused');
|
||||
$table->date('acceptance_date')->nullable()->after('acceptance_status')
|
||||
->comment('인수일자');
|
||||
$table->date('acceptance_refusal_date')->nullable()->after('acceptance_date')
|
||||
->comment('인수거절일');
|
||||
$table->string('acceptance_refusal_reason', 50)->nullable()->after('acceptance_refusal_date')
|
||||
->comment('인수거절사유');
|
||||
|
||||
// === 받을어음 전용 ===
|
||||
$table->string('endorsement', 30)->nullable()->after('acceptance_refusal_reason')
|
||||
->comment('배서여부: endorsable/nonEndorsable');
|
||||
$table->string('endorsement_order', 5)->nullable()->after('endorsement')
|
||||
->comment('배서차수: 1~20');
|
||||
$table->string('storage_place', 30)->nullable()->after('endorsement_order')
|
||||
->comment('보관장소: safe/bank/other');
|
||||
$table->string('issuer_bank', 100)->nullable()->after('storage_place')
|
||||
->comment('발행은행');
|
||||
|
||||
// 할인 정보
|
||||
$table->boolean('is_discounted')->default(false)->after('issuer_bank')
|
||||
->comment('할인여부');
|
||||
$table->date('discount_date')->nullable()->after('is_discounted')
|
||||
->comment('할인일자');
|
||||
$table->string('discount_bank', 100)->nullable()->after('discount_date')
|
||||
->comment('할인처 (은행)');
|
||||
$table->decimal('discount_rate', 5, 2)->nullable()->after('discount_bank')
|
||||
->comment('할인율 (%)');
|
||||
$table->decimal('discount_amount', 15, 2)->nullable()->after('discount_rate')
|
||||
->comment('할인금액');
|
||||
|
||||
// 배서양도 정보
|
||||
$table->date('endorsement_date')->nullable()->after('discount_amount')
|
||||
->comment('배서일자');
|
||||
$table->string('endorsee', 100)->nullable()->after('endorsement_date')
|
||||
->comment('피배서인 (양수인)');
|
||||
$table->string('endorsement_reason', 30)->nullable()->after('endorsee')
|
||||
->comment('배서사유: payment/guarantee/collection/other');
|
||||
|
||||
// 추심 정보
|
||||
$table->string('collection_bank', 100)->nullable()->after('endorsement_reason')
|
||||
->comment('추심은행');
|
||||
$table->date('collection_request_date')->nullable()->after('collection_bank')
|
||||
->comment('추심의뢰일');
|
||||
$table->decimal('collection_fee', 15, 2)->nullable()->after('collection_request_date')
|
||||
->comment('추심수수료');
|
||||
$table->date('collection_complete_date')->nullable()->after('collection_fee')
|
||||
->comment('추심완료일');
|
||||
$table->string('collection_result', 20)->nullable()->after('collection_complete_date')
|
||||
->comment('추심결과: success/partial/failed/pending');
|
||||
$table->date('collection_deposit_date')->nullable()->after('collection_result')
|
||||
->comment('추심입금일');
|
||||
$table->decimal('collection_deposit_amount', 15, 2)->nullable()->after('collection_deposit_date')
|
||||
->comment('추심입금액 (수수료 차감후)');
|
||||
|
||||
// === 지급어음 전용 ===
|
||||
$table->string('settlement_bank', 100)->nullable()->after('collection_deposit_amount')
|
||||
->comment('결제은행');
|
||||
$table->string('payment_method', 30)->nullable()->after('settlement_bank')
|
||||
->comment('결제방법: autoTransfer/currentAccount/other');
|
||||
$table->date('actual_payment_date')->nullable()->after('payment_method')
|
||||
->comment('실제결제일');
|
||||
|
||||
// === 공통 ===
|
||||
$table->string('payment_place', 30)->nullable()->after('actual_payment_date')
|
||||
->comment('지급장소: issuerBank/issuerBankBranch/payerAddress/designatedBank/other');
|
||||
$table->string('payment_place_detail', 200)->nullable()->after('payment_place')
|
||||
->comment('지급장소 상세 (기타 선택 시)');
|
||||
|
||||
// === 개서 정보 ===
|
||||
$table->date('renewal_date')->nullable()->after('payment_place_detail')
|
||||
->comment('개서일자');
|
||||
$table->string('renewal_new_bill_no', 50)->nullable()->after('renewal_date')
|
||||
->comment('신어음번호');
|
||||
$table->string('renewal_reason', 30)->nullable()->after('renewal_new_bill_no')
|
||||
->comment('개서사유: maturityExtension/amountChange/conditionChange/other');
|
||||
|
||||
// === 소구 정보 ===
|
||||
$table->date('recourse_date')->nullable()->after('renewal_reason')
|
||||
->comment('소구일자');
|
||||
$table->decimal('recourse_amount', 15, 2)->nullable()->after('recourse_date')
|
||||
->comment('소구금액');
|
||||
$table->string('recourse_target', 100)->nullable()->after('recourse_amount')
|
||||
->comment('소구대상 (청구인)');
|
||||
$table->string('recourse_reason', 30)->nullable()->after('recourse_target')
|
||||
->comment('소구사유: endorsedDishonor/discountDishonor/other');
|
||||
|
||||
// === 환매 정보 ===
|
||||
$table->date('buyback_date')->nullable()->after('recourse_reason')
|
||||
->comment('환매일자');
|
||||
$table->decimal('buyback_amount', 15, 2)->nullable()->after('buyback_date')
|
||||
->comment('환매금액');
|
||||
$table->string('buyback_bank', 100)->nullable()->after('buyback_amount')
|
||||
->comment('환매요청 은행');
|
||||
|
||||
// === 부도/법적절차 ===
|
||||
$table->date('dishonored_date')->nullable()->after('buyback_bank')
|
||||
->comment('부도일자');
|
||||
$table->string('dishonored_reason', 30)->nullable()->after('dishonored_date')
|
||||
->comment('부도사유');
|
||||
$table->boolean('has_protest')->default(false)->after('dishonored_reason')
|
||||
->comment('거절증서 작성 여부');
|
||||
$table->date('protest_date')->nullable()->after('has_protest')
|
||||
->comment('거절증서 작성일');
|
||||
$table->date('recourse_notice_date')->nullable()->after('protest_date')
|
||||
->comment('소구 통지일');
|
||||
$table->date('recourse_notice_deadline')->nullable()->after('recourse_notice_date')
|
||||
->comment('소구 통지 기한 (부도일+4영업일)');
|
||||
|
||||
// === 분할배서 ===
|
||||
$table->boolean('is_split')->default(false)->after('recourse_notice_deadline')
|
||||
->comment('분할배서 허용 여부');
|
||||
});
|
||||
|
||||
// bill_installments 에 처리구분, 상대처 추가
|
||||
Schema::table('bill_installments', function (Blueprint $table) {
|
||||
$table->string('type', 30)->default('other')->after('bill_id')
|
||||
->comment('처리구분: received/endorsement/splitEndorsement/collection/...');
|
||||
$table->string('counterparty', 100)->nullable()->after('amount')
|
||||
->comment('상대처 (거래처/은행)');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('bill_installments', function (Blueprint $table) {
|
||||
$table->dropColumn(['type', 'counterparty']);
|
||||
});
|
||||
|
||||
Schema::table('bills', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'instrument_type', 'medium', 'bill_category',
|
||||
'electronic_bill_no', 'registration_org',
|
||||
'drawee', 'acceptance_status', 'acceptance_date',
|
||||
'acceptance_refusal_date', 'acceptance_refusal_reason',
|
||||
'endorsement', 'endorsement_order', 'storage_place', 'issuer_bank',
|
||||
'is_discounted', 'discount_date', 'discount_bank', 'discount_rate', 'discount_amount',
|
||||
'endorsement_date', 'endorsee', 'endorsement_reason',
|
||||
'collection_bank', 'collection_request_date', 'collection_fee',
|
||||
'collection_complete_date', 'collection_result', 'collection_deposit_date', 'collection_deposit_amount',
|
||||
'settlement_bank', 'payment_method', 'actual_payment_date',
|
||||
'payment_place', 'payment_place_detail',
|
||||
'renewal_date', 'renewal_new_bill_no', 'renewal_reason',
|
||||
'recourse_date', 'recourse_amount', 'recourse_target', 'recourse_reason',
|
||||
'buyback_date', 'buyback_amount', 'buyback_bank',
|
||||
'dishonored_date', 'dishonored_reason', 'has_protest', 'protest_date',
|
||||
'recourse_notice_date', 'recourse_notice_deadline',
|
||||
'is_split',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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('loans', function (Blueprint $table) {
|
||||
$table->json('metadata')->nullable()->after('category');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('loans', function (Blueprint $table) {
|
||||
$table->dropColumn('metadata');
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user