import { createAdminCustomer } from 'pages/api/doormileApi'; // ==============================|| Doormile AI — write actions ||============================== // // // The FIRST write capability in the assistant. Read the rules before adding // another one. // // The contract (assistant/CLAUDE.md §4): an intent never mutates. It returns a // PROPOSAL — the exact payload it would submit — and nothing reaches the API // until the operator presses Create in the panel. Parsing, validation and // execution are separated here so the proposal can be built, shown and // discarded without any possibility of a request going out. // // Customer creation was chosen as the first write deliberately: it needs two // required fields where an order needs fourteen, it has no CityGate pincode // gate, no geocoding and no delivery slot, and a wrong record is an edit // rather than a rider dispatched to the wrong address. // "create a customer", "add new client" — an explicit verb + noun. Deliberately // narrow: nothing here should fire on a question that merely mentions customers. export const CREATE_CUSTOMER_TRIGGER = /\b(?:create|add|register|new)\s+(?:a\s+|an\s+|the\s+)?(?:new\s+)?(?:customer|client)\b/i; // Words that are part of the instruction rather than the person's name. const FILLER = /\b(?:create|add|register|new|a|an|the|customer|client|named|called|with|phone|number|mobile|contact|no|email|id|please)\b/gi; // Pulls what it can out of free text. Anything it can't find stays undefined // and is asked for — never guessed. export const parseCustomerDraft = (text) => { const raw = String(text || ''); const email = (raw.match(/[\w.+-]+@[\w-]+\.[\w.]{2,}/) || [])[0]; // Exactly ten digits, standalone. A longer run is not a phone number and // must not be silently truncated into one. const phone = (raw.match(/(? w.length > 1); return { firstname: nameWords[0], lastname: nameWords.slice(1).join(' ') || undefined, phone, email }; }; // Mirrors createCustomer.js's own checks — a name, and a phone of exactly ten // digits. If the form would refuse it, the assistant refuses it too, rather // than letting the server decide. // // No tenant check: a customer record carries no tenantid, and the documented // POST body doesn't take one. The page's own `isStaffLogin && !tid` guard // exists because its dropdown sends a speculative tenantid; the assistant // doesn't send one at all. export const validateCustomerDraft = (draft) => { const missing = []; if (!draft.firstname) missing.push('the customer’s name'); if (!draft.phone || !/^\d{10}$/.test(String(draft.phone))) missing.push('a 10-digit mobile number'); return { ok: missing.length === 0, missing }; }; // The exact body that will be POSTed. // // Documented body for POST /admin/tenantcustomers is // { firstname, lastname, phone, email } — and a customer record carries NO // tenantid, which is why the tenant field was removed. A real // GET /admin/customers response does carry address, doorno, landmark, suburb, // city, state, postcode, latitude and longitude, so those are sent on a // best-effort basis: empty strings are dropped rather than sent as noise, and // if the server ignores the rest nothing breaks. const clean = (obj) => Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined && v !== null && v !== '')); export const buildCustomerPayload = (draft) => clean({ firstname: draft.firstname, lastname: draft.lastname || '', phone: draft.phone, email: draft.email || '', address: draft.address, doorno: draft.doorno, landmark: draft.landmark, suburb: draft.suburb, city: draft.city, state: draft.state, postcode: draft.postcode, latitude: draft.latitude, longitude: draft.longitude }); // The ONLY function in the assistant that mutates anything. Called exclusively // from the panel's confirm handler — never from an intent's run(). // // ---- Which endpoint, and why ----------------------------------------------- // // Writes to POST /admin/customers — the same resource the Customers page // reads, so a created customer shows up there immediately. // // Note for anyone reading express-console-api.md: that document lists // /admin/customers as GET + PATCH only, and describes a customer there as // something created by a booking (POST /admin/expressbooking documents // `customer_phone` as "creates a Guest customer if unknown"). This call was // pointed here deliberately anyway. If the route turns out not to exist the // server answers 404/405 and the assistant shows that verbatim alongside the // endpoint name, so the failure is loud rather than silent. // // The previous target was POST /admin/tenantcustomers, which succeeded but // wrote to a different store with its own id sequence — a customer created // that way came back as id 2 while the Customers list ran to id 44, and was // invisible on that page. That is the bug this change fixes. // // The payload already mirrors this resource's own GET shape (firstname, // lastname, phone, email, address, doorno, landmark, suburb, city, state, // postcode, latitude, longitude), so nothing else had to move. export const executeCreateCustomer = async (payload) => { const started = Date.now(); const call = { name: 'createAdminCustomer', target: 'POST /admin/customers', stats: Object.keys(payload).join(', ') }; try { const res = await createAdminCustomer(payload); const duration = `${Date.now() - started}ms`; // doormileApi mutations return the full envelope, so a `success: false` // arrives as a resolved promise, not a rejection. if (res && res.success === false) { return { ok: false, message: res.message || 'The server rejected the customer.', sourceCalls: [{ ...call, duration, status: 'error', errorMessage: res.message || 'Rejected' }] }; } const created = res?.data || res; return { ok: true, id: created?.appcustomerid ?? created?.customerid ?? created?.id, created, message: 'Customer created.', sourceCalls: [{ ...call, duration, status: 'complete', stats: `created id ${created?.appcustomerid ?? created?.id ?? '—'}` }] }; } catch (err) { // An HTTP failure used to throw straight past this function, and the panel // printed a generic "could not be created" with the status thrown away — // which is the one detail needed to tell "the route does not exist" apart // from "the body was wrong". Report the status, the server's own message, // and name the endpoint. const duration = `${Date.now() - started}ms`; const status = err.response?.status; const serverMessage = err.response?.data?.message || err.response?.data?.error; let message; if (status === 404 || status === 405) { // express-console-api.md documents /admin/customers as GET + PATCH only. // If the server agrees, say so plainly rather than implying the operator // typed something wrong. message = `POST /admin/customers returned ${status} — that route does not exist on the server. ` + 'A customer there is created by a booking, or via POST /admin/tenantcustomers.'; } else if (status) { message = `POST /admin/customers returned ${status}${serverMessage ? ` — ${serverMessage}` : ''}. Nothing was saved.`; } else { message = `${err.message || 'The request failed'} — the server could not be reached. Nothing was saved.`; } return { ok: false, status, message, sourceCalls: [ { ...call, duration, status: 'error', errorMessage: `${status || 'network'}${serverMessage ? ` · ${serverMessage}` : ''}` } ] }; } };