DEV TECHNICAL REFERENCE · 2026-06-22 · branch feat/a0-customer-brain-capture

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 ໄປສ້າງຕໍ່.

NestJS 11Prisma 7.8Next.js 16PostgreSQLrepo: loo-platformmigrations → 20260622100000_driver_pin_auth+ Track B: driver app✓ cross-checked vs live repo · 2026-06-22
ไปที่ Checklist (ตารางงาน) ↓

01 ສະຖາປັດຕະຍະກຳ — North-Star loop 4 ຂັ້ນ ແມັບໂມດູນຈິງ

North Star: "ຄົນຂັບບັນທຶກເທື່ອດຽວຕອນເຮັດວຽກ → ຫຼັງບ້ານເຫັນຄວາມຈິງ real-time → ຮອບຕໍ່ໄປ plan ແມ່ນຂຶ້ນເອງ." ວົງຈอน stage4→1→2 ປັດຈຸບັນ ຂາດ (ບໍ່ persist recency, ບໍ່ເກັບ GPS ຕอนส่ง).

Stageໂມດູນຈິງ (live repo)ສະຖານະ loop
1 · Customer brainmodules/customers/* + customer-priorities/types/groups · model Customer (schema:856)master-data ຄົບ · ຂາດ field ປັນຍາ (lastReceivedDate/satisfaction/stars/grade)
2 · Plandelivery-runs/services/delivery-dispatch.service.ts (autoGenerateForDate, listEligibleCustomersForZone) + orders.generateForTenant + customer-subscriptionsauto-gen ຕາມ calendar ມີ · blind ຕໍ່ delivery history — ບໍ່ມີ lapse/overdue/cadence
3 · Field deliverydelivery-runs/services/driver-day.service.ts · deliver-stop-modal.tsx · findTodayForDriverdispatch→driver ໃຊ້ໄດ້ · sequencing manual (reorderStops)
4 · Record-oncedelivery-runs/services/delivery-operations.service.tsdeliverOrder() (~1129–2176)ແຂງແຮງ POD+payment+bottles 1 tx · Block A: ເພີ່ມ GPS + lastReceivedDate
OK
keystone ທີ່ປິດ loop: ຂຽນ GPS + Customer.lastReceivedDate ໃນ tx ດຽວກັນກັບ deliverOrder ທີ່ມີຢູ່ແລ້ວ — ນີ້ຄື Block A0 (ดู §4). ທຸກຢ່າງໃນ A/B/C ຕໍ່ຍอดจากจุดนี้.

02 Data-model reality

ສະຖານະຈິງຂອງແຕ່ລະ field (verify ກັບ backend/prisma/schema.prisma) ແລະ ສ່ວນທີ່ Block A ກ່ຽວຂ້ອງ.

Field / capabilityStateບ່ອນຢູ່ຈິງ / "absent"ການແກ້
Customer.lastReceivedDateBlock Aບໍ່ມີມາກ່อน · ມີແຕ່ Order.deliveredAt (:1126) computed read-timeBlock A: DateTime? + index + backfill
Customer.deliveryCountBlock Aບໍ່ມີ — totalPurchaseCount ປົນ counter-sale + ນັບຊ້ຳBlock A: Int @default(0), guard !isReRecord
DeliveryRunOrder.deliveringUserIdBlock Aບໍ່ມີ — pay ผูก crew[0]Block A: String? @db.Uuid + FK SetNull + index
GPS deliveredLatitude/LongitudeBlock Aschema:1739-1740 ມีก่อน ແຕ່ບໍ່ມີໃຜຂຽນຜ່ານ deliverOrder (updateStopStatus = trap)Block A: ຂຽນໃນ deliverOrder ໂດຍກົງ
Customer.satisfaction (1-5)MISSINGບໍ່ມີທີ່ໃດdeferred (Service Score input · D4)
Customer.stars (1-5)MISSINGບໍ່ມີ grade columndeferred (computed/locked · D4)
importance (S/A/B/C)PARTIALcustomerPriorityIdCustomerPriority (:1561) ແລະ customerType="VIP" = 2 ສັນຍານຊ້ອນD3: ເລືອກ customerPriorityId ເປັນ grade ຫຼັກ
route geometry ເທິง DeliveryRunMISSING(:1285) ມີແຕ່ branch/vehicle/zone/date/status · ບໍ່ມີ color/day/name/geometryWeek 3: entity Route + DeliveryRun.routeId?
Vehicle.capacity (CVRP)มีแต่ตาย(:1624) Int? ແກ້ໄດ້ใน UI ແຕ່ບໍ່ມີໃຜ read · scalar ດຽວBlock C: CVRP ຕ້ອງເພີ່ມ volume/slot ຕໍ່ Product
demand ຕໍ່ລູກຄ້າEXISTSOrderItem.quantity (:1250) + CustomerSubscriptionItem.quantity (:1921)input ດຽວທີ່ພ້อมจริง

03 Gap analysis R1–R10

effort: S=ມື້ · M=1-2 ອາທິດ · L=multi-week.

Rrequirementstateimplementing modulesຂາດຫຍັງeffblk
R1customer-brain fields + GPSBlock ACustomer · CUSTOMER_SELECT customers.service.ts:33-138satisfaction/stars (deferred)MA
R2lapse tiers 7/14/20/30 → relative cadencepartialrecency-face.tsx (7/14 ຫຍາບ)tier helper + median-gap cadenceMA/C
R3multi-route draw (color/day/palette)missingRoute entity + line-draw + day/color filterLA
R4corridor reveal (incl cross-branch)missingBranch.boundary (:462) · ops-map.tsxnear-line query + multi-attr pin · cross-branch carve-outLA/B
R5plan-gen 5 steps (who's-due → seq)partiallistEligibleCustomersForZone · isSubscriptionDueOnDate (shared:675) · autoGenerateForDate (:866)selection = calendar-only · step4 manualLA/C
R6cross-branch rescue ≠ poachpartialoverdue LATERAL orders.service.ts:664-755ownership/release/claim model · 30-min auto-releaseLB
R7pay-follows-workpartialPaymentCollection.driverId (:2294) · Settlement (:2326) · LoyaltyTransaction (:2928)credit collection-based · R7-2 driverId attribution · Settlement สร้างใน DriverDayService (standalone create ลบ)LB
R8Service Score (computed + audited)missingບໍ່ມี model · raw signals ກະຈາຍທັງ engine · ບໍ່ມี SLA baseline · complaint ບໍ່ link driverLB
R9record-once POD+GPS+ชำระ+ถัง + recencyBlock AdeliverOrder (:1129-2176)ขาด GPS+lastReceivedDate (Block A)S–MA
R10optimizer (OR-Tools + OSRM + CVRP)missingບໍ່ມี · reorderStops manualທັງ optimizer · OSRM ບໍ່ມีใน repo (Google Maps)LC

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+18DeliverOrderDto +lat/lng validators
…/delivery-runs.service.ts+3wrapper dto type +GPS
…/services/delivery-operations.service.ts+34GPS+actor · lastReceivedDate+deliveryCount · pay
…/migrations/20260622090000_customer_brain_capture/migration.sqlnewDDL + FK + index + backfill

4.1 · schema diff

backend/prisma/schema.prisma — model Customer
// Customer-brain capture (Block A)
  lastReceivedDate    DateTime?
  deliveryCount       Int      @default(0)
  ...
  @@index([tenantId, lastReceivedDate])
model DeliveryRunOrder (GPS cols :1739-1740ມີຢູ່ກ່อนแล้ว)
  deliveringUserId    String?   @db.Uuid
  ...
  deliveringUser User? @relation("DeliveryRunOrderDeliveredBy", fields: [deliveringUserId], references: [id], onDelete: SetNull)
  ...
  @@index([deliveringUserId])
model User — inverse relation
  deliveredRunOrders DeliveryRunOrder[] @relation("DeliveryRunOrderDeliveredBy")

4.2 · migration SQL

migrations/20260622090000_customer_brain_capture/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";
OK
apply ด้วย npx prisma migrate deploy (ບໍ່ຕ້อง shadow DB). ຖ້າຈะใช้ migrate dev ຕ້องสร้าง loo_shadow ກ່อน.

4.3 · service diff — deliverOrder()

delivery-operations.service.ts — tx.deliveryRunOrder.updateMany (~1802, ຂ້າງ podPhotoUrl)
  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 } : {}),
  },
delivery-operations.service.ts — tx.customer.update (~1966, ใน if(order.customerId))
  data: {
    totalPurchaseCount: { increment: 1 }, isFirstTimeCustomer: false,
    // FIRST delivery only — re-record (admin correction) ບໍ່ advance/ບໍ່ນັບຊ້ຳ
    ...(isReRecord ? {} : { lastReceivedDate: new Date(), deliveryCount: { increment: 1 } }),
  },
delivery-operations.service.ts — PaymentCollection.create (~1580) · R7-2
  orderId,
  driverId: order.deliveryRun?.crew?.[0]?.userId ?? null,   // OLD: crew[0]
  driverId: currentUser.sub,                                // NEW: acting user (pay-follows-work)
delivery-runs.controller.ts — DeliverOrderDto (:230 · GPS fields :341, validator จาก UpdateStopStatusDto:460-473)
  @IsOptional() @Type(() => Number) @IsNumber() @Min(-90)  @Max(90)  deliveredLatitude?: number;
  @IsOptional() @Type(() => Number) @IsNumber() @Min(-180) @Max(180) deliveredLongitude?: number;
!
trap ທີ່ຫຼີກໄດ້: per-stop GPS column ຂຽນຜ່ານ 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 · 22/06 · keystone A0 ✅ DONE

วางแผน/ออกแบบระบบ + schema + migration + GPS/actor write + lastReceivedDate/deliveryCount + R7-2 pay. (ดู §4) · ของจริง 1 วัน (แผน 5.5)

ມື້ 2 · 23/06 · tests + PR

A0-spec-extend — jest: GPS present/absent · lastReceived/re-record · idempotency replay · walk-in skip + QC/loo-check + PR-A0/R7-2

ມື້ 3-5 · 24-26/06 · brain reads + who's-due

FE one-shot getCurrentPosition · card อ่าน recency · listDueCustomers (CALENDAR∪OVERDUE∪LAPSED∪SUBLESS) + GET /due-customers · mount customer-subscriptions

ມື້ 6-9 · 27-30/06 · Route entity + draw

model Route + DeliveryRun.routeId? · module routes/ · route-LINE draw บน ops-map · route-lines.tsx (ROUTE_PALETTE)

ມື້ 10-13 · 01-04/07 · 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)

idtitleblkeffwk
A0-schema-lrdCustomer/DeliveryRunOrder cols + indexesASW1
A0-migration-backfillmigrate + backfillAMW1
A0-dto-controllerDeliverOrderDto lat/lngAXSW1
A0-svc-gps-writeGPS + deliveringUserId in txASW1
A0-svc-lastreceivedlastReceivedDate + deliveryCountASW1
R7-2-pay-attributiondriverId = acting userB-gwSW1
A0-spec-extendspec: GPS/lastReceived/re-record/idempotencyAMW1
A1-api-payloadFE deliverOrder payload lat/lngAXSW2
A1-modal-getpositionone-shot GPS at submit (best-effort)AMW2
A1-brain-cardcard reads persisted lastReceivedDateASW2
A2-1-cadence-fnpure medianGapDays helperASW2
A2-2-due-querylistDueCustomers (calendar∪overdue∪lapsed∪subless)ALW2
A2-3-endpointGET /due-customers + DTO + permASW2
R5-1-mount-subs-listmount customer-subscriptions controllerASW2
A3-route-schemaRoute entity + DeliveryRun.routeIdASW3
A3-routes-moduleroutes/ CRUD (whereWithBranch + broadcast)AMW3
A3-routes-api-hooksFE Route api + SWR hooksASW3
A3-route-draw-toolroute-LINE draw (single active tool)ALW3
A3-route-render-layerroute-lines.tsx (ROUTE_PALETTE)AMW3
A4-corridor-near-querycustomersNearRoute (bbox + point-near-line)ALW4
A4-day-color-filterday + per-route on/off filterAMW4
A4-route-pin-encodingring=route, fill=lapse + legendAMW4
R8-1-verified-derivationisVerifiedStop + per-driver verifiedPctB-gwMW4
R8-2-signal-inventoryper-driver raw +/− counts (no score)B-gwLW4
สถานะปัจจุบัน: 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 · 22/06/2026 · A0 keystone (รากฐาน)

A0·0วางแผน + ออกแบบระบบ (architecture · data-model · loop stage4→1 · ก่อนลงมือเขียนโค้ด)
A0·1A0-schema-lrd — schema +lastReceivedDate/deliveryCount/deliveringUserId + index + prisma generate
A0·2A0-migration-backfill — migrate + backfill MAX(deliveredAt) + verify บน DB
A0·3A0-dto-controller + A0-svc-gps-write — DTO lat/lng + เขียน GPS+deliveringUserId ใน deliverOrder tx
A0·4A0-svc-lastreceived + R7-2-pay-attribution — lastReceivedDate/deliveryCount (guard !isReRecord) + driverId=currentUser.sub
หมายเหตุของจริงทำใน 1 วัน (แผนเดิม 5.5 วัน) → นำหน้าแผน ~4 วัน · commit e2d9b7d3

ມື້ 2 · 23/06 · ชุดทดสอบ A0 + PR ที่ยังเหลือ

ມื้ 2A0-spec-extend — jest: GPS present/absent · lastReceived/re-record · idempotency replay · walk-in skip~1 วัน
เก็บงานQC + /loo-check + เปิด PR (PR-A0 / PR-R7-2)½ วัน

ມື້ 3-5 · 24-26/06 · สมองลูกค้า + ใครถึงรอบ ~3 วัน · 2 track

ມื้ 3 · 24/06(A) A1-api-payload + เริ่ม A1-modal-getposition · (B) A2-1-cadence-fn (medianGapDays)~1 วัน
ມื้ 4 · 25/06(A) A1-modal-getposition (จบ) + A1-brain-card (อ่าน recency) · (B) A2-2-due-query (CALENDAR∪OVERDUE∪LAPSED∪SUBLESS)~1 วัน
ມื้ 5 · 26/06(A) R5-1-mount-subs-list + R5-2-subless-feed · (B) A2-2 (จบ) + A2-3-endpoint (GET /due-customers) + integration/PR~1 วัน

ມื้ 6-9 · 27-30/06 · Route entity + วาดเส้น ~4 วัน · 2 track

ມื้ 6 · 27/06A3-route-schemaRoute entity + DeliveryRun.routeId + migration + generate~1 วัน
ມื้ 7 · 28/06A3-routes-module (เริ่ม) — CRUD, whereWithBranch, broadcast ROUTE_UPDATED~1 วัน
ມื้ 8 · 29/06(A) A3-routes-module (จบ) + A3-routes-api-hooks · (B) เริ่ม A3-route-render-layer~1 วัน
ມື้ 9 · 30/06(A) A3-route-draw-tool (Google Maps · single active tool) · (B) A3-route-render-layer (จบ · ROUTE_PALETTE) + ทดสอบ/PR-A3~1 วัน

ມื้ 10-13 · 01-04/07 · กรอง + corridor + วัดผล ~4 วัน · 2 track

ມื้ 10-11 · 01-02/07(A) A4-corridor-near-query (bbox + point-near-line · in-branch) · (B) A4-day-color-filter~2 วัน
ມື้ 12 · 03/07A4-route-pin-encoding — ring=route, fill=lapse + legend + idsKey memo~1 วัน
ມื้ 13 · 04/07R8-1-verified-derivation + R8-2-signal-inventory (เริ่ม) + /loo-check + PR-A4/R8 · L · อาจล้น +1-2 วัน~1 วัน

เริ่ม 22/06/2026 · ทำ 7 วัน/สัปดาห์ ไม่มีวันพัก (วันที่ไล่ติดกัน). A0 keystone ของจริงทำ 1 วัน (แผนเดิม 5.5) → นำหน้า ~4 วัน. Track A (มื้ 1-13) จบ ~04/07/2026 · ประเมินตามจริง ไม่บีบ. ทุก PR ผ่าน /loo-check + tsc (CI = tsc+jest+e2e · lint ยังไม่ gate). แอปคนขับ = ทำใหม่ทั้งหมดด้วย Flutter (§5c · เฟสถัดไป).

5c Track B — Driver Mobile App (V2) · ລວມຕາมมติรวมงาน

มติ: ทำแอปคนขับใหม่ทั้งหมดด้วย Flutter (ทิ้งเดโม/โค้ดเก่า Capacitor+React · V2_DRIVER_APP/app.html v0.22 = UX reference เท่านั้น) → scope = เต็มตามเดโมทุกฟีเจอร์ · backend loop ของคนขับมีแล้ว (reuse ได้) · ดู capacity §5c.3.

!
การตัดสินใจ (framework): แอปคนขับ ทำใหม่ทั้งหมดด้วย Flutter — เดโม/โค้ดเก่า (Vite + React + Capacitor ใน loo-platform/driver/) ไม่นับเป็นความคืบหน้า ใช้เป็นแบบ UX/flow อ้างอิงเท่านั้น. ข้อดี: backend loop ของคนขับมีอยู่แล้ว (GPS · POD · deliver/fail · payment · ถังคืน · truck-load · settlement · /auth/driver/login · /today/me) → Flutter ต่อ endpoint ที่มีได้เลย ไม่ต้องสร้าง backend ใหม่.
!
การค้นพบที่เปลี่ยนภาพทั้งหมด: เอกสาร 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.tsFE: live pin บนแผนที่ + staleness display
POD พิกัด+รูปsnap()+confirmDeliver()มี deliveredLatitude/Longitude · podPhotoUrl/podSignatureUrl · deliveringUserId (Block A)Flutter FE กล้อง+GPS+confirm (ทำใหม่)
ส่ง / ส่งไม่ได้ / ข้ามdone/failSave/driveSkipมี status PENDING/DELIVERED/FAILED/SKIPPED · enum DeliveryFailureCategory 8 เหตุ · /fail endpointFE: ผูกปุ่ม + เหตุผล dropdown
ชำระหน้างาน 4 แบบPAYMETA canonicalมี PaymentCollection · method CASH/QR_CODE/BANK_TRANSFER/CREDIT · driverId=actor (R7-2)Flutter FE picker CASH/QR/BANK (ทำใหม่)
ถังคืนแยกประเภท + มัดจำret{barrel,crate} · depositมี AssetMovement/AssetBalance · emptyCollected · collect-empties/add-bottlesFE: ผูก UI · deposit=liability แยก
โหลดน้ำขึ้นรถ + เสียหายLOAD+SRC+DMGRมี TruckLoad+TruckLoadItem · close-variance · TRUCK_LOAD/UNLOADFE: ผูก UI + damage capture
ปิดกะ + รายงานรายวันdailyReport mockมี run close + Settlement · close-summary · close-varianceFE: driver close UI
สมองลูกค้า (lapse บนจอจุดส่ง)drvLapse 7/14/20/30มี lastReceivedDate+deliveryCount (Block A) · GET /customer-cardFlutter FE brain chip (ทำใหม่)
login รหัส+PINmock PIN 4 หลักมี POST /auth/driver/login · module driver-auth · User.employeeCode/driverPinHash (mig 20260622100000)BE มี · FE keypad = Flutter
POS ยิงขายหน้างานopenSell/SaleOnSiteบางส่วน sell-water (web) แยก · มี add-order-itemsnet-new POS-on-delivery
แจ้งลูกค้า (กำลังไป/ส่งแล้ว/เลื่อน)notifyCust mockไม่มี integration WhatsApp/SMSnet-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ไม่มี FuelLognet-new fuel module + anomaly
SOS แจ้งเหตุ → ผู้จัดการfEvt 5 เหตุบางส่วน Issue model มี · ไม่ผูก deliverynet-new wire SOS → Issue+notify
offline-first + PWAmanifest.json+sw.jsไม่มี PWA / offline queuenet-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%.

!
constants ที่ต้อง honor (จากเดโม v0.22): lapse 7/14/20/30 (เขียว/เหลือง/แดง) · semantic ชำระ cash=เขียว · QR=ม่วง · transfer=ฟ้า · debt/ค้าง=แดง (ห้ามสลับ) · 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)

idtitlekindeff
B0-mobile-shellFlutter app shell + bottom-nav 4 แท็บ + routingFlutterL
B0-pwamanifest + service worker + offline shell · strategy → Capacitor-native · re-scopenet-newM
B0-pin-authemp-code + PIN login — BE มี /auth/driver/login · FE keypad = FlutterFlutterM
B1-run-screenGET /today/me → run (stops + progress)FlutterM
B1-stop-screenstop "จบในจอเดียว": brain+pay(CASH/QR/BANK)+bottle+photo POD+GPS+confirm(idempotency)FlutterL
B1-pod-receiptPOD recap + ใบเสร็จ (logo+QR) · รูป POD เก็บใน stop แล้ว · เหลือหน้าใบเสร็จwireM
B1-driver-mapdriver map บน Google Maps (route + live pin + จุดต่อไป)rebuildM
B1-pretripcheck-in + crew + ตรวจรถ 7 จุด + โหลดน้ำ (/pre-trip-check BE มี)FlutterL
B1-money-wallet4 ก้อน (cash/QR/transfer/ค้าง) drill-downwireS
B1-close-shiftclose run + settlement summary + ปิดกะwireM
B2-pos-ondeliveryยิงขายหน้างาน → SaleOnSite (เชื่อม sell-water)net-newL
B2-fuel-logFuelLog model + endpoint + UI + anomaly สิ้นเปลืองnet-newM
B2-leaderboarddriver perf/points engine + leaderboard + redeemnet-newL
B2-mypaydriver earnings aggregation (verified-work-based)net-newM
B2-sos-wireSOS 5 เหตุ → Issue + notify ผู้จัดการnet-newM
B2-notify-customerotw/done/fail → WhatsApp/SMS servicenet-newM
B2-offline-queueoffline delivery capture + sync-on-reconnectnet-newL
B3-extrasskip / incoming-banner / driving-mode / voice / onboarding tourpolishM

18 task · L=3.5 / M=2 / S=1 person-days → Track B (Flutter ทำใหม่ทั้งหมด) ≈ ~45 person-days. โค้ด Capacitor/React เดิมไม่นับ · wire=ต่อ backend ที่มี (เร็ว) · net-new=สร้างโมดูลใหม่ (ช้า). ถ้าต้องตัดเพื่อเฟสแรก → net-new (POS/fuel/leaderboard/mypay/offline/notify) คือกลุ่มที่เลื่อนได้.

5c.3 · capacity reality — 2 track เต็ม / 2 dev / 1 เดือน พอไหม?

scopeperson-days2 devs (44 pd/เดือน)
Track A เต็ม (สมองลูกค้า + routing + corridor + R8)~41 pd~4 สัปดาห์
Track B (แอปคนขับ · Flutter ทำใหม่ทั้งหมด)~45 pd~4 สัปดาห์
รวม 2 track เต็ม~86 pd≈ 8 สัปดาห์ (~2 เดือน)
!
flag ตรงๆ (ตามที่หัวหน้าขอ): 2 track เต็มสเปก + ทีม 2 คน ≈ ~8 สัปดาห์ ไม่ใช่ 4. ใน 1 เดือนแรก 2 devs ส่งได้ ~ครึ่งทาง. ปัจจัยที่ช่วย = backend loop ของคนขับมีแล้ว (งาน wire เร็วกว่าสร้างใหม่) · FE ทำใหม่ด้วย Flutter (backend reuse ได้ · FE เริ่มศูนย์).
OK
ถ้าต้องจบใน 1 เดือนจริง — เส้น MVP cut (แนะนำเป็นทางเลือก): ส่งใน 1 เดือน: Track A core (A0–A2 สมองลูกค้า + who's-due) + Track B MVP (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 → Flutter ได้ background-GPS + Bluetooth printer/scanner ดีกว่า PWA. แอปทำใหม่ทั้งหมดด้วย Flutter · backend loop ของคนขับมีแล้ว (reuse endpoint ได้เลย).

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.

concerndemo (aspirational)live code (authoritative)
maps/routingLeaflet + CartoDB + public OSRM (dpFetchRoadRoute, dpDrawSnap, lasso/polygon/corridor)Google Maps (@vis.gl/react-google-maps ^1.8.3) → demo map code reuse ไม่ได้
demo-only fnsdpNewPlan, DP_RUNS, dpZone, starsEl, dpPlanByDriver, dpClaim*, DP_SVCSCOREບໍ່ມีในโค้ดจริง (client-state ເດໂມ)
optimizerdpPlanByDriver = CUSTOMERS.filter(zone).slice(0,5) + qty ປอม (lines 4231-4235)ບໍ່ມี optimizer · reorderStops manual
Service ScoreDP_SVCSCORE −1 in-memory counterບໍ່ມี model/persistence
live GPS trucks4 hardcoded trucks animate ตาม OSRM geometryreal driver GPS via POST /delivery-runs/me/location (ReportLocationDto)
OK
เดโม validate แนวทางนี้: web 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 / แดง #F2545BA1-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 60A4-corridor-near-query
snap ลูกค้าใกล้สุด~660m threshold (dpNearestCust brute) · lasso cap 40 · polygon ray-casting cap 60A3-route-draw-tool
work process"เลือก → จัด → ส่ง" (panel ①②③ · รอบ 56)กรอบ UX ทั้ง Track A
ชื่อเมนูวาดเส้นວາງເສັ້ນ (ไม่ใช่ "วาดเส้น" · Bou เคาะ)label ของ draw tool
rescue / Service Scoreหมุดเงียบ (เหลือง/แดง/ม่วง) มีปุ่ม "ຄວ້າ·ຮັບໄປສົ່ง" · เขียว=ไม่มี (rescue≠poach) · คว้า→เจ้าของ −1Block B (เลื่อน · DP_SVCSCORE = in-memory เดโมล้วน)
!
demo-only (ไม่มี backend — อย่าสับสนว่าทำเสร็จ): Service Score persistence · 30-min auto-release · force-assign · optimizer (dpPlanByDriver=filter(zone).slice(0,5)) · 4 trucks hardcode animate ตาม OSRM. ทั้งหมดนี้คือ Block B/C ที่ยังต้องสร้าง backend จริง.

6.1 · enum / data-contract (เสี่ยงสูงสุด — ต้อง honor)

!
หมายเหตุ schema (ความแม่นยำ): แถวด้านล่างส่วนใหญ่เก็บเป็น 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 (ไม่มี).
fieldcanonical (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.statusPENDING / IN_PROGRESS / COMPLETED / CANCELLEDບໍ່ມี DELIVERED · demo done/failed = local · live failDeliveryCANCELLED
DeliveryRunOrder.statusPENDING / DELIVERED / FAILED / SKIPPEDenum ແຍກ · ຢ່າສັບສົນກັບ 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 ປอม
!
data contract (00 §5): enum/names ຕ້ອງ match backend ເປ໊ະ ບໍ່ດັ່ງນັ້น data ບໍ່ flow (ມີ bug payment lowercase-vs-uppercase ມາแล้ว). reconcile payment method ກັບ Bou ກ່อนต่อ flow ຄົນຂັບ.

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 (2026060920260622100000_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 (MFA 20260617120000) · 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.csv ships last_received_date + total_ordersforward-compatible กับ Block A.
  • demo consumes positional JS arrays (LOO_CUSTDATA, LOO_ORD, LOO_CUSTPTS, LOO_DASH) ไม่ใช่ CSV · CSV = standalone fixture, ยังไม่ wired.
!
bug ก่อน import: 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)

  1. GPS-at-delivery trap: ຂຽນ GPS ໃນ deliverOrder ໂดยตรง · ຢ່าผ่าน updateStopStatus (reject DELIVERED on not-COMPLETED, :398-405).
  2. Order.status ບໍ່ມี DELIVERED — DELIVERED อยู่บน DeliveryRunOrder (/loo-check สแกน).
  3. AuditInterceptor global (APP_INTERCEPTOR) — ห้าม @UseInterceptors(AuditInterceptor) ต่อ method. path ที่มองไม่เห็น → AuditService.record().
  4. 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).
  5. Prisma 7 nested-create ห้าม scalar FK — ใช้ connect: { id }. (update/top-level create ใส่ scalar ได้ — Block A ทำถูก).
  6. broadcastTenant หลังทุก tenant mutation (หลัง $transaction commit).
  7. re-record double-count: deliveryCount/lastReceivedDate guard !isReRecord. totalPurchaseCount นับซ้ำอยู่แล้ว (flag in PR).
  8. idempotency replay return ก่อน tx → spec assert $transaction ไม่ถูกเรียกตอน replay.
  9. walk-in customerId=NULL — write ใน if(order.customerId) · backfill customerId IS NOT NULL.
  10. take:20000 cap — corridor query bbox ก่อน take ไม่ใช่ JS-filter หลัง.
  11. 3 color systems แยก: semantic colorCode · TERRITORY_PALETTE · route palette · lapse ladder.
  12. run-creation uniqueness app-enforced — path ใหม่ copy guard ครบ.
  13. Settlement.create แบบ standalone ลบ (Phase 4.3) — ไม่ใช่ "ตารางว่าง": settlement ตอนนี้สร้างใน DriverDayService (delivery-runs Phase 9.1) · SettlementsController ถูกลบ เหลือ read + approve/dispute (ปัจจุบันเรียกโดย test เท่านั้น) · R7 pay design ต้องรู้.
  14. delivery-runs = god-service (โตกลับ) — facade 2,033 LOC (เคยแยกเหลือ 729 ใน SPLIT_PLAN.md · ตอนนี้โตกลับ) · delivery-operations.service.ts 2,208 LOC · งาน Block A / Track B ใหม่ ควรลงใน services/ sub-services ไม่ใช่ facade (อย่าซ้ำรอย regression). หมายเหตุ: reports.service.ts = 10,319 LOC คือก้อนใหญ่สุดของ repo.
  15. money Decimal (18,2)/(18,4) · re-derive on reverse · 2 ledgers (PaymentCollection vs CustomerPayment) ต้อง union.
  16. Lao UI labels บังคับ ผ่าน Xieng QC + lao_corruption_scan.py · mono SVG ห้าม emoji.
  17. no persisted recency = hot-path cost — Block A persist + index แก้แล้ว (เดิม getLastDelivered MAX groupBy 3+ ที่).

10 Decisions

build decisions (D1–D13)

#decisiondefaultblocks
D1Route entity vs DeliveryRun extensionstandalone Route + DeliveryRun.routeId?W3
D2Google vs Leaflet drawGoogle (no Leaflet in package.json)
D3importance sourcecustomerPriorityId onlyW2/3
D4stars/satisfactiondefer/locked (Service Score input)
D5verifiedLat/Lng vs overwritewrite only in deliverOrder onto existing colsW1
D6capture deliveringUserId nowyes (hardest to backfill)W1
D7deliveryCount columnaddW1
D8lastReceivedDate re-recordstamp only !isReRecordW1
D9LAPSED tolerance + min sample1.5× median · suppress <3 COMPLETEDW2
D10SUBLESS windowcap 90 daysW2
D11permission codesdelivery.manage · customers.viewW2/3
D12GPS privacy (Lao compliance)confirm Bou ก่อน FE capture (W2)W2
D13corridorMetersfixed 150mW4

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/signaturevari-stamp-sig.png = ตรา+ลายเซ็นจริง · ลบก่อน demo ภายนอก.
  • VARI price authorityVARI_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. 1 page = 1 main job, 3-second grasp.
  2. record once while working (driver ไม่มีเอกสารเพิ่ม).
  3. real data ก่อน decoration — ทุกเลข click-trace ได้ · no dead-end · totals = line items.
  4. one-handed, harsh sunlight, low-skill — big thumb buttons · drive mode strips to essentials.
  5. phone IS the GPS, ติดรถ ไม่ใช่ติดคน (driver swappable).
  6. Lao-only ผ่าน Xieng QC + lao_corruption_scan.py (zero Thai codepoints) · mono SVG · no emoji.
  7. 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

!
design system = aspirational/transitional ยังไม่ freeze · เป็น re-theme ไม่ใช่แค่สลับ token · master CI §5.2 ยัง list LIGHT palette เก่า ขัดกับ tokens.css. anti-neon law: สี = ความหมาย (60-30-10, 8pt grid) ไม่ใช่ decoration.

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).