vat_wms/app/Http/Controllers/Api/StockSectionController.php

712 lines
26 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\StockBatch;
use App\Models\StockEntries2Section;
use App\Models\StockEntry;
use App\Models\StockPosition;
use App\Models\StockSection;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
class StockSectionController extends Controller
{
public function getSection(Request $request, int $id)
{
try {
return response()->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 <= sections 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 <= sections 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 <= sections 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": <string>,
* "capacity": <integer ≥ 0>,
* "retrievable": <boolean>
* }
*
* 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);
}
}