diff --git a/.env.example b/.env.example index 8668634..67b9104 100644 --- a/.env.example +++ b/.env.example @@ -81,3 +81,5 @@ PAYMONGO_SUCCESS_URL="${APP_URL}/checkout/success" PAYMONGO_FAILED_URL="${APP_URL}/checkout/failed" GEMINI_API_KEY= + +RFID_API_SECRET= diff --git a/app/Http/Controllers/Api/RfidController.php b/app/Http/Controllers/Api/RfidController.php new file mode 100644 index 0000000..aa02461 --- /dev/null +++ b/app/Http/Controllers/Api/RfidController.php @@ -0,0 +1,46 @@ +itemService->adjustQuantityByCode( + $request->validated()['item_code'], + $request->validated()['action'], + $request->validated()['quantity'] + ); + + if (! $result['success']) { + return response()->json([ + 'status' => 'error', + 'message' => $result['message'] + ], 422); + } + + return response()->json([ + 'status' => 'success', + 'message' => $result['message'], + 'data' => $result['item'], + ], 200); + } +} diff --git a/app/Http/Middleware/ApiSecretMiddleware.php b/app/Http/Middleware/ApiSecretMiddleware.php new file mode 100644 index 0000000..22a9381 --- /dev/null +++ b/app/Http/Middleware/ApiSecretMiddleware.php @@ -0,0 +1,29 @@ +header('X-API-Secret') !== $secret) { + return response()->json([ + 'status' => 'error', + 'message' => 'Unauthorized' + ], 401); + } + + return $next($request); + } +} diff --git a/app/Http/Requests/Rfid/RfidScanRequest.php b/app/Http/Requests/Rfid/RfidScanRequest.php new file mode 100644 index 0000000..32dc1a2 --- /dev/null +++ b/app/Http/Requests/Rfid/RfidScanRequest.php @@ -0,0 +1,30 @@ +|string> + */ + public function rules(): array + { + return [ + 'item_code' => 'required|string', + 'action' => 'required|string|in:add,deduct', + 'quantity' => 'required|integer|min:1' + ]; + } +} diff --git a/app/Repositories/Interfaces/ItemRepositoryInterface.php b/app/Repositories/Interfaces/ItemRepositoryInterface.php index b89ff27..27c25e6 100644 --- a/app/Repositories/Interfaces/ItemRepositoryInterface.php +++ b/app/Repositories/Interfaces/ItemRepositoryInterface.php @@ -9,4 +9,5 @@ interface ItemRepositoryInterface extends BaseRepositoryInterface { public function latestByCategory(string $category): ?Item; public function getLowStockItems(): Collection; + public function findByCode(string $itemCode): ?Item; } diff --git a/app/Repositories/ItemRepository.php b/app/Repositories/ItemRepository.php index dcc9827..67863e3 100644 --- a/app/Repositories/ItemRepository.php +++ b/app/Repositories/ItemRepository.php @@ -42,4 +42,17 @@ public function getLowStockItems(): Collection ->orderBy('quantity', 'asc') ->get(); } + + /** + * Find an item by its item_code + * + * @param string $itemCode + * @return Item|null + */ + public function findByCode(string $itemCode): ?Item + { + return $this->query() + ->where('item_code', $itemCode) + ->first(); + } } diff --git a/app/Services/ItemService.php b/app/Services/ItemService.php index 2fa8305..c7b0bd8 100644 --- a/app/Services/ItemService.php +++ b/app/Services/ItemService.php @@ -220,4 +220,44 @@ protected function deleteImages(Item $item) } } } + + /** + * Adjust item quantity via RFID scan. + * Use action 'add' to increase and 'deduct' to decrease. + * + * @param string $itemCode + * @param string $action 'add' | 'deduct' + * @param int $quantity + * @return array{success: bool, message: string, item?: Item} + */ + public function adjustQuantityByCode(string $itemCode, string $action, int $quantity): array + { + $item = $this->itemRepo->findByCode($itemCode); + + if (! $item) { + return ['success' => false, 'message' => 'Item not found']; + } + + if ($action === 'deduct') { + if ($item->quantity < $quantity) { + return ['success' => false, 'message' => 'Insufficient stock']; + } + $newQuantity = $item->quantity - $quantity; + } elseif ($action === 'add') { + $newQuantity = $item->quantity + $quantity; + } else { + return ['success' => false, 'message' => 'Invalid action. Use "add" or "deduct"']; + } + + $this->itemRepo->update($item->id, ['quantity' => $newQuantity]); + + // Re-fetch for fresh data + $item->refresh(); + + return [ + 'success' => true, + 'message' => 'Quantity updated successfully', + 'item' => $item + ]; + } } diff --git a/bootstrap/app.php b/bootstrap/app.php index 4378463..133db2f 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,5 +1,6 @@ withRouting( web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', channels: __DIR__.'/../routes/channels.php', health: '/up', @@ -26,6 +28,7 @@ $middleware->alias([ 'role' => RoleMiddleware::class, + 'api.secret' => ApiSecretMiddleware::class ]); }) ->withExceptions(function (Exceptions $exceptions): void { diff --git a/config/app.php b/config/app.php index 423eed5..f5d84af 100644 --- a/config/app.php +++ b/config/app.php @@ -123,4 +123,14 @@ 'store' => env('APP_MAINTENANCE_STORE', 'database'), ], + /* + |-------------------------------------------------------------------------- + | RFID API Secret + |-------------------------------------------------------------------------- + | + | This secret key is used to authenticate requests from the Python RFID + | scanner. It must match the X-API-Secret header sent by the scanner. + | + */ + 'rfid_api_secret' => env('RFID_API_SECRET'), ]; diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..dcdc1d2 --- /dev/null +++ b/routes/api.php @@ -0,0 +1,12 @@ +json([ + 'status' => 'ok', + 'message' => 'API is running', + ]); +}); + +require __DIR__ . '/groups/rfid.php'; diff --git a/routes/groups/rfid.php b/routes/groups/rfid.php new file mode 100644 index 0000000..3bead31 --- /dev/null +++ b/routes/groups/rfid.php @@ -0,0 +1,14 @@ +prefix('rfid') + ->name('rfid.') + ->group(function () { + + // Scan item by item_code + Route::post('/scan', [RfidController::class, 'scan']) + ->name('scan'); + });