json([ 'section' => StockSection::with(['position', 'entries.physicalItem'])->findOrFail($id) ], 200); } catch (\Exception $e) { return response()->json([ 'success' => false, 'error' => 'not_found', 'message' => 'StockSection not found.', ], 404); } } public function setSection(Request $request) { // 1) Validate input try { $data = $request->validate([ 'section_id' => 'required|integer|exists:stock_section,section_id', 'entry_id' => 'required|integer|exists:stock_entries,id', 'count_to_be_stored' => 'required|integer|min:1', ]); } catch (ValidationException $e) { return response()->json([ 'success' => false, 'error' => 'validation_failed', 'message' => $e->errors(), ], 422); } $sectionId = $data['section_id']; $entryId = $data['entry_id']; $count = $data['count_to_be_stored']; try { // 2) Locate section and entry (404 if not found) $section = StockSection::findOrFail($sectionId); $entry = StockEntry::findOrFail($entryId); // 3) If the section is already occupied → 409 Conflict if ($section->occupied()) { return response()->json([ 'success' => false, 'error' => 'section_occupied', 'message' => 'That section is already occupied.', ], 409); } // 4) Check capacity: count_to_be_stored must be <= section’s capacity if ($count > $section->capacity) { return response()->json([ 'success' => false, 'error' => 'insufficient_capacity', 'message' => 'Not enough capacity in this section.', 'available_capacity' => $section->capacity, ], 409); } // 5) Create the pivot record and mark section as occupied StockEntries2Section::create([ 'entry_id' => $entry->id, 'section_id' => $section->section_id, 'count' => $count, ]); return response()->json([ 'success' => true, 'message' => 'Entry assigned to section successfully.', ], 200); } catch (ModelNotFoundException $e) { // Either the section or the entry was not found return response()->json([ 'success' => false, 'error' => 'not_found', 'message' => 'StockSection or StockEntry not found.', ], 404); } catch (\Exception $e) { // Log unexpected exceptions Log::error('Error in setSection', [ 'section_id' => $sectionId, 'entry_id' => $entryId, 'count' => $count, 'exception' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); return response()->json([ 'success' => false, 'error' => 'server_error', 'message' => 'An unexpected error occurred.', ], 500); } } public function storeStock(Request $request) { // 1) Validate input try { $data = $request->validate([ 'section_id' => 'required|integer|exists:stock_section,section_id', // new section 'current_section' => 'required|integer|exists:stock_section,section_id', // old section 'entry_id' => 'required|integer|exists:stock_entries,id', 'count_to_be_stored' => 'required|integer|min:1', ]); } catch (ValidationException $e) { return response()->json([ 'success' => false, 'error' => 'validation_failed', 'message' => $e->errors(), ], 422); } $newSectionId = $data['section_id']; $currentSectionId = $data['current_section']; $entryId = $data['entry_id']; $moveCount = $data['count_to_be_stored']; try { DB::transaction(function () use ( $newSectionId, $currentSectionId, $entryId, $moveCount ) { // 2) Load sections and entry $currentSection = StockSection::findOrFail($currentSectionId); $newSection = StockSection::findOrFail($newSectionId); $entry = StockEntry::findOrFail($entryId); // 3) Check occupation: allow if occupied only by this same entry $existingInNew = StockEntries2Section::where('section_id', $newSectionId) ->where('entry_id', $entryId) ->first(); if ($newSection->occupied() && ! $existingInNew) { abort(409, json_encode([ 'success' => false, 'error' => 'section_occupied', 'message' => 'Target section is already occupied by another item.', ])); } // 4) Locate pivot in current section $pivot = StockEntries2Section::where('section_id', $currentSectionId) ->where('entry_id', $entryId) ->firstOrFail(); // 5) Ensure there is enough count in current section if ($pivot->count < $moveCount) { abort(409, json_encode([ 'success' => false, 'error' => 'insufficient_count', 'message' => 'Not enough items in the current section to move.', 'available' => $pivot->count, ])); } // 6) Decrement or remove pivot in current section $remaining = $pivot->count - $moveCount; if ($remaining > 0) { DB::table('stock_entries2section') ->where('entry_id', $entryId) ->where('section_id', $currentSectionId) ->limit(1) ->update(['count' => $remaining]); } else { DB::table('stock_entries2section') ->where('entry_id', $entryId) ->where('section_id', $currentSectionId) ->limit(1) ->delete(); // if no more entries in this section, mark it unoccupied if (! DB::table('stock_entries2section')->where('section_id', $currentSectionId)->exists()) { $currentSection->update(['occupied' => false]); } } // 7) Upsert into new section, merging counts if already present // lockForUpdate to avoid race conditions $existing = DB::table('stock_entries2section') ->where('entry_id', $entryId) ->where('section_id', $newSectionId) ->lockForUpdate() ->first(); $currentInNew = $existing->count ?? 0; $totalAfter = $currentInNew + $moveCount; // 8) Ensure capacity in new section if ($totalAfter > $newSection->capacity) { abort(409, json_encode([ 'success' => false, 'error' => 'insufficient_capacity', 'message' => 'Not enough capacity in target section.', 'available_capacity' => $newSection->capacity - $currentInNew, ])); } if ($existing) { DB::table('stock_entries2section') ->where('entry_id', $entryId) ->where('section_id', $newSectionId) ->limit(1) ->update(['count' => $totalAfter]); } else { StockEntries2Section::create([ 'entry_id' => $entry->id, 'section_id' => $newSectionId, 'count' => $moveCount, ]); } // 9) Mark new section occupied $newSection->update(['occupied' => true]); }); // success response return response()->json([ 'success' => true, 'message' => 'Moved stock successfully.', ], 200); } catch (ModelNotFoundException $e) { return response()->json([ 'success' => false, 'error' => 'not_found', 'message' => 'StockSection or StockEntry not found.', ], 404); } catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) { $payload = json_decode($e->getMessage(), true); return response()->json($payload, $e->getStatusCode()); } catch (\Exception $e) { Log::error('Error in storeStock', [ 'current_section' => $currentSectionId, 'new_section' => $newSectionId, 'entry_id' => $entryId, 'count' => $moveCount, 'exception' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); return response()->json([ 'success' => false, 'error' => 'server_error', 'message' => 'An unexpected error occurred.', ], 500); } } public function setSectionForBatch(Request $request) { // 1) Validate input try { $data = $request->validate([ 'position_id' => 'required|integer|exists:stock_position,position_id', 'batch_id' => 'required|integer|exists:stock_batch,id', ]); } catch (ValidationException $e) { return response()->json([ 'success' => false, 'error' => 'validation_failed', 'message' => $e->errors(), ], 422); } $positionId = $data['position_id']; $batchId = $data['batch_id']; try { // 2) Locate section and entry (404 if not found) $position = StockPosition::findOrFail($positionId); $batch = StockBatch::with(['stockEntries'])->findOrFail($batchId); $anyOccupied = $position->sections->contains(function($section) { return $section->occupied(); }); // 3) If the section is already occupied → 409 Conflict if ($anyOccupied) { return response()->json([ 'success' => false, 'error' => 'position_occupied', 'message' => 'That position is already occupied.', ], 409); } if (!$position->temporary) { return response()->json([ 'success' => false, 'error' => 'section_not_tmp', 'message' => 'That section is not suitable for entire batch temporary storage.', ], 409); } // Delete all existing sections for this position StockSection::where('position_id', $position->position_id)->delete(); // Create new sections for each entry $sectionCounter = 1; foreach ($batch->stockEntries as $entry) { // Create new section $section = StockSection::create([ 'position_id' => $position->position_id, 'section_name' => (string)$sectionCounter, 'section_symbol' => (string)$sectionCounter, 'capacity' => $entry->count, 'retrievable' => true ]); // Create mapping StockEntries2Section::create([ 'entry_id' => $entry->id, 'section_id' => $section->section_id, 'count' => $entry->count ]); $sectionCounter++; } return response()->json([ 'success' => true, 'message' => 'Batch stored to position successfully.', ], 200); } catch (ModelNotFoundException $e) { // Either the section or the entry was not found return response()->json([ 'success' => false, 'error' => 'not_found', 'message' => 'StockSection or StockEntry not found.', ], 404); } catch (\Exception $e) { // Log unexpected exceptions Log::error('Error in setSectionForBatch', [ 'position_id' => $positionId, 'batch_id' => $batchId, 'exception' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); return response()->json([ 'success' => false, 'error' => 'server_error', 'message' => 'An unexpected error occurred.', ], 500); } } public function movePosition(Request $request) { // 1) Validate input try { $data = $request->validate([ 'section_id' => 'required|integer|exists:stock_section,section_id', 'new_section_id' => 'required|integer|exists:stock_section,section_id', 'entry_id' => 'required|integer|exists:stock_entries,id', 'count' => 'required|integer|min:1', ]); } catch (ValidationException $e) { return response()->json([ 'success' => false, 'error' => 'validation_failed', 'message' => $e->errors(), ], 422); } $sectionId = $data['section_id']; $newSectionId = $data['new_section_id']; $entryId = $data['entry_id']; $count = $data['count']; try { // 2) Locate section and entry (404 if not found) $new_section = StockSection::findOrFail($newSectionId); $entry = StockEntry::findOrFail($entryId); // 3) If the section is already occupied → 409 Conflict if ($new_section->occupied()) { return response()->json([ 'success' => false, 'error' => 'section_occupied', 'message' => 'That section is already occupied.', ], 409); } // 4) Check capacity: count_to_be_stored must be <= section’s capacity if ($count > $new_section->capacity) { return response()->json([ 'success' => false, 'error' => 'insufficient_capacity', 'message' => 'Not enough capacity in this section.', 'available_capacity' => $new_section->capacity, ], 409); } // use query builder because of composite PK DB::table('stock_entries2section') ->where('entry_id', $entryId) ->where('section_id', $sectionId) ->limit(1) // ensure only one row is touched ->update([ 'section_id' => $new_section->section_id, 'count' => $count, ]); return response()->json([ 'success' => true, 'message' => 'Section changed successfully.', ], 200); } catch (ModelNotFoundException $e) { // Either the section or the entry was not found return response()->json([ 'success' => false, 'error' => 'not_found', 'message' => 'StockSection or StockEntry not found.', ], 404); } catch (\Exception $e) { // Log unexpected exceptions Log::error('Error in setSection', [ 'section_id' => $sectionId, 'new_section_id' => $newSectionId, 'entry_id' => $entryId, 'count' => $count, 'exception' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); return response()->json([ 'success' => false, 'error' => 'server_error', 'message' => 'An unexpected error occurred.', ], 500); } } public function changeCount(Request $request) { // 1) Validate input try { $data = $request->validate([ 'section_id' => 'required|integer|exists:stock_section,section_id', 'entry_id' => 'required|integer|exists:stock_entries,id', 'count' => 'required|integer|min:1', 'new_count' => 'required|integer|min:1', ]); } catch (ValidationException $e) { return response()->json([ 'success' => false, 'error' => 'validation_failed', 'message' => $e->errors(), ], 422); } $sectionId = $data['section_id']; $entryId = $data['entry_id']; $count = $data['count']; $new_count = $data['new_count']; try { // 2) Locate section and entry (404 if not found) $section = StockSection::findOrFail($sectionId); $entry = StockEntry::findOrFail($entryId); // Check capacity: count_to_be_stored must be <= section’s capacity if ($new_count > $section->capacity) { return response()->json([ 'success' => false, 'error' => 'insufficient_capacity', 'message' => 'Not enough capacity in this section.', 'available_capacity' => $section->capacity, ], 409); } // use query builder because of composite PK DB::table('stock_entries2section') ->where('entry_id', $entryId) ->where('section_id', $sectionId) ->limit(1) // ensure only one row is touched ->update([ 'count' => $new_count, ]); return response()->json([ 'success' => true, 'message' => 'Section changed successfully.', ], 200); } catch (ModelNotFoundException $e) { // Either the section or the entry was not found return response()->json([ 'success' => false, 'error' => 'not_found', 'message' => 'StockSection or StockEntry not found.', ], 404); } catch (\Exception $e) { // Log unexpected exceptions Log::error('Error in setSection', [ 'section_id' => $sectionId, 'new_section_id' => $newSectionId, 'entry_id' => $entryId, 'count' => $count, 'exception' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); return response()->json([ 'success' => false, 'error' => 'server_error', 'message' => 'An unexpected error occurred.', ], 500); } } public function store(Request $request) { // 1) Validate incoming payload $validator = Validator::make($request->all(), [ 'position_id' => 'required|integer|exists:stock_position,position_id', 'section_symbol' => 'required|string|max:5', 'section_name' => 'required|string|max:32', 'capacity' => 'required|integer|min:0', 'retrievable' => 'required|boolean', ]); if ($validator->fails()) { return response() ->json([ 'success' => false, 'error' => 'validation_failed', 'message' => $validator->errors()->first(), ], 422); } // 2) Find the parent position $position = StockPosition::find($request->input('position_id')); if (!$position) { return response() ->json([ 'success' => false, 'error' => 'not_found', 'message' => 'Position not found.', ], 404); } $newCapacity = $request->input('capacity'); // 3) Check that (existing sections sum) + newCapacity ≤ position.capacity $existingSum = $position ->sections() ->sum('capacity'); if ($existingSum + $newCapacity > $position->capacity) { return response() ->json([ 'success' => false, 'error' => 'insufficient_capacity', 'message' => "Creating this section would push total capacity ({$existingSum} + {$newCapacity}) over position capacity ({$position->capacity}).", ], 422); } // 4) Create the new section $section = new StockSection(); $section->position_id = $position->position_id; $section->section_symbol = $request->input('section_symbol'); $section->section_name = $request->input('section_name'); $section->capacity = $newCapacity; $section->retrievable = $request->input('retrievable'); $section->save(); return response() ->json([ 'success' => true, 'section_id' => $section->section_id, ], 200); } /** * Update an existing StockSection. * * Endpoint: PUT /api/stockSections/{id} * * Request payload: * { * "section_name": , * "capacity": , * "retrievable": * } * * Responses: * • 200 → { success: true } * • 404 → { success: false, error: "not_found", message: "Section not found." } * • 422 → { success: false, error: "validation_failed" / "insufficient_capacity", message: "..." } */ public function update(Request $request, $id) { // 1) Find the section $section = StockSection::find($id); if (!$section) { return response() ->json([ 'success' => false, 'error' => 'not_found', 'message' => 'Section not found.', ], 404); } // 2) Validate incoming payload $validator = Validator::make($request->all(), [ 'section_name' => 'required|string|max:255', 'capacity' => 'required|integer|min:0', 'retrievable' => 'required|boolean', ]); if ($validator->fails()) { return response() ->json([ 'success' => false, 'error' => 'validation_failed', 'message' => $validator->errors()->first(), ], 422); } $updatedCapacity = $request->input('capacity'); $position = $section->position; // via relation // 3) Check that sum of *other* sections + updatedCapacity ≤ position.capacity $otherSectionsSum = $position ->sections() ->where('section_id', '!=', $id) ->sum('capacity'); if ($otherSectionsSum + $updatedCapacity > $position->capacity) { return response() ->json([ 'success' => false, 'error' => 'insufficient_capacity', 'message' => "Updating to capacity {$updatedCapacity} would push total of other sections ({$otherSectionsSum}) over position capacity ({$position->capacity}).", ], 422); } // 4) Apply changes $section->section_name = $request->input('section_name'); $section->capacity = $updatedCapacity; $section->retrievable = $request->input('retrievable'); $section->save(); return response()->json(['success' => true], 200); } /** * Delete a StockSection by its ID. * * Endpoint: DELETE /api/stockSections/{id} * * Responses: * • 200 → { success: true } * • 404 → { success: false, error: "not_found", message: "Section not found." } */ public function destroy($id) { $section = StockSection::find($id); if (!$section) { return response() ->json([ 'success' => false, 'error' => 'not_found', 'message' => 'Section not found.', ], 404); } $section->delete(); return response()->json(['success' => true], 200); } }