Files
dailygrubs_console/IMPLEMENTATION_PLAN.md

275 lines
13 KiB
Markdown
Raw Permalink 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.
# 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.
```js
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
```js
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
```js
resolveCustomers(parsedRows, existingCustomers, { proposeFn, threshold = 0.85 })
-> [{ index, match: { customerid, confidence, method }, proposedNew }]
```
`method` is `'phone-exact' | 'agent' | 'none'`.
1. Deterministic exact phone match against `customers/gettenantcustomers` — no model, no cost.
2. Only unmatched rows go to `proposeFn`, the injected agent call. Injection keeps tests offline.
3. 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
```js
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
```js
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.
1. **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.
2. **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.*
3. **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.*
4. **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.*
5. **Who may commit a batch** — auth in this console is localStorage, and `AuthGuard`/`GuestGuard`
are 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 one `TOTAL` row