mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-06-07 15:14:25 -04:00
Implement comprehensive REST API for OSPOS with the following: Database: - Migration for ospos_api_keys table - Seeder for module permissions Models: - ApiKey model with key generation, validation, revocation - SHA-256 hashing for secure key storage - Support for key expiration Filters: - ApiAuth filter for X-API-Key header authentication - CSRF exemption for API routes Controllers: - Api/BaseController with response helpers and field transformation - Api/Customers (CRUD + batch delete, suggestions) - Api/Suppliers (CRUD + batch delete, suggestions) - Api/Items (CRUD + batch delete, quantities endpoint) - Api/Inventory (adjustments with set/adjust modes, bulk support) - ApiKeys (UI controller for key management) Routes: - /api/v1/* endpoints with apiauth filter - /office/api-keys/* endpoints for key management UI Tests: - ApiKeyTest for model functionality - ApiAuthTest for authentication filter Features: - camelCase JSON field names (API standard) - Offset/limit pagination - Soft delete support - Permission-based authorization - Key prefix for UI identification - Last used timestamp tracking Refs: #2463, #615, #3789, #3809, #1680, #876, #1959, #157
212 lines
7.3 KiB
PHP
212 lines
7.3 KiB
PHP
<?php
|
|
|
|
namespace App\Controllers\Api;
|
|
|
|
use App\Models\Inventory as InventoryModel;
|
|
use App\Models\Item;
|
|
use App\Models\Item_quantity;
|
|
use CodeIgniter\HTTP\ResponseInterface;
|
|
|
|
class Inventory extends BaseController
|
|
{
|
|
protected InventoryModel $inventory;
|
|
protected Item $item;
|
|
protected Item_quantity $itemQuantity;
|
|
|
|
protected array $allowedSortFields = ['trans_id', 'trans_date', 'trans_items'];
|
|
|
|
public function initController(\CodeIgniter\HTTP\RequestInterface $request, \CodeIgniter\HTTP\ResponseInterface $response, \Psr\Log\LoggerInterface $logger): void
|
|
{
|
|
parent::initController($request, $response, $logger);
|
|
$this->inventory = model(InventoryModel::class);
|
|
$this->item = model(Item::class);
|
|
$this->itemQuantity = model(Item_quantity::class);
|
|
}
|
|
|
|
public function index(): ResponseInterface
|
|
{
|
|
if (!$this->hasPermission('items')) {
|
|
return $this->respondUnauthorized();
|
|
}
|
|
|
|
$itemId = $this->request->getGet('itemId');
|
|
$locationId = $this->request->getGet('locationId');
|
|
$pagination = $this->getPagination();
|
|
$sort = $this->getSort($this->allowedSortFields, 'trans_date');
|
|
|
|
$builder = $this->inventory->builder();
|
|
|
|
if ($itemId) {
|
|
$builder->where('trans_items', $itemId);
|
|
}
|
|
|
|
if ($locationId) {
|
|
$builder->where('trans_location', $locationId);
|
|
}
|
|
|
|
$total = $builder->countAllResults(false);
|
|
|
|
$builder->orderBy($sort['sort'], $sort['order']);
|
|
$builder->limit($pagination['limit'], $pagination['offset']);
|
|
|
|
$transactions = $builder->get()->getResultArray();
|
|
|
|
return $this->respondSuccess([
|
|
'total' => $total,
|
|
'offset' => $pagination['offset'],
|
|
'limit' => $pagination['limit'],
|
|
'rows' => $this->transformCollection($transactions)
|
|
]);
|
|
}
|
|
|
|
public function create(): ResponseInterface
|
|
{
|
|
if (!$this->hasPermission('items')) {
|
|
return $this->respondUnauthorized();
|
|
}
|
|
|
|
$data = $this->request->getJSON(true);
|
|
|
|
if (isset($data['adjustments']) && is_array($data['adjustments'])) {
|
|
return $this->bulkAdjust($data['adjustments']);
|
|
}
|
|
|
|
return $this->singleAdjust($data);
|
|
}
|
|
|
|
private function singleAdjust(array $data): ResponseInterface
|
|
{
|
|
if (empty($data['itemId'])) {
|
|
return $this->respondError('itemId is required');
|
|
}
|
|
|
|
if (!isset($data['quantity'])) {
|
|
return $this->respondError('quantity is required');
|
|
}
|
|
|
|
$mode = $data['mode'] ?? 'adjust';
|
|
|
|
if (!in_array($mode, ['adjust', 'set'])) {
|
|
return $this->respondError('mode must be "adjust" or "set"');
|
|
}
|
|
|
|
$item = $this->item->find($data['itemId']);
|
|
if (!$item || $item->deleted) {
|
|
return $this->respondNotFound('Item not found');
|
|
}
|
|
|
|
$locationId = $data['locationId'] ?? 1;
|
|
$comment = $data['comment'] ?? 'API inventory adjustment';
|
|
$quantity = (float) $data['quantity'];
|
|
|
|
if ($mode === 'set') {
|
|
$currentQty = $this->itemQuantity->get_item_quantity($data['itemId'], $locationId);
|
|
$currentQty = $currentQty ? (float) $currentQty->quantity : 0;
|
|
$adjustment = $quantity - $currentQty;
|
|
|
|
if ($adjustment == 0) {
|
|
return $this->respondSuccess([
|
|
'itemId' => (int) $data['itemId'],
|
|
'locationId' => (int) $locationId,
|
|
'newQuantity' => $quantity,
|
|
'mode' => $mode
|
|
], 200, 'Quantity already at requested level');
|
|
}
|
|
} else {
|
|
$adjustment = $quantity;
|
|
}
|
|
|
|
$invData = [
|
|
'trans_date' => date('Y-m-d H:i:s'),
|
|
'trans_items' => $data['itemId'],
|
|
'trans_user' => $this->employeeId,
|
|
'trans_location' => $locationId,
|
|
'trans_comment' => $comment,
|
|
'trans_inventory' => $adjustment
|
|
];
|
|
|
|
$this->inventory->insert($invData);
|
|
$this->itemQuantity->change_quantity($data['itemId'], $locationId, $adjustment);
|
|
|
|
$newQty = $this->itemQuantity->get_item_quantity($data['itemId'], $locationId);
|
|
|
|
return $this->respondSuccess([
|
|
'itemId' => (int) $data['itemId'],
|
|
'locationId' => (int) $locationId,
|
|
'adjustment' => $adjustment,
|
|
'newQuantity' => $newQty ? (float) $newQty->quantity : 0,
|
|
'mode' => $mode
|
|
], 200, 'Inventory adjusted successfully');
|
|
}
|
|
|
|
private function bulkAdjust(array $adjustments): ResponseInterface
|
|
{
|
|
$results = [];
|
|
$processed = 0;
|
|
$errors = [];
|
|
|
|
$this->inventory->db->transStart();
|
|
|
|
foreach ($adjustments as $adjustment) {
|
|
$itemId = $adjustment['itemId'] ?? $adjustment['item_id'] ?? null;
|
|
|
|
if (!$itemId) {
|
|
$errors[] = ['itemId' => null, 'success' => false, 'error' => 'itemId is required'];
|
|
continue;
|
|
}
|
|
|
|
$item = $this->item->find($itemId);
|
|
if (!$item || $item->deleted) {
|
|
$errors[] = ['itemId' => $itemId, 'success' => false, 'error' => 'Item not found'];
|
|
continue;
|
|
}
|
|
|
|
$mode = $adjustment['mode'] ?? 'adjust';
|
|
$locationId = $adjustment['locationId'] ?? $adjustment['location_id'] ?? 1;
|
|
$quantity = (float) ($adjustment['quantity'] ?? 0);
|
|
$comment = $adjustment['comment'] ?? 'Bulk API inventory adjustment';
|
|
|
|
if ($mode === 'set') {
|
|
$currentQty = $this->itemQuantity->get_item_quantity($itemId, $locationId);
|
|
$currentQty = $currentQty ? (float) $currentQty->quantity : 0;
|
|
$adjustmentQty = $quantity - $currentQty;
|
|
} else {
|
|
$adjustmentQty = $quantity;
|
|
}
|
|
|
|
$invData = [
|
|
'trans_date' => date('Y-m-d H:i:s'),
|
|
'trans_items' => $itemId,
|
|
'trans_user' => $this->employeeId,
|
|
'trans_location' => $locationId,
|
|
'trans_comment' => $comment,
|
|
'trans_inventory' => $adjustmentQty
|
|
];
|
|
|
|
$this->inventory->insert($invData);
|
|
$this->itemQuantity->change_quantity($itemId, $locationId, $adjustmentQty);
|
|
|
|
$results[] = ['itemId' => $itemId, 'success' => true];
|
|
$processed++;
|
|
}
|
|
|
|
$this->inventory->db->transComplete();
|
|
|
|
$response = [
|
|
'processed' => $processed,
|
|
'total' => count($adjustments),
|
|
'results' => $results
|
|
];
|
|
|
|
if (!empty($errors)) {
|
|
$response['errors'] = $errors;
|
|
$response['success'] = false;
|
|
$response['message'] = 'Some adjustments failed';
|
|
} else {
|
|
$response['success'] = true;
|
|
$response['message'] = 'All adjustments processed successfully';
|
|
}
|
|
|
|
return $this->respondSuccess($response);
|
|
}
|
|
} |