public function saveBulkDraft(Request $request)
{
try {
// Relaxed validation to handle string JSON and boolean strings from frontend
$request->validate([
'draft_id' => 'nullable|integer|exists:rfqs,id',
'category_id' => 'required|integer|exists:item_categories,id',
'require_attachment' => 'nullable',
'selected_items' => 'required|string',
'quantities' => 'required|string',
'descriptions' => 'required|string',
'require_attachment_items' => 'required|string',
]);
// Decode JSON strings safely
$selectedItems = json_decode($request->selected_items, true) ?? [];
$quantities = json_decode($request->quantities, true) ?? [];
$descriptions = json_decode($request->descriptions, true) ?? [];
$reqAttachItems = json_decode($request->require_attachment_items, true) ?? [];
// Validate decoded data
if (! is_array($selectedItems) || ! is_array($quantities) || ! is_array($descriptions) || ! is_array($reqAttachItems)) {
throw new \Exception('Invalid JSON data received');
}
// Validate selected items exist
if (! empty($selectedItems)) {
$validItems = Item::whereIn('id', $selectedItems)->pluck('id')->toArray();
$invalid = array_diff($selectedItems, $validItems);
if (! empty($invalid)) {
throw new \Exception('Invalid items selected: '.implode(', ', $invalid));
}
}
$userId = auth()->id();
// Get or create draft
if ($request->draft_id) {
$draft = Rfq::findOrFail($request->draft_id);
if (Schema::hasColumn('rfqs', 'created_by') && $draft->created_by !== $userId) {
return response()->json(['error' => 'Unauthorized'], 403);
}
if (! $draft->is_draft) {
return response()->json(['error' => 'Not a draft'], 400);
}
} else {
$draft = Rfq::create([
'order_id' => 'DRAFT-'.str_pad(mt_rand(0, 999999), 6, '0', STR_PAD_LEFT),
'order_date' => now(),
'expected_submission' => now()->addDay()->setTime(17, 0, 0),
'status' => 'draft',
'is_draft' => true,
'draft_expires_at' => now()->addDays(7),
'message' => 'Bulk order draft',
'require_attachment' => false,
'created_by' => Schema::hasColumn('rfqs', 'created_by') ? $userId : null,
]);
}
// Cast require_attachment safely
$requireAttachment = filter_var($request->require_attachment, FILTER_VALIDATE_BOOLEAN);
$draft->require_attachment = $requireAttachment;
$draft->draft_expires_at = now()->addDays(7);
$draft->save();
// Pre-load item names
$items = Item::whereIn('id', $selectedItems)->pluck('name', 'id');
$attach = [];
foreach ($selectedItems as $itemId) {
$qty = max(1, (int) ($quantities[$itemId] ?? 1));
$desc = trim($descriptions[$itemId] ?? '');
$req = in_array((string) $itemId, $reqAttachItems) ? 1 : 0;
$attach[$itemId] = [
'quantity' => $qty,
'specification' => $items[$itemId] ?? 'Unknown Item',
'kh_specification' => $desc ?: ($items[$itemId] ?? 'Unknown Item'),
'require_attachment' => $req,
'created_at' => now(),
'updated_at' => now(),
];
}
$draft->items()->sync($attach);
return response()->json([
'status' => 'saved',
'draft_id' => $draft->id,
]);
} catch (\Exception $e) {
Log::error('saveBulkDraft failed', [
'user_id' => auth()->id(),
'input' => $request->all(),
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
return response()->json([
'error' => 'Save failed: '.$e->getMessage(),
], 500);
}
}
Enterprise multi-tenancy and high-throughput architectural patterns require dedicated database partitioning, connection pooling, and resilient worker strategies...
By subscribing to our developer tier, you unlock complete production source code, downloadable PowerPoint decks, and interactive labs.
Premium Developer Content
Join our membership for KES 500/month to unlock all comprehensive engineering deep-dives and slide decks.
Unlock Full Access (KES 500)Accelerate Your Engineering Career
Get monthly masterclasses, exclusive code snippets, and direct architectural reviews.
Become a Supporter • KES 500 / Month