feat: 어음 관리(Bill Management) API 추가

- BillController: 어음 CRUD + 상태변경 + 요약 API
- BillService: 비즈니스 로직 (멀티테넌트 지원)
- Bill, BillInstallment 모델: 날짜 포맷(Y-m-d) toArray 오버라이드
- FormRequest: Store/Update/UpdateStatus 유효성 검사
- Swagger 문서: BillApi.php
- 마이그레이션: bills, bill_installments 테이블
- DummyBillSeeder: 테스트 데이터 30건 + 차수 12건
- API Routes: /api/v1/bills 엔드포인트 7개
This commit is contained in:
2025-12-23 23:42:02 +09:00
parent d6d004f32b
commit 71123128ff
11 changed files with 1442 additions and 0 deletions

View File

@@ -0,0 +1,112 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Helpers\ApiResponse;
use App\Http\Controllers\Controller;
use App\Http\Requests\V1\Bill\StoreBillRequest;
use App\Http\Requests\V1\Bill\UpdateBillRequest;
use App\Http\Requests\V1\Bill\UpdateBillStatusRequest;
use App\Services\BillService;
use Illuminate\Http\Request;
class BillController extends Controller
{
public function __construct(
private readonly BillService $service
) {}
/**
* 어음 목록
*/
public function index(Request $request)
{
$params = $request->only([
'search',
'bill_type',
'status',
'client_id',
'is_electronic',
'issue_start_date',
'issue_end_date',
'maturity_start_date',
'maturity_end_date',
'sort_by',
'sort_dir',
'per_page',
'page',
]);
$bills = $this->service->index($params);
return ApiResponse::success($bills, __('message.fetched'));
}
/**
* 어음 등록
*/
public function store(StoreBillRequest $request)
{
$bill = $this->service->store($request->validated());
return ApiResponse::success($bill, __('message.created'), [], 201);
}
/**
* 어음 상세
*/
public function show(int $id)
{
$bill = $this->service->show($id);
return ApiResponse::success($bill, __('message.fetched'));
}
/**
* 어음 수정
*/
public function update(int $id, UpdateBillRequest $request)
{
$bill = $this->service->update($id, $request->validated());
return ApiResponse::success($bill, __('message.updated'));
}
/**
* 어음 삭제
*/
public function destroy(int $id)
{
$this->service->destroy($id);
return ApiResponse::success(null, __('message.deleted'));
}
/**
* 어음 상태 변경
*/
public function updateStatus(int $id, UpdateBillStatusRequest $request)
{
$bill = $this->service->updateStatus($id, $request->validated()['status']);
return ApiResponse::success($bill, __('message.updated'));
}
/**
* 어음 요약 (기간별 합계)
*/
public function summary(Request $request)
{
$params = $request->only([
'bill_type',
'issue_start_date',
'issue_end_date',
'maturity_start_date',
'maturity_end_date',
]);
$summary = $this->service->summary($params);
return ApiResponse::success($summary, __('message.fetched'));
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Http\Requests\V1\Bill;
use Illuminate\Foundation\Http\FormRequest;
class StoreBillRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'bill_number' => ['nullable', 'string', 'max:50'],
'bill_type' => ['required', 'string', 'in:received,issued'],
'client_id' => ['nullable', 'integer', 'exists:clients,id'],
'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'],
'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'],
'installments' => ['nullable', 'array'],
'installments.*.date' => ['required_with:installments', 'date'],
'installments.*.amount' => ['required_with:installments', 'numeric', 'min:0'],
'installments.*.note' => ['nullable', 'string', 'max:255'],
];
}
public function messages(): array
{
return [
'bill_type.required' => __('validation.required', ['attribute' => __('validation.attributes.bill_type')]),
'bill_type.in' => __('validation.in', ['attribute' => __('validation.attributes.bill_type')]),
'amount.required' => __('validation.required', ['attribute' => __('validation.attributes.amount')]),
'amount.min' => __('validation.min.numeric', ['attribute' => __('validation.attributes.amount'), 'min' => 0]),
'issue_date.required' => __('validation.required', ['attribute' => __('validation.attributes.issue_date')]),
'maturity_date.required' => __('validation.required', ['attribute' => __('validation.attributes.maturity_date')]),
'maturity_date.after_or_equal' => __('validation.after_or_equal', ['attribute' => __('validation.attributes.maturity_date'), 'date' => __('validation.attributes.issue_date')]),
];
}
public function attributes(): array
{
return [
'bill_number' => __('validation.attributes.bill_number'),
'bill_type' => __('validation.attributes.bill_type'),
'client_id' => __('validation.attributes.client_id'),
'client_name' => __('validation.attributes.client_name'),
'amount' => __('validation.attributes.amount'),
'issue_date' => __('validation.attributes.issue_date'),
'maturity_date' => __('validation.attributes.maturity_date'),
'status' => __('validation.attributes.status'),
'reason' => __('validation.attributes.reason'),
'note' => __('validation.attributes.note'),
'is_electronic' => __('validation.attributes.is_electronic'),
'bank_account_id' => __('validation.attributes.bank_account_id'),
];
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Http\Requests\V1\Bill;
use Illuminate\Foundation\Http\FormRequest;
class UpdateBillRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
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'],
'client_name' => ['nullable', 'string', 'max:100'],
'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'],
'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'],
'installments' => ['nullable', 'array'],
'installments.*.date' => ['required_with:installments', 'date'],
'installments.*.amount' => ['required_with:installments', 'numeric', 'min:0'],
'installments.*.note' => ['nullable', 'string', 'max:255'],
];
}
public function messages(): array
{
return [
'bill_type.in' => __('validation.in', ['attribute' => __('validation.attributes.bill_type')]),
'amount.min' => __('validation.min.numeric', ['attribute' => __('validation.attributes.amount'), 'min' => 0]),
'maturity_date.after_or_equal' => __('validation.after_or_equal', ['attribute' => __('validation.attributes.maturity_date'), 'date' => __('validation.attributes.issue_date')]),
];
}
public function attributes(): array
{
return [
'bill_number' => __('validation.attributes.bill_number'),
'bill_type' => __('validation.attributes.bill_type'),
'client_id' => __('validation.attributes.client_id'),
'client_name' => __('validation.attributes.client_name'),
'amount' => __('validation.attributes.amount'),
'issue_date' => __('validation.attributes.issue_date'),
'maturity_date' => __('validation.attributes.maturity_date'),
'status' => __('validation.attributes.status'),
'reason' => __('validation.attributes.reason'),
'note' => __('validation.attributes.note'),
'is_electronic' => __('validation.attributes.is_electronic'),
'bank_account_id' => __('validation.attributes.bank_account_id'),
];
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Http\Requests\V1\Bill;
use Illuminate\Foundation\Http\FormRequest;
class UpdateBillStatusRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'status' => ['required', 'string', 'in:stored,maturityAlert,maturityResult,paymentComplete,dishonored,collectionRequest,collectionComplete,suing'],
];
}
public function messages(): array
{
return [
'status.required' => __('validation.required', ['attribute' => __('validation.attributes.status')]),
'status.in' => __('validation.in', ['attribute' => __('validation.attributes.status')]),
];
}
public function attributes(): array
{
return [
'status' => __('validation.attributes.status'),
];
}
}

