feat: Phase 6.1 악성채권 추심관리 API 구현
- 테이블 3개: bad_debts, bad_debt_documents, bad_debt_memos - 모델 3개: BadDebt, BadDebtDocument, BadDebtMemo - BadDebtService: CRUD, 요약 통계, 서류/메모 관리 - API 엔드포인트 11개 (목록, 등록, 상세, 수정, 삭제, 토글, 서류/메모 CRUD) - Swagger 문서 작성 완료
This commit is contained in:
@@ -1,10 +1,31 @@
|
||||
# 논리적 데이터베이스 관계 문서
|
||||
|
||||
> **자동 생성**: 2025-12-19 10:11:32
|
||||
> **자동 생성**: 2025-12-19 15:55:41
|
||||
> **소스**: Eloquent 모델 관계 분석
|
||||
|
||||
## 📊 모델별 관계 현황
|
||||
|
||||
### bad_debts
|
||||
**모델**: `App\Models\BadDebts\BadDebt`
|
||||
|
||||
- **client()**: belongsTo → `clients`
|
||||
- **assignedUser()**: belongsTo → `users`
|
||||
- **creator()**: belongsTo → `users`
|
||||
- **documents()**: hasMany → `bad_debt_documents`
|
||||
- **memos()**: hasMany → `bad_debt_memos`
|
||||
|
||||
### bad_debt_documents
|
||||
**모델**: `App\Models\BadDebts\BadDebtDocument`
|
||||
|
||||
- **badDebt()**: belongsTo → `bad_debts`
|
||||
- **file()**: belongsTo → `files`
|
||||
|
||||
### bad_debt_memos
|
||||
**모델**: `App\Models\BadDebts\BadDebtMemo`
|
||||
|
||||
- **badDebt()**: belongsTo → `bad_debts`
|
||||
- **creator()**: belongsTo → `users`
|
||||
|
||||
### boards
|
||||
**모델**: `App\Models\Boards\Board`
|
||||
|
||||
@@ -231,6 +252,10 @@ ### user_tenants
|
||||
- **user()**: belongsTo → `users`
|
||||
- **tenant()**: belongsTo → `tenants`
|
||||
|
||||
### notification_settings
|
||||
**모델**: `App\Models\NotificationSetting`
|
||||
|
||||
|
||||
### clients
|
||||
**모델**: `App\Models\Orders\Client`
|
||||
|
||||
@@ -546,3 +571,10 @@ ### withdrawals
|
||||
|
||||
- **bankAccount()**: belongsTo → `bank_accounts`
|
||||
|
||||
### user_invitations
|
||||
**모델**: `App\Models\UserInvitation`
|
||||
|
||||
- **tenant()**: belongsTo → `tenants`
|
||||
- **role()**: belongsTo → `roles`
|
||||
- **inviter()**: belongsTo → `users`
|
||||
|
||||
|
||||
142
app/Http/Controllers/Api/V1/BadDebtController.php
Normal file
142
app/Http/Controllers/Api/V1/BadDebtController.php
Normal file
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\V1\BadDebt\StoreBadDebtDocumentRequest;
|
||||
use App\Http\Requests\V1\BadDebt\StoreBadDebtMemoRequest;
|
||||
use App\Http\Requests\V1\BadDebt\StoreBadDebtRequest;
|
||||
use App\Http\Requests\V1\BadDebt\UpdateBadDebtRequest;
|
||||
use App\Services\BadDebtService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BadDebtController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly BadDebtService $service
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 악성채권 목록
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$params = $request->only([
|
||||
'client_id',
|
||||
'status',
|
||||
'is_active',
|
||||
'search',
|
||||
'sort_by',
|
||||
'sort_dir',
|
||||
'per_page',
|
||||
'page',
|
||||
]);
|
||||
|
||||
$badDebts = $this->service->index($params);
|
||||
|
||||
return ApiResponse::success($badDebts, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 악성채권 요약 통계
|
||||
*/
|
||||
public function summary(Request $request)
|
||||
{
|
||||
$params = $request->only(['client_id']);
|
||||
|
||||
$summary = $this->service->summary($params);
|
||||
|
||||
return ApiResponse::success($summary, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 악성채권 등록
|
||||
*/
|
||||
public function store(StoreBadDebtRequest $request)
|
||||
{
|
||||
$badDebt = $this->service->store($request->validated());
|
||||
|
||||
return ApiResponse::success($badDebt, __('message.created'), [], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* 악성채권 상세
|
||||
*/
|
||||
public function show(int $id)
|
||||
{
|
||||
$badDebt = $this->service->show($id);
|
||||
|
||||
return ApiResponse::success($badDebt, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 악성채권 수정
|
||||
*/
|
||||
public function update(int $id, UpdateBadDebtRequest $request)
|
||||
{
|
||||
$badDebt = $this->service->update($id, $request->validated());
|
||||
|
||||
return ApiResponse::success($badDebt, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 악성채권 삭제
|
||||
*/
|
||||
public function destroy(int $id)
|
||||
{
|
||||
$this->service->destroy($id);
|
||||
|
||||
return ApiResponse::success(null, __('message.deleted'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 설정 토글 (is_active)
|
||||
*/
|
||||
public function toggle(int $id)
|
||||
{
|
||||
$badDebt = $this->service->toggle($id);
|
||||
|
||||
return ApiResponse::success($badDebt, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 서류 첨부
|
||||
*/
|
||||
public function addDocument(int $id, StoreBadDebtDocumentRequest $request)
|
||||
{
|
||||
$document = $this->service->addDocument($id, $request->validated());
|
||||
|
||||
return ApiResponse::success($document, __('message.created'), [], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* 서류 삭제
|
||||
*/
|
||||
public function removeDocument(int $id, int $documentId)
|
||||
{
|
||||
$this->service->removeDocument($id, $documentId);
|
||||
|
||||
return ApiResponse::success(null, __('message.deleted'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 메모 추가
|
||||
*/
|
||||
public function addMemo(int $id, StoreBadDebtMemoRequest $request)
|
||||
{
|
||||
$memo = $this->service->addMemo($id, $request->validated());
|
||||
|
||||
return ApiResponse::success($memo, __('message.created'), [], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* 메모 삭제
|
||||
*/
|
||||
public function removeMemo(int $id, int $memoId)
|
||||
{
|
||||
$this->service->removeMemo($id, $memoId);
|
||||
|
||||
return ApiResponse::success(null, __('message.deleted'));
|
||||
}
|
||||
}
|
||||
46
app/Http/Requests/V1/BadDebt/StoreBadDebtDocumentRequest.php
Normal file
46
app/Http/Requests/V1/BadDebt/StoreBadDebtDocumentRequest.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\BadDebt;
|
||||
|
||||
use App\Models\BadDebts\BadDebtDocument;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreBadDebtDocumentRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'document_type' => ['required', 'string', Rule::in(array_keys(BadDebtDocument::DOCUMENT_TYPES))],
|
||||
'file_id' => ['required', 'integer', 'exists:files,id'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'document_type.required' => __('validation.required', ['attribute' => '서류유형']),
|
||||
'document_type.in' => __('validation.in', ['attribute' => '서류유형']),
|
||||
'file_id.required' => __('validation.required', ['attribute' => '파일']),
|
||||
'file_id.exists' => __('validation.exists', ['attribute' => '파일']),
|
||||
];
|
||||
}
|
||||
}
|
||||
41
app/Http/Requests/V1/BadDebt/StoreBadDebtMemoRequest.php
Normal file
41
app/Http/Requests/V1/BadDebt/StoreBadDebtMemoRequest.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\BadDebt;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreBadDebtMemoRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'content' => ['required', 'string', 'max:5000'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'content.required' => __('validation.required', ['attribute' => '메모 내용']),
|
||||
'content.max' => __('validation.max.string', ['attribute' => '메모 내용', 'max' => 5000]),
|
||||
];
|
||||
}
|
||||
}
|
||||
55
app/Http/Requests/V1/BadDebt/StoreBadDebtRequest.php
Normal file
55
app/Http/Requests/V1/BadDebt/StoreBadDebtRequest.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\BadDebt;
|
||||
|
||||
use App\Models\BadDebts\BadDebt;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreBadDebtRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'client_id' => ['required', 'integer', 'exists:clients,id'],
|
||||
'debt_amount' => ['required', 'numeric', 'min:0'],
|
||||
'status' => ['nullable', 'string', Rule::in(array_keys(BadDebt::STATUSES))],
|
||||
'overdue_days' => ['nullable', 'integer', 'min:0'],
|
||||
'assigned_user_id' => ['nullable', 'integer', 'exists:users,id'],
|
||||
'occurred_at' => ['nullable', 'date'],
|
||||
'closed_at' => ['nullable', 'date', 'after_or_equal:occurred_at'],
|
||||
'is_active' => ['nullable', 'boolean'],
|
||||
'options' => ['nullable', 'array'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'client_id.required' => __('validation.required', ['attribute' => '거래처']),
|
||||
'client_id.exists' => __('validation.exists', ['attribute' => '거래처']),
|
||||
'debt_amount.required' => __('validation.required', ['attribute' => '채권금액']),
|
||||
'debt_amount.numeric' => __('validation.numeric', ['attribute' => '채권금액']),
|
||||
'status.in' => __('validation.in', ['attribute' => '상태']),
|
||||
'closed_at.after_or_equal' => __('validation.after_or_equal', ['attribute' => '종료일', 'date' => '발생일']),
|
||||
];
|
||||
}
|
||||
}
|
||||
53
app/Http/Requests/V1/BadDebt/UpdateBadDebtRequest.php
Normal file
53
app/Http/Requests/V1/BadDebt/UpdateBadDebtRequest.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\V1\BadDebt;
|
||||
|
||||
use App\Models\BadDebts\BadDebt;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateBadDebtRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'client_id' => ['sometimes', 'integer', 'exists:clients,id'],
|
||||
'debt_amount' => ['sometimes', 'numeric', 'min:0'],
|
||||
'status' => ['sometimes', 'string', Rule::in(array_keys(BadDebt::STATUSES))],
|
||||
'overdue_days' => ['sometimes', 'integer', 'min:0'],
|
||||
'assigned_user_id' => ['nullable', 'integer', 'exists:users,id'],
|
||||
'occurred_at' => ['nullable', 'date'],
|
||||
'closed_at' => ['nullable', 'date', 'after_or_equal:occurred_at'],
|
||||
'is_active' => ['sometimes', 'boolean'],
|
||||
'options' => ['nullable', 'array'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'client_id.exists' => __('validation.exists', ['attribute' => '거래처']),
|
||||
'debt_amount.numeric' => __('validation.numeric', ['attribute' => '채권금액']),
|
||||
'status.in' => __('validation.in', ['attribute' => '상태']),
|
||||
'closed_at.after_or_equal' => __('validation.after_or_equal', ['attribute' => '종료일', 'date' => '발생일']),
|
||||
];
|
||||
}
|
||||
}
|
||||
158
app/Models/BadDebts/BadDebt.php
Normal file
158
app/Models/BadDebts/BadDebt.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\BadDebts;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use App\Models\Orders\Client;
|
||||
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 BadDebt extends Model
|
||||
{
|
||||
use BelongsToTenant, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'client_id',
|
||||
'debt_amount',
|
||||
'status',
|
||||
'overdue_days',
|
||||
'assigned_user_id',
|
||||
'occurred_at',
|
||||
'closed_at',
|
||||
'is_active',
|
||||
'options',
|
||||
'created_by',
|
||||
'updated_by',
|
||||
'deleted_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'debt_amount' => 'decimal:2',
|
||||
'overdue_days' => 'integer',
|
||||
'is_active' => 'boolean',
|
||||
'options' => 'array',
|
||||
'occurred_at' => 'date',
|
||||
'closed_at' => 'date',
|
||||
];
|
||||
|
||||
/**
|
||||
* 상태 상수
|
||||
*/
|
||||
public const STATUS_COLLECTING = 'collecting';
|
||||
|
||||
public const STATUS_LEGAL_ACTION = 'legal_action';
|
||||
|
||||
public const STATUS_RECOVERED = 'recovered';
|
||||
|
||||
public const STATUS_BAD_DEBT = 'bad_debt';
|
||||
|
||||
/**
|
||||
* 상태 목록
|
||||
*/
|
||||
public const STATUSES = [
|
||||
self::STATUS_COLLECTING => '추심중',
|
||||
self::STATUS_LEGAL_ACTION => '법적조치',
|
||||
self::STATUS_RECOVERED => '회수완료',
|
||||
self::STATUS_BAD_DEBT => '대손처리',
|
||||
];
|
||||
|
||||
/**
|
||||
* 거래처 관계
|
||||
*/
|
||||
public function client(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Client::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 담당자 관계
|
||||
*/
|
||||
public function assignedUser(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'assigned_user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 생성자 관계
|
||||
*/
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* 서류 관계
|
||||
*/
|
||||
public function documents(): HasMany
|
||||
{
|
||||
return $this->hasMany(BadDebtDocument::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 메모 관계
|
||||
*/
|
||||
public function memos(): HasMany
|
||||
{
|
||||
return $this->hasMany(BadDebtMemo::class)->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태 라벨 속성
|
||||
*/
|
||||
public function getStatusLabelAttribute(): string
|
||||
{
|
||||
return self::STATUSES[$this->status] ?? $this->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 활성 스코프
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태별 스코프
|
||||
*/
|
||||
public function scopeStatus($query, string $status)
|
||||
{
|
||||
return $query->where('status', $status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 추심중 스코프
|
||||
*/
|
||||
public function scopeCollecting($query)
|
||||
{
|
||||
return $query->where('status', self::STATUS_COLLECTING);
|
||||
}
|
||||
|
||||
/**
|
||||
* 법적조치 스코프
|
||||
*/
|
||||
public function scopeLegalAction($query)
|
||||
{
|
||||
return $query->where('status', self::STATUS_LEGAL_ACTION);
|
||||
}
|
||||
|
||||
/**
|
||||
* 회수완료 스코프
|
||||
*/
|
||||
public function scopeRecovered($query)
|
||||
{
|
||||
return $query->where('status', self::STATUS_RECOVERED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 대손처리 스코프
|
||||
*/
|
||||
public function scopeBadDebt($query)
|
||||
{
|
||||
return $query->where('status', self::STATUS_BAD_DEBT);
|
||||
}
|
||||
}
|
||||
58
app/Models/BadDebts/BadDebtDocument.php
Normal file
58
app/Models/BadDebts/BadDebtDocument.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\BadDebts;
|
||||
|
||||
use App\Models\Commons\File;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class BadDebtDocument extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'bad_debt_id',
|
||||
'document_type',
|
||||
'file_id',
|
||||
];
|
||||
|
||||
/**
|
||||
* 서류 유형 상수
|
||||
*/
|
||||
public const TYPE_BUSINESS_LICENSE = 'business_license';
|
||||
|
||||
public const TYPE_TAX_INVOICE = 'tax_invoice';
|
||||
|
||||
public const TYPE_ADDITIONAL = 'additional';
|
||||
|
||||
/**
|
||||
* 서류 유형 목록
|
||||
*/
|
||||
public const DOCUMENT_TYPES = [
|
||||
self::TYPE_BUSINESS_LICENSE => '사업자등록증',
|
||||
self::TYPE_TAX_INVOICE => '세금계산서',
|
||||
self::TYPE_ADDITIONAL => '추가서류',
|
||||
];
|
||||
|
||||
/**
|
||||
* 악성채권 관계
|
||||
*/
|
||||
public function badDebt(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(BadDebt::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 파일 관계
|
||||
*/
|
||||
public function file(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(File::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 서류 유형 라벨 속성
|
||||
*/
|
||||
public function getDocumentTypeLabelAttribute(): string
|
||||
{
|
||||
return self::DOCUMENT_TYPES[$this->document_type] ?? $this->document_type;
|
||||
}
|
||||
}
|
||||
32
app/Models/BadDebts/BadDebtMemo.php
Normal file
32
app/Models/BadDebts/BadDebtMemo.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\BadDebts;
|
||||
|
||||
use App\Models\Members\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class BadDebtMemo extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'bad_debt_id',
|
||||
'content',
|
||||
'created_by',
|
||||
];
|
||||
|
||||
/**
|
||||
* 악성채권 관계
|
||||
*/
|
||||
public function badDebt(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(BadDebt::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 작성자 관계
|
||||
*/
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
}
|
||||
307
app/Services/BadDebtService.php
Normal file
307
app/Services/BadDebtService.php
Normal file
@@ -0,0 +1,307 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\BadDebts\BadDebt;
|
||||
use App\Models\BadDebts\BadDebtDocument;
|
||||
use App\Models\BadDebts\BadDebtMemo;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class BadDebtService extends Service
|
||||
{
|
||||
/**
|
||||
* 악성채권 목록 조회
|
||||
*/
|
||||
public function index(array $params): LengthAwarePaginator
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$query = BadDebt::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with(['client:id,name,client_code', 'assignedUser:id,name']);
|
||||
|
||||
// 거래처 필터
|
||||
if (! empty($params['client_id'])) {
|
||||
$query->where('client_id', $params['client_id']);
|
||||
}
|
||||
|
||||
// 상태 필터
|
||||
if (! empty($params['status'])) {
|
||||
$query->where('status', $params['status']);
|
||||
}
|
||||
|
||||
// 활성화 필터
|
||||
if (isset($params['is_active'])) {
|
||||
$query->where('is_active', $params['is_active']);
|
||||
}
|
||||
|
||||
// 검색어 필터
|
||||
if (! empty($params['search'])) {
|
||||
$search = $params['search'];
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->whereHas('client', function ($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
->orWhere('client_code', 'like', "%{$search}%");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 정렬
|
||||
$sortBy = $params['sort_by'] ?? 'created_at';
|
||||
$sortDir = $params['sort_dir'] ?? 'desc';
|
||||
$query->orderBy($sortBy, $sortDir);
|
||||
|
||||
// 페이지네이션
|
||||
$perPage = $params['per_page'] ?? 20;
|
||||
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 악성채권 요약 통계
|
||||
*/
|
||||
public function summary(array $params = []): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$query = BadDebt::query()
|
||||
->where('tenant_id', $tenantId);
|
||||
|
||||
// 거래처 필터
|
||||
if (! empty($params['client_id'])) {
|
||||
$query->where('client_id', $params['client_id']);
|
||||
}
|
||||
|
||||
// 전체 합계
|
||||
$totalAmount = (clone $query)->sum('debt_amount');
|
||||
|
||||
// 상태별 합계
|
||||
$collectingAmount = (clone $query)->collecting()->sum('debt_amount');
|
||||
$legalActionAmount = (clone $query)->legalAction()->sum('debt_amount');
|
||||
$recoveredAmount = (clone $query)->recovered()->sum('debt_amount');
|
||||
$badDebtAmount = (clone $query)->badDebt()->sum('debt_amount');
|
||||
|
||||
return [
|
||||
'total_amount' => (float) $totalAmount,
|
||||
'collecting_amount' => (float) $collectingAmount,
|
||||
'legal_action_amount' => (float) $legalActionAmount,
|
||||
'recovered_amount' => (float) $recoveredAmount,
|
||||
'bad_debt_amount' => (float) $badDebtAmount,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 악성채권 상세 조회
|
||||
*/
|
||||
public function show(int $id): BadDebt
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
return BadDebt::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->with([
|
||||
'client',
|
||||
'assignedUser:id,name',
|
||||
'creator:id,name',
|
||||
'documents.file',
|
||||
'memos.creator:id,name',
|
||||
])
|
||||
->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 악성채권 등록
|
||||
*/
|
||||
public function store(array $data): BadDebt
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($data, $tenantId, $userId) {
|
||||
$badDebt = new BadDebt;
|
||||
$badDebt->tenant_id = $tenantId;
|
||||
$badDebt->client_id = $data['client_id'];
|
||||
$badDebt->debt_amount = $data['debt_amount'];
|
||||
$badDebt->status = $data['status'] ?? BadDebt::STATUS_COLLECTING;
|
||||
$badDebt->overdue_days = $data['overdue_days'] ?? 0;
|
||||
$badDebt->assigned_user_id = $data['assigned_user_id'] ?? null;
|
||||
$badDebt->occurred_at = $data['occurred_at'] ?? null;
|
||||
$badDebt->closed_at = $data['closed_at'] ?? null;
|
||||
$badDebt->is_active = $data['is_active'] ?? true;
|
||||
$badDebt->options = $data['options'] ?? null;
|
||||
$badDebt->created_by = $userId;
|
||||
$badDebt->updated_by = $userId;
|
||||
$badDebt->save();
|
||||
|
||||
return $badDebt->load(['client:id,name,client_code', 'assignedUser:id,name']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 악성채권 수정
|
||||
*/
|
||||
public function update(int $id, array $data): BadDebt
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($id, $data, $tenantId, $userId) {
|
||||
$badDebt = BadDebt::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
if (isset($data['client_id'])) {
|
||||
$badDebt->client_id = $data['client_id'];
|
||||
}
|
||||
if (isset($data['debt_amount'])) {
|
||||
$badDebt->debt_amount = $data['debt_amount'];
|
||||
}
|
||||
if (isset($data['status'])) {
|
||||
$badDebt->status = $data['status'];
|
||||
}
|
||||
if (isset($data['overdue_days'])) {
|
||||
$badDebt->overdue_days = $data['overdue_days'];
|
||||
}
|
||||
if (array_key_exists('assigned_user_id', $data)) {
|
||||
$badDebt->assigned_user_id = $data['assigned_user_id'];
|
||||
}
|
||||
if (array_key_exists('occurred_at', $data)) {
|
||||
$badDebt->occurred_at = $data['occurred_at'];
|
||||
}
|
||||
if (array_key_exists('closed_at', $data)) {
|
||||
$badDebt->closed_at = $data['closed_at'];
|
||||
}
|
||||
if (isset($data['is_active'])) {
|
||||
$badDebt->is_active = $data['is_active'];
|
||||
}
|
||||
if (array_key_exists('options', $data)) {
|
||||
$badDebt->options = $data['options'];
|
||||
}
|
||||
|
||||
$badDebt->updated_by = $userId;
|
||||
$badDebt->save();
|
||||
|
||||
return $badDebt->fresh(['client:id,name,client_code', 'assignedUser:id,name']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 악성채권 삭제
|
||||
*/
|
||||
public function destroy(int $id): bool
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($id, $tenantId, $userId) {
|
||||
$badDebt = BadDebt::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
$badDebt->deleted_by = $userId;
|
||||
$badDebt->save();
|
||||
$badDebt->delete();
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 설정 토글 (is_active)
|
||||
*/
|
||||
public function toggle(int $id): BadDebt
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($id, $tenantId, $userId) {
|
||||
$badDebt = BadDebt::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
$badDebt->is_active = ! $badDebt->is_active;
|
||||
$badDebt->updated_by = $userId;
|
||||
$badDebt->save();
|
||||
|
||||
return $badDebt->fresh(['client:id,name,client_code', 'assignedUser:id,name']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 서류 첨부
|
||||
*/
|
||||
public function addDocument(int $id, array $data): BadDebtDocument
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$badDebt = BadDebt::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
$document = new BadDebtDocument;
|
||||
$document->bad_debt_id = $badDebt->id;
|
||||
$document->document_type = $data['document_type'];
|
||||
$document->file_id = $data['file_id'];
|
||||
$document->save();
|
||||
|
||||
return $document->load('file');
|
||||
}
|
||||
|
||||
/**
|
||||
* 서류 삭제
|
||||
*/
|
||||
public function removeDocument(int $id, int $documentId): bool
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$badDebt = BadDebt::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
$document = BadDebtDocument::query()
|
||||
->where('bad_debt_id', $badDebt->id)
|
||||
->findOrFail($documentId);
|
||||
|
||||
return $document->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 메모 추가
|
||||
*/
|
||||
public function addMemo(int $id, array $data): BadDebtMemo
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
$badDebt = BadDebt::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
$memo = new BadDebtMemo;
|
||||
$memo->bad_debt_id = $badDebt->id;
|
||||
$memo->content = $data['content'];
|
||||
$memo->created_by = $userId;
|
||||
$memo->save();
|
||||
|
||||
return $memo->load('creator:id,name');
|
||||
}
|
||||
|
||||
/**
|
||||
* 메모 삭제
|
||||
*/
|
||||
public function removeMemo(int $id, int $memoId): bool
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$badDebt = BadDebt::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->findOrFail($id);
|
||||
|
||||
$memo = BadDebtMemo::query()
|
||||
->where('bad_debt_id', $badDebt->id)
|
||||
->findOrFail($memoId);
|
||||
|
||||
return $memo->delete();
|
||||
}
|
||||
}
|
||||
433
app/Swagger/v1/BadDebtApi.php
Normal file
433
app/Swagger/v1/BadDebtApi.php
Normal file
@@ -0,0 +1,433 @@
|
||||
<?php
|
||||
|
||||
namespace App\Swagger\v1;
|
||||
|
||||
/**
|
||||
* @OA\Tag(name="BadDebt", description="악성채권 추심관리")
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="BadDebt",
|
||||
*
|
||||
* @OA\Property(property="id", type="integer", example=1),
|
||||
* @OA\Property(property="tenant_id", type="integer", example=1),
|
||||
* @OA\Property(property="client_id", type="integer", example=1),
|
||||
* @OA\Property(property="debt_amount", type="number", format="decimal", example="10000000.00"),
|
||||
* @OA\Property(property="status", type="string", enum={"collecting", "legal_action", "recovered", "bad_debt"}, example="collecting"),
|
||||
* @OA\Property(property="overdue_days", type="integer", example=100),
|
||||
* @OA\Property(property="assigned_user_id", type="integer", nullable=true, example=1),
|
||||
* @OA\Property(property="occurred_at", type="string", format="date", nullable=true, example="2025-12-12"),
|
||||
* @OA\Property(property="closed_at", type="string", format="date", nullable=true, example=null),
|
||||
* @OA\Property(property="is_active", type="boolean", example=true),
|
||||
* @OA\Property(property="options", type="object", nullable=true),
|
||||
* @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",
|
||||
* @OA\Property(property="id", type="integer"),
|
||||
* @OA\Property(property="name", type="string"),
|
||||
* @OA\Property(property="client_code", type="string")
|
||||
* ),
|
||||
* @OA\Property(
|
||||
* property="assigned_user",
|
||||
* type="object",
|
||||
* nullable=true,
|
||||
* @OA\Property(property="id", type="integer"),
|
||||
* @OA\Property(property="name", type="string")
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="BadDebtPagination",
|
||||
* allOf={
|
||||
* @OA\Schema(ref="#/components/schemas/PaginationMeta"),
|
||||
* @OA\Schema(
|
||||
*
|
||||
* @OA\Property(
|
||||
* property="data",
|
||||
* type="array",
|
||||
*
|
||||
* @OA\Items(ref="#/components/schemas/BadDebt")
|
||||
* )
|
||||
* )
|
||||
* }
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="BadDebtSummary",
|
||||
*
|
||||
* @OA\Property(property="total_amount", type="number", example=50000000),
|
||||
* @OA\Property(property="collecting_amount", type="number", example=30000000),
|
||||
* @OA\Property(property="legal_action_amount", type="number", example=10000000),
|
||||
* @OA\Property(property="recovered_amount", type="number", example=5000000),
|
||||
* @OA\Property(property="bad_debt_amount", type="number", example=5000000)
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="BadDebtCreateRequest",
|
||||
* required={"client_id", "debt_amount"},
|
||||
*
|
||||
* @OA\Property(property="client_id", type="integer", example=1),
|
||||
* @OA\Property(property="debt_amount", type="number", example=10000000),
|
||||
* @OA\Property(property="status", type="string", enum={"collecting", "legal_action", "recovered", "bad_debt"}, example="collecting"),
|
||||
* @OA\Property(property="overdue_days", type="integer", example=100),
|
||||
* @OA\Property(property="assigned_user_id", type="integer", nullable=true, example=1),
|
||||
* @OA\Property(property="occurred_at", type="string", format="date", nullable=true, example="2025-12-12"),
|
||||
* @OA\Property(property="closed_at", type="string", format="date", nullable=true),
|
||||
* @OA\Property(property="is_active", type="boolean", example=true),
|
||||
* @OA\Property(property="options", type="object", nullable=true)
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="BadDebtUpdateRequest",
|
||||
*
|
||||
* @OA\Property(property="client_id", type="integer", example=1),
|
||||
* @OA\Property(property="debt_amount", type="number", example=10000000),
|
||||
* @OA\Property(property="status", type="string", enum={"collecting", "legal_action", "recovered", "bad_debt"}, example="legal_action"),
|
||||
* @OA\Property(property="overdue_days", type="integer", example=120),
|
||||
* @OA\Property(property="assigned_user_id", type="integer", nullable=true, example=2),
|
||||
* @OA\Property(property="occurred_at", type="string", format="date", nullable=true),
|
||||
* @OA\Property(property="closed_at", type="string", format="date", nullable=true),
|
||||
* @OA\Property(property="is_active", type="boolean", example=true),
|
||||
* @OA\Property(property="options", type="object", nullable=true)
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="BadDebtDocument",
|
||||
*
|
||||
* @OA\Property(property="id", type="integer", example=1),
|
||||
* @OA\Property(property="bad_debt_id", type="integer", example=1),
|
||||
* @OA\Property(property="document_type", type="string", enum={"business_license", "tax_invoice", "additional"}, example="business_license"),
|
||||
* @OA\Property(property="file_id", type="integer", example=1),
|
||||
* @OA\Property(property="created_at", type="string", format="date-time"),
|
||||
* @OA\Property(
|
||||
* property="file",
|
||||
* type="object",
|
||||
* @OA\Property(property="id", type="integer"),
|
||||
* @OA\Property(property="name", type="string"),
|
||||
* @OA\Property(property="url", type="string")
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="BadDebtDocumentCreateRequest",
|
||||
* required={"document_type", "file_id"},
|
||||
*
|
||||
* @OA\Property(property="document_type", type="string", enum={"business_license", "tax_invoice", "additional"}, example="business_license"),
|
||||
* @OA\Property(property="file_id", type="integer", example=1)
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="BadDebtMemo",
|
||||
*
|
||||
* @OA\Property(property="id", type="integer", example=1),
|
||||
* @OA\Property(property="bad_debt_id", type="integer", example=1),
|
||||
* @OA\Property(property="content", type="string", example="2025-12-12 홍길동 과장님 메모 내용"),
|
||||
* @OA\Property(property="created_at", type="string", format="date-time"),
|
||||
* @OA\Property(
|
||||
* property="creator",
|
||||
* type="object",
|
||||
* @OA\Property(property="id", type="integer"),
|
||||
* @OA\Property(property="name", type="string")
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* @OA\Schema(
|
||||
* schema="BadDebtMemoCreateRequest",
|
||||
* required={"content"},
|
||||
*
|
||||
* @OA\Property(property="content", type="string", example="메모 내용", maxLength=5000)
|
||||
* )
|
||||
*/
|
||||
class BadDebtApi
|
||||
{
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/bad-debts",
|
||||
* tags={"BadDebt"},
|
||||
* summary="악성채권 목록 조회",
|
||||
* security={{"ApiKeyAuth": {}, "BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(name="client_id", in="query", @OA\Schema(type="integer"), description="거래처 ID"),
|
||||
* @OA\Parameter(name="status", in="query", @OA\Schema(type="string", enum={"collecting", "legal_action", "recovered", "bad_debt"}), description="상태"),
|
||||
* @OA\Parameter(name="is_active", in="query", @OA\Schema(type="boolean"), description="활성화 여부"),
|
||||
* @OA\Parameter(name="search", in="query", @OA\Schema(type="string"), description="검색어 (거래처명, 거래처코드)"),
|
||||
* @OA\Parameter(name="sort_by", in="query", @OA\Schema(type="string", default="created_at"), description="정렬 기준"),
|
||||
* @OA\Parameter(name="sort_dir", in="query", @OA\Schema(type="string", enum={"asc", "desc"}, default="desc"), description="정렬 방향"),
|
||||
* @OA\Parameter(name="per_page", in="query", @OA\Schema(type="integer", default=20), description="페이지당 항목 수"),
|
||||
* @OA\Parameter(name="page", in="query", @OA\Schema(type="integer", default=1), description="페이지 번호"),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="성공",
|
||||
*
|
||||
* @OA\JsonContent(ref="#/components/schemas/BadDebtPagination")
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function index() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/bad-debts/summary",
|
||||
* tags={"BadDebt"},
|
||||
* summary="악성채권 요약 통계",
|
||||
* security={{"ApiKeyAuth": {}, "BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(name="client_id", in="query", @OA\Schema(type="integer"), description="거래처 ID (선택)"),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
*
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string"),
|
||||
* @OA\Property(property="data", ref="#/components/schemas/BadDebtSummary")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function summary() {}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/bad-debts",
|
||||
* tags={"BadDebt"},
|
||||
* summary="악성채권 등록",
|
||||
* security={{"ApiKeyAuth": {}, "BearerAuth": {}}},
|
||||
*
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
*
|
||||
* @OA\JsonContent(ref="#/components/schemas/BadDebtCreateRequest")
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=201,
|
||||
* description="생성 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
*
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string"),
|
||||
* @OA\Property(property="data", ref="#/components/schemas/BadDebt")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function store() {}
|
||||
|
||||
/**
|
||||
* @OA\Get(
|
||||
* path="/api/v1/bad-debts/{id}",
|
||||
* tags={"BadDebt"},
|
||||
* summary="악성채권 상세 조회",
|
||||
* security={{"ApiKeyAuth": {}, "BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, @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"),
|
||||
* @OA\Property(property="data", ref="#/components/schemas/BadDebt")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function show() {}
|
||||
|
||||
/**
|
||||
* @OA\Put(
|
||||
* path="/api/v1/bad-debts/{id}",
|
||||
* tags={"BadDebt"},
|
||||
* summary="악성채권 수정",
|
||||
* security={{"ApiKeyAuth": {}, "BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
*
|
||||
* @OA\JsonContent(ref="#/components/schemas/BadDebtUpdateRequest")
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="수정 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
*
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string"),
|
||||
* @OA\Property(property="data", ref="#/components/schemas/BadDebt")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function update() {}
|
||||
|
||||
/**
|
||||
* @OA\Delete(
|
||||
* path="/api/v1/bad-debts/{id}",
|
||||
* tags={"BadDebt"},
|
||||
* summary="악성채권 삭제",
|
||||
* security={{"ApiKeyAuth": {}, "BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, @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"),
|
||||
* @OA\Property(property="data", type="null")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function destroy() {}
|
||||
|
||||
/**
|
||||
* @OA\Patch(
|
||||
* path="/api/v1/bad-debts/{id}/toggle",
|
||||
* tags={"BadDebt"},
|
||||
* summary="악성채권 설정 ON/OFF 토글",
|
||||
* security={{"ApiKeyAuth": {}, "BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, @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"),
|
||||
* @OA\Property(property="data", ref="#/components/schemas/BadDebt")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function toggle() {}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/bad-debts/{id}/documents",
|
||||
* tags={"BadDebt"},
|
||||
* summary="서류 첨부",
|
||||
* security={{"ApiKeyAuth": {}, "BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
*
|
||||
* @OA\JsonContent(ref="#/components/schemas/BadDebtDocumentCreateRequest")
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=201,
|
||||
* description="첨부 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
*
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string"),
|
||||
* @OA\Property(property="data", ref="#/components/schemas/BadDebtDocument")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function addDocument() {}
|
||||
|
||||
/**
|
||||
* @OA\Delete(
|
||||
* path="/api/v1/bad-debts/{id}/documents/{documentId}",
|
||||
* tags={"BadDebt"},
|
||||
* summary="서류 삭제",
|
||||
* security={{"ApiKeyAuth": {}, "BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||
* @OA\Parameter(name="documentId", in="path", required=true, @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"),
|
||||
* @OA\Property(property="data", type="null")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function removeDocument() {}
|
||||
|
||||
/**
|
||||
* @OA\Post(
|
||||
* path="/api/v1/bad-debts/{id}/memos",
|
||||
* tags={"BadDebt"},
|
||||
* summary="메모 추가",
|
||||
* security={{"ApiKeyAuth": {}, "BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||
*
|
||||
* @OA\RequestBody(
|
||||
* required=true,
|
||||
*
|
||||
* @OA\JsonContent(ref="#/components/schemas/BadDebtMemoCreateRequest")
|
||||
* ),
|
||||
*
|
||||
* @OA\Response(
|
||||
* response=201,
|
||||
* description="추가 성공",
|
||||
*
|
||||
* @OA\JsonContent(
|
||||
*
|
||||
* @OA\Property(property="success", type="boolean", example=true),
|
||||
* @OA\Property(property="message", type="string"),
|
||||
* @OA\Property(property="data", ref="#/components/schemas/BadDebtMemo")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function addMemo() {}
|
||||
|
||||
/**
|
||||
* @OA\Delete(
|
||||
* path="/api/v1/bad-debts/{id}/memos/{memoId}",
|
||||
* tags={"BadDebt"},
|
||||
* summary="메모 삭제",
|
||||
* security={{"ApiKeyAuth": {}, "BearerAuth": {}}},
|
||||
*
|
||||
* @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")),
|
||||
* @OA\Parameter(name="memoId", in="path", required=true, @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"),
|
||||
* @OA\Property(property="data", type="null")
|
||||
* )
|
||||
* )
|
||||
* )
|
||||
*/
|
||||
public function removeMemo() {}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?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::create('bad_debts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
|
||||
$table->unsignedBigInteger('client_id')->comment('거래처 ID');
|
||||
$table->decimal('debt_amount', 15, 2)->default(0)->comment('채권 금액');
|
||||
$table->string('status', 20)->default('collecting')->comment('상태: collecting(추심중), legal_action(법적조치), recovered(회수완료), bad_debt(대손처리)');
|
||||
$table->integer('overdue_days')->default(0)->comment('연체일수');
|
||||
$table->unsignedBigInteger('assigned_user_id')->nullable()->comment('담당자 ID');
|
||||
$table->date('occurred_at')->nullable()->comment('악성채권 발생일');
|
||||
$table->date('closed_at')->nullable()->comment('악성채권 종료일');
|
||||
$table->boolean('is_active')->default(true)->comment('설정 활성화 여부');
|
||||
$table->json('options')->nullable()->comment('추가 옵션 JSON');
|
||||
$table->unsignedBigInteger('created_by')->nullable()->comment('생성자 ID');
|
||||
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자 ID');
|
||||
$table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자 ID');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index('tenant_id');
|
||||
$table->index('client_id');
|
||||
$table->index('status');
|
||||
$table->index('is_active');
|
||||
$table->index(['tenant_id', 'status']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('bad_debts');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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::create('bad_debt_documents', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('bad_debt_id')->comment('악성채권 ID');
|
||||
$table->string('document_type', 50)->comment('서류 유형: business_license(사업자등록증), tax_invoice(세금계산서), additional(추가서류)');
|
||||
$table->unsignedBigInteger('file_id')->comment('파일 ID');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('bad_debt_id');
|
||||
$table->index('document_type');
|
||||
|
||||
$table->foreign('bad_debt_id')
|
||||
->references('id')
|
||||
->on('bad_debts')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('bad_debt_documents');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?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::create('bad_debt_memos', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('bad_debt_id')->comment('악성채권 ID');
|
||||
$table->text('content')->comment('메모 내용');
|
||||
$table->unsignedBigInteger('created_by')->nullable()->comment('작성자 ID');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('bad_debt_id');
|
||||
|
||||
$table->foreign('bad_debt_id')
|
||||
->references('id')
|
||||
->on('bad_debts')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('bad_debt_memos');
|
||||
}
|
||||
};
|
||||
@@ -9,6 +9,7 @@
|
||||
use App\Http\Controllers\Api\V1\ApprovalFormController;
|
||||
use App\Http\Controllers\Api\V1\ApprovalLineController;
|
||||
use App\Http\Controllers\Api\V1\AttendanceController;
|
||||
use App\Http\Controllers\Api\V1\BadDebtController;
|
||||
use App\Http\Controllers\Api\V1\BankAccountController;
|
||||
use App\Http\Controllers\Api\V1\BarobillSettingController;
|
||||
use App\Http\Controllers\Api\V1\BoardController;
|
||||
@@ -515,6 +516,23 @@
|
||||
Route::get('/{id}/check-status', [TaxInvoiceController::class, 'checkStatus'])->whereNumber('id')->name('v1.tax-invoices.check-status');
|
||||
});
|
||||
|
||||
// Bad Debt API (악성채권 추심관리)
|
||||
Route::prefix('bad-debts')->group(function () {
|
||||
Route::get('', [BadDebtController::class, 'index'])->name('v1.bad-debts.index');
|
||||
Route::post('', [BadDebtController::class, 'store'])->name('v1.bad-debts.store');
|
||||
Route::get('/summary', [BadDebtController::class, 'summary'])->name('v1.bad-debts.summary');
|
||||
Route::get('/{id}', [BadDebtController::class, 'show'])->whereNumber('id')->name('v1.bad-debts.show');
|
||||
Route::put('/{id}', [BadDebtController::class, 'update'])->whereNumber('id')->name('v1.bad-debts.update');
|
||||
Route::delete('/{id}', [BadDebtController::class, 'destroy'])->whereNumber('id')->name('v1.bad-debts.destroy');
|
||||
Route::patch('/{id}/toggle', [BadDebtController::class, 'toggle'])->whereNumber('id')->name('v1.bad-debts.toggle');
|
||||
// 서류
|
||||
Route::post('/{id}/documents', [BadDebtController::class, 'addDocument'])->whereNumber('id')->name('v1.bad-debts.documents.store');
|
||||
Route::delete('/{id}/documents/{documentId}', [BadDebtController::class, 'removeDocument'])->whereNumber(['id', 'documentId'])->name('v1.bad-debts.documents.destroy');
|
||||
// 메모
|
||||
Route::post('/{id}/memos', [BadDebtController::class, 'addMemo'])->whereNumber('id')->name('v1.bad-debts.memos.store');
|
||||
Route::delete('/{id}/memos/{memoId}', [BadDebtController::class, 'removeMemo'])->whereNumber(['id', 'memoId'])->name('v1.bad-debts.memos.destroy');
|
||||
});
|
||||
|
||||
// Report API (보고서)
|
||||
Route::prefix('reports')->group(function () {
|
||||
Route::get('/daily', [ReportController::class, 'daily'])->name('v1.reports.daily');
|
||||
|
||||
Reference in New Issue
Block a user