'/inv(?:oice)?(?:\s*(?:number|num|no\.?))?\s*(?:=|is| |equal\s+to)\s*([^\s,\.]+)/i', 'handler' => 'handleInvoice', ], // ── Invoice report: item type lookup ────────────────────────────────── // Accepts patterns like: // item = 674071 plant = Vahinie Unit 2 // item code = 674071 plant = Vahinie Unit 2 // check item 674071 for plant Vahinie Unit 2 [ 'pattern' => '/item(?:\s*code)?\s*(?:=|is|:)?\s*([^\s,]+)\s+(?:for\s+)?plant\s*(?:=|is|:)?\s*(.+)/i', 'handler' => 'handleInvoiceReport', ], // ── Add more commands here ──────────────────────────────────────────── // Example: // [ // 'pattern' => '/^\s*serial\s*=\s*(.+)/i', // 'handler' => 'handleSerial', // ], ]; // ───────────────────────────────────────────────────────────────────────── // Public entry point // ───────────────────────────────────────────────────────────────────────── /** * Dispatch the user's input to the matching handler. */ public function ask(string $input): string { $input = trim($input); foreach ($this->handlers as $entry) { if (preg_match($entry['pattern'], $input, $matches)) { $value = trim($matches[1] ?? ''); $value2 = trim($matches[2] ?? ''); return $this->{$entry['handler']}($value, $value2); } } return $this->unknownCommand($input); } // ───────────────────────────────────────────────────────────────────────── // Handler: invoice = // ───────────────────────────────────────────────────────────────────────── /** * Looks up scan status for an invoice number in invoice_validations. * Returns a plain-English string (used by the Advanced / free-text path). * Structured callers should use getInvoiceData() directly. */ private function handleInvoice(string $invoiceNumber, string $_unused = ''): string { $data = $this->getInvoiceData($invoiceNumber); // For the plain-text path (advanced mode / ChatbotService::ask()), // reassemble a human-readable sentence from the structured data. if (in_array($data['type'], ['invalid', 'error', 'not_found'], true)) { return $data['message']; } if ($data['type'] === 'all_scanned') { $n = $data['total']; $itemWord = $n === 1 ? 'serial number' : 'serial numbers'; return "For invoice number {$data['invoice_number']}, all {$n} {$itemWord} " . ($n === 1 ? 'has' : 'have') . ' been scanned. ✅'; } // partial or none_scanned $total = $data['total']; $scanned = $data['scanned']; $notScan = $data['not_scanned']; $inv = $data['invoice_number']; $itemWord = $total === 1 ? 'serial number' : 'serial numbers'; if ($scanned === 0) { $msg = "For invoice number {$inv}, there " . ($total === 1 ? 'is' : 'are') . " {$total} {$itemWord} " . 'and none have been scanned.'; } else { $msg = "For invoice number {$inv}, there " . ($total === 1 ? 'is' : 'are') . " {$total} {$itemWord} in total. " . "Out of which {$scanned} " . ($scanned === 1 ? 'has' : 'have') . ' been scanned and ' . "{$notScan} " . ($notScan === 1 ? 'has' : 'have') . ' not been scanned.'; } if (! empty($data['unscanned_serials'])) { $msg .= ' Unscanned serial numbers are: ' . implode(', ', $data['unscanned_serials']) . '.'; } return $msg; } // ───────────────────────────────────────────────────────────────────────── // Public structured accessor — used by ChatBot (Basic mode) // ───────────────────────────────────────────────────────────────────────── /** * Returns structured scan-status data for an invoice number. * * Return shape: * [ * 'type' => 'all_scanned' | 'partial' | 'none_scanned' * | 'not_found' | 'error' | 'invalid', * 'message' => string, // one-line human summary (no serial list) * 'invoice_number' => string, * 'total' => int, * 'scanned' => int, * 'not_scanned' => int, * 'unscanned_serials' => string[], // full list — may be large * ] */ public function getInvoiceData(string $invoiceNumber): array { $invoiceNumber = preg_replace('/\s+/', '', $invoiceNumber); if (empty($invoiceNumber)) { return [ 'type' => 'invalid', 'message' => 'Please provide a valid invoice number. Example: invoice = 3RA0013333', 'invoice_number' => '', 'total' => 0, 'scanned' => 0, 'not_scanned' => 0, 'unscanned_serials' => [], ]; } try { $rows = DB::select(" SELECT COALESCE(scanned_status, 'not scanned') AS status, COUNT(*) AS total_count, STRING_AGG( CASE WHEN scanned_status IS NULL THEN serial_number::text END, ', ' ) AS serial_numbers_not_scanned FROM invoice_validations WHERE invoice_number = ? GROUP BY scanned_status ", [$invoiceNumber]); } catch (\Exception $e) { Log::error('ChatbotService: invoice query failed', [ 'invoice' => $invoiceNumber, 'error' => $e->getMessage(), ]); return [ 'type' => 'error', 'message' => "Sorry, I couldn't fetch data for invoice {$invoiceNumber}. " . 'Please try again or contact support if this keeps happening.', 'invoice_number' => $invoiceNumber, 'total' => 0, 'scanned' => 0, 'not_scanned' => 0, 'unscanned_serials' => [], ]; } if (empty($rows)) { return [ 'type' => 'not_found', 'message' => "No records found for invoice number {$invoiceNumber}. " . 'Please double-check the invoice number and try again.', 'invoice_number' => $invoiceNumber, 'total' => 0, 'scanned' => 0, 'not_scanned' => 0, 'unscanned_serials' => [], ]; } // ── Aggregate rows ──────────────────────────────────────────────────── $totalScanned = 0; $totalNotScanned = 0; $unscannedSerials = []; foreach ($rows as $row) { if ($row->status === 'not scanned') { $totalNotScanned = (int) $row->total_count; if (! empty($row->serial_numbers_not_scanned)) { $unscannedSerials = array_values( array_filter( array_map('trim', explode(',', $row->serial_numbers_not_scanned)) ) ); } } else { $totalScanned += (int) $row->total_count; } } $grandTotal = $totalScanned + $totalNotScanned; // ── All scanned ─────────────────────────────────────────────────────── if ($totalNotScanned === 0) { $n = $grandTotal; $itemWord = $n === 1 ? 'serial number' : 'serial numbers'; return [ 'type' => 'all_scanned', 'message' => "All {$n} {$itemWord} scanned for invoice {$invoiceNumber}. ✅", 'invoice_number' => $invoiceNumber, 'total' => $grandTotal, 'scanned' => $totalScanned, 'not_scanned' => 0, 'unscanned_serials' => [], ]; } // ── None / partial scanned ──────────────────────────────────────────── $type = $totalScanned === 0 ? 'none_scanned' : 'partial'; $itemWord = $grandTotal === 1 ? 'serial number' : 'serial numbers'; if ($totalScanned === 0) { $summary = "Invoice {$invoiceNumber} — {$grandTotal} {$itemWord}, none scanned yet."; } else { $summary = "Invoice {$invoiceNumber} — {$grandTotal} {$itemWord} total: " . "{$totalScanned} scanned, {$totalNotScanned} not scanned."; } return [ 'type' => $type, 'message' => $summary, 'invoice_number' => $invoiceNumber, 'total' => $grandTotal, 'scanned' => $totalScanned, 'not_scanned' => $totalNotScanned, 'unscanned_serials' => $unscannedSerials, ]; } // ───────────────────────────────────────────────────────────────────────── // Handler: item = plant = // ───────────────────────────────────────────────────────────────────────── /** * Determines whether an item is a serial invoice or material invoice * for a given plant, using the sticker_masters table. * * @param string $itemCode Extracted item code (capture group 1) * @param string $plantName Extracted plant name (capture group 2) */ private function handleInvoiceReport(string $itemCode, string $plantName): string { $itemCode = trim($itemCode); $plantName = trim($plantName); if (empty($itemCode)) { return 'Please provide an item code. Example: item = 674071 plant = Vahinie Unit 2'; } if (empty($plantName)) { return 'Please provide a plant name. Example: item = 674071 plant = Vahinie Unit 2'; } try { $rows = DB::select(" WITH plant_item AS ( SELECT ? AS user_plant, ? AS user_item_code ), t1 AS ( SELECT plants.id AS plant_id, plants.name AS plant_name, ARRAY_AGG(items.code) AS item_codes FROM plants LEFT JOIN items ON plants.id = items.plant_id GROUP BY plants.id, plants.name ), t2 AS ( SELECT t1.plant_id, t1.plant_name, CASE WHEN plant_item.user_item_code = ANY(t1.item_codes) THEN 1 ELSE 0 END AS exists_flag FROM t1 CROSS JOIN plant_item WHERE t1.plant_name = plant_item.user_plant ), t3 AS ( SELECT t2.plant_id, t2.plant_name, t2.exists_flag, plant_item.user_item_code FROM t2 LEFT JOIN plant_item ON plant_item.user_plant = t2.plant_name ), t4 AS ( SELECT items.id AS item_id, t3.plant_id, t3.plant_name, t3.exists_flag, t3.user_item_code FROM t3 LEFT JOIN items ON t3.plant_id = items.plant_id AND t3.user_item_code = items.code ) SELECT t4.item_id, t4.plant_id, t4.plant_name, t4.exists_flag, t4.user_item_code, COALESCE(sticker_masters.material_type, 0) AS material_type, CASE WHEN sticker_masters.item_id IS NULL THEN 'no match found' WHEN COALESCE(sticker_masters.material_type, 0) = 0 THEN 'serial invoice' ELSE 'material invoice' END AS invoice_description FROM t4 LEFT JOIN sticker_masters ON sticker_masters.plant_id = t4.plant_id AND sticker_masters.item_id = t4.item_id ", [$plantName, $itemCode]); } catch (\Exception $e) { Log::error('ChatbotService: invoice report query failed', [ 'plant' => $plantName, 'item_code' => $itemCode, 'error' => $e->getMessage(), ]); return "Sorry, I couldn't fetch data for item {$itemCode} in plant {$plantName}. " . 'Please try again or contact support.'; } if (empty($rows)) { return "No data found for plant \"{$plantName}\". Please check the plant name and try again."; } $row = $rows[0]; if ((int) $row->exists_flag === 0) { return 'Provided item code does not exist in the item table.'; } return match ($row->invoice_description) { 'no match found' => "Item not found in sticker master for the plant {$row->plant_name}.", 'serial invoice' => 'It is a serial invoice item.', 'material invoice' => 'It is a material invoice item.', default => 'Unexpected result. Please contact support.', }; } // ───────────────────────────────────────────────────────────────────────── // Fallback for unrecognised input // ───────────────────────────────────────────────────────────────────────── private function unknownCommand(string $input): string { return "I didn't recognise that command. Please include a supported keyword with a value after '='.\n\n" . "• Invoice scan status:\n" . " invoice = 3RA0013333\n" . " what is the status of invoice = 3RA0013333\n\n" . "• Invoice type lookup:\n" . " item = 674071 plant = Vahinie Unit 2\n" . " item code = 674071 plant = Vahinie Unit 2\n\n" . 'Any sentence containing keyword = value will work.'; } }