175
app/Models/Tenants/Bill.php Normal file
View File

@@ -0,0 +1,175 @@
<?php
namespace App\Models\Tenants;
use App\Traits\BelongsToTenant;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class Bill extends Model
{
use BelongsToTenant, SoftDeletes;
protected $fillable = [
'tenant_id',
'bill_number',
'bill_type',
'client_id',
'client_name',
'amount',
'issue_date',
'maturity_date',
'status',
'reason',
'installment_count',
'note',
'is_electronic',
'bank_account_id',
'created_by',
'updated_by',
'deleted_by',
];
protected $casts = [
'issue_date' => 'date',
'maturity_date' => 'date',
'amount' => 'decimal:2',
'client_id' => 'integer',
'bank_account_id' => 'integer',
'installment_count' => 'integer',
'is_electronic' => 'boolean',
];
/**
* 배열/JSON 변환 시 날짜 형식 지정
*/
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');
}
return $array;
}
/**
* 어음 구분 목록
*/
public const BILL_TYPES = [
'received' => '수취',
'issued' => '발행',
];
/**
* 수취 어음 상태 목록
*/
public const RECEIVED_STATUSES = [
'stored' => '보관중',
'maturityAlert' => '만기입금(7일전)',
'maturityResult' => '만기결과',
'paymentComplete' => '결제완료',
'dishonored' => '부도',
];
/**
* 발행 어음 상태 목록
*/
public const ISSUED_STATUSES = [
'stored' => '보관중',
'maturityAlert' => '만기입금(7일전)',
'collectionRequest' => '추심의뢰',
'collectionComplete' => '추심완료',
'suing' => '추소중',
'dishonored' => '부도',
];
/**
* 거래처 관계
*/
public function client(): BelongsTo
{
return $this->belongsTo(\App\Models\Orders\Client::class);
}
/**
* 입금/출금 계좌 관계
*/
public function bankAccount(): BelongsTo
{
return $this->belongsTo(BankAccount::class);
}
/**
* 차수 관계
*/
public function installments(): HasMany
{
return $this->hasMany(BillInstallment::class);
}
/**
* 생성자 관계
*/
public function creator(): BelongsTo
{
return $this->belongsTo(\App\Models\Members\User::class, 'created_by');
}
/**
* 거래처명 조회 (회원/비회원 통합)
*/
public function getDisplayClientNameAttribute(): string
{
if ($this->client) {
return $this->client->name;
}
return $this->client_name ?? '';
}
/**
* 어음 구분 라벨
*/
public function getBillTypeLabelAttribute(): string
{
return self::BILL_TYPES[$this->bill_type] ?? $this->bill_type;
}
/**
* 상태 라벨
*/
public function getStatusLabelAttribute(): string
{
if ($this->bill_type === 'received') {
return self::RECEIVED_STATUSES[$this->status] ?? $this->status;
}
return self::ISSUED_STATUSES[$this->status] ?? $this->status;
}
/**
* 만기까지 남은 일수
*/
public function getDaysToMaturityAttribute(): int
{
return now()->diffInDays($this->maturity_date, false);
}
/**
* 만기 7일 전 여부
*/
public function isMaturityAlertPeriod(): bool
{
$days = $this->days_to_maturity;
return $days >= 0 && $days <= 7;
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Models\Tenants;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class BillInstallment extends Model
{
protected $fillable = [
'bill_id',
'installment_date',
'amount',
'note',
'created_by',
];
protected $casts = [
'installment_date' => 'date',
'amount' => 'decimal:2',
'bill_id' => 'integer',
];
/**
* 배열/JSON 변환 시 날짜 형식 지정
*/
public function toArray(): array
{
$array = parent::toArray();
if (isset($array['installment_date']) && $this->installment_date) {
$array['installment_date'] = $this->installment_date->format('Y-m-d');
}
return $array;
}
/**
* 어음 관계
*/
public function bill(): BelongsTo
{
return $this->belongsTo(Bill::class);
}
/**
* 생성자 관계
*/
public function creator(): BelongsTo
{
return $this->belongsTo(\App\Models\Members\User::class, 'created_by');
}
}

View File

@@ -0,0 +1,352 @@
<?php
namespace App\Services;
use App\Models\Tenants\Bill;
use App\Models\Tenants\BillInstallment;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
class BillService extends Service
{
/**
* 어음 목록 조회
*/
public function index(array $params): LengthAwarePaginator
{
$tenantId = $this->tenantId();
$query = Bill::query()
->where('tenant_id', $tenantId)
->with(['client:id,name', 'bankAccount:id,bank_name,account_name', 'installments']);
// 검색어 필터
if (! empty($params['search'])) {
$search = $params['search'];
$query->where(function ($q) use ($search) {
$q->where('bill_number', 'like', "%{$search}%")
->orWhere('client_name', 'like', "%{$search}%")
->orWhere('note', 'like', "%{$search}%")
->orWhereHas('client', function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%");
});
});
}
// 어음 구분 필터 (received/issued)
if (! empty($params['bill_type'])) {
$query->where('bill_type', $params['bill_type']);
}
// 상태 필터
if (! empty($params['status'])) {
$query->where('status', $params['status']);
}
// 거래처 필터
if (! empty($params['client_id'])) {
$query->where('client_id', $params['client_id']);
}
// 전자어음 필터
if (isset($params['is_electronic']) && $params['is_electronic'] !== '') {
$query->where('is_electronic', (bool) $params['is_electronic']);
}
// 발행일 범위 필터
if (! empty($params['issue_start_date'])) {
$query->where('issue_date', '>=', $params['issue_start_date']);
}
if (! empty($params['issue_end_date'])) {
$query->where('issue_date', '<=', $params['issue_end_date']);
}
// 만기일 범위 필터
if (! empty($params['maturity_start_date'])) {
$query->where('maturity_date', '>=', $params['maturity_start_date']);
}
if (! empty($params['maturity_end_date'])) {
$query->where('maturity_date', '<=', $params['maturity_end_date']);
}
// 정렬
$sortBy = $params['sort_by'] ?? 'issue_date';
$sortDir = $params['sort_dir'] ?? 'desc';
$query->orderBy($sortBy, $sortDir);
// 페이지네이션
$perPage = $params['per_page'] ?? 20;
return $query->paginate($perPage);
}
/**
* 어음 상세 조회
*/
public function show(int $id): Bill
{
$tenantId = $this->tenantId();
return Bill::query()
->where('tenant_id', $tenantId)
->with(['client:id,name', 'bankAccount:id,bank_name,account_name', 'installments', 'creator:id,name'])
->findOrFail($id);
}
/**
* 어음 등록
*/
public function store(array $data): Bill
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($data, $tenantId, $userId) {
// 어음번호 자동 생성 (없을 경우)
$billNumber = $data['bill_number'] ?? $this->generateBillNumber($tenantId);
$bill = new Bill;
$bill->tenant_id = $tenantId;
$bill->bill_number = $billNumber;
$bill->bill_type = $data['bill_type'];
$bill->client_id = $data['client_id'] ?? null;
$bill->client_name = $data['client_name'] ?? null;
$bill->amount = $data['amount'];
$bill->issue_date = $data['issue_date'];
$bill->maturity_date = $data['maturity_date'];
$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;
$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();
}
return $bill->load(['client:id,name', 'bankAccount:id,bank_name,account_name', 'installments']);
});
}
/**
* 어음 수정
*/
public function update(int $id, array $data): Bill
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($id, $data, $tenantId, $userId) {
$bill = Bill::query()
->where('tenant_id', $tenantId)
->findOrFail($id);
if (isset($data['bill_number'])) {
$bill->bill_number = $data['bill_number'];
}
if (isset($data['bill_type'])) {
$bill->bill_type = $data['bill_type'];
}
if (array_key_exists('client_id', $data)) {
$bill->client_id = $data['client_id'];
}
if (array_key_exists('client_name', $data)) {
$bill->client_name = $data['client_name'];
}
if (isset($data['amount'])) {
$bill->amount = $data['amount'];
}
if (isset($data['issue_date'])) {
$bill->issue_date = $data['issue_date'];
}
if (isset($data['maturity_date'])) {
$bill->maturity_date = $data['maturity_date'];
}
if (isset($data['status'])) {
$bill->status = $data['status'];
}
if (array_key_exists('reason', $data)) {
$bill->reason = $data['reason'];
}
if (array_key_exists('note', $data)) {
$bill->note = $data['note'];
}
if (isset($data['is_electronic'])) {
$bill->is_electronic = $data['is_electronic'];
}
if (array_key_exists('bank_account_id', $data)) {
$bill->bank_account_id = $data['bank_account_id'];
}
$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();
}
return $bill->fresh(['client:id,name', 'bankAccount:id,bank_name,account_name', 'installments']);
});
}
/**
* 어음 삭제
*/
public function destroy(int $id): bool
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($id, $tenantId, $userId) {
$bill = Bill::query()
->where('tenant_id', $tenantId)
->findOrFail($id);
$bill->deleted_by = $userId;
$bill->save();
$bill->delete();
return true;
});
}
/**
* 어음 상태 변경
*/
public function updateStatus(int $id, string $status): Bill
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$bill = Bill::query()
->where('tenant_id', $tenantId)
->findOrFail($id);
$bill->status = $status;
$bill->updated_by = $userId;
$bill->save();
return $bill->fresh(['client:id,name', 'bankAccount:id,bank_name,account_name', 'installments']);
}
/**
* 어음 요약 (기간별 합계)
*/
public function summary(array $params): array
{
$tenantId = $this->tenantId();
$query = Bill::query()
->where('tenant_id', $tenantId);
// 어음 구분 필터
if (! empty($params['bill_type'])) {
$query->where('bill_type', $params['bill_type']);
}
// 발행일 범위 필터
if (! empty($params['issue_start_date'])) {
$query->where('issue_date', '>=', $params['issue_start_date']);
}
if (! empty($params['issue_end_date'])) {
$query->where('issue_date', '<=', $params['issue_end_date']);
}
// 만기일 범위 필터
if (! empty($params['maturity_start_date'])) {
$query->where('maturity_date', '>=', $params['maturity_start_date']);
}
if (! empty($params['maturity_end_date'])) {
$query->where('maturity_date', '<=', $params['maturity_end_date']);
}
// 전체 합계
$total = (clone $query)->sum('amount');
$count = (clone $query)->count();
// 구분별 합계
$byType = (clone $query)
->select('bill_type', DB::raw('SUM(amount) as total'), DB::raw('COUNT(*) as count'))
->groupBy('bill_type')
->get()
->keyBy('bill_type')
->toArray();
// 상태별 합계
$byStatus = (clone $query)
->select('status', DB::raw('SUM(amount) as total'), DB::raw('COUNT(*) as count'))
->groupBy('status')
->get()
->keyBy('status')
->toArray();
// 만기 임박 (7일 이내)
$maturityAlert = (clone $query)
->where('maturity_date', '>=', now()->toDateString())
->where('maturity_date', '<=', now()->addDays(7)->toDateString())
->whereNotIn('status', ['paymentComplete', 'collectionComplete', 'dishonored'])
->sum('amount');
return [
'total_amount' => (float) $total,
'total_count' => $count,
'by_type' => $byType,
'by_status' => $byStatus,
'maturity_alert_amount' => (float) $maturityAlert,
];
}
/**
* 어음번호 자동 생성
*/
private function generateBillNumber(int $tenantId): string
{
$prefix = date('Ym');
$lastBill = Bill::query()
->where('tenant_id', $tenantId)
->where('bill_number', 'like', $prefix.'%')
->orderBy('bill_number', 'desc')
->first();
if ($lastBill) {
$lastNumber = (int) substr($lastBill->bill_number, strlen($prefix));
$nextNumber = $lastNumber + 1;
} else {
$nextNumber = 1;
}
return $prefix.str_pad((string) $nextNumber, 6, '0', STR_PAD_LEFT);
}
}

