Files
doormilxpress_astryx/DOORMILE_PARCEL_FLOW.html
2026-08-29 15:42:56 +05:30

677 lines
43 KiB
HTML
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.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
:root { color-scheme: light dark; }
html, body { margin: 0; }
img { max-width: 100%; }
[hidden] { display: none !important; }
</style>
<title>Doormile Parcel Flow</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=Archivo:wght@500;600;700&family=Source+Sans+3:ital,wght@0,400;0,600;1,400&family=JetBrains+Mono:wght@400;600&display=swap">
<style>
:root {
--paper: #F6F4F5;
--surface: #FFFFFF;
--surface-2: #EFEBED;
--ink: #17141A;
--ink-2: #453E48;
--muted: #6E6670;
--line: #DCD5D8;
--line-2: #C6BCC0;
--accent: #C01227;
--accent-ink: #C01227;
--accent-soft:#F6E2E5;
--alt: #10646F;
--alt-soft: #DDEEF0;
--shadow: 0 1px 2px rgba(23,20,26,.06), 0 8px 24px -16px rgba(23,20,26,.35);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--paper: #121014;
--surface: #1A171C;
--surface-2: #221E24;
--ink: #EFEAEC;
--ink-2: #C4BBC0;
--muted: #968D94;
--line: #322C34;
--line-2: #443C46;
--accent: #FF6076;
--accent-ink: #FF8090;
--accent-soft:#3A1B22;
--alt: #4FC7D4;
--alt-soft: #10333A;
--shadow: 0 1px 2px rgba(0,0,0,.5), 0 8px 24px -16px rgba(0,0,0,.9);
}
}
:root[data-theme="dark"] {
--paper: #121014;
--surface: #1A171C;
--surface-2: #221E24;
--ink: #EFEAEC;
--ink-2: #C4BBC0;
--muted: #968D94;
--line: #322C34;
--line-2: #443C46;
--accent: #FF6076;
--accent-ink: #FF8090;
--accent-soft:#3A1B22;
--alt: #4FC7D4;
--alt-soft: #10333A;
--shadow: 0 1px 2px rgba(0,0,0,.5), 0 8px 24px -16px rgba(0,0,0,.9);
}
* { box-sizing: border-box; }
body {
background: var(--paper);
color: var(--ink);
font-family: "Source Sans 3", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
font-size: 16.5px;
line-height: 1.62;
margin: 0;
padding: 0 24px 96px;
-webkit-font-smoothing: antialiased;
}
.wrap { max-width: 1180px; margin: 0 auto; }
.measure { max-width: 68ch; }
/* ---------- masthead ---------- */
header.masthead {
padding: 64px 0 40px;
border-bottom: 2px solid var(--ink);
margin-bottom: 44px;
}
.eyebrow {
font-family: "JetBrains Mono", ui-monospace, "SFMono-Regular", Menlo, monospace;
font-size: 11.5px;
letter-spacing: .14em;
text-transform: uppercase;
color: var(--accent-ink);
margin: 0 0 18px;
}
h1 {
font-family: Archivo, "Helvetica Neue", Arial, sans-serif;
font-weight: 700;
font-size: clamp(2.3rem, 5.4vw, 3.6rem);
line-height: 1.03;
letter-spacing: -.03em;
text-wrap: balance;
margin: 0 0 20px;
}
.standfirst {
font-size: 1.16rem;
color: var(--ink-2);
max-width: 62ch;
margin: 0;
}
.standfirst strong { color: var(--ink); font-weight: 600; }
/* ---------- sections ---------- */
section { margin: 0 0 72px; }
h2 {
font-family: Archivo, "Helvetica Neue", Arial, sans-serif;
font-weight: 600;
font-size: 1.62rem;
letter-spacing: -.017em;
line-height: 1.2;
text-wrap: balance;
margin: 0 0 6px;
}
h3 {
font-family: Archivo, "Helvetica Neue", Arial, sans-serif;
font-weight: 600;
font-size: 1.09rem;
letter-spacing: -.01em;
margin: 34px 0 8px;
}
.sec-head {
display: flex;
align-items: baseline;
gap: 16px;
border-top: 1px solid var(--line);
padding-top: 16px;
margin-bottom: 20px;
}
.sec-num {
font-family: "JetBrains Mono", monospace;
font-size: 12px;
font-weight: 600;
color: var(--accent-ink);
padding-top: 6px;
flex: none;
}
p { margin: 0 0 15px; }
a { color: var(--accent-ink); }
code, .mono {
font-family: "JetBrains Mono", ui-monospace, "SFMono-Regular", Menlo, monospace;
font-size: .86em;
}
p code, li code, td code {
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: 3px;
padding: .07em .34em;
color: var(--ink);
}
/* ---------- figures ---------- */
figure {
margin: 30px 0 34px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 4px;
box-shadow: var(--shadow);
overflow: hidden;
}
.fig-scroll { overflow-x: auto; padding: 22px 22px 6px; }
figure svg { display: block; max-width: 100%; height: auto; min-width: 680px; margin: 0 auto; }
figcaption {
border-top: 1px solid var(--line);
background: var(--surface-2);
padding: 12px 22px 14px;
font-size: 14px;
color: var(--ink-2);
line-height: 1.5;
}
figcaption b {
font-family: "JetBrains Mono", monospace;
font-size: 11px;
letter-spacing: .1em;
text-transform: uppercase;
color: var(--accent-ink);
display: block;
margin-bottom: 4px;
font-weight: 600;
}
/* ---------- tables ---------- */
.table-scroll { overflow-x: auto; margin: 26px 0; border: 1px solid var(--line); border-radius: 4px; background: var(--surface); }
table { border-collapse: collapse; width: 100%; font-size: 14.6px; min-width: 620px; }
th, td { text-align: left; padding: 11px 16px; border-bottom: 1px solid var(--line); vertical-align: top; }
thead th {
font-family: "JetBrains Mono", monospace;
font-size: 11px;
letter-spacing: .1em;
text-transform: uppercase;
color: var(--muted);
font-weight: 600;
background: var(--surface-2);
}
tbody tr:last-child td { border-bottom: 0; }
td.yes { color: var(--alt); font-weight: 600; }
td.no { color: var(--accent-ink); font-weight: 600; }
/* ---------- callout ---------- */
.callout {
border-left: 3px solid var(--accent);
background: var(--accent-soft);
padding: 16px 20px;
border-radius: 0 4px 4px 0;
margin: 26px 0;
font-size: 15.4px;
}
.callout p:last-child { margin-bottom: 0; }
.callout .label {
font-family: "JetBrains Mono", monospace;
font-size: 11px;
letter-spacing: .1em;
text-transform: uppercase;
font-weight: 600;
color: var(--accent-ink);
display: block;
margin-bottom: 5px;
}
/* ---------- stage list ---------- */
ol.stages { list-style: none; padding: 0; margin: 24px 0; display: grid; gap: 1px; background: var(--line); border: 1px solid var(--line); border-radius: 4px; overflow: hidden; }
ol.stages li { background: var(--surface); padding: 14px 18px; display: grid; grid-template-columns: 34px 1fr; gap: 14px; align-items: start; font-size: 15.2px; }
ol.stages .n { font-family: "JetBrains Mono", monospace; font-size: 12px; font-weight: 600; color: var(--accent-ink); padding-top: 4px; }
ol.stages b { font-weight: 600; }
ol.stages .d { color: var(--ink-2); }
ul.plain { padding-left: 1.15em; margin: 12px 0 18px; }
ul.plain li { margin-bottom: 7px; }
footer {
border-top: 1px solid var(--line);
padding-top: 20px;
color: var(--muted);
font-size: 13.6px;
}
@media (max-width: 640px) {
body { padding: 0 16px 64px; font-size: 16px; }
header.masthead { padding: 44px 0 30px; }
}
</style>
</head>
<body>
<div class="wrap">
<header class="masthead">
<p class="eyebrow">Architecture walkthrough · doormile_backend</p>
<h1>Doormile Parcel Flow</h1>
<p class="standfirst">How a parcel actually moves through Doormile — from a sales rep's GPS-stamped doorstep survey to a consignee reading six digits aloud. And where that pipeline, built for <strong>on-demand courier</strong>, meets a business run as <strong>milk rounds</strong>.</p>
</header>
<!-- ============ 1 ============ -->
<section>
<div class="sec-head">
<span class="sec-num">01</span>
<div>
<h2>The parcel's path, end to end</h2>
</div>
</div>
<p class="measure">Seven stages, two route branches, one convergence. Every arrow below is a real state transition in the Go backend — the labels are the endpoints that cause them and the statuses they write.</p>
<figure>
<div class="fig-scroll">
<svg viewBox="0 0 1120 1268" role="img" aria-label="End-to-end Doormile lifecycle: field survey creates a client that converts to a tenant with a rate card; an order from one of three sources becomes a PickupBooking; Redis geo-search plus Claude scoring assigns a miler; the miler accepts, reaches and completes pickup, converting the booking into a Consignment with a receiver-only OTP; matching three-digit pincode prefixes route hyperlocal straight to out-for-delivery, everything else via hub, tripsheet and linehaul; both branches close on OTP verification and a stored delivery proof.">
<defs>
<marker id="ar" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="currentColor"/>
</marker>
<marker id="ar-a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="var(--accent)"/>
</marker>
<marker id="ar-t" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="var(--alt)"/>
</marker>
</defs>
<g font-family="'Source Sans 3', sans-serif" color="var(--line-2)">
<!-- ============ STAGE 1 ============ -->
<text x="0" y="34" font-family="'JetBrains Mono', monospace" font-size="10.5" font-weight="600" letter-spacing="1.2" fill="var(--muted)">01 · ACQUISITION</text>
<line x1="0" y1="44" x2="1120" y2="44" stroke="var(--line)" stroke-width="1"/>
<rect x="0" y="62" width="196" height="62" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="98" y="88" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">Field rep on site</text>
<text x="98" y="106" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">surveylat / surveylong</text>
<line x1="200" y1="93" x2="246" y2="93" stroke="currentColor" stroke-width="1.4" marker-end="url(#ar)"/>
<rect x="250" y="62" width="212" height="62" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="356" y="88" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">DoormileClient</text>
<text x="356" y="106" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">POST /crm/clients</text>
<line x1="466" y1="93" x2="524" y2="93" stroke="currentColor" stroke-width="1.4" marker-end="url(#ar)"/>
<text x="495" y="84" text-anchor="middle" font-size="10" fill="var(--muted)">converts</text>
<rect x="528" y="62" width="180" height="62" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="618" y="88" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">Tenant</text>
<text x="618" y="106" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">billable account</text>
<line x1="712" y1="93" x2="758" y2="93" stroke="currentColor" stroke-width="1.4" marker-end="url(#ar)"/>
<rect x="762" y="62" width="212" height="62" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="868" y="86" text-anchor="middle" font-size="13" font-weight="600" fill="var(--ink)">TenantPricing</text>
<text x="868" y="105" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">+ TenantLocation sites</text>
<!-- ============ STAGE 2 ============ -->
<text x="0" y="176" font-family="'JetBrains Mono', monospace" font-size="10.5" font-weight="600" letter-spacing="1.2" fill="var(--muted)">02 · ORIGINATION</text>
<line x1="0" y1="186" x2="1120" y2="186" stroke="var(--line)" stroke-width="1"/>
<rect x="0" y="204" width="196" height="44" rx="3" fill="var(--surface)" stroke="var(--line-2)"/>
<text x="98" y="231" text-anchor="middle" font-size="12.5" fill="var(--ink)">Customer App</text>
<rect x="0" y="256" width="196" height="44" rx="3" fill="var(--surface)" stroke="var(--line-2)"/>
<text x="98" y="283" text-anchor="middle" font-size="12.5" fill="var(--ink)">CRM Console</text>
<rect x="0" y="308" width="196" height="44" rx="3" fill="var(--surface)" stroke="var(--line-2)"/>
<text x="98" y="329" text-anchor="middle" font-size="12.5" fill="var(--ink)">Express Console</text>
<text x="98" y="344" text-anchor="middle" font-size="10" font-family="'JetBrains Mono', monospace" fill="var(--muted)">bulk Excel upload</text>
<path d="M200,226 H236 V278" fill="none" stroke="currentColor" stroke-width="1.3"/>
<path d="M200,278 H236" fill="none" stroke="currentColor" stroke-width="1.3"/>
<path d="M200,330 H236 V278" fill="none" stroke="currentColor" stroke-width="1.3"/>
<line x1="236" y1="278" x2="292" y2="278" stroke="currentColor" stroke-width="1.4" marker-end="url(#ar)"/>
<rect x="296" y="244" width="378" height="68" rx="3" fill="var(--surface-2)" stroke="var(--ink)" stroke-width="1.5"/>
<text x="485" y="271" text-anchor="middle" font-size="14" font-weight="600" fill="var(--ink)">PickupBooking</text>
<text x="485" y="292" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">status: Pending_Pickup · priced vs TenantPricing</text>
<line x1="678" y1="278" x2="722" y2="278" stroke="var(--alt)" stroke-width="1.4" stroke-dasharray="4 3" marker-end="url(#ar-t)"/>
<rect x="726" y="248" width="248" height="60" rx="3" fill="var(--alt-soft)" stroke="var(--alt)" stroke-dasharray="4 3"/>
<text x="850" y="272" text-anchor="middle" font-size="11.5" font-weight="600" fill="var(--alt)">Preferredpickupfrom / to</text>
<text x="850" y="290" text-anchor="middle" font-size="10.5" fill="var(--alt)">the time-window field — unused as a round</text>
<!-- ============ STAGE 3 ============ -->
<text x="0" y="382" font-family="'JetBrains Mono', monospace" font-size="10.5" font-weight="600" letter-spacing="1.2" fill="var(--muted)">03 · DISPATCH</text>
<line x1="0" y1="392" x2="1120" y2="392" stroke="var(--line)" stroke-width="1"/>
<rect x="0" y="412" width="204" height="64" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="102" y="437" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">Redis GEOSEARCH</text>
<text x="102" y="456" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">milers:locations · 10 km</text>
<line x1="208" y1="444" x2="252" y2="444" stroke="currentColor" stroke-width="1.4" marker-end="url(#ar)"/>
<text x="230" y="435" text-anchor="middle" font-size="9.5" fill="var(--muted)">candidates</text>
<rect x="256" y="404" width="238" height="80" rx="3" fill="var(--accent-soft)" stroke="var(--accent)"/>
<text x="375" y="428" text-anchor="middle" font-size="13" font-weight="600" fill="var(--accent-ink)">Claude Sonnet scoring</text>
<text x="375" y="446" text-anchor="middle" font-size="10.5" fill="var(--accent-ink)">distance · load · rating</text>
<text x="375" y="463" text-anchor="middle" font-size="10.5" fill="var(--accent-ink)">30-day on-time record</text>
<line x1="375" y1="508" x2="375" y2="488" stroke="var(--accent)" stroke-width="1.3" marker-end="url(#ar-a)"/>
<rect x="256" y="510" width="238" height="34" rx="3" fill="var(--surface)" stroke="var(--line-2)"/>
<text x="375" y="532" text-anchor="middle" font-size="11" font-family="'JetBrains Mono', monospace" fill="var(--muted)">pgvector RAG memory</text>
<line x1="498" y1="444" x2="542" y2="444" stroke="currentColor" stroke-width="1.4" marker-end="url(#ar)"/>
<rect x="546" y="412" width="184" height="64" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="638" y="437" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">AgentDecision</text>
<text x="638" y="456" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">reasoning logged</text>
<line x1="734" y1="444" x2="778" y2="444" stroke="currentColor" stroke-width="1.4" marker-end="url(#ar)"/>
<rect x="782" y="412" width="192" height="64" rx="3" fill="var(--surface-2)" stroke="var(--ink)" stroke-width="1.5"/>
<text x="878" y="437" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">BookingAssignment</text>
<text x="878" y="456" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">Miler_Assigned</text>
<path d="M102,480 V566 H878 V480" fill="none" stroke="var(--muted)" stroke-width="1.2" stroke-dasharray="5 4" marker-end="url(#ar)"/>
<rect x="380" y="554" width="220" height="24" rx="3" fill="var(--surface)" stroke="var(--line)"/>
<text x="490" y="571" text-anchor="middle" font-size="10.5" fill="var(--muted)">5 s timeout → greedy nearest miler</text>
<!-- ============ STAGE 4 ============ -->
<text x="0" y="616" font-family="'JetBrains Mono', monospace" font-size="10.5" font-weight="600" letter-spacing="1.2" fill="var(--muted)">04 · FIRST MILE</text>
<line x1="0" y1="626" x2="1120" y2="626" stroke="var(--line)" stroke-width="1"/>
<rect x="0" y="646" width="286" height="62" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="143" y="671" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">Accept</text>
<text x="143" y="690" text-anchor="middle" font-size="10" font-family="'JetBrains Mono', monospace" fill="var(--muted)">/assignments/:id/accept</text>
<line x1="290" y1="677" x2="334" y2="677" stroke="currentColor" stroke-width="1.4" marker-end="url(#ar)"/>
<rect x="338" y="646" width="286" height="62" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="481" y="671" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">Reached doorstep</text>
<text x="481" y="690" text-anchor="middle" font-size="10" font-family="'JetBrains Mono', monospace" fill="var(--muted)">arrivedat + arrival GPS</text>
<line x1="628" y1="677" x2="672" y2="677" stroke="currentColor" stroke-width="1.4" marker-end="url(#ar)"/>
<rect x="676" y="646" width="298" height="62" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="825" y="671" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">Pickup complete</text>
<text x="825" y="690" text-anchor="middle" font-size="10" font-family="'JetBrains Mono', monospace" fill="var(--muted)">COD collected · Idempotency-Key</text>
<!-- ============ STAGE 5 ============ -->
<text x="0" y="748" font-family="'JetBrains Mono', monospace" font-size="10.5" font-weight="600" letter-spacing="1.2" fill="var(--muted)">05 · CUSTODY HANDOFF</text>
<line x1="0" y1="758" x2="1120" y2="758" stroke="var(--line)" stroke-width="1"/>
<rect x="0" y="778" width="490" height="72" rx="3" fill="var(--surface-2)" stroke="var(--ink)" stroke-width="1.5"/>
<text x="245" y="806" text-anchor="middle" font-size="14" font-weight="600" fill="var(--ink)">Consignment created</text>
<text x="245" y="827" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">Collected_By_Miler · tracking number issued</text>
<line x1="494" y1="814" x2="538" y2="814" stroke="currentColor" stroke-width="1.4" marker-end="url(#ar)"/>
<rect x="542" y="778" width="432" height="72" rx="3" fill="var(--accent-soft)" stroke="var(--accent)"/>
<text x="758" y="806" text-anchor="middle" font-size="14" font-weight="600" fill="var(--accent-ink)">6-digit delivery OTP</text>
<text x="758" y="827" text-anchor="middle" font-size="10.5" fill="var(--accent-ink)">to the receiver only — stripped from rider &amp; console APIs</text>
<!-- ============ STAGE 6 ============ -->
<text x="0" y="890" font-family="'JetBrains Mono', monospace" font-size="10.5" font-weight="600" letter-spacing="1.2" fill="var(--muted)">06 · ROUTE FORK</text>
<line x1="0" y1="900" x2="1120" y2="900" stroke="var(--line)" stroke-width="1"/>
<rect x="300" y="918" width="380" height="46" rx="23" fill="var(--surface)" stroke="var(--ink)" stroke-width="1.5"/>
<text x="490" y="946" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">first 3 pincode digits match?</text>
<path d="M300,941 H180 V1000" fill="none" stroke="var(--alt)" stroke-width="1.5" marker-end="url(#ar-t)"/>
<text x="196" y="978" font-size="10.5" font-weight="600" fill="var(--alt)">YES · hyperlocal</text>
<path d="M680,941 H800 V1000" fill="none" stroke="currentColor" stroke-width="1.5" marker-end="url(#ar)"/>
<text x="700" y="978" font-size="10.5" font-weight="600" fill="var(--muted)">NO · hub &amp; spoke</text>
<!-- left branch -->
<rect x="0" y="1002" width="360" height="42" rx="3" fill="var(--alt-soft)" stroke="var(--alt)"/>
<text x="180" y="1029" text-anchor="middle" font-size="11.5" font-family="'JetBrains Mono', monospace" fill="var(--alt)">/consignments/:id/start-delivery</text>
<line x1="180" y1="1046" x2="180" y2="1062" stroke="var(--alt)" stroke-width="1.4" marker-end="url(#ar-t)"/>
<rect x="0" y="1064" width="360" height="42" rx="3" fill="var(--alt-soft)" stroke="var(--alt)"/>
<text x="180" y="1091" text-anchor="middle" font-size="12" font-weight="600" fill="var(--alt)">Out_for_Delivery</text>
<!-- right branch -->
<rect x="620" y="1002" width="360" height="42" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="800" y="1029" text-anchor="middle" font-size="11.5" fill="var(--ink)">inbound scan → <tspan font-family="'JetBrains Mono', monospace" font-size="10.5">Inwarded_at_Hub</tspan></text>
<line x1="800" y1="1046" x2="800" y2="1062" stroke="currentColor" stroke-width="1.4" marker-end="url(#ar)"/>
<rect x="620" y="1064" width="360" height="42" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="800" y="1091" text-anchor="middle" font-size="11.5" fill="var(--ink)">tripsheet loaded → <tspan font-family="'JetBrains Mono', monospace" font-size="10.5">In_Transit</tspan></text>
<line x1="800" y1="1108" x2="800" y2="1124" stroke="currentColor" stroke-width="1.4" marker-end="url(#ar)"/>
<rect x="620" y="1126" width="360" height="42" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="800" y="1153" text-anchor="middle" font-size="11.5" fill="var(--ink)">destination hub arrive → assign-miler</text>
<!-- ============ STAGE 7 ============ -->
<path d="M180,1108 V1206 H420" fill="none" stroke="var(--alt)" stroke-width="1.5" marker-end="url(#ar-t)"/>
<path d="M800,1170 V1206 H560" fill="none" stroke="currentColor" stroke-width="1.5" marker-end="url(#ar)"/>
<rect x="290" y="1182" width="540" height="66" rx="3" fill="var(--surface-2)" stroke="var(--ink)" stroke-width="2"/>
<text x="560" y="1210" text-anchor="middle" font-size="14" font-weight="600" fill="var(--ink)">OTP verified → DeliveryProof stored</text>
<text x="560" y="1231" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">signature · photo · geolocation → Delivered</text>
</g>
</svg>
</div>
<figcaption><b>Figure 1 — the lifecycle</b>The booking is a promise to collect; the consignment is a parcel in custody. <code>pickup-complete</code> is the pivot between them, and the only place the OTP is minted. The teal branch never touches a hub.</figcaption>
</figure>
<ol class="stages">
<li><span class="n">01</span><span><b>Acquisition.</b> <span class="d">A rep registers a business on site; the survey coordinates are the proof they stood there. The lead converts to a Tenant with its own rate card and depot sites.</span></span></li>
<li><span class="n">02</span><span><b>Origination.</b> <span class="d">Three doors, one object. <code>Bookingsource</code> is the only thing that distinguishes them.</span></span></li>
<li><span class="n">03</span><span><b>Dispatch.</b> <span class="d">Nearby available riders from Redis, scored by Claude with reasoning logged, degrading to greedy-nearest after five seconds.</span></span></li>
<li><span class="n">04</span><span><b>First mile.</b> <span class="d">Accept, reach, collect — each an idempotent state transition, because riders work on flaky mobile networks.</span></span></li>
<li><span class="n">05</span><span><b>Custody handoff.</b> <span class="d">Booking becomes Consignment. A six-digit code goes to the receiver and to nobody else.</span></span></li>
<li><span class="n">06</span><span><b>Route fork.</b> <span class="d">Matching 3-digit pincode prefixes skip the hub entirely. Everything else rides a tripsheet.</span></span></li>
<li><span class="n">07</span><span><b>Close.</b> <span class="d">The consignee reads six digits aloud. Nothing else marks a parcel delivered.</span></span></li>
</ol>
</section>
<!-- ============ 2 ============ -->
<section>
<div class="sec-head">
<span class="sec-num">02</span>
<div><h2>Where the model and the code disagree</h2></div>
</div>
<p class="measure">A milk round has five properties, and all five are anti-on-demand: a fixed beat, a rider who owns it, standing orders that recur by default, a promised slot, and density economics. Stage 03 above is built on the opposite premise.</p>
<figure>
<div class="fig-scroll">
<svg viewBox="0 0 1120 452" role="img" aria-label="Left: today, four parcels each independently scored and assigned to whichever rider is nearest, producing crossing assignment lines and a rider set that changes per parcel. Right: the milkman model, one rider owning one ordered sequence of stops that repeats daily, with a substitution table covering absence.">
<defs>
<marker id="ar2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="currentColor"/>
</marker>
<marker id="ar2t" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="var(--alt)"/>
</marker>
</defs>
<g font-family="'Source Sans 3', sans-serif" color="var(--line-2)">
<!-- divider -->
<line x1="560" y1="0" x2="560" y2="452" stroke="var(--line)" stroke-width="1" stroke-dasharray="3 4"/>
<!-- LEFT -->
<text x="0" y="22" font-family="'JetBrains Mono', monospace" font-size="11" font-weight="600" letter-spacing="1.1" fill="var(--accent-ink)">TODAY · ASSIGNED JOB</text>
<text x="0" y="44" font-size="13.5" fill="var(--ink-2)">Each parcel scored on its own. The rider set is</text>
<text x="0" y="62" font-size="13.5" fill="var(--ink-2)">re-drawn from scratch every single time.</text>
<rect x="0" y="104" width="74" height="34" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/><text x="37" y="126" text-anchor="middle" font-size="11.5" fill="var(--ink)">P1</text>
<rect x="0" y="154" width="74" height="34" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/><text x="37" y="176" text-anchor="middle" font-size="11.5" fill="var(--ink)">P2</text>
<rect x="0" y="204" width="74" height="34" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/><text x="37" y="226" text-anchor="middle" font-size="11.5" fill="var(--ink)">P3</text>
<rect x="0" y="254" width="74" height="34" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/><text x="37" y="276" text-anchor="middle" font-size="11.5" fill="var(--ink)">P4</text>
<rect x="152" y="104" width="128" height="184" rx="3" fill="var(--accent-soft)" stroke="var(--accent)"/>
<text x="216" y="182" text-anchor="middle" font-size="12" font-weight="600" fill="var(--accent-ink)">GEOSEARCH</text>
<text x="216" y="200" text-anchor="middle" font-size="12" font-weight="600" fill="var(--accent-ink)">+ scoring</text>
<line x1="78" y1="121" x2="148" y2="150" stroke="currentColor" stroke-width="1.2" marker-end="url(#ar2)"/>
<line x1="78" y1="171" x2="148" y2="180" stroke="currentColor" stroke-width="1.2" marker-end="url(#ar2)"/>
<line x1="78" y1="221" x2="148" y2="212" stroke="currentColor" stroke-width="1.2" marker-end="url(#ar2)"/>
<line x1="78" y1="271" x2="148" y2="242" stroke="currentColor" stroke-width="1.2" marker-end="url(#ar2)"/>
<circle cx="430" cy="121" r="19" fill="var(--surface)" stroke="var(--line-2)"/><text x="430" y="126" text-anchor="middle" font-size="11" fill="var(--ink)">R1</text>
<circle cx="430" cy="171" r="19" fill="var(--surface)" stroke="var(--line-2)"/><text x="430" y="176" text-anchor="middle" font-size="11" fill="var(--ink)">R2</text>
<circle cx="430" cy="221" r="19" fill="var(--surface)" stroke="var(--line-2)"/><text x="430" y="226" text-anchor="middle" font-size="11" fill="var(--ink)">R3</text>
<circle cx="430" cy="271" r="19" fill="var(--surface)" stroke="var(--line-2)"/><text x="430" y="276" text-anchor="middle" font-size="11" fill="var(--ink)">R4</text>
<line x1="284" y1="150" x2="407" y2="221" stroke="var(--accent)" stroke-width="1.3" marker-end="url(#ar2)" color="var(--accent)"/>
<line x1="284" y1="180" x2="407" y2="121" stroke="var(--accent)" stroke-width="1.3" marker-end="url(#ar2)" color="var(--accent)"/>
<line x1="284" y1="212" x2="407" y2="271" stroke="var(--accent)" stroke-width="1.3" marker-end="url(#ar2)" color="var(--accent)"/>
<line x1="284" y1="242" x2="407" y2="171" stroke="var(--accent)" stroke-width="1.3" marker-end="url(#ar2)" color="var(--accent)"/>
<text x="0" y="336" font-size="12.5" fill="var(--muted)">No Route, Beat or Round entity exists in</text>
<text x="0" y="354" font-size="12.5" fill="var(--muted)">doormile_backend — only Tripsheet, which is</text>
<text x="0" y="372" font-size="12.5" fill="var(--muted)">a hub-to-hub manifest, not a delivery beat.</text>
<text x="0" y="412" font-family="'JetBrains Mono', monospace" font-size="11" fill="var(--accent-ink)">stops per rider: unstable, day to day</text>
<!-- RIGHT -->
<text x="600" y="22" font-family="'JetBrains Mono', monospace" font-size="11" font-weight="600" letter-spacing="1.1" fill="var(--alt)">MILK ROUND · OWNED BEAT</text>
<text x="600" y="44" font-size="13.5" fill="var(--ink-2)">One rider, one ordered sequence, unchanged</text>
<text x="600" y="62" font-size="13.5" fill="var(--ink-2)">tomorrow. Familiarity is the cost saving.</text>
<rect x="600" y="168" width="104" height="56" rx="3" fill="var(--alt-soft)" stroke="var(--alt)"/>
<text x="652" y="194" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--alt)">Rider A</text>
<text x="652" y="211" text-anchor="middle" font-size="10" fill="var(--alt)">owns beat 7</text>
<path d="M708,196 H752" fill="none" stroke="var(--alt)" stroke-width="1.5" marker-end="url(#ar2t)"/>
<path d="M782,196 H842 M872,196 H932 M962,196 H1022" fill="none" stroke="var(--alt)" stroke-width="1.5" marker-end="url(#ar2t)"/>
<circle cx="767" cy="196" r="19" fill="var(--alt-soft)" stroke="var(--alt)"/><text x="767" y="201" text-anchor="middle" font-size="11.5" font-weight="600" fill="var(--alt)">1</text>
<circle cx="857" cy="196" r="19" fill="var(--alt-soft)" stroke="var(--alt)"/><text x="857" y="201" text-anchor="middle" font-size="11.5" font-weight="600" fill="var(--alt)">2</text>
<circle cx="947" cy="196" r="19" fill="var(--alt-soft)" stroke="var(--alt)"/><text x="947" y="201" text-anchor="middle" font-size="11.5" font-weight="600" fill="var(--alt)">3</text>
<circle cx="1037" cy="196" r="19" fill="var(--alt-soft)" stroke="var(--alt)"/><text x="1037" y="201" text-anchor="middle" font-size="11.5" font-weight="600" fill="var(--alt)">4</text>
<path d="M1037,219 V262 H767 V219" fill="none" stroke="var(--alt)" stroke-width="1.3" stroke-dasharray="5 4" marker-end="url(#ar2t)"/>
<rect x="826" y="250" width="152" height="24" rx="3" fill="var(--surface)" stroke="var(--alt)"/>
<text x="902" y="267" text-anchor="middle" font-size="10.5" fill="var(--alt)">repeats tomorrow</text>
<rect x="600" y="316" width="474" height="62" rx="3" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="618" y="340" font-size="12.5" font-weight="600" fill="var(--ink)">rider_substitutions</text>
<text x="618" y="360" font-size="11.5" fill="var(--muted)">absent rider → sub rider, per tenant, per date</text>
<text x="1056" y="352" text-anchor="end" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--alt)">jupiter</text>
<text x="600" y="412" font-family="'JetBrains Mono', monospace" font-size="11" fill="var(--alt)">stops per rider: stable, learned, dense</text>
</g>
</svg>
</div>
<figcaption><b>Figure 2 — the seam</b>The right-hand pattern only needs a substitution table because a round has an owner. Doormile has no owner and no round; <code>backend_jupiter</code> has both. Two lineages, one repo.</figcaption>
</figure>
<div class="callout">
<span class="label">The tell</span>
<p><code>rider_substitutions</code> — absent rider, substitute rider, per tenant, per date, with a scheduled → active → completed lifecycle. On-demand dispatch has no concept of an absent rider; you simply assign someone else. That table can only exist if a specific person owns a specific round.</p>
</div>
<div class="table-scroll">
<table>
<thead>
<tr><th>Milk-round property</th><th>In Doormile today</th><th>Where it lives</th></tr>
</thead>
<tbody>
<tr><td>Promised time slot</td><td class="yes">Present</td><td><code>Preferredpickupfrom</code> / <code>Preferredpickupto</code> on <code>PickupBooking</code></td></tr>
<tr><td>Recurring account shape</td><td class="yes">Present</td><td>CRM captures <code>shipping_frequency</code>, <code>parcel_volume</code>, <code>active_contracts</code></td></tr>
<tr><td>Depot / dairy origin</td><td class="yes">Present</td><td><code>Tenant</code> → <code>TenantLocation</code>, per-site reporting on <code>Tenantlocationid</code></td></tr>
<tr><td>Dense local drop</td><td class="yes">Present</td><td>3-digit pincode prefix match skips the hub entirely</td></tr>
<tr><td>Day's order book in one drop</td><td class="yes">Present</td><td><code>POST /admin/expressbooking/bulk</code></td></tr>
<tr><td>Round ownership</td><td class="no">Absent</td><td>only <code>rider_substitutions</code>, and only in <code>backend_jupiter</code></td></tr>
<tr><td>Beat with ordered stops</td><td class="no">Absent</td><td>no Route / Beat / Round model — <code>Tripsheet</code> is linehaul only</td></tr>
<tr><td>Standing orders</td><td class="no">Absent</td><td>the only "subscription" in the backend is NATS message subscriptions</td></tr>
<tr><td>Waves as delivery rounds</td><td class="no">Absent</td><td>frontend display filter in <code>utils/batchBucket.js</code> — see below</td></tr>
</tbody>
</table>
</div>
</section>
<!-- ============ 3 ============ -->
<section>
<div class="sec-head">
<span class="sec-num">03</span>
<div><h2>Why a wave isn't a round</h2></div>
</div>
<p class="measure">Morning / Afternoon / Evening look like rounds. They aren't. They bucket on <code>orderdate</code> — when the order was <em>placed</em> — so a wave describes arrival, not departure. The dispatch folder's own <code>CLAUDE.md</code> records that the promised-delivery field was tried here and deliberately rejected.</p>
<figure>
<div class="fig-scroll">
<svg viewBox="0 0 1120 330" role="img" aria-label="A 24-hour timeline split into morning, afternoon and evening bands. One parcel placed at 12:22 with a promised delivery of 16:30 falls in the Afternoon bucket when bucketed on order date, but in the Evening bucket when bucketed on expected delivery time.">
<defs>
<marker id="ar3" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="currentColor"/>
</marker>
</defs>
<g font-family="'Source Sans 3', sans-serif" color="var(--line-2)">
<!-- bands: x = 60 + hour*42 (24h -> 1008px) -->
<rect x="60" y="56" width="378" height="52" fill="var(--surface-2)" stroke="var(--line-2)"/>
<rect x="438" y="56" width="294" height="52" fill="var(--surface-2)" stroke="var(--line-2)"/>
<rect x="732" y="56" width="336" height="52" fill="var(--surface-2)" stroke="var(--line-2)"/>
<text x="249" y="87" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">Morning · 0–9</text>
<text x="585" y="87" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">Afternoon · 9–16</text>
<text x="900" y="87" text-anchor="middle" font-size="12.5" font-weight="600" fill="var(--ink)">Evening · 16–24</text>
<text x="60" y="42" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">12 AM</text>
<text x="438" y="42" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">9 AM</text>
<text x="732" y="42" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">4 PM</text>
<text x="1068" y="42" text-anchor="middle" font-size="10.5" font-family="'JetBrains Mono', monospace" fill="var(--muted)">12 AM</text>
<!-- row A: orderdate -->
<text x="0" y="168" font-size="12.5" font-weight="600" fill="var(--accent-ink)">bucket on orderdate</text>
<text x="0" y="185" font-size="11" fill="var(--muted)">current</text>
<line x1="60" y1="176" x2="1068" y2="176" stroke="var(--line)" stroke-width="1"/>
<circle cx="578" cy="176" r="7" fill="var(--accent)"/>
<text x="578" y="207" text-anchor="middle" font-size="11.5" font-family="'JetBrains Mono', monospace" fill="var(--accent-ink)">placed 12:22</text>
<line x1="578" y1="112" x2="578" y2="166" stroke="var(--accent)" stroke-width="1.3" stroke-dasharray="4 3"/>
<text x="600" y="132" font-size="11.5" font-weight="600" fill="var(--accent-ink)">→ Afternoon</text>
<!-- row B: expecteddeliverytime -->
<text x="0" y="264" font-size="12.5" font-weight="600" fill="var(--alt)">bucket on expecteddeliverytime</text>
<text x="0" y="281" font-size="11" fill="var(--muted)">jupiter · rejected here</text>
<line x1="60" y1="272" x2="1068" y2="272" stroke="var(--line)" stroke-width="1"/>
<circle cx="753" cy="272" r="7" fill="var(--alt)"/>
<text x="753" y="303" text-anchor="middle" font-size="11.5" font-family="'JetBrains Mono', monospace" fill="var(--alt)">promised 16:30</text>
<path d="M753,262 V232 H820" fill="none" stroke="var(--alt)" stroke-width="1.3" stroke-dasharray="4 3" marker-end="url(#ar3)" color="var(--alt)"/>
<text x="828" y="236" font-size="11.5" font-weight="600" fill="var(--alt)">→ Evening</text>
<line x1="578" y1="176" x2="753" y2="272" stroke="var(--muted)" stroke-width="1" stroke-dasharray="2 4"/>
</g>
</svg>
</div>
<figcaption><b>Figure 3 — the same parcel, two waves</b>Only the lower one describes when the parcel goes out. Bucketing on arrival is correct for a courier queue and wrong for a round — which is why the windows inherited from jupiter had to be widened to cover all 24 hours once the field changed.</figcaption>
</figure>
<p class="measure">There is a second constraint recorded in that file worth carrying forward: the filter field and the bucket field must be the same field. An earlier attempt to admit rows on <code>updatedat</code> while bucketing on <code>createdat</code> put six orders in the Evening batch at 10:41 in the morning on a day with no orders at all.</p>
</section>
<!-- ============ 4 ============ -->
<section>
<div class="sec-head">
<span class="sec-num">04</span>
<div><h2>What a native milk round would need</h2></div>
</div>
<p class="measure">Four additions, roughly in dependency order. None of them fight the existing pipeline — the consignment, OTP and proof-of-delivery machinery downstream of stage 05 stays exactly as it is.</p>
<ol class="stages">
<li><span class="n">01</span><span><b>A <code>Beat</code> entity</b> <span class="d">— an ordered stop sequence owned by a tenant and a hub, with a slot window. This is the object the whole model is missing.</span></span></li>
<li><span class="n">02</span><span><b>Rider-to-beat assignment that persists</b> <span class="d">— across days, not per parcel. <code>rider_substitutions</code> in jupiter is already the covering mechanism; it just needs a beat to point at.</span></span></li>
<li><span class="n">03</span><span><b>A standing-order generator</b> <span class="d">— materialises tomorrow's bookings from a recurrence rule against a <code>TenantLocation</code>, so the day's round exists before anyone books it.</span></span></li>
<li><span class="n">04</span><span><b>Bucketing moved to the slot field</b> <span class="d">— at which point the original clustered windows become correct again, and a wave finally means a departure.</span></span></li>
</ol>
<div class="callout">
<span class="label">What the AI layer becomes</span>
<p>Dispatch stops choosing a rider per parcel and starts doing two different jobs: <b>sequencing</b> a beat's stops before it goes out, and handling <b>exceptions</b> — the overflow parcel, the absent rider, the address that doesn't fit today's round. That is a smaller, sharper problem than scoring every candidate on every booking, and it preserves the density that makes the model pay.</p>
</div>
</section>
<footer>
Traced from <code>DOORMILE_LOGISTICS_ARCHITECTURE.md</code> and read against <code>doormile_backend</code>, <code>doormile_express_console</code> and <code>backend_jupiter</code> in the nearle-de workspace. Endpoint names and status strings are as they appear in the Go models and route definitions.
</footer>
</div>
</body>
</html>