Customer-Brain + Flexible Routing — ເອກະສານເຕັກນິກສຳລັບ developer
ລວມທຸກຢ່າງໃນບ່ອນດຽວ: ສະຖາปัດຕະຍະກຳ loop 4 ຂັ້ນແມັບກັບໂມດູນຈິງ, data-model reality, gap analysis R1–R10, ການແກ້ Block A (schema/migration/diff + file:line), ແຜນ build, demo-vs-real-code + enum contract, snapshot delta, seed data, landmines, ແລະ decisions. ສະບັບນີ້ລວມ Track B — ແອปຄนขับ mobile (V2) ເຂົ້າແຜนเดียวกัน ตามมติรวมงาน (§5c). ສຳລັບ dev ທີ່ຮັບ branch ໄປສ້າງຕໍ່.
01 ສະຖາປັດຕະຍະກຳ — North-Star loop 4 ຂັ້ນ ແມັບໂມດູນຈິງ
North Star: "ຄົນຂັບບັນທຶກເທື່ອດຽວຕອນເຮັດວຽກ → ຫຼັງບ້ານເຫັນຄວາມຈິງ real-time → ຮອບຕໍ່ໄປ plan ແມ່ນຂຶ້ນເອງ." ວົງຈอน stage4→1→2 ປັດຈຸບັນ ຂາດ (ບໍ່ persist recency, ບໍ່ເກັບ GPS ຕอนส่ง).
| Stage | ໂມດູນຈິງ (live repo) | ສະຖານະ loop |
|---|---|---|
| 1 · Customer brain | modules/customers/* + customer-priorities/types/groups · model Customer (schema:856) | master-data ຄົບ · ຂາດ field ປັນຍາ (lastReceivedDate/satisfaction/stars/grade) |
| 2 · Plan | delivery-runs/services/delivery-dispatch.service.ts (autoGenerateForDate, listEligibleCustomersForZone) + orders.generateForTenant + customer-subscriptions | auto-gen ຕາມ calendar ມີ · blind ຕໍ່ delivery history — ບໍ່ມີ lapse/overdue/cadence |
| 3 · Field delivery | delivery-runs/services/driver-day.service.ts · deliver-stop-modal.tsx · findTodayForDriver | dispatch→driver ໃຊ້ໄດ້ · sequencing manual (reorderStops) |
| 4 · Record-once | delivery-runs/services/delivery-operations.service.ts → deliverOrder() (~1129–2176) | ແຂງແຮງ POD+payment+bottles 1 tx · Block A: ເພີ່ມ GPS + lastReceivedDate |
Customer.lastReceivedDate ໃນ tx ດຽວກັນກັບ deliverOrder ທີ່ມີຢູ່ແລ້ວ — ນີ້ຄື Block A0 (ดู §4). ທຸກຢ່າງໃນ A/B/C ຕໍ່ຍอดจากจุดนี้.02 Data-model reality
ສະຖານະຈິງຂອງແຕ່ລະ field (verify ກັບ backend/prisma/schema.prisma) ແລະ ສ່ວນທີ່ Block A ກ່ຽວຂ້ອງ.
| Field / capability | State | ບ່ອນຢູ່ຈິງ / "absent" | ການແກ້ |
|---|---|---|---|
Customer.lastReceivedDate | Block A | ບໍ່ມີມາກ່อน · ມີແຕ່ Order.deliveredAt (:1126) computed read-time | Block A: DateTime? + index + backfill |
Customer.deliveryCount | Block A | ບໍ່ມີ — totalPurchaseCount ປົນ counter-sale + ນັບຊ້ຳ | Block A: Int @default(0), guard !isReRecord |
DeliveryRunOrder.deliveringUserId | Block A | ບໍ່ມີ — pay ผูก crew[0] | Block A: String? @db.Uuid + FK SetNull + index |
GPS deliveredLatitude/Longitude | Block A | schema:1739-1740 ມีก่อน ແຕ່ບໍ່ມີໃຜຂຽນຜ່ານ deliverOrder (updateStopStatus = trap) | Block A: ຂຽນໃນ deliverOrder ໂດຍກົງ |
Customer.satisfaction (1-5) | MISSING | ບໍ່ມີທີ່ໃດ | deferred (Service Score input · D4) |
Customer.stars (1-5) | MISSING | ບໍ່ມີ grade column | deferred (computed/locked · D4) |
| importance (S/A/B/C) | PARTIAL | customerPriorityId → CustomerPriority (:1561) ແລະ customerType="VIP" = 2 ສັນຍານຊ້ອນ | D3: ເລືອກ customerPriorityId ເປັນ grade ຫຼັກ |
route geometry ເທິง DeliveryRun | MISSING | (:1285) ມີແຕ່ branch/vehicle/zone/date/status · ບໍ່ມີ color/day/name/geometry | Week 3: entity Route + DeliveryRun.routeId? |
Vehicle.capacity (CVRP) | มีแต่ตาย | (:1624) Int? ແກ້ໄດ້ใน UI ແຕ່ບໍ່ມີໃຜ read · scalar ດຽວ | Block C: CVRP ຕ້ອງເພີ່ມ volume/slot ຕໍ່ Product |
| demand ຕໍ່ລູກຄ້າ | EXISTS | OrderItem.quantity (:1250) + CustomerSubscriptionItem.quantity (:1921) | input ດຽວທີ່ພ້อมจริง |
03 Gap analysis R1–R10
effort: S=ມື້ · M=1-2 ອາທິດ · L=multi-week.
| R | requirement | state | implementing modules | ຂາດຫຍັງ | eff | blk |
|---|---|---|---|---|---|---|
| R1 | customer-brain fields + GPS | Block A | Customer · CUSTOMER_SELECT customers.service.ts:33-138 | satisfaction/stars (deferred) | M | A |
| R2 | lapse tiers 7/14/20/30 → relative cadence | partial | recency-face.tsx (7/14 ຫຍາບ) | tier helper + median-gap cadence | M | A/C |
| R3 | multi-route draw (color/day/palette) | missing | — | Route entity + line-draw + day/color filter | L | A |
| R4 | corridor reveal (incl cross-branch) | missing | Branch.boundary (:462) · ops-map.tsx | near-line query + multi-attr pin · cross-branch carve-out | L | A/B |
| R5 | plan-gen 5 steps (who's-due → seq) | partial | listEligibleCustomersForZone · isSubscriptionDueOnDate (shared:675) · autoGenerateForDate (:866) | selection = calendar-only · step4 manual | L | A/C |
| R6 | cross-branch rescue ≠ poach | partial | overdue LATERAL orders.service.ts:664-755 | ownership/release/claim model · 30-min auto-release | L | B |
| R7 | pay-follows-work | partial | PaymentCollection.driverId (:2294) · Settlement (:2326) · LoyaltyTransaction (:2928) | credit collection-based · R7-2 driverId attribution · Settlement สร้างใน DriverDayService (standalone create ลบ) | L | B |
| R8 | Service Score (computed + audited) | missing | ບໍ່ມี model · raw signals ກະຈາຍ | ທັງ engine · ບໍ່ມี SLA baseline · complaint ບໍ່ link driver | L | B |
| R9 | record-once POD+GPS+ชำระ+ถัง + recency | Block A | deliverOrder (:1129-2176) | ขาด GPS+lastReceivedDate (Block A) | S–M | A |
| R10 | optimizer (OR-Tools + OSRM + CVRP) | missing | ບໍ່ມี · reorderStops manual | ທັງ optimizer · OSRM ບໍ່ມีใน repo (Google Maps) | L | C |
04 Block A — ການແກ້ໂຄ້ດ (schema / migration / diff)
ການແກ້ໃນ branch feat/a0-customer-brain-capture — files + migration + diff ດ້ານລຸ່ມ. (tsc --noEmit ຜ່ານ 0 error · otplib 12 + @sentry/node 10 ຕິດຕັ້ງຄົບ ບໍ່ມີ type/peer conflict — ບໍ່ແມ່ນບັນຫາແລ້ວ)
| file | Δ | ສິ່ງທີ່ทำ |
|---|---|---|
backend/prisma/schema.prisma | +44/−27* | Customer/DeliveryRunOrder/User (* −27 = prisma format realign) |
…/delivery-runs.controller.ts | +18 | DeliverOrderDto +lat/lng validators |
…/delivery-runs.service.ts | +3 | wrapper dto type +GPS |
…/services/delivery-operations.service.ts | +34 | GPS+actor · lastReceivedDate+deliveryCount · pay |
…/migrations/20260622090000_customer_brain_capture/migration.sql | new | DDL + FK + index + backfill |
4.1 · schema diff
// Customer-brain capture (Block A) lastReceivedDate DateTime? deliveryCount Int @default(0) ... @@index([tenantId, lastReceivedDate])
deliveringUserId String? @db.Uuid ... deliveringUser User? @relation("DeliveryRunOrderDeliveredBy", fields: [deliveringUserId], references: [id], onDelete: SetNull) ... @@index([deliveringUserId])
deliveredRunOrders DeliveryRunOrder[] @relation("DeliveryRunOrderDeliveredBy")
4.2 · migration SQL
ALTER TABLE "Customer" ADD COLUMN "lastReceivedDate" TIMESTAMP(3); ALTER TABLE "Customer" ADD COLUMN "deliveryCount" INTEGER NOT NULL DEFAULT 0; ALTER TABLE "DeliveryRunOrder" ADD COLUMN "deliveringUserId" UUID; CREATE INDEX "Customer_tenantId_lastReceivedDate_idx" ON "Customer"("tenantId", "lastReceivedDate"); CREATE INDEX "DeliveryRunOrder_deliveringUserId_idx" ON "DeliveryRunOrder"("deliveringUserId"); ALTER TABLE "DeliveryRunOrder" ADD CONSTRAINT "DeliveryRunOrder_deliveringUserId_fkey" FOREIGN KEY ("deliveringUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; -- backfill: lastReceivedDate = latest COMPLETED delivery (walk-ins excluded) UPDATE "Customer" c SET "lastReceivedDate" = sub.maxd FROM (SELECT "customerId", MAX("deliveredAt") AS maxd FROM "Order" WHERE "status"='COMPLETED' AND "deliveredAt" IS NOT NULL AND "customerId" IS NOT NULL GROUP BY "customerId") sub WHERE c."id" = sub."customerId";
npx prisma migrate deploy (ບໍ່ຕ້อง shadow DB). ຖ້າຈะใช้ migrate dev ຕ້องสร้าง loo_shadow ກ່อน.4.3 · service diff — deliverOrder()
data: {
status: DELIVERY_RUN_ORDER_STATUS.DELIVERED,
deliveredAt: new Date(), attemptedAt: new Date(),
deliveringUserId: currentUser.sub, // who actually delivered
...(dto.podPhotoUrl !== undefined ? { podPhotoUrl: dto.podPhotoUrl || null } : {}),
...(dto.podSignatureUrl !== undefined ? { podSignatureUrl: dto.podSignatureUrl || null } : {}),
...(dto.deliveredLatitude !== undefined ? { deliveredLatitude: dto.deliveredLatitude } : {}),
...(dto.deliveredLongitude !== undefined ? { deliveredLongitude: dto.deliveredLongitude } : {}),
},
data: {
totalPurchaseCount: { increment: 1 }, isFirstTimeCustomer: false,
// FIRST delivery only — re-record (admin correction) ບໍ່ advance/ບໍ່ນັບຊ້ຳ
...(isReRecord ? {} : { lastReceivedDate: new Date(), deliveryCount: { increment: 1 } }),
},
orderId, driverId: order.deliveryRun?.crew?.[0]?.userId ?? null, // OLD: crew[0] driverId: currentUser.sub, // NEW: acting user (pay-follows-work)
@IsOptional() @Type(() => Number) @IsNumber() @Min(-90) @Max(90) deliveredLatitude?: number; @IsOptional() @Type(() => Number) @IsNumber() @Min(-180) @Max(180) deliveredLongitude?: number;
updateStopStatus ໄດ້ບ່ອນດຽວ ซึ่ง ปฏิเสธ DELIVERED ຖ້າ order ຍັງບໍ່ COMPLETED (delivery-stops.service.ts:398-405) → ຂຽນໃນ deliverOrder ໂດຍກົງ. idempotencyKey (H1) ครอบ tx ຢູ່ແລ້ว → field ใหม่ retry-safe ฟรี.05 ແຜນ build 4 ອາທິດ (Block A end-to-end + B/C groundwork)
1 ເດືອນ ship Block A + ວາງ data-capture ໃຫ້ B/C. OUT: Service Score engine ເຕັມ, cross-branch rescue, OSRM/OR-Tools optimizer (gate: GPS≥80% + ≥8 verified rounds — ວັນนี้ວັດบ่ได้).
ມື້ 1 · keystone A0 ✅ DONE
schema + migration + GPS/actor write + lastReceivedDate/deliveryCount + R7-2 pay. (ดู §4) · ของจริงทำใน 1 วัน (แผนเดิม 5.5) → นำหน้า ~4 วัน
ມື້ 2 · tests + PR
A0-spec-extend — jest: GPS present/absent · lastReceived/re-record · idempotency replay · walk-in skip + QC/loo-check + PR-A0/R7-2 (ที่ยังเหลือจาก Block A)
ມື້ 3-5 · brain reads + who's-due
FE one-shot getCurrentPosition · card อ่าน recency · listDueCustomers (CALENDAR∪OVERDUE∪LAPSED∪SUBLESS) + GET /due-customers · mount customer-subscriptions
ມື້ 6-9 · Route entity + draw
model Route + DeliveryRun.routeId? · module routes/ · route-LINE draw บน ops-map · route-lines.tsx (ROUTE_PALETTE)
ມື້ 10-13 · filter + pins + corridor + measure
day/route filter · pin (ring=route, fill=lapse) · customersNearRoute (bbox + point-near-line) · isVerifiedStop + coverage report
5.1 · task table (master checklist)
| id | title | blk | eff | wk |
|---|---|---|---|---|
A0-schema-lrd | Customer/DeliveryRunOrder cols + indexes | A | S | W1 |
A0-migration-backfill | migrate + backfill | A | M | W1 |
A0-dto-controller | DeliverOrderDto lat/lng | A | XS | W1 |
A0-svc-gps-write | GPS + deliveringUserId in tx | A | S | W1 |
A0-svc-lastreceived | lastReceivedDate + deliveryCount | A | S | W1 |
R7-2-pay-attribution | driverId = acting user | B-gw | S | W1 |
A0-spec-extend | spec: GPS/lastReceived/re-record/idempotency | A | M | W1 |
A1-api-payload | FE deliverOrder payload lat/lng | A | XS | W2 |
A1-modal-getposition | one-shot GPS at submit (best-effort) | A | M | W2 |
A1-brain-card | card reads persisted lastReceivedDate | A | S | W2 |
A2-1-cadence-fn | pure medianGapDays helper | A | S | W2 |
A2-2-due-query | listDueCustomers (calendar∪overdue∪lapsed∪subless) | A | L | W2 |
A2-3-endpoint | GET /due-customers + DTO + perm | A | S | W2 |
R5-1-mount-subs-list | mount customer-subscriptions controller | A | S | W2 |
A3-route-schema | Route entity + DeliveryRun.routeId | A | S | W3 |
A3-routes-module | routes/ CRUD (whereWithBranch + broadcast) | A | M | W3 |
A3-routes-api-hooks | FE Route api + SWR hooks | A | S | W3 |
A3-route-draw-tool | route-LINE draw (single active tool) | A | L | W3 |
A3-route-render-layer | route-lines.tsx (ROUTE_PALETTE) | A | M | W3 |
A4-corridor-near-query | customersNearRoute (bbox + point-near-line) | A | L | W4 |
A4-day-color-filter | day + per-route on/off filter | A | M | W4 |
A4-route-pin-encoding | ring=route, fill=lapse + legend | A | M | W4 |
R8-1-verified-derivation | isVerifiedStop + per-driver verifiedPct | B-gw | M | W4 |
R8-2-signal-inventory | per-driver raw +/− counts (no score) | B-gw | L | W4 |
A0-* (schema/migration/backfill/GPS/lastReceived) + R7-2-pay-attribution = DONE ทำใน Day 1 (ตรวจในโค้ดจริง · commit e2d9b7d3 · working tree สะอาด) · เหลือเฉพาะ A0-spec-extend (ชุดทดสอบ = Day 2). งาน W2-W4 (A1-A4/R5/R8) ยังไม่เริ่ม.ເอกสารแผนเต็ม (migration plan, risk register 22 ข้อ, DoD): docs/FLEXIBLE_ROUTING_BUILD_PLAN.md
5b ตารางรายวัน + เวลา (day-by-day)
ประเมิน ~2 devs เต็มเวลา = ~4 สัปดาห์ (20 วันทำงาน) · 1 dev ≈ 8 สัปดาห์ · L=3–4 วัน · M=2 · S=1 · XS=0.5 · บางวันแยก 2 track (A/B) ขนาน · เวลาเป็นประมาณ
ມື້ 1 · A0 keystone (รากฐาน) ✓ DONE · ตรวจแล้ว
A0-schema-lrd — schema +lastReceivedDate/deliveryCount/deliveringUserId + index + prisma generate✓A0-migration-backfill — migrate + backfill MAX(deliveredAt) + verify บน DB✓A0-dto-controller + A0-svc-gps-write — DTO lat/lng + เขียน GPS+deliveringUserId ใน deliverOrder tx✓A0-svc-lastreceived + R7-2-pay-attribution — lastReceivedDate/deliveryCount (guard !isReRecord) + driverId=currentUser.sub✓e2d9b7d3—ມື້ 2 · ชุดทดสอบ A0 + PR ที่ยังเหลือ
A0-spec-extend — jest: GPS present/absent · lastReceived/re-record · idempotency replay · walk-in skip~1 วัน/loo-check + เปิด PR (PR-A0 / PR-R7-2)~½ วันມື້ 3-5 · สมองลูกค้า + ใครถึงรอบ ~3 วัน · 2 track
A1-api-payload + เริ่ม A1-modal-getposition · (B) A2-1-cadence-fn (medianGapDays)~1 วันA1-modal-getposition (จบ) + A1-brain-card (อ่าน recency) · (B) A2-2-due-query (CALENDAR∪OVERDUE∪LAPSED∪SUBLESS)~1 วันR5-1-mount-subs-list + R5-2-subless-feed · (B) A2-2 (จบ) + A2-3-endpoint (GET /due-customers) + integration/PR~1 วันມື້ 6-9 · Route entity + วาดเส้น ~4 วัน · 2 track
A3-route-schema — Route entity + DeliveryRun.routeId + migration + generate~1 วันA3-routes-module (เริ่ม) — CRUD, whereWithBranch, broadcast ROUTE_UPDATED~1 วันA3-routes-module (จบ) + A3-routes-api-hooks · (B) เริ่ม A3-route-render-layer~1 วันA3-route-draw-tool (Google Maps · single active tool) · (B) A3-route-render-layer (จบ · ROUTE_PALETTE) + ทดสอบ/PR-A3~1 วันມื้ 10-13 · กรอง + corridor + วัดผล ~4 วัน · 2 track
A4-corridor-near-query (bbox + point-near-line · in-branch) · (B) A4-day-color-filter~2 วันA4-route-pin-encoding — ring=route, fill=lapse + legend + idsKey memo~1 วันR8-1-verified-derivation (isVerifiedStop + verifiedPct) + R8-2-signal-inventory (เริ่ม) + /loo-check + PR-A4/R8 · L · อาจล้น +1-2 วัน~1 วันนำหน้าแผน: A0 keystone ของจริงทำ 1 วัน (แผนเดิม 5.5) → นำหน้า ~4 วัน. งานที่เหลือ (มื้ 2-13) ประเมินตามจริง ไม่บีบ ≈ ~12-13 วัน (Track A 2 devs). เวลาเป็น "ประมาณ" · ทำงาน 6 วัน/สัปดาห์ · ทุก PR ผ่าน /loo-check + tsc (CI จริง = tsc+jest+e2e · lint ยังไม่ gate). หมายเหตุ: ตารางนี้ Track A · Track B (แอปคนขับ) ดู §5c (เริ่มแล้ว ~25-30%).
5c Track B — Driver Mobile App (V2) · ລວມຕາมมติรวมงาน
มติหัวหน้า: รวมแอปคนขับ (เดโม V2_DRIVER_APP/app.html v0.22) เข้ากับงานสมองลูกค้า/routing → ตั้งเป้าส่งทั้งคู่ ~1 เดือน · scope = เต็มตามเดโมทุกฟีเจอร์ · ทีม 2 คนเท่าเดิม (ยอมรับว่าอาจล้น — ดู §5c.3).
loo-platform/driver/) ที่ spine หลัก เสร็จและต่อ backend จริงแล้ว: B0-mobile-shell · B0-pin-auth · B1-run-screen · B1-stop-screen (ชำระ CASH/QR/BANK + ถังคืน + รูป POD + GPS + confirm) DONE · B1-pretrip truck-load partial. เหลือ = map · wallet · close-shift · pod-receipt + ทั้ง B2/B3 · ~25-30% ของ Track B เสร็จแล้ว (ส่วนเสี่ยงสุด = delivery spine ทำก่อน) · กลยุทธ์เปลี่ยน: Capacitor-native ตั้งแต่ต้น ไม่ใช่ PWA-first.01_BACKEND_LINK_AUDIT.md ของเดโม (gap B1–B10 = "🔧 backend") เทียบกับ mock loop ในเดโม ไม่ใช่ repo จริง. ตรวจ repo จริงแล้ว — data-loop ของคนขับมีอยู่เกือบครบใน backend จริง (GPS report · POD พิกัด+รูป · deliver/fail/skip · PaymentCollection · AssetMovement/Balance ถังคืน · TruckLoad+close-variance · Settlement). งานที่เหลือจริงส่วนใหญ่ = frontend mobile shell + ต่อ endpoint ที่มีอยู่ + โมดูลใหม่ไม่กี่ตัว (PIN auth · fuel · leaderboard · POS-on-delivery · offline-queue · notify).5c.1 · Reconciliation — เดโมคนขับ ↔ โค้ดจริง (EXISTS / net-new)
| ความสามารถ | เดโมแสดง (app.html) | โค้ดจริง backend | งานที่เหลือจริง |
|---|---|---|---|
| GPS live คนขับ | gps_pings ทุก 20วิ | มี POST /delivery-runs/me/location · DriverLocation+DriverLocationHistory · hook use-driver-location-reporter.ts | FE: live pin บนแผนที่ + staleness display |
| POD พิกัด+รูป | snap()+confirmDeliver() | มี deliveredLatitude/Longitude · podPhotoUrl/podSignatureUrl · deliveringUserId (Block A) | DONE FE กล้อง+GPS+confirm ใน StopScreen |
| ส่ง / ส่งไม่ได้ / ข้าม | done/failSave/driveSkip | มี status PENDING/DELIVERED/FAILED/SKIPPED · enum DeliveryFailureCategory 8 เหตุ · /fail endpoint | FE: ผูกปุ่ม + เหตุผล dropdown |
| ชำระหน้างาน 4 แบบ | PAYMETA canonical | มี PaymentCollection · method CASH/QR_CODE/BANK_TRANSFER/CREDIT · driverId=actor (R7-2) | DONE FE picker ใน StopScreen · QR flow ยัง |
| ถังคืนแยกประเภท + มัดจำ | ret{barrel,crate} · deposit | มี AssetMovement/AssetBalance · emptyCollected · collect-empties/add-bottles | FE: ผูก UI · deposit=liability แยก |
| โหลดน้ำขึ้นรถ + เสียหาย | LOAD+SRC+DMGR | มี TruckLoad+TruckLoadItem · close-variance · TRUCK_LOAD/UNLOAD | FE: ผูก UI + damage capture |
| ปิดกะ + รายงานรายวัน | dailyReport mock | มี run close + Settlement · close-summary · close-variance | FE: driver close UI |
| สมองลูกค้า (lapse บนจอจุดส่ง) | drvLapse 7/14/20/30 | มี lastReceivedDate+deliveryCount (Block A) · GET /customer-card | DONE FE brain chip ใน StopScreen |
| login รหัส+PIN | mock PIN 4 หลัก | มี POST /auth/driver/login · module driver-auth · User.employeeCode/driverPinHash (mig 20260622100000) | DONE BE+FE keypad (driver/LoginScreen.tsx) · เหลือ PIN reset flow |
| POS ยิงขายหน้างาน | openSell/SaleOnSite | บางส่วน sell-water (web) แยก · มี add-order-items | net-new POS-on-delivery |
| แจ้งลูกค้า (กำลังไป/ส่งแล้ว/เลื่อน) | notifyCust mock | ไม่มี integration WhatsApp/SMS | net-new notify service |
| Leaderboard / แต้ม / แลกของ | gamification strip | ไม่มี (LoyaltyTransaction = ฝั่งลูกค้า) | net-new driver leaderboard+points |
| myPay รายได้คนขับ | myPay = adoption lever | ไม่มี (Settlement = cash close เท่านั้น) | net-new earnings aggregation |
| เติมน้ำมัน + OCR ใบเสร็จ | fuel modal | ไม่มี FuelLog | net-new fuel module + anomaly |
| SOS แจ้งเหตุ → ผู้จัดการ | fEvt 5 เหตุ | บางส่วน Issue model มี · ไม่ผูก delivery | net-new wire SOS → Issue+notify |
| offline-first + PWA | manifest.json+sw.js | ไม่มี PWA / offline queue | net-new PWA + offline-sync queue |
| แผนที่ | Leaflet 1.9.4 + OSRM | มี Google Maps @vis.gl/react-google-maps | โค้ดแผนที่ ทำใหม่บน Google Maps (reuse ไม่ได้) |
เดโม screen (8): login(emp+PIN) → shift/pre-trip 4 ขั้น(check-in→ยิง crew ขึ้นรถ→ตรวจรถ 7 จุด+ไมล์+รูป→โหลดน้ำ) → run(progress ring+เงินสด+ถัง+quickbar) → stop "จบในจอเดียว"(brain chips+stepper+promo+ชำระ 4+ถังคืน+มัดจำ+รูป+ยืนยัน) → POD(recap+ใบเสร็จ QR) → map → money(4 ก้อน drill-down) → me(leaderboard+points+redeem+myPay+ปิดกะ). หลักออกแบบ 3: ① บันทึกครั้งเดียวตอนทำงาน ② จบในจอเดียว ③ มือถือคือ GPS · + offline-first (เส้นตาย) · ปุ่มใหญ่มือเดียว ลาว 100%.
VARI_CATALOG 9 SKU (driver app · Sales KB v1.4): 18L=28,000₭ องค์กร 25k · บาดำ=20,000 · แก้ว=150,000/ลัง · 2L=51,000 · pH8+=88,000 · Mini=120,000 ⚠ ราคาขัดกับ products.json/DEMO_LOOP (Master Cost · Bou 2026-06-11: บาดำ 18k · 2L 38k · pH8+ 55k · Mini 85k · แก้ว 130k) → reconcile ก่อน lock · มัดจำ=liability ไม่บวกยอดขาย (first-principles) · promo VARI10/BULK5/BUY10GET1 · payment enum canonical (driver sync แล้ว · label ลาวคงเดิม ຄ້າງ(ໜີ້)) · ไอคอน mono SVG (ห้าม emoji) · ทุกปุ่ม P0 ต้องมี busy-guard (เดโมเจอบั๊ก confirmDeliver บวกเงินซ้ำ).5c.2 · Track B task list (wire-existing + net-new)
| id | title | kind | eff |
|---|---|---|---|
B0-mobile-shell | driver mobile route + bottom-nav 4 แท็บ + phone frame | DONE | L |
B0-pwa | manifest + service worker + offline shell · strategy → Capacitor-native · re-scope | net-new | M |
B0-pin-auth | emp-code + PIN login: BE /auth/driver/login + FE keypad เสร็จ · เหลือ reset flow | DONE | S |
B1-run-screen | GET /today/me → run (stops + progress) | DONE | M |
B1-stop-screen | stop "จบในจอเดียว": brain+pay(CASH/QR/BANK)+bottle+photo POD+GPS+confirm(idempotency) | DONE | L |
B1-pod-receipt | POD recap + ใบเสร็จ (logo+QR) · รูป POD เก็บใน stop แล้ว · เหลือหน้าใบเสร็จ | wire | M |
B1-driver-map | driver map บน Google Maps (route + live pin + จุดต่อไป) | rebuild | M |
B1-pretrip | โหลดน้ำ check (/pre-trip-check) เสร็จ · เหลือ check-in + ตรวจรถ 7 จุด + crew | partial | L |
B1-money-wallet | 4 ก้อน (cash/QR/transfer/ค้าง) drill-down | wire | S |
B1-close-shift | close run + settlement summary + ปิดกะ | wire | M |
B2-pos-ondelivery | ยิงขายหน้างาน → SaleOnSite (เชื่อม sell-water) | net-new | L |
B2-fuel-log | FuelLog model + endpoint + UI + anomaly สิ้นเปลือง | net-new | M |
B2-leaderboard | driver perf/points engine + leaderboard + redeem | net-new | L |
B2-mypay | driver earnings aggregation (verified-work-based) | net-new | M |
B2-sos-wire | SOS 5 เหตุ → Issue + notify ผู้จัดการ | net-new | M |
B2-notify-customer | otw/done/fail → WhatsApp/SMS service | net-new | M |
B2-offline-queue | offline delivery capture + sync-on-reconnect | net-new | L |
B3-extras | skip / incoming-banner / driving-mode / voice / onboarding tour | polish | M |
18 task · L=3.5 / M=2 / S=1 person-days → Track B เต็ม ≈ ~45 person-days. อัปเดต: spine (B0-shell/pin + B1-run/stop) เสร็จแล้ว ~25-30% → เหลือจริง ≈ ~32 pd (map · wallet · close-shift · pod-receipt + B2/B3). wire=ต่อ backend ที่มี (เร็ว) · net-new=สร้างโมดูลใหม่ (ช้า). ถ้าต้องตัดเพื่อ 1 เดือน → net-new (POS/fuel/leaderboard/mypay/offline/notify) คือกลุ่มที่เลื่อนได้.
5c.3 · capacity reality — 2 track เต็ม / 2 dev / 1 เดือน พอไหม?
| scope | person-days | 2 devs (44 pd/เดือน) |
|---|---|---|
| Track A เต็ม (สมองลูกค้า + routing + corridor + R8) | ~41 pd | ~4 สัปดาห์ |
| Track B เต็ม (แอปคนขับ ทุกฟีเจอร์) · spine done ~25-30% | ~45 pd (เหลือ ~32) | ~4 สัปดาห์ (เหลือ ~3) |
| รวม 2 track เต็ม | ~86 pd | ≈ 8 สัปดาห์ (~2 เดือน) |
B0-shell/pwa/pin + B1-* wire run/stop/pod/map/close + กล้อง/GPS). เลื่อนเฟสถัดไป: routing W3–4 (multi-route/corridor/optimizer) + Track B net-new (POS/fuel/leaderboard/myPay/offline-queue/notify). MVP นี้ = แอปคนขับใช้งานได้จริง pilot 2–3 คัน + สมองลูกค้าเริ่มเก็บ data.GPS ตลอดกะ (จอดับ/ในกระเป๋า) = ต้อง native. อัปเดต: แอป driver ตั้งเป็น Capacitor (Android) แล้ว (capacitor.config.ts · @capacitor/android) แต่ยังไม่ generate native project (driver/android/) และยังไม่มี offline queue/SW. เหลือ ~3–5 วัน setup + background-GPS plugin.
06 Demo vs real code — authoritative-truth map
ກฎ: demo = UX target · live repo = law. ຂັດກັນ → ຍຶດ live code. ເດໂມ (V2_DEMO_LOOP/index.html 2.18MB, V2_DRIVER_APP/app.html 271KB) = vanilla JS + Leaflet 1.9.4 + OSRM.
| concern | demo (aspirational) | live code (authoritative) |
|---|---|---|
| maps/routing | Leaflet + CartoDB + public OSRM (dpFetchRoadRoute, dpDrawSnap, lasso/polygon/corridor) | Google Maps (@vis.gl/react-google-maps ^1.8.3) → demo map code reuse ไม่ได้ |
| demo-only fns | dpNewPlan, DP_RUNS, dpZone, starsEl, dpPlanByDriver, dpClaim*, DP_SVCSCORE | ບໍ່ມีในโค้ดจริง (client-state ເດໂມ) |
| optimizer | dpPlanByDriver = CUSTOMERS.filter(zone).slice(0,5) + qty ປอม (lines 4231-4235) | ບໍ່ມี optimizer · reorderStops manual |
| Service Score | DP_SVCSCORE −1 in-memory counter | ບໍ່ມี model/persistence |
| live GPS trucks | 4 hardcoded trucks animate ตาม OSRM geometry | real driver GPS via POST /delivery-runs/me/location (ReportLocationDto) |
custLapseTier + driver drvLapse คำนวณ lapse จาก lastReceivedDate ตรงเป๊ะ · driver snap() = real geolocation + confirmDeliver() stamp coords + LAST_RECV=now (reset lapse). หลาย "missing backend" ใน audit (POD, gps_pings, fail+reason) มีใน live repo อยู่แล้ว.6.2 · เดโม web-loop (V2_DEMO_LOOP) — UX spec + constants ที่ลอกได้
เดโมหลังบ้าน V2_DEMO_LOOP/index.html (2.18MB · 60 รอบ design) = spec ของ ops-map งาน Track A. ลอก: ดีไซน์/สเกล/สี/flow/สูตร · ทำใหม่: ทุกอย่างที่แตะแผนที่ (Leaflet→Google Maps).
| เรื่อง | ค่าจากเดโม (honor) | task เรา |
|---|---|---|
| lapse สี (บัตรลูกค้า) | บันได 7/14/20/30 · เขียว #2BA372 / เหลือง #F5B544 / แดง #F2545B | A1-brain-card |
| lapse สี (บนแผนที่ · Bou แก้รอบ 43) | ต่างจากบัตร: เขียว≤7 (ซ่อน) · เหลือง 8–13 · แดง ≥14 · ม่วง #A78BFA = แจ้งขอวันนี้ | A4-route-pin-encoding · ถาม Bou ใช้สเกลไหน |
| ดาว (custScore) | 0.40·buy + 0.30·freq + 0.20·profit + 0.10·reputation → 1–5 ดาว | deferred (D4) |
| route palette (8 สี) | #2563EB #06B6D4 #14B8A6 #6366F1 #8B5CF6 #EC4899 #38BDF8 #A78BFA (DP_DRAW_PALETTE) | A3-route-render-layer (ROUTE_PALETTE) |
| corridor "ใกล้เส้น" | 200m · point-to-polyline · 111000 m/deg · cap 60 | A4-corridor-near-query |
| snap ลูกค้าใกล้สุด | ~660m threshold (dpNearestCust brute) · lasso cap 40 · polygon ray-casting cap 60 | A3-route-draw-tool |
| work process | "เลือก → จัด → ส่ง" (panel ①②③ · รอบ 56) | กรอบ UX ทั้ง Track A |
| ชื่อเมนูวาดเส้น | ວາງເສັ້ນ (ไม่ใช่ "วาดเส้น" · Bou เคาะ) | label ของ draw tool |
| rescue / Service Score | หมุดเงียบ (เหลือง/แดง/ม่วง) มีปุ่ม "ຄວ້າ·ຮັບໄປສົ່ง" · เขียว=ไม่มี (rescue≠poach) · คว้า→เจ้าของ −1 | Block B (เลื่อน · DP_SVCSCORE = in-memory เดโมล้วน) |
dpPlanByDriver=filter(zone).slice(0,5)) · 4 trucks hardcode animate ตาม OSRM. ทั้งหมดนี้คือ Block B/C ที่ยังต้องสร้าง backend จริง.6.1 · enum / data-contract (เสี่ยงสูงสุด — ต้อง honor)
String field + canonical comment (pipe-list) ไม่ใช่ Prisma enum. enum จริงใน schema มีตัวเดียวคือ DeliveryFailureCategory (:1234 · 8 ค่า). Order.status / DeliveryRunOrder.status / AssetMovement.* เก็บเป็น String + comment (เช่น order-status.constants.ts) — ค่าที่ระบุถูกต้อง แต่อย่าไปหา enum Order_status ใน schema.prisma (ไม่มี).| field | canonical (live) | note |
|---|---|---|
CustomerPayment.method | { CASH, QR_CODE, BANK_TRANSFER } (3 ค่า · payment-method.constants.ts) | "5-value enum" = reports-filter union ไม่ใช่ method. OUTSTANDING=Order.paymentStatus · SPONSORSHIP=Order.isSponsorship · doc CREDIT ເປັນ method ที่ 4 |
Order.status | PENDING / IN_PROGRESS / COMPLETED / CANCELLED | ບໍ່ມี DELIVERED · demo done/failed = local · live failDelivery → CANCELLED |
DeliveryRunOrder.status | PENDING / DELIVERED / FAILED / SKIPPED | enum ແຍກ · ຢ່າສັບສົນກັບ Order.status |
AssetMovement.movementType | { ISSUED, RETURNED, TRANSFERRED, DAMAGED, LOST, ADJUSTED } | seed asset_movements.csv ໃຊ້ enum ປອມ (DELIVERY_OUT/EMPTY_RETURN) → import ບໍ່ໄດ້ |
AssetMovement.bottleCondition | { GOOD, DAMAGED, EXPIRED, DIRTY, LEAKING } | seed ໃຊ້ FULL/EMPTY ປอม |
07 source_code/ snapshot vs LIVE repo
- snapshot cut ≈ 2026-06-08 (last migration
20260608000000_subscription_product_optional) — same project, additive subset. - live ahead 25 migrations (
20260609→20260622100000_driver_pin_auth) + 3 modules:customer-departments,health,stock-counts· zero snapshot-only items. - Block A lands ONLY in live.
deliveredLatitude/Longitudeມีก่อน Block A ใน 4 snapshot files. - live +
@sentry/node+otplib(MFA20260617120000) · schema 4,048 → 4,359 lines · 96 models · 153 migrations รวม (20260328→20260622) · 70 modules · 87 services ·tsc --noEmitผ่าน 0 error. source_code/docs/*ເກ່າກວ່າໂຄ້ดตัวเอง. อ่าน snapshot ເพื่อ orientation · ຢ່າแก้ · build บน live repo.
08 Seed dataset — _SEED_DATA/
- deterministic (
random.seed(20260620)) · ~51 tables / ~206k rows / 5,000 ลูกค้าเวียงจันทน์ (พิกัดอาคารจริง) · Apr–Jun 2026. - big:
transactions(31,925),transaction_items(35,361),delivery_run_orders(31,476 + POD),loyalty_transactions(30,885),asset_movements(19,413),payment_collections(18,477),driver_locations(3,871),customer_photos(3,000),settlements(1,297). - realism: data-mined จาก VARI Drive จริง ·
products.json= 11 SKU จริง + ราคา/GM%/deposit. customers.csvshipslast_received_date+total_orders→ forward-compatible กับ Block A.- demo consumes positional JS arrays (
LOO_CUSTDATA,LOO_ORD,LOO_CUSTPTS,LOO_DASH) ไม่ใช่ CSV · CSV = standalone fixture, ยังไม่ wired.
asset_movements.csv enum ปลอม (§6.1) · 18L price split (catalog 28,000 vs txn 25,000) · 3 เดือน history (lapse/churn tail flat) · _specs/* มี Drive path + distributor จริง (NDA).09 Landmines / conventions (ต้อง honor)
- GPS-at-delivery trap: ຂຽນ GPS ໃນ
deliverOrderໂดยตรง · ຢ່าผ่านupdateStopStatus(reject DELIVERED on not-COMPLETED, :398-405). - Order.status ບໍ່ມี DELIVERED — DELIVERED อยู่บน
DeliveryRunOrder(/loo-checkสแกน). - AuditInterceptor global (
APP_INTERCEPTOR) — ห้าม@UseInterceptors(AuditInterceptor)ต่อ method. path ที่มองไม่เห็น →AuditService.record(). - tenant+branch isolation ทุก query (
TenantScope.whereWithBranch) — R4/R6 cross-branch ต้อง carve-out + audit. เคารพisCustomerBranchScopeExempt. มี ESLint rule (no-restricted-syntax) บังคับtenantIdใน service where-clause กัน cross-tenant — แต่ ยังไม่ถูกรันใน CI · ค้าง migrate ~29 จุด (eslint-disable). - Prisma 7 nested-create ห้าม scalar FK — ใช้
connect: { id }. (update/top-level create ใส่ scalar ได้ — Block A ทำถูก). - broadcastTenant หลังทุก tenant mutation (หลัง $transaction commit).
- re-record double-count:
deliveryCount/lastReceivedDateguard!isReRecord.totalPurchaseCountนับซ้ำอยู่แล้ว (flag in PR). - idempotency replay return ก่อน tx → spec assert
$transactionไม่ถูกเรียกตอน replay. - walk-in customerId=NULL — write ใน
if(order.customerId)· backfillcustomerId IS NOT NULL. - take:20000 cap — corridor query bbox ก่อน take ไม่ใช่ JS-filter หลัง.
- 3 color systems แยก: semantic
colorCode·TERRITORY_PALETTE· route palette · lapse ladder. - run-creation uniqueness app-enforced — path ใหม่ copy guard ครบ.
- Settlement.create แบบ standalone ลบ (Phase 4.3) — ไม่ใช่ "ตารางว่าง": settlement ตอนนี้สร้างใน
DriverDayService(delivery-runs Phase 9.1) ·SettlementsControllerถูกลบ เหลือ read + approve/dispute (ปัจจุบันเรียกโดย test เท่านั้น) · R7 pay design ต้องรู้. - delivery-runs = god-service (โตกลับ) — facade 2,033 LOC (เคยแยกเหลือ 729 ใน
SPLIT_PLAN.md· ตอนนี้โตกลับ) ·delivery-operations.service.ts2,208 LOC · งาน Block A / Track B ใหม่ ควรลงในservices/sub-services ไม่ใช่ facade (อย่าซ้ำรอย regression). หมายเหตุ:reports.service.ts= 10,319 LOC คือก้อนใหญ่สุดของ repo. - money Decimal (18,2)/(18,4) · re-derive on reverse · 2 ledgers (
PaymentCollectionvsCustomerPayment) ต้อง union. - Lao UI labels บังคับ ผ่าน Xieng QC +
lao_corruption_scan.py· mono SVG ห้าม emoji. - no persisted recency = hot-path cost — Block A persist + index แก้แล้ว (เดิม
getLastDeliveredMAX groupBy 3+ ที่).
10 Decisions
build decisions (D1–D13)
| # | decision | default | blocks |
|---|---|---|---|
| D1 | Route entity vs DeliveryRun extension | standalone Route + DeliveryRun.routeId? | W3 |
| D2 | Google vs Leaflet draw | Google (no Leaflet in package.json) | — |
| D3 | importance source | customerPriorityId only | W2/3 |
| D4 | stars/satisfaction | defer/locked (Service Score input) | — |
| D5 | verifiedLat/Lng vs overwrite | write only in deliverOrder onto existing cols | W1 |
| D6 | capture deliveringUserId now | yes (hardest to backfill) | W1 |
| D7 | deliveryCount column | add | W1 |
| D8 | lastReceivedDate re-record | stamp only !isReRecord | W1 |
| D9 | LAPSED tolerance + min sample | 1.5× median · suppress <3 COMPLETED | W2 |
| D10 | SUBLESS window | cap 90 days | W2 |
| D11 | permission codes | delivery.manage · customers.view | W2/3 |
| D12 | GPS privacy (Lao compliance) | confirm Bou ก่อน FE capture (W2) | W2 |
| D13 | corridorMeters | fixed 150m | W4 |
Bou ต้องเคาะ (จากแพ็กเกจ)
- payment enum migration (lowercase→uppercase) — parked since 2026-06-15 · canonical method = 3 ค่า (§6.1) → ต้อง resolve.
- optimizer gate = GPS ≥ 80% (~10/30 ตอนนี้) AND ≥ 8 deliveries/customer.
- PWA vs native — blueprint = PWA แต่ Bluetooth printer + scanner อาจบังคับ native.
- VARI stamp/signature —
vari-stamp-sig.png= ตรา+ลายเซ็นจริง · ลบก่อน demo ภายนอก. - VARI price authority —
VARI_CATALOG(driver app · KB v1.4) ขัดกับproducts.json/DEMO_LOOP (Master Cost · Bou 2026-06-11) ใน 4-5 SKU → เคาะแหล่งราคาจริงก่อน lock catalog.
11 Blueprint constitution + brand tokens
7 Design Principles (00 §3 — ทุก feature ต้องผ่านครบ)
- 1 page = 1 main job, 3-second grasp.
- record once while working (driver ไม่มีเอกสารเพิ่ม).
- real data ก่อน decoration — ทุกเลข click-trace ได้ · no dead-end · totals = line items.
- one-handed, harsh sunlight, low-skill — big thumb buttons · drive mode strips to essentials.
- phone IS the GPS, ติดรถ ไม่ใช่ติดคน (driver swappable).
- Lao-only ผ่าน Xieng QC +
lao_corruption_scan.py(zero Thai codepoints) · mono SVG · no emoji. - using system = full pay — points/bonus นับเฉพาะ system-verified (photo+GPS) work.
DoD (00 §6): serves the one job · ไม่ละเมิด 7 หลัก · real-data closes loop (traceable, totals match, no dead-end) · Lao QC clean · browser-verified (0 console error + screenshot) · ไม่พังของเดิม (incremental) · design-log recorded.
View/Filter standard (01)
4 modes: List / Board / Calendar / Map · filter registry ~18 ตัว (F_SEARCH, F_DATE, F_BRANCH, F_DRIVER, F_ZONE, F_GRADE, F_PAYSTATUS, F_ORDERSTATUS, F_PAYMENT, F_SHIFT…) · hard rule: filter change → re-render ทุก view + KPI พร้อมกัน. build registry + view-bar ครั้งเดียว แล้ว clone.
Reports reframe (05 · อย่า rebuild)
มี 9 reports แล้ว (Phase 10) → (1) re-skin tokens (no logic change) (2) ยุบเมนู 9→7 via tab-wrapper (route ยังอยู่) (3) GET /reports/daily-summary + cron 19:00 + PDF + auto-WhatsApp (เสี่ยงแค่ gateway → PDF/share fallback).
Brand tokens — ⚠ 2 ชุด ไม่ตรงกัน
brand_kit (DARK v2.0 · aspirational)
--loo-blue #2563EB · bg #0A0E1A · surface #0F172A
money: cash #34D399 · transfer #60A5FA · QR #A78BFA · debt #F2545B
fonts: Plus Jakarta Sans / Manrope / JetBrains Mono
live app (ปัจจุบัน)
--primary #5d87ff (Modernize default) → --color-cyan-500/600/700
fonts: Outfit / Noto Sans Lao / Phetsarath
auth/onboarding เริ่ม prototype #2563EB แต่ dashboard ยัง #5d87ff
LOO Suite framing
LOO = branded-house suite (MS Office): LOO Delivery Business (V1 LIVE ที่ VARI ตั้งแต่ 2026-06-11) + LOO Workplace (HR) + LOO Waste. "LOO Platform" = multi-tenant engine · share Bible/tokens (swap --primary per vertical). domain: loolao.com + app./api./admin. · payroll/checkin/teampoints = link-out stub → LOO Workplace (draft).