347
app/Swagger/v1/BillApi.php Normal file
View File

@@ -0,0 +1,347 @@
<?php
namespace App\Swagger\v1;
/**
* @OA\Tag(
* name="Bills",
* description="어음 관리 API"
* )
*
* @OA\Schema(
* schema="Bill",
* type="object",
* required={"id", "tenant_id", "bill_number", "bill_type", "amount", "issue_date", "maturity_date", "status"},
* @OA\Property(property="id", type="integer", example=1),
* @OA\Property(property="tenant_id", type="integer", example=1),
* @OA\Property(property="bill_number", type="string", example="202412000001", description="어음번호"),
* @OA\Property(property="bill_type", type="string", enum={"received", "issued"}, example="received", description="어음 구분: received=수취, issued=발행"),
* @OA\Property(property="client_id", type="integer", nullable=true, example=10, description="거래처 ID"),
* @OA\Property(property="client_name", type="string", nullable=true, example="(주)삼성전자", description="거래처명 (비회원용)"),
* @OA\Property(property="amount", type="number", format="float", example=10000000, description="금액"),
* @OA\Property(property="issue_date", type="string", format="date", example="2024-12-01", description="발행일"),
* @OA\Property(property="maturity_date", type="string", format="date", example="2025-03-01", description="만기일"),
* @OA\Property(property="status", type="string", example="stored", description="상태: stored/maturityAlert/maturityResult/paymentComplete/dishonored/collectionRequest/collectionComplete/suing"),
* @OA\Property(property="reason", type="string", nullable=true, example="거래 대금", description="사유"),
* @OA\Property(property="installment_count", type="integer", example=0, description="차수"),
* @OA\Property(property="note", type="string", nullable=true, example="메모 내용", description="메모/비고"),
* @OA\Property(property="is_electronic", type="boolean", example=false, description="전자어음 여부"),
* @OA\Property(property="bank_account_id", type="integer", nullable=true, example=5, description="입금/출금 계좌 ID"),
* @OA\Property(property="created_by", type="integer", nullable=true, example=1),
* @OA\Property(property="updated_by", type="integer", nullable=true, example=1),
* @OA\Property(property="created_at", type="string", format="date-time"),
* @OA\Property(property="updated_at", type="string", format="date-time"),
* @OA\Property(
* property="client",
* type="object",
* nullable=true,
* @OA\Property(property="id", type="integer", example=10),
* @OA\Property(property="name", type="string", example="(주)삼성전자")
* ),
* @OA\Property(
* property="bank_account",
* type="object",
* nullable=true,
* @OA\Property(property="id", type="integer", example=5),
* @OA\Property(property="bank_name", type="string", example="국민은행"),
* @OA\Property(property="account_name", type="string", example="운영계좌")
* ),
* @OA\Property(
* property="installments",
* type="array",
* @OA\Items(ref="#/components/schemas/BillInstallment")
* )
* )
*
* @OA\Schema(
* schema="BillInstallment",
* type="object",
* @OA\Property(property="id", type="integer", example=1),
* @OA\Property(property="bill_id", type="integer", example=1),
* @OA\Property(property="installment_date", type="string", format="date", example="2024-12-15", description="차수 일자"),
* @OA\Property(property="amount", type="number", format="float", example=5000000, description="차수 금액"),
* @OA\Property(property="note", type="string", nullable=true, example="1차 분할", description="비고"),
* @OA\Property(property="created_at", type="string", format="date-time"),
* @OA\Property(property="updated_at", type="string", format="date-time")
* )
*
* @OA\Schema(
* schema="BillPagination",
* type="object",
* @OA\Property(
* property="data",
* type="array",
* @OA\Items(ref="#/components/schemas/Bill")
* ),
* @OA\Property(property="current_page", type="integer", example=1),
* @OA\Property(property="last_page", type="integer", example=5),
* @OA\Property(property="per_page", type="integer", example=20),
* @OA\Property(property="total", type="integer", example=100),
* @OA\Property(property="from", type="integer", example=1),
* @OA\Property(property="to", type="integer", example=20)
* )
*
* @OA\Schema(
* schema="BillCreateRequest",
* type="object",
* required={"bill_type", "amount", "issue_date", "maturity_date"},
* @OA\Property(property="bill_number", type="string", nullable=true, example="202412000001", description="어음번호 (미입력시 자동생성)"),
* @OA\Property(property="bill_type", type="string", enum={"received", "issued"}, example="received", description="어음 구분"),
* @OA\Property(property="client_id", type="integer", nullable=true, example=10, description="거래처 ID"),
* @OA\Property(property="client_name", type="string", nullable=true, example="(주)삼성전자", description="거래처명"),
* @OA\Property(property="amount", type="number", example=10000000, description="금액"),
* @OA\Property(property="issue_date", type="string", format="date", example="2024-12-01", description="발행일"),
* @OA\Property(property="maturity_date", type="string", format="date", example="2025-03-01", description="만기일"),
* @OA\Property(property="status", type="string", nullable=true, example="stored", description="상태"),
* @OA\Property(property="reason", type="string", nullable=true, example="거래 대금", description="사유"),
* @OA\Property(property="note", type="string", nullable=true, example="메모 내용", description="메모"),
* @OA\Property(property="is_electronic", type="boolean", nullable=true, example=false, description="전자어음 여부"),
* @OA\Property(property="bank_account_id", type="integer", nullable=true, example=5, description="계좌 ID"),
* @OA\Property(
* property="installments",
* type="array",
* nullable=true,
* @OA\Items(
* type="object",
* @OA\Property(property="date", type="string", format="date", example="2024-12-15"),
* @OA\Property(property="amount", type="number", example=5000000),
* @OA\Property(property="note", type="string", nullable=true, example="1차 분할")
* )
* )
* )
*
* @OA\Schema(
* schema="BillUpdateRequest",
* type="object",
* @OA\Property(property="bill_number", type="string", nullable=true, example="202412000001"),
* @OA\Property(property="bill_type", type="string", enum={"received", "issued"}, nullable=true),
* @OA\Property(property="client_id", type="integer", nullable=true),
* @OA\Property(property="client_name", type="string", nullable=true),
* @OA\Property(property="amount", type="number", nullable=true),
* @OA\Property(property="issue_date", type="string", format="date", nullable=true),
* @OA\Property(property="maturity_date", type="string", format="date", nullable=true),
* @OA\Property(property="status", type="string", nullable=true),
* @OA\Property(property="reason", type="string", nullable=true),
* @OA\Property(property="note", type="string", nullable=true),
* @OA\Property(property="is_electronic", type="boolean", nullable=true),
* @OA\Property(property="bank_account_id", type="integer", nullable=true),
* @OA\Property(property="installments", type="array", nullable=true, @OA\Items(type="object"))
* )
*
* @OA\Schema(
* schema="BillSummary",
* type="object",
* @OA\Property(property="total_amount", type="number", example=50000000, description="총 금액"),
* @OA\Property(property="total_count", type="integer", example=10, description="총 건수"),
* @OA\Property(
* property="by_type",
* type="object",
* description="구분별 합계",
* @OA\AdditionalProperties(
* type="object",
* @OA\Property(property="bill_type", type="string"),
* @OA\Property(property="total", type="number"),
* @OA\Property(property="count", type="integer")
* )
* ),
* @OA\Property(
* property="by_status",
* type="object",
* description="상태별 합계"
* ),
* @OA\Property(property="maturity_alert_amount", type="number", example=10000000, description="만기 임박 금액 (7일 이내)")
* )
*/
class BillApi
{
/**
* @OA\Get(
* path="/api/v1/bills",
* operationId="getBills",
* tags={"Bills"},
* summary="어음 목록 조회",
* description="어음 목록을 페이지네이션하여 조회합니다.",
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
* @OA\Parameter(name="search", in="query", description="검색어 (어음번호, 거래처명, 메모)", @OA\Schema(type="string")),
* @OA\Parameter(name="bill_type", in="query", description="어음 구분", @OA\Schema(type="string", enum={"received", "issued"})),
* @OA\Parameter(name="status", in="query", description="상태", @OA\Schema(type="string")),
* @OA\Parameter(name="client_id", in="query", description="거래처 ID", @OA\Schema(type="integer")),
* @OA\Parameter(name="is_electronic", in="query", description="전자어음 여부", @OA\Schema(type="boolean")),
* @OA\Parameter(name="issue_start_date", in="query", description="발행일 시작", @OA\Schema(type="string", format="date")),
* @OA\Parameter(name="issue_end_date", in="query", description="발행일 종료", @OA\Schema(type="string", format="date")),
* @OA\Parameter(name="maturity_start_date", in="query", description="만기일 시작", @OA\Schema(type="string", format="date")),
* @OA\Parameter(name="maturity_end_date", in="query", description="만기일 종료", @OA\Schema(type="string", format="date")),
* @OA\Parameter(name="sort_by", in="query", description="정렬 기준", @OA\Schema(type="string", default="issue_date")),
* @OA\Parameter(name="sort_dir", in="query", description="정렬 방향", @OA\Schema(type="string", enum={"asc", "desc"}, default="desc")),
* @OA\Parameter(name="per_page", in="query", description="페이지당 건수", @OA\Schema(type="integer", default=20)),
* @OA\Parameter(name="page", in="query", description="페이지 번호", @OA\Schema(type="integer", default=1)),
* @OA\Response(
* response=200,
* description="성공",
* @OA\JsonContent(
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="데이터를 조회했습니다."),
* @OA\Property(property="data", ref="#/components/schemas/BillPagination")
* )
* )
* )
*/
public function index() {}
/**
* @OA\Post(
* path="/api/v1/bills",
* operationId="storeBill",
* tags={"Bills"},
* summary="어음 등록",
* description="새로운 어음을 등록합니다.",
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(ref="#/components/schemas/BillCreateRequest")
* ),
* @OA\Response(
* response=201,
* description="생성 성공",
* @OA\JsonContent(
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="생성되었습니다."),
* @OA\Property(property="data", ref="#/components/schemas/Bill")
* )
* ),
* @OA\Response(response=422, description="유효성 검사 실패")
* )
*/
public function store() {}
/**
* @OA\Get(
* path="/api/v1/bills/{id}",
* operationId="showBill",
* tags={"Bills"},
* summary="어음 상세 조회",
* description="특정 어음의 상세 정보를 조회합니다.",
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="어음 ID", @OA\Schema(type="integer")),
* @OA\Response(
* response=200,
* description="성공",
* @OA\JsonContent(
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="데이터를 조회했습니다."),
* @OA\Property(property="data", ref="#/components/schemas/Bill")
* )
* ),
* @OA\Response(response=404, description="어음을 찾을 수 없음")
* )
*/
public function show() {}
/**
* @OA\Put(
* path="/api/v1/bills/{id}",
* operationId="updateBill",
* tags={"Bills"},
* summary="어음 수정",
* description="기존 어음 정보를 수정합니다.",
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="어음 ID", @OA\Schema(type="integer")),
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(ref="#/components/schemas/BillUpdateRequest")
* ),
* @OA\Response(
* response=200,
* description="수정 성공",
* @OA\JsonContent(
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="수정되었습니다."),
* @OA\Property(property="data", ref="#/components/schemas/Bill")
* )
* ),
* @OA\Response(response=404, description="어음을 찾을 수 없음"),
* @OA\Response(response=422, description="유효성 검사 실패")
* )
*/
public function update() {}
/**
* @OA\Delete(
* path="/api/v1/bills/{id}",
* operationId="deleteBill",
* tags={"Bills"},
* summary="어음 삭제",
* description="어음을 삭제합니다. (Soft Delete)",
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="어음 ID", @OA\Schema(type="integer")),
* @OA\Response(
* response=200,
* description="삭제 성공",
* @OA\JsonContent(
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="삭제되었습니다."),
* @OA\Property(property="data", type="null")
* )
* ),
* @OA\Response(response=404, description="어음을 찾을 수 없음")
* )
*/
public function destroy() {}
/**
* @OA\Patch(
* path="/api/v1/bills/{id}/status",
* operationId="updateBillStatus",
* tags={"Bills"},
* summary="어음 상태 변경",
* description="어음의 상태를 변경합니다.",
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
* @OA\Parameter(name="id", in="path", required=true, description="어음 ID", @OA\Schema(type="integer")),
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(
* required={"status"},
* @OA\Property(property="status", type="string", example="paymentComplete", description="변경할 상태")
* )
* ),
* @OA\Response(
* response=200,
* description="상태 변경 성공",
* @OA\JsonContent(
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="수정되었습니다."),
* @OA\Property(property="data", ref="#/components/schemas/Bill")
* )
* ),
* @OA\Response(response=404, description="어음을 찾을 수 없음"),
* @OA\Response(response=422, description="유효성 검사 실패")
* )
*/
public function updateStatus() {}
/**
* @OA\Get(
* path="/api/v1/bills/summary",
* operationId="getBillSummary",
* tags={"Bills"},
* summary="어음 요약 조회",
* description="기간별 어음 요약 정보를 조회합니다.",
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
* @OA\Parameter(name="bill_type", in="query", description="어음 구분", @OA\Schema(type="string", enum={"received", "issued"})),
* @OA\Parameter(name="issue_start_date", in="query", description="발행일 시작", @OA\Schema(type="string", format="date")),
* @OA\Parameter(name="issue_end_date", in="query", description="발행일 종료", @OA\Schema(type="string", format="date")),
* @OA\Parameter(name="maturity_start_date", in="query", description="만기일 시작", @OA\Schema(type="string", format="date")),
* @OA\Parameter(name="maturity_end_date", in="query", description="만기일 종료", @OA\Schema(type="string", format="date")),
* @OA\Response(
* response=200,
* description="성공",
* @OA\JsonContent(
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="데이터를 조회했습니다."),
* @OA\Property(property="data", ref="#/components/schemas/BillSummary")
* )
* )
* )
*/
public function summary() {}
}