677 lines
43 KiB
HTML
677 lines
43 KiB
HTML
<!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 & 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 & 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>
|