Skip to content

Commit 088998d

Browse files
authored
Merge pull request #4 from RLASH18/feat/rfid-scan-api
feat(rfid): add RFID/NFC scan API endpoint
2 parents ae86380 + 93f9f9a commit 088998d

11 files changed

Lines changed: 200 additions & 0 deletions

File tree

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,5 @@ PAYMONGO_SUCCESS_URL="${APP_URL}/checkout/success"
8181
PAYMONGO_FAILED_URL="${APP_URL}/checkout/failed"
8282

8383
GEMINI_API_KEY=
84+
85+
RFID_API_SECRET=
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Api;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Http\Requests\Rfid\RfidScanRequest;
7+
use App\Services\ItemService;
8+
use Illuminate\Http\JsonResponse;
9+
use Illuminate\Http\Request;
10+
11+
class RfidController extends Controller
12+
{
13+
/**
14+
* Inject Item Service
15+
*
16+
* @param ItemService $itemService
17+
*/
18+
public function __construct(
19+
protected ItemService $itemService
20+
) {}
21+
22+
/**
23+
* Handle RFID scan and adjust item quantity.
24+
*/
25+
public function scan(RfidScanRequest $request): JsonResponse
26+
{
27+
$result = $this->itemService->adjustQuantityByCode(
28+
$request->validated()['item_code'],
29+
$request->validated()['action'],
30+
$request->validated()['quantity']
31+
);
32+
33+
if (! $result['success']) {
34+
return response()->json([
35+
'status' => 'error',
36+
'message' => $result['message']
37+
], 422);
38+
}
39+
40+
return response()->json([
41+
'status' => 'success',
42+
'message' => $result['message'],
43+
'data' => $result['item'],
44+
], 200);
45+
}
46+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace App\Http\Middleware;
4+
5+
use Closure;
6+
use Illuminate\Http\Request;
7+
use Symfony\Component\HttpFoundation\Response;
8+
9+
class ApiSecretMiddleware
10+
{
11+
/**
12+
* Handle an incoming request.
13+
*
14+
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
15+
*/
16+
public function handle(Request $request, Closure $next): Response
17+
{
18+
$secret = config('app.rfid_api_secret');
19+
20+
if (! $secret || $request->header('X-API-Secret') !== $secret) {
21+
return response()->json([
22+
'status' => 'error',
23+
'message' => 'Unauthorized'
24+
], 401);
25+
}
26+
27+
return $next($request);
28+
}
29+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
namespace App\Http\Requests\Rfid;
4+
5+
use Illuminate\Foundation\Http\FormRequest;
6+
7+
class RfidScanRequest extends FormRequest
8+
{
9+
/**
10+
* Determine if the user is authorized to make this request.
11+
*/
12+
public function authorize(): bool
13+
{
14+
return true; // Middleware handles authorization
15+
}
16+
17+
/**
18+
* Get the validation rules that apply to the request.
19+
*
20+
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
21+
*/
22+
public function rules(): array
23+
{
24+
return [
25+
'item_code' => 'required|string',
26+
'action' => 'required|string|in:add,deduct',
27+
'quantity' => 'required|integer|min:1'
28+
];
29+
}
30+
}

app/Repositories/Interfaces/ItemRepositoryInterface.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ interface ItemRepositoryInterface extends BaseRepositoryInterface
99
{
1010
public function latestByCategory(string $category): ?Item;
1111
public function getLowStockItems(): Collection;
12+
public function findByCode(string $itemCode): ?Item;
1213
}

app/Repositories/ItemRepository.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,17 @@ public function getLowStockItems(): Collection
4242
->orderBy('quantity', 'asc')
4343
->get();
4444
}
45+
46+
/**
47+
* Find an item by its item_code
48+
*
49+
* @param string $itemCode
50+
* @return Item|null
51+
*/
52+
public function findByCode(string $itemCode): ?Item
53+
{
54+
return $this->query()
55+
->where('item_code', $itemCode)
56+
->first();
57+
}
4558
}

app/Services/ItemService.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,4 +220,44 @@ protected function deleteImages(Item $item)
220220
}
221221
}
222222
}
223+
224+
/**
225+
* Adjust item quantity via RFID scan.
226+
* Use action 'add' to increase and 'deduct' to decrease.
227+
*
228+
* @param string $itemCode
229+
* @param string $action 'add' | 'deduct'
230+
* @param int $quantity
231+
* @return array{success: bool, message: string, item?: Item}
232+
*/
233+
public function adjustQuantityByCode(string $itemCode, string $action, int $quantity): array
234+
{
235+
$item = $this->itemRepo->findByCode($itemCode);
236+
237+
if (! $item) {
238+
return ['success' => false, 'message' => 'Item not found'];
239+
}
240+
241+
if ($action === 'deduct') {
242+
if ($item->quantity < $quantity) {
243+
return ['success' => false, 'message' => 'Insufficient stock'];
244+
}
245+
$newQuantity = $item->quantity - $quantity;
246+
} elseif ($action === 'add') {
247+
$newQuantity = $item->quantity + $quantity;
248+
} else {
249+
return ['success' => false, 'message' => 'Invalid action. Use "add" or "deduct"'];
250+
}
251+
252+
$this->itemRepo->update($item->id, ['quantity' => $newQuantity]);
253+
254+
// Re-fetch for fresh data
255+
$item->refresh();
256+
257+
return [
258+
'success' => true,
259+
'message' => 'Quantity updated successfully',
260+
'item' => $item
261+
];
262+
}
223263
}

bootstrap/app.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?php
22

3+
use App\Http\Middleware\ApiSecretMiddleware;
34
use App\Http\Middleware\HandleAppearance;
45
use App\Http\Middleware\HandleInertiaRequests;
56
use App\Http\Middleware\RoleMiddleware;
@@ -11,6 +12,7 @@
1112
return Application::configure(basePath: dirname(__DIR__))
1213
->withRouting(
1314
web: __DIR__.'/../routes/web.php',
15+
api: __DIR__.'/../routes/api.php',
1416
commands: __DIR__.'/../routes/console.php',
1517
channels: __DIR__.'/../routes/channels.php',
1618
health: '/up',
@@ -26,6 +28,7 @@
2628

2729
$middleware->alias([
2830
'role' => RoleMiddleware::class,
31+
'api.secret' => ApiSecretMiddleware::class
2932
]);
3033
})
3134
->withExceptions(function (Exceptions $exceptions): void {

config/app.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,14 @@
123123
'store' => env('APP_MAINTENANCE_STORE', 'database'),
124124
],
125125

126+
/*
127+
|--------------------------------------------------------------------------
128+
| RFID API Secret
129+
|--------------------------------------------------------------------------
130+
|
131+
| This secret key is used to authenticate requests from the Python RFID
132+
| scanner. It must match the X-API-Secret header sent by the scanner.
133+
|
134+
*/
135+
'rfid_api_secret' => env('RFID_API_SECRET'),
126136
];

routes/api.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
use Illuminate\Support\Facades\Route;
4+
5+
Route::get('/health', function () {
6+
return response()->json([
7+
'status' => 'ok',
8+
'message' => 'API is running',
9+
]);
10+
});
11+
12+
require __DIR__ . '/groups/rfid.php';

0 commit comments

Comments
 (0)