13 KiB
Weekly Order Intake — Implementation Plan
Status: Draft, awaiting decisions in §7
Date: 2026-09-04
Scope: dailygrubs_console (+ one new Jupiter endpoint from phase 2)
Goal: A weekly spreadsheet of orders is turned into created orders automatically, with agent assistance at the ambiguous steps.
1. Summary
The requested outcome is achievable, but not from the file currently being used as the example.
Orders_Detail_2026-09-02_073552.xlsx is a report of orders that already happened, not an
order intake sheet. Phase 0 therefore defines a real intake template; everything else builds on it.
The build is six phases. Phases 0–2 write nothing. The first real order is created in phase 3, behind an operator commit button. Full autonomy is phase 5, after idempotency is proven in phase 4.
2. Why the attached file cannot be the input
2.1 It records outcomes, not intent
Every distinguishing column is a result: orderstatus=delivered, deliverytime, rider,
assigntime, kms, deliverycharge. Re-submitting it would re-create twelve deliveries that
were already ridden and already charged.
2.2 It fails the console's existing upload contract
src/pages/nearle/orders/multipleOrders.js maps incoming headers through headerMap and marks
six columns required (suffix *). Measured against the 12 data rows of the export:
| Upload column | Required | Report field | Filled | Status |
|---|---|---|---|---|
sendername* |
yes | locationname |
12/12 | Mappable |
senderphone* |
yes | locationcontactno |
12/12 | Mappable |
senderaddress* |
yes | Pickupaddress |
0/12 | Present but empty |
receivername* |
yes | deliverycustomer |
12/12 | Mappable |
receiveralternatephone* |
yes | — | — | Absent |
itemdescription* |
yes | — | 1/12 | Absent |
receiverphone |
no | deliverycontactno |
12/12 | Mappable |
receiverfulladdress |
no | deliveryaddress |
12/12 | Mappable |
receiverlatitude |
no | deliverylat |
12/12 | Mappable |
receiverlongitude |
no | deliverylong |
12/12 | Mappable |
pickupdate(yyyy-mmm-dd) |
no | deliverydate |
12/12 | Timezone-suspect — see §6 |
Quantity |
no | — | — | Absent |
Two required columns are absent outright and one is blank on every row. The underlying customer data is all present, so a mapping is possible — it just needs a purpose-built template.
2.3 Orders attach to customer records, not to addresses
This is the constraint that decides the architecture. POST /orders/createorders takes an array
in which every element carries customerid and deliveryid. A row of free text is not enough:
each line must resolve to an existing customer, or one must be created first. This is why the
current UI says "Press Continue to add as drop customers" between upload and creation.
2.4 Three values never come from any sheet
| Value | Source |
|---|---|
applocationid, partnerid, locationid, moduleid, pickup lat/long |
The selected pickup location record |
deliverytime |
The pickup slot chosen for the batch |
deliverycharge, orderamount, ordervalue |
Computed from tenant pricing (gettenantpricing) and distance |
On the sample data the pricing rule fits ₹60 up to 4 km, then ₹12/km (6 km → ₹84, 5 km → ₹72), but that is inferred from one day and one tenant, not read from configuration. Never copy a charge from the sheet.
Consequence: the weekly file supplies who and what. The pickup point, the slot and the money are supplied by the run. Automation must bind them explicitly rather than inherit them.
3. Where an agent earns its place
Most of this pipeline is not an AI problem. Parsing, validating, pricing and posting are deterministic and must stay that way — they are the steps where a confident wrong answer costs money.
| Deterministic — no model | Agent — proposes, never commits |
|---|---|
Parse XLSX/CSV, drop TOTAL rows |
Customer resolution — match name + phone + address to an existing record, or propose a new one |
| Validate required columns, reject early | Address normalisation — split free text into locality / landmark / pincode, including Tamil-script entries |
| Compute charge from tenant pricing | Anomaly triage — explain in operator language why a row looks wrong |
| Duplicate detection by run key | |
Build and POST the createorders payload |
|
Reuse scanDataQuality as a pre-commit gate |
The rule: the agent returns a proposal with a confidence score and its reasoning. Deterministic code applies a threshold. Anything below it goes to the operator queue rather than into the batch. A model never writes an order.
4. Files
| File | Action |
|---|---|
src/pages/nearle/ai/intakeTemplate.js |
new — column contract + downloadable template |
src/pages/nearle/ai/intakeParser.js |
new — parse, validate, classify rows |
src/pages/nearle/ai/intakeParser.test.js |
new |
src/pages/nearle/ai/customerResolver.js |
new — phone match, agent seam |
src/pages/nearle/ai/customerResolver.test.js |
new |
src/pages/nearle/ai/runKey.js |
new — idempotency |
src/pages/nearle/ai/runKey.test.js |
new |
src/pages/nearle/ai/buildOrderPayload.js |
new — the createorders array |
src/pages/nearle/ai/buildOrderPayload.test.js |
new |
src/pages/nearle/orders/WeeklyIntake.js |
new — review & commit screen |
src/pages/nearle/api/api.js |
modify — fetchTenantCustomers, createOrdersBatch, proposeCustomerMatches |
src/routes/MainRoutes.js |
modify — add nearle/orders/weekly-intake |
src/menu-items/nearle.js |
modify — menu entry under Orders |
Existing modules reused: opsAnalysis.js (readField, onlyOrders, scanDataQuality),
xlsx and papaparse (already dependencies).
5. Phases
Each phase is useful on its own. Nothing writes an order until phase 3.
Phase 0 — Define the intake template
intakeTemplate.js holds one contract; everything else derives from it.
export const INTAKE_COLUMNS = [
{ key: 'receivername', label: 'Receiver Name*', required: true },
{ key: 'receiverphone', label: 'Receiver Phone*', required: true },
{ key: 'receiveraddress', label: 'Receiver Address*', required: true },
{ key: 'itemdescription', label: 'Item Description*', required: true },
{ key: 'quantity', label: 'Quantity', required: false, default: 1 },
{ key: 'collectcash', label: 'Collect Cash', required: false, default: 0 },
{ key: 'receiverlat', label: 'Receiver Latitude', required: false },
{ key: 'receiverlong', label: 'Receiver Longitude', required: false },
{ key: 'notes', label: 'Notes', required: false }
];
export const downloadTemplate = () => { /* xlsx, already a dependency */ };
Note what is deliberately absent: no sender columns (that is the batch's pickup location), no delivery date (that is the slot), no charge (computed). Those are the §2.4 values.
Acceptance: template downloads and round-trips through the phase 1 parser with zero findings. Gate: nothing else starts until the column list is agreed and one real sample week exists.
Phase 1 — Parser and validator, dry run only
parseIntakeFile(sheetRows) -> {
missingColumns: string[],
unknownColumns: string[], // fail loudly — see §6 "silent field drift"
rows: [{ index, raw, normalised, status, reasons: string[] }],
summary: { total, ok, needsReview, rejected }
}
status is 'ok' | 'needs-review' | 'rejected'.
- Rejected: missing required field, unparseable phone, coordinates outside the tenant radius.
- Needs review: no coordinates, non-Latin locality, suspected duplicate within the same file.
Pure module, no React and no network, tested the way opsAnalysis.js is.
Acceptance: run a real week through it and read the report. Zero writes.
Phase 2 — Customer resolution
resolveCustomers(parsedRows, existingCustomers, { proposeFn, threshold = 0.85 })
-> [{ index, match: { customerid, confidence, method }, proposedNew }]
method is 'phone-exact' | 'agent' | 'none'.
- Deterministic exact phone match against
customers/gettenantcustomers— no model, no cost. - Only unmatched rows go to
proposeFn, the injected agent call. Injection keeps tests offline. - Below
threshold→method: 'none'→ operator queue.
Acceptance: measure match accuracy against a week whose correct answers are already known, before resolution is allowed to influence anything.
Phase 3 — Review and commit screen
buildCreateOrdersPayload({ resolvedRows, pickupLocation, pickupSlot, pricing }) -> object[]
Emits exactly the shape multipleOrders.js posts today: configid: 9, paymenttype: 42,
paymentstatus: 1, orderstatus: 'created', deliverytype: 'B', itemcount: 1, plus
customerid/deliveryid from phase 2 and the location fields from the selected pickup record.
WeeklyIntake.js renders the proposed batch (resolved customer, address, charge, flags), takes
the pickup location and slot, and commits via POST /orders/createorders.
This is the first phase that creates real orders. Ship behind a role check (§7.5).
Phase 4 — Scheduling and idempotency
runKeyFor(row, { tenantid, weekStart })
// sha256(tenantid | weekStart | normalisedPhone | normalisedAddress | itemdescription)
Persisted server-side. createorders performs no deduplication of its own, so this layer is
the only thing between a retry and double-billing a customer.
Acceptance: deliberately re-run the previous week's file and confirm zero new orders. This is the single most important test in the build.
Phase 5 — Supervised autonomy
Auto-commit only rows that are ok, resolved at or above threshold, and unflagged. Everything
else waits in the queue and the operator receives a digest rather than a surprise. Requires a
kill switch and an audit row per created order naming the run that produced it.
Gate: enable only after several consecutive clean weeks at phase 4.
6. Risks and guardrails
| Risk | Why it is real here | Guardrail |
|---|---|---|
| Duplicate orders | Same file re-sent, or a retry after timeout. createorders does not dedupe. |
Persisted run key per row; replay is a no-op (phase 4). |
| Wrong customer | Fuzzy names (HAMEEZ RAMEEZ has a trailing space); one locality appears as both ராமவர்மபுரம் and Ramavarmapuram. |
Exact phone match first; agent proposals below threshold never auto-commit. |
| Bad geocode | Addresses carry plus-codes and free text; the pickup store itself resolves to two precisions (8.1841265 and 8.184126). |
Reject rows whose coordinates fall outside the tenant's service radius. |
| Wrong slot / date | The deliverydate timezone defect is unresolved and reads 5½ hours early. |
Operator sets the slot per batch; never inherit a date from a sheet until the backend is fixed. |
| Silent field drift | Already observed in this codebase: the API sends ridername/deliverycharges while the CSV export writes rider/deliverycharge. Both fail as blanks and zeros, not errors. |
Validate the parsed shape; fail loudly on unknown or missing columns. |
7. Open decisions
Defaults below are what the plan currently assumes. Only #1 changes the design materially.
- What is actually in the weekly file — recurring standing orders for the same customers, or a fresh list each week? Assumed: fresh list. Standing orders would make phase 2 nearly free.
- One pickup location or several — this tenant has two (Bawa Medical / King Nagar, and Bawaa Medicals 2 / Weavers Colony). If one file mixes both, the template needs a per-row sender column instead of one choice per batch. Assumed: one location per batch, operator-selected.
- How the file arrives — console upload is simplest and needs no new infrastructure; a watched folder or mailbox is more automatic and more to build and secure. Assumed: console upload.
- Where the agent runs — inside
backend_jupiter, or a separate DailyGrubs service it calls. Phases 0–1 do not depend on the answer. Assumed: a new Jupiter endpoint. - Who may commit a batch — auth in this console is localStorage, and
AuthGuard/GuestGuardare currently commented out in both route files. A screen that creates real orders needs this settled first. Assumed: a role check, to be specified.
8. Sequencing note
Phases 0–2 write nothing and depend on none of the open decisions except #1, so they can begin immediately. Phase 3 must not ship before decision #5.
Appendix — sources
- Upload contract:
src/pages/nearle/orders/multipleOrders.js(headerMap,handleFileDirectUpload) - Payload shape:
createorders()in the same file →POST ${REACT_APP_URL}/orders/createorders - Customer lookup:
customers/gettenantcustomers,customers/search - Pricing:
tenants/gettenantpricing - Data-quality findings reused from:
src/pages/nearle/ai/opsAnalysis.js - Sample analysed:
Orders_Detail_2026-09-02_073552.xlsx— 13 rows, 12 orders plus oneTOTALrow