Skip to content

Commit 2ea54bb

Browse files
committed
feat(inventory): Phase 150 — Inventory RMA Requests
Adds RmaRequest and RmaRequestItem models with full lifecycle transitions: approve → receive → inspect → close. RMA numbers auto-generated as RMA-YYYY-NNNNN. Supports customer and supplier return types with configurable disposition (restock/scrap/repair/replace/credit). https://claude.ai/code/session_01RdUGwo74JXChRCF88Yu27d
1 parent 9b5812f commit 2ea54bb

13 files changed

Lines changed: 821 additions & 0 deletions

File tree

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
3+
namespace App\Modules\Inventory\Http\Controllers;
4+
5+
use App\Modules\Inventory\Models\RmaRequest;
6+
use Illuminate\Http\RedirectResponse;
7+
use Illuminate\Http\Request;
8+
use Inertia\Inertia;
9+
use Inertia\Response;
10+
11+
class RmaRequestController
12+
{
13+
public function index(): Response
14+
{
15+
$rmaRequests = RmaRequest::with('warehouse')
16+
->orderByDesc('created_at')
17+
->paginate(20);
18+
19+
return Inertia::render('Inventory/RmaRequests/Index', compact('rmaRequests'));
20+
}
21+
22+
public function create(): Response
23+
{
24+
return Inertia::render('Inventory/RmaRequests/Create');
25+
}
26+
27+
public function store(Request $request): RedirectResponse
28+
{
29+
$data = $request->validate([
30+
'type' => 'required|string|in:customer_return,supplier_return',
31+
'reason' => 'required|string',
32+
'contact_name' => 'nullable|string|max:255',
33+
'reference' => 'nullable|string|max:255',
34+
'disposition' => 'nullable|string|in:restock,scrap,repair,replace,credit',
35+
'requested_date' => 'nullable|date',
36+
'notes' => 'nullable|string',
37+
'warehouse_id' => 'nullable|exists:warehouses,id',
38+
]);
39+
40+
$data['tenant_id'] = app('tenant')->id;
41+
$data['created_by'] = auth()->id();
42+
43+
RmaRequest::create($data);
44+
45+
return redirect()->route('inventory.rma-requests.index');
46+
}
47+
48+
public function show(RmaRequest $rmaRequest): Response
49+
{
50+
$rmaRequest->load('items.product', 'warehouse', 'creator', 'approver');
51+
return Inertia::render('Inventory/RmaRequests/Show', compact('rmaRequest'));
52+
}
53+
54+
public function edit(RmaRequest $rmaRequest): Response
55+
{
56+
return Inertia::render('Inventory/RmaRequests/Edit', compact('rmaRequest'));
57+
}
58+
59+
public function update(Request $request, RmaRequest $rmaRequest): RedirectResponse
60+
{
61+
$data = $request->validate([
62+
'reason' => 'required|string',
63+
'contact_name' => 'nullable|string|max:255',
64+
'reference' => 'nullable|string|max:255',
65+
'disposition' => 'nullable|string|in:restock,scrap,repair,replace,credit',
66+
'requested_date' => 'nullable|date',
67+
'notes' => 'nullable|string',
68+
'warehouse_id' => 'nullable|exists:warehouses,id',
69+
]);
70+
71+
$rmaRequest->update($data);
72+
73+
return redirect()->route('inventory.rma-requests.index');
74+
}
75+
76+
public function destroy(RmaRequest $rmaRequest): RedirectResponse
77+
{
78+
$rmaRequest->delete();
79+
return redirect()->route('inventory.rma-requests.index');
80+
}
81+
82+
public function approve(RmaRequest $rmaRequest): RedirectResponse
83+
{
84+
$rmaRequest->approve(auth()->id());
85+
return redirect()->route('inventory.rma-requests.index');
86+
}
87+
88+
public function receive(RmaRequest $rmaRequest): RedirectResponse
89+
{
90+
$rmaRequest->receive();
91+
return redirect()->route('inventory.rma-requests.index');
92+
}
93+
94+
public function inspect(RmaRequest $rmaRequest): RedirectResponse
95+
{
96+
$rmaRequest->inspect();
97+
return redirect()->route('inventory.rma-requests.index');
98+
}
99+
100+
public function close(RmaRequest $rmaRequest): RedirectResponse
101+
{
102+
$rmaRequest->close();
103+
return redirect()->route('inventory.rma-requests.index');
104+
}
105+
106+
public function reject(RmaRequest $rmaRequest): RedirectResponse
107+
{
108+
$rmaRequest->reject();
109+
return redirect()->route('inventory.rma-requests.index');
110+
}
111+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<?php
2+
3+
namespace App\Modules\Inventory\Models;
4+
5+
use App\Models\User;
6+
use App\Modules\Core\Traits\BelongsToTenant;
7+
use Illuminate\Database\Eloquent\Casts\Attribute;
8+
use Illuminate\Database\Eloquent\Model;
9+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
10+
use Illuminate\Database\Eloquent\Relations\HasMany;
11+
use Illuminate\Database\Eloquent\SoftDeletes;
12+
13+
class RmaRequest extends Model
14+
{
15+
use BelongsToTenant, SoftDeletes;
16+
17+
protected $table = 'rma_requests';
18+
19+
protected $fillable = [
20+
'tenant_id',
21+
'rma_number',
22+
'type',
23+
'status',
24+
'contact_name',
25+
'reference',
26+
'reason',
27+
'disposition',
28+
'requested_date',
29+
'received_date',
30+
'inspected_date',
31+
'notes',
32+
'warehouse_id',
33+
'created_by',
34+
'approved_by',
35+
];
36+
37+
protected $casts = [
38+
'requested_date' => 'date',
39+
'received_date' => 'date',
40+
'inspected_date' => 'date',
41+
];
42+
43+
protected $attributes = [
44+
'type' => 'customer_return',
45+
'status' => 'pending',
46+
'disposition' => 'restock',
47+
];
48+
49+
public function items(): HasMany
50+
{
51+
return $this->hasMany(RmaRequestItem::class);
52+
}
53+
54+
public function warehouse(): BelongsTo
55+
{
56+
return $this->belongsTo(Warehouse::class);
57+
}
58+
59+
public function creator(): BelongsTo
60+
{
61+
return $this->belongsTo(User::class, 'created_by');
62+
}
63+
64+
public function approver(): BelongsTo
65+
{
66+
return $this->belongsTo(User::class, 'approved_by');
67+
}
68+
69+
public function approve(int $userId): void
70+
{
71+
if ($this->rma_number === null) {
72+
$this->rma_number = $this->generateRmaNumber();
73+
}
74+
$this->status = 'approved';
75+
$this->approved_by = $userId;
76+
$this->save();
77+
}
78+
79+
public function receive(): void
80+
{
81+
$this->status = 'received';
82+
$this->received_date = now()->toDateString();
83+
$this->save();
84+
}
85+
86+
public function inspect(): void
87+
{
88+
$this->status = 'inspected';
89+
$this->inspected_date = now()->toDateString();
90+
$this->save();
91+
}
92+
93+
public function close(): void
94+
{
95+
$this->status = 'closed';
96+
$this->save();
97+
}
98+
99+
public function reject(): void
100+
{
101+
$this->status = 'rejected';
102+
$this->save();
103+
}
104+
105+
public function generateRmaNumber(): string
106+
{
107+
return 'RMA-' . date('Y') . '-' . str_pad((string) $this->id, 5, '0', STR_PAD_LEFT);
108+
}
109+
110+
protected function isPending(): Attribute
111+
{
112+
return Attribute::make(get: fn () => $this->status === 'pending');
113+
}
114+
115+
protected function isApproved(): Attribute
116+
{
117+
return Attribute::make(get: fn () => $this->status === 'approved');
118+
}
119+
120+
protected function isReceived(): Attribute
121+
{
122+
return Attribute::make(get: fn () => $this->status === 'received');
123+
}
124+
125+
protected function totalItems(): Attribute
126+
{
127+
return Attribute::make(get: fn () => $this->items()->count());
128+
}
129+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
namespace App\Modules\Inventory\Models;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
7+
8+
class RmaRequestItem extends Model
9+
{
10+
protected $table = 'rma_request_items';
11+
12+
protected $fillable = [
13+
'rma_request_id',
14+
'product_id',
15+
'description',
16+
'quantity_requested',
17+
'quantity_received',
18+
'condition',
19+
'disposition',
20+
'notes',
21+
];
22+
23+
protected $casts = [
24+
'quantity_requested' => 'float',
25+
'quantity_received' => 'float',
26+
];
27+
28+
protected $attributes = [
29+
'condition' => 'good',
30+
'quantity_received' => 0,
31+
];
32+
33+
public function rmaRequest(): BelongsTo
34+
{
35+
return $this->belongsTo(RmaRequest::class);
36+
}
37+
38+
public function product(): BelongsTo
39+
{
40+
return $this->belongsTo(Product::class);
41+
}
42+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
namespace App\Modules\Inventory\Policies;
4+
5+
use App\Models\User;
6+
use App\Modules\Inventory\Models\RmaRequest;
7+
8+
class RmaRequestPolicy
9+
{
10+
public function viewAny(User $user): bool
11+
{
12+
return $user->can('inventory.view');
13+
}
14+
15+
public function view(User $user, RmaRequest $rmaRequest): bool
16+
{
17+
return $user->can('inventory.view');
18+
}
19+
20+
public function create(User $user): bool
21+
{
22+
return $user->can('inventory.create');
23+
}
24+
25+
public function update(User $user, RmaRequest $rmaRequest): bool
26+
{
27+
return $user->can('inventory.create');
28+
}
29+
30+
public function approve(User $user, RmaRequest $rmaRequest): bool
31+
{
32+
return $user->can('inventory.create');
33+
}
34+
35+
public function receive(User $user, RmaRequest $rmaRequest): bool
36+
{
37+
return $user->can('inventory.create');
38+
}
39+
40+
public function inspect(User $user, RmaRequest $rmaRequest): bool
41+
{
42+
return $user->can('inventory.create');
43+
}
44+
45+
public function close(User $user, RmaRequest $rmaRequest): bool
46+
{
47+
return $user->can('inventory.create');
48+
}
49+
50+
public function reject(User $user, RmaRequest $rmaRequest): bool
51+
{
52+
return $user->can('inventory.delete');
53+
}
54+
55+
public function delete(User $user, RmaRequest $rmaRequest): bool
56+
{
57+
return $user->can('inventory.delete');
58+
}
59+
}

erp/app/Modules/Inventory/Providers/InventoryServiceProvider.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@
8686
use App\Modules\Inventory\Models\Shipment;
8787
use App\Modules\Inventory\Models\ShipmentItem;
8888
use App\Modules\Inventory\Policies\ShipmentPolicy;
89+
use App\Modules\Inventory\Models\RmaRequest;
90+
use App\Modules\Inventory\Models\RmaRequestItem;
91+
use App\Modules\Inventory\Policies\RmaRequestPolicy;
8992
use Illuminate\Support\Facades\Gate;
9093
use Illuminate\Support\ServiceProvider;
9194

@@ -150,5 +153,7 @@ public function boot(): void
150153
Gate::policy(GoodsReceiptItem::class, GoodsReceiptPolicy::class);
151154
Gate::policy(Shipment::class, ShipmentPolicy::class);
152155
Gate::policy(ShipmentItem::class, ShipmentPolicy::class);
156+
Gate::policy(RmaRequest::class, RmaRequestPolicy::class);
157+
Gate::policy(RmaRequestItem::class, RmaRequestPolicy::class);
153158
}
154159
}

erp/app/Modules/Inventory/routes/inventory.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,3 +319,14 @@
319319
Route::post('shipments/{shipment}/cancel', [ShipmentController::class, 'cancel'])->name('shipments.cancel');
320320
Route::resource('shipments', ShipmentController::class);
321321
});
322+
323+
// RMA Requests
324+
use App\Modules\Inventory\Http\Controllers\RmaRequestController;
325+
Route::middleware(['web', 'auth', 'verified'])->prefix('inventory')->name('inventory.')->group(function () {
326+
Route::post('rma-requests/{rma_request}/approve', [RmaRequestController::class, 'approve'])->name('rma-requests.approve');
327+
Route::post('rma-requests/{rma_request}/receive', [RmaRequestController::class, 'receive'])->name('rma-requests.receive');
328+
Route::post('rma-requests/{rma_request}/inspect', [RmaRequestController::class, 'inspect'])->name('rma-requests.inspect');
329+
Route::post('rma-requests/{rma_request}/close', [RmaRequestController::class, 'close'])->name('rma-requests.close');
330+
Route::post('rma-requests/{rma_request}/reject', [RmaRequestController::class, 'reject'])->name('rma-requests.reject');
331+
Route::resource('rma-requests', RmaRequestController::class);
332+
});

0 commit comments

Comments
 (0)