284 lines
20 KiB
HTML
284 lines
20 KiB
HTML
<title>Weekly Order Intake</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Newsreader:opsz,wght@6..72,400;6..72,500;6..72,600&family=Public+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap">
|
|
<style>
|
|
:root{
|
|
--paper:#f6f5f8; --surface:#ffffff; --surface-2:#eeecf2; --sunken:#e6e3ec;
|
|
--ink:#1a1526; --ink-2:#4a4459; --ink-3:#7b7389; --rule:#dcd8e4; --rule-2:#c8c2d4;
|
|
--brand:#662582; --brand-2:#9255ab; --brand-soft:#f0e7f5; --on-brand:#ffffff;
|
|
--crit:#b3261e; --crit-soft:#fae4e2; --warn:#8a5600; --warn-soft:#f8ead2;
|
|
--ok:#2c6b45; --ok-soft:#dceae2;
|
|
--shadow:0 1px 2px rgba(26,21,38,.05), 0 8px 22px -14px rgba(26,21,38,.3);
|
|
}
|
|
@media (prefers-color-scheme: dark){
|
|
:root:not([data-theme="light"]){
|
|
--paper:#131019; --surface:#1c1826; --surface-2:#262032; --sunken:#0e0c13;
|
|
--ink:#ece9f1; --ink-2:#b0a8bf; --ink-3:#847b93; --rule:#332c40; --rule-2:#443b53;
|
|
--brand:#c79ade; --brand-2:#a679c4; --brand-soft:#2a1b35; --on-brand:#1c0f24;
|
|
--crit:#f0938a; --crit-soft:#3b1a17; --warn:#dca85c; --warn-soft:#332510;
|
|
--ok:#7cc79c; --ok-soft:#132f21;
|
|
--shadow:0 1px 2px rgba(0,0,0,.45), 0 10px 28px -16px rgba(0,0,0,.8);
|
|
}
|
|
}
|
|
:root[data-theme="dark"]{
|
|
--paper:#131019; --surface:#1c1826; --surface-2:#262032; --sunken:#0e0c13;
|
|
--ink:#ece9f1; --ink-2:#b0a8bf; --ink-3:#847b93; --rule:#332c40; --rule-2:#443b53;
|
|
--brand:#c79ade; --brand-2:#a679c4; --brand-soft:#2a1b35; --on-brand:#1c0f24;
|
|
--crit:#f0938a; --crit-soft:#3b1a17; --warn:#dca85c; --warn-soft:#332510;
|
|
--ok:#7cc79c; --ok-soft:#132f21;
|
|
--shadow:0 1px 2px rgba(0,0,0,.45), 0 10px 28px -16px rgba(0,0,0,.8);
|
|
}
|
|
*{box-sizing:border-box}
|
|
body{margin:0;background:var(--paper);color:var(--ink);font-family:"Public Sans",system-ui,-apple-system,"Segoe UI",sans-serif;font-size:15px;line-height:1.6;-webkit-font-smoothing:antialiased}
|
|
h1,h2,h3{font-family:Newsreader,Georgia,serif;font-weight:600;text-wrap:balance;margin:0;letter-spacing:-.005em}
|
|
.mono,code{font-family:"JetBrains Mono",ui-monospace,monospace;font-variant-numeric:tabular-nums}
|
|
code{font-size:.87em;background:var(--sunken);padding:1px 5px;border-radius:4px}
|
|
.wrap{max-width:920px;margin:0 auto;padding:0 24px 72px}
|
|
|
|
header.top{border-bottom:1px solid var(--rule);background:var(--surface)}
|
|
.top-in{max-width:920px;margin:0 auto;padding:20px 24px;display:flex;flex-wrap:wrap;gap:14px;align-items:baseline;justify-content:space-between}
|
|
.top-in h1{font-size:21px}
|
|
.top-in .sub{font-size:12px;color:var(--ink-3);letter-spacing:.05em;text-transform:uppercase;font-weight:600;font-family:"Public Sans",sans-serif}
|
|
|
|
.verdict{margin:26px 0 34px;border:1px solid var(--rule);border-left:4px solid var(--crit);border-radius:10px;background:var(--surface);box-shadow:var(--shadow);padding:20px 22px}
|
|
.verdict h2{font-size:24px;line-height:1.3;max-width:30ch}
|
|
.verdict p{margin:10px 0 0;color:var(--ink-2);max-width:72ch}
|
|
.verdict p:last-child{margin-bottom:0}
|
|
|
|
section{margin-bottom:40px}
|
|
.eyebrow{font-size:10.5px;letter-spacing:.11em;text-transform:uppercase;color:var(--brand);font-weight:700;margin-bottom:5px}
|
|
h2.sec{font-size:22px;margin-bottom:6px}
|
|
h3{font-size:16.5px;margin:22px 0 6px}
|
|
p.lead{color:var(--ink-2);max-width:72ch;margin:0 0 16px}
|
|
p{max-width:72ch}
|
|
ul,ol{max-width:72ch;padding-left:20px;color:var(--ink-2)}
|
|
li{margin-bottom:6px}
|
|
li strong{color:var(--ink)}
|
|
|
|
.tblwrap{border:1px solid var(--rule);border-radius:10px;overflow-x:auto;background:var(--surface);box-shadow:var(--shadow)}
|
|
table{border-collapse:collapse;width:100%;min-width:640px;font-size:13.5px}
|
|
th{font-family:"Public Sans",sans-serif;font-size:10px;letter-spacing:.07em;text-transform:uppercase;color:var(--ink-3);font-weight:700;text-align:left;padding:10px 14px;background:var(--surface-2);border-bottom:1px solid var(--rule);white-space:nowrap}
|
|
td{padding:9px 14px;border-bottom:1px solid var(--rule);vertical-align:top}
|
|
tr:last-child td{border-bottom:0}
|
|
td.st{white-space:nowrap;font-weight:600;font-size:12.5px}
|
|
.ok{color:var(--ok)} .no{color:var(--crit)} .wn{color:var(--warn)}
|
|
.req{display:inline-block;font-family:"JetBrains Mono",monospace;font-size:9.5px;font-weight:600;padding:1px 5px;border-radius:3px;background:var(--crit-soft);color:var(--crit);margin-left:6px;vertical-align:1px}
|
|
|
|
.split{display:grid;grid-template-columns:1fr 1fr;gap:1px;background:var(--rule);border:1px solid var(--rule);border-radius:10px;overflow:hidden}
|
|
@media(max-width:760px){.split{grid-template-columns:1fr}}
|
|
.half{background:var(--surface);padding:16px 18px}
|
|
.half h3{margin:0 0 4px;font-size:15.5px}
|
|
.half .tag{font-size:10px;letter-spacing:.08em;text-transform:uppercase;font-weight:700;margin-bottom:8px}
|
|
.half ul{padding-left:18px;margin:0;font-size:13.5px}
|
|
|
|
.phases{display:flex;flex-direction:column;gap:1px;background:var(--rule);border:1px solid var(--rule);border-radius:10px;overflow:hidden}
|
|
.phase{background:var(--surface);padding:16px 18px;display:grid;grid-template-columns:auto 1fr;gap:16px}
|
|
.pnum{font-family:"JetBrains Mono",monospace;font-size:12px;font-weight:600;color:var(--brand);padding-top:2px}
|
|
.phase h3{margin:0 0 3px;font-size:16px}
|
|
.phase .gate{font-size:12.5px;color:var(--ink-2);margin-top:8px;padding-top:8px;border-top:1px dashed var(--rule-2)}
|
|
.phase .gate b{font-family:"Public Sans",sans-serif;font-size:9.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--brand);margin-right:7px}
|
|
.phase ul{margin:6px 0 0;font-size:13.5px;padding-left:18px}
|
|
|
|
.callout{border:1px solid var(--rule);border-left:3px solid var(--warn);background:var(--surface);border-radius:8px;padding:13px 16px;margin:16px 0;font-size:13.5px;color:var(--ink-2)}
|
|
.callout b{color:var(--ink)}
|
|
.callout.brand{border-left-color:var(--brand)}
|
|
|
|
.decisions{counter-reset:d;list-style:none;padding:0;margin:0}
|
|
.decisions li{counter-increment:d;background:var(--surface);border:1px solid var(--rule);border-radius:9px;padding:13px 16px 13px 46px;position:relative;margin-bottom:8px;color:var(--ink-2);font-size:14px;max-width:none}
|
|
.decisions li::before{content:counter(d);position:absolute;left:16px;top:13px;font-family:"JetBrains Mono",monospace;font-size:12px;font-weight:600;color:var(--brand)}
|
|
.decisions li b{color:var(--ink);display:block;font-family:"Public Sans",sans-serif}
|
|
|
|
footer{margin-top:36px;padding-top:16px;border-top:1px solid var(--rule);font-size:12.5px;color:var(--ink-3)}
|
|
@media (prefers-reduced-motion: reduce){*{animation:none!important;transition:none!important}}
|
|
</style>
|
|
|
|
<header class="top">
|
|
<div class="top-in">
|
|
<h1>Weekly Order Intake</h1>
|
|
<span class="sub">DailyGrubs Console · implementation plan</span>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="wrap">
|
|
|
|
<div class="verdict">
|
|
<h2>The attached file cannot be the weekly input. It is a report of orders that already happened.</h2>
|
|
<p>Every column in <code>Orders_Detail_2026-09-02</code> is an <em>outcome</em>: <code>orderstatus=delivered</code>, <code>deliverytime</code>, <code>rider</code>, <code>assigntime</code>, <code>kms</code>. Feeding it back would re-create twelve deliveries that were already made, already ridden and already charged.</p>
|
|
<p>It also fails the console’s existing upload contract outright. Of the six columns the bulk uploader marks required, <strong>two are absent from the report entirely and one is blank on all twelve rows.</strong> This is fixable — the underlying customer data is all there — but it needs a purpose-built intake template, and that is decision one below.</p>
|
|
</div>
|
|
|
|
<section>
|
|
<div class="eyebrow">Analysis · the gap</div>
|
|
<h2 class="sec">Report export vs. the upload contract</h2>
|
|
<p class="lead">The console already accepts CSV/XLSX bulk uploads via <code>orders/multipleOrders.js</code>, which maps incoming headers through a fixed <code>headerMap</code> and rejects the file if a required column is missing. Here is that contract against what the report actually provides.</p>
|
|
<div class="tblwrap">
|
|
<table>
|
|
<thead><tr><th>Upload column</th><th>Report field</th><th>Filled</th><th>Status</th></tr></thead>
|
|
<tbody>
|
|
<tr><td class="mono">sendername<span class="req">req</span></td><td class="mono">locationname</td><td class="mono">12/12</td><td class="st ok">Mappable</td></tr>
|
|
<tr><td class="mono">senderphone<span class="req">req</span></td><td class="mono">locationcontactno</td><td class="mono">12/12</td><td class="st ok">Mappable</td></tr>
|
|
<tr><td class="mono">senderaddress<span class="req">req</span></td><td class="mono">Pickupaddress</td><td class="mono">0/12</td><td class="st wn">Present but empty</td></tr>
|
|
<tr><td class="mono">receivername<span class="req">req</span></td><td class="mono">deliverycustomer</td><td class="mono">12/12</td><td class="st ok">Mappable</td></tr>
|
|
<tr><td class="mono">receiveralternatephone<span class="req">req</span></td><td class="mono">—</td><td class="mono">—</td><td class="st no">Absent</td></tr>
|
|
<tr><td class="mono">itemdescription<span class="req">req</span></td><td class="mono">—</td><td class="mono">1/12</td><td class="st no">Absent</td></tr>
|
|
<tr><td class="mono">receiverphone</td><td class="mono">deliverycontactno</td><td class="mono">12/12</td><td class="st ok">Mappable</td></tr>
|
|
<tr><td class="mono">receiverfulladdress</td><td class="mono">deliveryaddress</td><td class="mono">12/12</td><td class="st ok">Mappable</td></tr>
|
|
<tr><td class="mono">receiverlatitude</td><td class="mono">deliverylat</td><td class="mono">12/12</td><td class="st ok">Mappable</td></tr>
|
|
<tr><td class="mono">receiverlongitude</td><td class="mono">deliverylong</td><td class="mono">12/12</td><td class="st ok">Mappable</td></tr>
|
|
<tr><td class="mono">pickupdate</td><td class="mono">deliverydate</td><td class="mono">12/12</td><td class="st wn">Timezone-suspect</td></tr>
|
|
<tr><td class="mono">Quantity</td><td class="mono">—</td><td class="mono">—</td><td class="st no">Absent</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<div class="eyebrow">Analysis · the harder constraint</div>
|
|
<h2 class="sec">Orders attach to customer records, not to addresses</h2>
|
|
<p class="lead">This is the part that decides the architecture. <code>createorders</code> posts an array in which every element carries <code>customerid</code> and <code>deliveryid</code>. A row of free text is not enough — each line of the weekly file has to <em>resolve to a customer that exists</em>, or one has to be created first. That is exactly why the current UI says “Press Continue to add as drop customers” between upload and creation.</p>
|
|
<p class="lead">Three more values are never in any sheet and must come from elsewhere at commit time:</p>
|
|
<ul>
|
|
<li><strong>The pickup location</strong> — <code>applocationid</code>, <code>partnerid</code>, <code>locationid</code>, <code>moduleid</code> and the pickup coordinates all come from the selected location record.</li>
|
|
<li><strong>The pickup slot</strong> — chosen per batch, and it drives <code>deliverytime</code>.</li>
|
|
<li><strong>The charge</strong> — computed from tenant pricing and distance, never copied from the sheet. On this data the rule fits ₹60 up to 4 km then ₹12/km, but that is inferred, not configuration.</li>
|
|
</ul>
|
|
<div class="callout"><b>Consequence:</b> the weekly file supplies <em>who and what</em>. The pickup point, the slot and the money are supplied by the run, not the sheet. Any automation has to bind them explicitly rather than inherit them by accident.</div>
|
|
</section>
|
|
|
|
<section>
|
|
<div class="eyebrow">Design</div>
|
|
<h2 class="sec">Where an agent earns its place — and where it does not</h2>
|
|
<p class="lead">Most of this pipeline is not an AI problem. Parsing, validating, pricing and posting are deterministic and must stay that way, because they are the steps where a confident wrong answer costs money. The agent belongs at the three genuinely ambiguous joints.</p>
|
|
<div class="split">
|
|
<div class="half">
|
|
<div class="tag" style="color:var(--ink-3)">Deterministic — no model</div>
|
|
<ul>
|
|
<li>Parse XLSX/CSV, drop <code>TOTAL</code> rows</li>
|
|
<li>Validate required columns, reject early</li>
|
|
<li>Compute charge from tenant pricing</li>
|
|
<li>Duplicate detection by run key</li>
|
|
<li>Build and POST the <code>createorders</code> payload</li>
|
|
<li>Reuse <code>scanDataQuality</code> as a pre-commit gate</li>
|
|
</ul>
|
|
</div>
|
|
<div class="half">
|
|
<div class="tag" style="color:var(--brand)">Agent — proposes, never commits</div>
|
|
<ul>
|
|
<li><strong>Customer resolution:</strong> match name + phone + address to an existing record, or propose a new one</li>
|
|
<li><strong>Address normalisation:</strong> split free text into locality/landmark/pincode, including Tamil-script entries</li>
|
|
<li><strong>Anomaly triage:</strong> explain why a row looks wrong, in operator language</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<div class="callout brand"><b>The rule:</b> 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.</div>
|
|
</section>
|
|
|
|
<section>
|
|
<div class="eyebrow">Build</div>
|
|
<h2 class="sec">Phases</h2>
|
|
<p class="lead">Ordered by dependency — each phase is useful on its own, and nothing writes an order until phase 4.</p>
|
|
<div class="phases">
|
|
<div class="phase">
|
|
<div class="pnum">00</div>
|
|
<div>
|
|
<h3>Define the intake template</h3>
|
|
<p style="margin:0;color:var(--ink-2);font-size:13.5px">A real order-intake sheet: sender, receiver, phone, address, item, quantity, requested date, collect-cash. Published as a downloadable template from the console so the weekly file is generated against a known contract instead of being whatever someone exported.</p>
|
|
<div class="gate"><b>Gate</b>Nothing else starts until the column list is agreed and a sample week exists.</div>
|
|
</div>
|
|
</div>
|
|
<div class="phase">
|
|
<div class="pnum">01</div>
|
|
<div>
|
|
<h3>Parser and validator, dry-run only</h3>
|
|
<ul>
|
|
<li>Pure module beside <code>opsAnalysis.js</code>, same testing approach</li>
|
|
<li>Per-row result: <code>ok</code> / <code>needs-review</code> / <code>rejected</code>, each with a reason</li>
|
|
<li>Reuses the existing <code>scanDataQuality</code> findings</li>
|
|
</ul>
|
|
<div class="gate"><b>Gate</b>Run a real week through it and read the report. Zero writes.</div>
|
|
</div>
|
|
</div>
|
|
<div class="phase">
|
|
<div class="pnum">02</div>
|
|
<div>
|
|
<h3>Customer resolution</h3>
|
|
<ul>
|
|
<li>Deterministic first: exact phone match against <code>gettenantcustomers</code></li>
|
|
<li>Agent second, only for what did not match exactly</li>
|
|
<li>Every proposal carries confidence + rationale; below threshold goes to the queue</li>
|
|
</ul>
|
|
<div class="gate"><b>Gate</b>Measure match accuracy on a known week before it influences anything.</div>
|
|
</div>
|
|
</div>
|
|
<div class="phase">
|
|
<div class="pnum">03</div>
|
|
<div>
|
|
<h3>Review and commit screen</h3>
|
|
<ul>
|
|
<li>Proposed batch shown as a table: resolved customer, address, charge, flags</li>
|
|
<li>Operator picks pickup location and slot — the values the sheet cannot supply</li>
|
|
<li>One commit button, posting to <code>orders/createorders</code></li>
|
|
</ul>
|
|
<div class="gate"><b>Gate</b>This is the first phase that creates real orders. Ship it behind a role check.</div>
|
|
</div>
|
|
</div>
|
|
<div class="phase">
|
|
<div class="pnum">04</div>
|
|
<div>
|
|
<h3>Scheduling and idempotency</h3>
|
|
<ul>
|
|
<li>Weekly trigger: watched folder, mailbox, or an upload endpoint — see decision 3</li>
|
|
<li><strong>Run key per row</strong> = hash(tenant, week, receiver phone, address, item). Persisted.</li>
|
|
<li>A replayed file is a no-op. The endpoint has no dedupe of its own, so this layer must.</li>
|
|
</ul>
|
|
<div class="gate"><b>Gate</b>Deliberately re-run last week’s file and confirm zero new orders.</div>
|
|
</div>
|
|
</div>
|
|
<div class="phase">
|
|
<div class="pnum">05</div>
|
|
<div>
|
|
<h3>Supervised autonomy</h3>
|
|
<ul>
|
|
<li>Auto-commit only rows that are fully resolved, unflagged and above threshold</li>
|
|
<li>Anything else waits in the queue; the operator gets a digest, not a surprise</li>
|
|
<li>Kill switch, and an audit row per created order naming the run that made it</li>
|
|
</ul>
|
|
<div class="gate"><b>Gate</b>Enable only after several consecutive clean weeks at phase 4.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<div class="eyebrow">Risk</div>
|
|
<h2 class="sec">What will go wrong, and the guardrail for each</h2>
|
|
<div class="tblwrap">
|
|
<table>
|
|
<thead><tr><th>Risk</th><th>Why it is real here</th><th>Guardrail</th></tr></thead>
|
|
<tbody>
|
|
<tr><td><strong>Duplicate orders</strong></td><td>The same file re-sent, or a retry after a timeout. <code>createorders</code> does not dedupe.</td><td>Persisted run key per row; replay is a no-op.</td></tr>
|
|
<tr><td><strong>Wrong customer</strong></td><td>Fuzzy matching on names like <code>HAMEEZ RAMEEZ</code> with trailing spaces, and two scripts for one locality.</td><td>Exact phone match first; agent proposals below threshold never auto-commit.</td></tr>
|
|
<tr><td><strong>Bad geocode</strong></td><td>Addresses carry plus-codes and free text; the store itself resolves to two precisions.</td><td>Reject rows whose coordinates fall outside the tenant’s service radius.</td></tr>
|
|
<tr><td><strong>Wrong slot / date</strong></td><td>The <code>deliverydate</code> timezone defect is unresolved and reads 5½ hours early.</td><td>Operator sets the slot per batch; never inherit a date from the sheet until the backend is fixed.</td></tr>
|
|
<tr><td><strong>Silent field drift</strong></td><td>Already observed: the API sends <code>ridername</code>/<code>deliverycharges</code>, the export writes <code>rider</code>/<code>deliverycharge</code>. Both fail as blanks and zeros, not errors.</td><td>Validate the parsed shape and fail loudly on an unknown or missing column.</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<div class="eyebrow">Blocked on you</div>
|
|
<h2 class="sec">Decisions needed before phase 0</h2>
|
|
<ol class="decisions">
|
|
<li><b>What is actually in the weekly file?</b> Recurring standing orders for the same customers, or a fresh list each week? The first makes customer resolution nearly free; the second makes it the hard part.</li>
|
|
<li><b>One pickup location, or several?</b> This tenant has two — Bawa Medical (King Nagar) and Bawaa Medicals 2 (Weavers Colony). If a single file mixes both, it needs a sender column per row rather than one choice per batch.</li>
|
|
<li><b>How does the file arrive?</b> Operator upload in the console is simplest and needs no new infrastructure. A watched folder or mailbox is more automatic and more to build and secure.</li>
|
|
<li><b>Where does the agent run?</b> Still open from before: inside <code>backend_jupiter</code>, or a separate DailyGrubs service it calls. Nothing in phases 0–1 depends on the answer, so building can start regardless.</li>
|
|
<li><b>Who may commit a batch?</b> Auth in this console is localStorage and the route guards are currently commented out. A screen that creates real orders needs that settled first.</li>
|
|
</ol>
|
|
</section>
|
|
|
|
<footer>Analysis from <span class="mono">Orders_Detail_2026-09-02_073552.xlsx</span> — 13 rows, 12 orders plus one TOTAL row · upload contract read from <span class="mono">orders/multipleOrders.js</span> · payload shape from <span class="mono">POST /orders/createorders</span></footer>
|
|
</div>
|