Add delivery upsert endpoint to prevent duplicates
PUT /api/deliveries/upsert matches by tracking_number (exact) or description+retailer (fuzzy 50% word overlap). Updates existing delivery if found, creates new if not. Status only progresses forward (shipped→in_transit→out_for_delivery→delivered).
This commit is contained in:
parent
ec3e5eea0b
commit
baf9db9c1e
BIN
dashboard-server
BIN
dashboard-server
Binary file not shown.
|
|
@ -833,5 +833,15 @@
|
||||||
"session_percent": 2,
|
"session_percent": 2,
|
||||||
"timestamp": "2026-02-13T21:19:34.051622Z",
|
"timestamp": "2026-02-13T21:19:34.051622Z",
|
||||||
"weekly_percent": 67
|
"weekly_percent": 67
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"session_percent": 2,
|
||||||
|
"timestamp": "2026-02-13T22:00:01.669723Z",
|
||||||
|
"weekly_percent": 67
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"session_percent": 2,
|
||||||
|
"timestamp": "2026-02-13T22:00:02.638257Z",
|
||||||
|
"weekly_percent": 67
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -1,357 +1,3 @@
|
||||||
{
|
{
|
||||||
"deliveries": [
|
"deliveries": []
|
||||||
{
|
|
||||||
"id": "0e67333b",
|
|
||||||
"carrier": "Pediatric Home Service",
|
|
||||||
"retailer": "All About Pediatrics",
|
|
||||||
"description": "Sophia monthly supplies (Feb 2026) - 4 boxes",
|
|
||||||
"tracking_number": "75175",
|
|
||||||
"expected_date": "2026-02-06",
|
|
||||||
"status": "delivered",
|
|
||||||
"notes": "Shipped Feb 3, 4 boxes. Expected Feb 6-7.",
|
|
||||||
"created_at": "2026-02-04T14:36:17.040575584-05:00",
|
|
||||||
"updated_at": "2026-02-08T03:48:44.120692938-05:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "4d72150d",
|
|
||||||
"carrier": "Amazon",
|
|
||||||
"retailer": "Amazon",
|
|
||||||
"description": "World Organic Chlorophyll Liquid",
|
|
||||||
"tracking_number": "112-4979107-5850639",
|
|
||||||
"expected_date": "2026-02-08",
|
|
||||||
"status": "shipped",
|
|
||||||
"notes": "Order #112-4979107-5850639, $13.42. Arriving Sunday.",
|
|
||||||
"created_at": "2026-02-04T14:36:17.076836995-05:00",
|
|
||||||
"updated_at": "2026-02-04T14:36:17.076837035-05:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "795c2095",
|
|
||||||
"carrier": "Amazon",
|
|
||||||
"retailer": "Amazon",
|
|
||||||
"description": "ARG ButyrEn Colon Lining, INCAS Butterfly Pea",
|
|
||||||
"tracking_number": "113-8193397-8031467",
|
|
||||||
"expected_date": "2026-02-07",
|
|
||||||
"status": "delivered",
|
|
||||||
"notes": "Arriving today 7 AM - 11 AM. Likely delivered Feb 7.",
|
|
||||||
"created_at": "2026-02-07T02:18:56.918641023-05:00",
|
|
||||||
"updated_at": "2026-02-08T03:48:34.238948912-05:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "a7c36153",
|
|
||||||
"carrier": "Amazon",
|
|
||||||
"retailer": "Amazon",
|
|
||||||
"description": "Pure Encapsulations Vitamin D3 \u0026 K2 Liquid",
|
|
||||||
"tracking_number": "113-1880998-3453029",
|
|
||||||
"expected_date": "2026-02-07",
|
|
||||||
"status": "delivered",
|
|
||||||
"notes": "Likely delivered Feb 7.",
|
|
||||||
"created_at": "2026-02-07T02:51:07.360704007-05:00",
|
|
||||||
"updated_at": "2026-02-08T03:48:34.250472485-05:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "9e4540ab",
|
|
||||||
"carrier": "Colorado Camel Milk",
|
|
||||||
"retailer": "Colorado Camel Milk",
|
|
||||||
"description": "Raw Camel Milk (6 Pints, Frozen)",
|
|
||||||
"status": "shipped",
|
|
||||||
"notes": "Order #16698, $115.00 (paid via Venmo)",
|
|
||||||
"created_at": "2026-02-07T17:42:53.341962693-05:00",
|
|
||||||
"updated_at": "2026-02-08T03:50:24.733529085-05:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "29790fb7",
|
|
||||||
"carrier": "UPS",
|
|
||||||
"description": "Package from Jimmy Le (signature required)",
|
|
||||||
"tracking_number": "1ZX866D54223587093",
|
|
||||||
"tracking_url": "https://www.ups.com/track?tracknum=1ZX866D54223587093",
|
|
||||||
"expected_date": "2026-02-07",
|
|
||||||
"status": "delivered",
|
|
||||||
"notes": "Signature required. ETA 2-6 PM",
|
|
||||||
"created_at": "2026-02-07T23:46:33.876959687-05:00",
|
|
||||||
"updated_at": "2026-02-08T03:48:34.244859442-05:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "30314356",
|
|
||||||
"carrier": "Amazon",
|
|
||||||
"retailer": "Amazon",
|
|
||||||
"description": "MagiCare 75% Large Alcohol Wipes",
|
|
||||||
"expected_date": "2026-02-09",
|
|
||||||
"status": "shipped",
|
|
||||||
"notes": "Order #112-6757146-7896258, $29.95",
|
|
||||||
"created_at": "2026-02-08T01:09:32.878277346-05:00",
|
|
||||||
"updated_at": "2026-02-08T03:50:24.739469433-05:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "e72c423c",
|
|
||||||
"carrier": "Amazon",
|
|
||||||
"retailer": "Amazon",
|
|
||||||
"description": "Master Supplements TruBifido - 30ct",
|
|
||||||
"tracking_number": "113-3964151-5665049",
|
|
||||||
"expected_date": "2026-02-10",
|
|
||||||
"status": "shipped",
|
|
||||||
"notes": "Sophia supplement (from Monica Rodriguez list). $37.46",
|
|
||||||
"created_at": "2026-02-08T01:34:44.900771986-05:00",
|
|
||||||
"updated_at": "2026-02-08T01:34:44.900772032-05:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "e67d6e34",
|
|
||||||
"carrier": "Amazon",
|
|
||||||
"retailer": "Amazon",
|
|
||||||
"description": "ButyrEn Colon Lining + INCAS Organic Butterfly Pea",
|
|
||||||
"expected_date": "2026-02-08",
|
|
||||||
"status": "out_for_delivery",
|
|
||||||
"notes": "Order 113-8193397-8031467, arriving today 7-11AM",
|
|
||||||
"created_at": "2026-02-08T07:37:44.383098362-05:00",
|
|
||||||
"updated_at": "2026-02-08T07:37:44.38309841-05:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "eb31bd8c",
|
|
||||||
"carrier": "Amazon",
|
|
||||||
"retailer": "Amazon",
|
|
||||||
"description": "OM Mushroom Cordyceps Powder",
|
|
||||||
"expected_date": "2026-02-08",
|
|
||||||
"status": "out_for_delivery",
|
|
||||||
"notes": "Order 113-3964151-5665049, arriving today",
|
|
||||||
"created_at": "2026-02-08T07:37:44.389777982-05:00",
|
|
||||||
"updated_at": "2026-02-08T07:37:44.389778035-05:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "1cc821ec",
|
|
||||||
"carrier": "Amazon",
|
|
||||||
"retailer": "Amazon",
|
|
||||||
"description": "ARG ButyrEn + INCAS Organic Butterfly Pea",
|
|
||||||
"tracking_number": "113-8193397-8031467",
|
|
||||||
"status": "delivered",
|
|
||||||
"notes": "Arrived Feb 7",
|
|
||||||
"created_at": "2026-02-08T09:07:54.018512029-05:00",
|
|
||||||
"updated_at": "2026-02-08T09:07:54.01851207-05:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "0bcd9ff8",
|
|
||||||
"carrier": "Amazon",
|
|
||||||
"retailer": "Amazon",
|
|
||||||
"description": "OM Mushroom Cordyceps Powder",
|
|
||||||
"tracking_number": "113-3964151-5665049",
|
|
||||||
"status": "delivered",
|
|
||||||
"notes": "Arrived Feb 7",
|
|
||||||
"created_at": "2026-02-08T09:07:54.023912332-05:00",
|
|
||||||
"updated_at": "2026-02-08T09:07:54.023912377-05:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "41944abc",
|
|
||||||
"carrier": "Amazon",
|
|
||||||
"retailer": "Amazon",
|
|
||||||
"description": "MagiCare 75% Large Alcohol Wipes",
|
|
||||||
"tracking_number": "112-6757146-7896258",
|
|
||||||
"expected_date": "2026-02-09",
|
|
||||||
"status": "in_transit",
|
|
||||||
"created_at": "2026-02-08T09:07:54.029203598-05:00",
|
|
||||||
"updated_at": "2026-02-08T09:07:54.02920364-05:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "1c992ade",
|
|
||||||
"carrier": "Amazon",
|
|
||||||
"retailer": "Amazon",
|
|
||||||
"description": "Aveda Men Pure-Formance Shampoo",
|
|
||||||
"tracking_number": "112-5748597-2525838",
|
|
||||||
"expected_date": "2026-02-09",
|
|
||||||
"status": "in_transit",
|
|
||||||
"created_at": "2026-02-08T09:07:54.034733552-05:00",
|
|
||||||
"updated_at": "2026-02-08T09:07:54.03473359-05:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "e37775f6",
|
|
||||||
"carrier": "3-day shipping",
|
|
||||||
"retailer": "Colorado Camel Milk",
|
|
||||||
"description": "Raw Camel Milk (6 Pints, frozen)",
|
|
||||||
"tracking_number": "16698",
|
|
||||||
"expected_date": "2026-02-10",
|
|
||||||
"status": "shipped",
|
|
||||||
"created_at": "2026-02-08T09:07:54.039884643-05:00",
|
|
||||||
"updated_at": "2026-02-08T09:07:54.039884684-05:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "15d39860",
|
|
||||||
"carrier": "Amazon",
|
|
||||||
"retailer": "Amazon",
|
|
||||||
"description": "MagiCare 75% Large Alcohol Wipes",
|
|
||||||
"tracking_number": "112-6757146-7896258",
|
|
||||||
"expected_date": "2026-02-09",
|
|
||||||
"status": "shipped",
|
|
||||||
"notes": "Arriving tomorrow per email",
|
|
||||||
"created_at": "2026-02-08T12:21:01.509773967-05:00",
|
|
||||||
"updated_at": "2026-02-08T12:21:01.509774016-05:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "11eabe68",
|
|
||||||
"carrier": "Amazon",
|
|
||||||
"retailer": "Amazon",
|
|
||||||
"description": "World Organic Chlorophyll",
|
|
||||||
"status": "delivered",
|
|
||||||
"notes": "Order #112-4979107-5850639. Delivered 2026-02-08, left near front door.",
|
|
||||||
"created_at": "2026-02-08T17:27:11.616850452-05:00",
|
|
||||||
"updated_at": "2026-02-08T17:27:11.616850498-05:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "73113b30",
|
|
||||||
"carrier": "Amazon",
|
|
||||||
"retailer": "Amazon",
|
|
||||||
"description": "MagiCare 75% Large Alcohol Wipes",
|
|
||||||
"expected_date": "2026-02-09",
|
|
||||||
"status": "shipped",
|
|
||||||
"notes": "Order #112-6757146-7896258. $29.95 total.",
|
|
||||||
"created_at": "2026-02-08T17:27:11.677036538-05:00",
|
|
||||||
"updated_at": "2026-02-08T17:27:11.677036594-05:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "a0c2d2a4",
|
|
||||||
"carrier": "Amazon",
|
|
||||||
"retailer": "Amazon",
|
|
||||||
"description": "ARG ButyrEn Colon Lining + INCAS Organic Butterfly Pea",
|
|
||||||
"expected_date": "2026-02-07",
|
|
||||||
"status": "delivered",
|
|
||||||
"notes": "Order #113-8193397-8031467. $46.18 total. Was arriving Feb 7.",
|
|
||||||
"created_at": "2026-02-08T17:27:11.735634844-05:00",
|
|
||||||
"updated_at": "2026-02-08T17:27:11.735634885-05:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "fd8973ed",
|
|
||||||
"carrier": "Amazon",
|
|
||||||
"retailer": "Amazon",
|
|
||||||
"description": "OM Mushroom Cordyceps Powder",
|
|
||||||
"expected_date": "2026-02-07",
|
|
||||||
"status": "delivered",
|
|
||||||
"notes": "Order #113-3964151-5665049. $32.38 total. Was arriving Feb 7.",
|
|
||||||
"created_at": "2026-02-08T17:27:11.792476354-05:00",
|
|
||||||
"updated_at": "2026-02-08T17:27:11.792476408-05:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "59a4837a",
|
|
||||||
"carrier": "Amazon",
|
|
||||||
"retailer": "Amazon",
|
|
||||||
"description": "Aveda Men Pure-Formance Shampoo",
|
|
||||||
"expected_date": "2026-02-09",
|
|
||||||
"status": "shipped",
|
|
||||||
"notes": "Order #112-5748597-2525838, $34.24",
|
|
||||||
"created_at": "2026-02-08T19:44:36.545183521-05:00",
|
|
||||||
"updated_at": "2026-02-08T19:44:36.545183561-05:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "1f84c0fa",
|
|
||||||
"carrier": "Amazon",
|
|
||||||
"retailer": "Amazon",
|
|
||||||
"description": "Aveda Men Pure-Formance Shampoo",
|
|
||||||
"expected_date": "2026-02-10",
|
|
||||||
"status": "shipped",
|
|
||||||
"created_at": "2026-02-09T06:05:19.42294246-05:00",
|
|
||||||
"updated_at": "2026-02-09T06:05:19.422942506-05:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "15658ca7",
|
|
||||||
"carrier": "UPS",
|
|
||||||
"retailer": "BeeVitamin",
|
|
||||||
"description": "RAW Wild Collected Organic Bee Pollen - Wellness Pack 13.5oz",
|
|
||||||
"tracking_number": "1ZG772R41219164330",
|
|
||||||
"tracking_url": "https://www.ups.com/track?tracknum=1ZG772R41219164330",
|
|
||||||
"expected_date": "2026-02-10",
|
|
||||||
"status": "delivered",
|
|
||||||
"notes": "Delivered Feb 10 at 12:22 PM",
|
|
||||||
"created_at": "2026-02-09T09:41:34.259459825-05:00",
|
|
||||||
"updated_at": "2026-02-10T13:48:06.246068906-05:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "46798d2a",
|
|
||||||
"carrier": "UPS",
|
|
||||||
"description": "MELBOURNE RETAIL LLC package",
|
|
||||||
"tracking_number": "1ZG772R41219164330",
|
|
||||||
"expected_date": "2026-02-10",
|
|
||||||
"status": "delivered",
|
|
||||||
"notes": "Delivered Tue 2/10 at 12:22 PM. BeeVitamin RAW Wild Collected Organic Bee Pollen Wellness Pack.",
|
|
||||||
"created_at": "2026-02-09T11:57:33.581373456-05:00",
|
|
||||||
"updated_at": "2026-02-10T14:10:06.549296379-05:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "2d1cd36b",
|
|
||||||
"carrier": "UPS",
|
|
||||||
"description": "Melbourne Retail LLC package",
|
|
||||||
"expected_date": "2026-02-10",
|
|
||||||
"status": "in_transit",
|
|
||||||
"notes": "Signature required. Delivery window 11am-3pm. Ship to 851 Brightwaters Blvd NE.",
|
|
||||||
"created_at": "2026-02-09T11:58:46.928219974-05:00",
|
|
||||||
"updated_at": "2026-02-09T11:58:46.928220012-05:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "66cdfe4f",
|
|
||||||
"carrier": "Amazon",
|
|
||||||
"retailer": "Amazon",
|
|
||||||
"description": "Frontier Co-op Organic Oatstraw",
|
|
||||||
"tracking_number": "114-4974113-8321869",
|
|
||||||
"expected_date": "2026-02-10",
|
|
||||||
"status": "shipped",
|
|
||||||
"notes": "$24.58",
|
|
||||||
"created_at": "2026-02-09T20:09:15.823706776-05:00",
|
|
||||||
"updated_at": "2026-02-09T20:09:15.823706816-05:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "ad240775",
|
|
||||||
"carrier": "Amazon",
|
|
||||||
"retailer": "Amazon",
|
|
||||||
"description": "Goodnites Girls Bedwetting Underwear x2",
|
|
||||||
"tracking_number": "113-7309601-6593066",
|
|
||||||
"tracking_url": "https://www.amazon.com/gp/your-account/order-details?orderID=113-7309601-6593066",
|
|
||||||
"expected_date": "2026-02-10",
|
|
||||||
"status": "in_transit",
|
|
||||||
"notes": "$116.98 total",
|
|
||||||
"created_at": "2026-02-10T01:13:15.372694153-05:00",
|
|
||||||
"updated_at": "2026-02-10T01:13:15.37269427-05:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "26af60bd",
|
|
||||||
"carrier": "Amazon",
|
|
||||||
"retailer": "Amazon",
|
|
||||||
"description": "Rubbermaid Brilliance Glass Storage",
|
|
||||||
"tracking_number": "112-3200486-0340244",
|
|
||||||
"expected_date": "2026-02-10",
|
|
||||||
"status": "out_for_delivery",
|
|
||||||
"notes": "$37.44 - Late, now expected today (Feb 11) by 10 PM",
|
|
||||||
"created_at": "2026-02-10T15:43:39.951663798-05:00",
|
|
||||||
"updated_at": "2026-02-11T04:19:48.557977901-05:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "00ffdf0c",
|
|
||||||
"carrier": "Amazon",
|
|
||||||
"retailer": "Amazon",
|
|
||||||
"description": "Rubbermaid Brilliance Glass Storage",
|
|
||||||
"tracking_number": "112-3200486-0340244",
|
|
||||||
"expected_date": "2026-02-10",
|
|
||||||
"status": "out_for_delivery",
|
|
||||||
"notes": "Arriving today 5-10 PM",
|
|
||||||
"created_at": "2026-02-10T18:07:38.038611526-05:00",
|
|
||||||
"updated_at": "2026-02-10T18:07:38.038611577-05:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "ffabc266",
|
|
||||||
"carrier": "Amazon",
|
|
||||||
"retailer": "Amazon",
|
|
||||||
"description": "500 Removable Freezer Pantry Labels",
|
|
||||||
"tracking_number": "113-3740111-3149065",
|
|
||||||
"expected_date": "2026-02-11",
|
|
||||||
"status": "in_transit",
|
|
||||||
"notes": "$8.55",
|
|
||||||
"created_at": "2026-02-10T22:03:38.220701198-05:00",
|
|
||||||
"updated_at": "2026-02-10T22:03:38.220701243-05:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "9cb9de1f",
|
|
||||||
"carrier": "Amazon",
|
|
||||||
"retailer": "Amazon",
|
|
||||||
"description": "EZVALO 3 Pack Motion Sensor Night Light",
|
|
||||||
"tracking_number": "112-9663114-1823458",
|
|
||||||
"expected_date": "2026-02-13",
|
|
||||||
"status": "delivered",
|
|
||||||
"notes": "Delivered Feb 13, left near front door. Order #112-9663114-1823458, $20.32",
|
|
||||||
"created_at": "2026-02-12T10:33:24.342828313-05:00",
|
|
||||||
"updated_at": "2026-02-13T16:05:39.180806618-05:00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
"items": {
|
"items": {
|
||||||
"claude": {
|
"claude": {
|
||||||
"key": "claude",
|
"key": "claude",
|
||||||
"value": "67% used · 4:00 PM",
|
"value": "67% used · 5:00 PM",
|
||||||
"type": "info",
|
"type": "info",
|
||||||
"updated_at": "2026-02-13T16:00:03.090690991-05:00"
|
"updated_at": "2026-02-13T17:00:02.730387548-05:00"
|
||||||
},
|
},
|
||||||
"claude-code-update": {
|
"claude-code-update": {
|
||||||
"key": "claude-code-update",
|
"key": "claude-code-update",
|
||||||
|
|
|
||||||
BIN
james-dashboard
BIN
james-dashboard
Binary file not shown.
169
server.go
169
server.go
|
|
@ -529,6 +529,135 @@ func (s *DeliveryStore) ListAll() []Delivery {
|
||||||
return s.Deliveries
|
return s.Deliveries
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Upsert finds an existing delivery by tracking_number (exact match) or
|
||||||
|
// description+retailer (fuzzy), then updates it. If no match, creates new.
|
||||||
|
// Returns the delivery, whether it was an update (vs create), and any error.
|
||||||
|
func (s *DeliveryStore) Upsert(d Delivery) (Delivery, bool, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
// 1. Match by tracking_number (strongest signal)
|
||||||
|
if d.TrackingNumber != "" {
|
||||||
|
for i, existing := range s.Deliveries {
|
||||||
|
if existing.TrackingNumber == d.TrackingNumber {
|
||||||
|
s.mergeDelivery(i, d)
|
||||||
|
s.save()
|
||||||
|
return s.Deliveries[i], true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Match by retailer + similar description (for orders without tracking yet)
|
||||||
|
if d.Retailer != "" && d.Description != "" {
|
||||||
|
for i, existing := range s.Deliveries {
|
||||||
|
if existing.Retailer == d.Retailer && existing.Status != "delivered" &&
|
||||||
|
deliveryDescSimilar(existing.Description, d.Description) {
|
||||||
|
s.mergeDelivery(i, d)
|
||||||
|
s.save()
|
||||||
|
return s.Deliveries[i], true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. No match — create new
|
||||||
|
d.ID = uuid.New().String()[:8]
|
||||||
|
d.CreatedAt = time.Now()
|
||||||
|
d.UpdatedAt = time.Now()
|
||||||
|
if d.Status == "" {
|
||||||
|
d.Status = "shipped"
|
||||||
|
}
|
||||||
|
s.Deliveries = append(s.Deliveries, d)
|
||||||
|
return d, false, s.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
// mergeDelivery updates existing delivery fields with non-empty values from the update.
|
||||||
|
// Status always progresses forward (shipped→in_transit→out_for_delivery→delivered), never backward.
|
||||||
|
func (s *DeliveryStore) mergeDelivery(i int, u Delivery) {
|
||||||
|
if u.Carrier != "" {
|
||||||
|
s.Deliveries[i].Carrier = u.Carrier
|
||||||
|
}
|
||||||
|
if u.Retailer != "" {
|
||||||
|
s.Deliveries[i].Retailer = u.Retailer
|
||||||
|
}
|
||||||
|
if u.Description != "" {
|
||||||
|
s.Deliveries[i].Description = u.Description
|
||||||
|
}
|
||||||
|
if u.TrackingNumber != "" {
|
||||||
|
s.Deliveries[i].TrackingNumber = u.TrackingNumber
|
||||||
|
}
|
||||||
|
if u.TrackingURL != "" {
|
||||||
|
s.Deliveries[i].TrackingURL = u.TrackingURL
|
||||||
|
}
|
||||||
|
if u.ExpectedDate != "" {
|
||||||
|
s.Deliveries[i].ExpectedDate = u.ExpectedDate
|
||||||
|
}
|
||||||
|
if u.Status != "" && deliveryStatusRank(u.Status) >= deliveryStatusRank(s.Deliveries[i].Status) {
|
||||||
|
s.Deliveries[i].Status = u.Status
|
||||||
|
}
|
||||||
|
if u.Notes != "" {
|
||||||
|
s.Deliveries[i].Notes = u.Notes
|
||||||
|
}
|
||||||
|
s.Deliveries[i].UpdatedAt = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
var deliveryStatusOrder = map[string]int{
|
||||||
|
"shipped": 0, "in_transit": 1, "out_for_delivery": 2, "delayed": 1, "delivered": 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
func deliveryStatusRank(s string) int {
|
||||||
|
if r, ok := deliveryStatusOrder[s]; ok {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// deliveryDescSimilar checks if two descriptions likely refer to the same product.
|
||||||
|
// Compares normalized words — if 50%+ of shorter description's words appear in longer, it's a match.
|
||||||
|
func deliveryDescSimilar(a, b string) bool {
|
||||||
|
normalize := func(s string) []string {
|
||||||
|
s = strings.ToLower(s)
|
||||||
|
// Remove common noise
|
||||||
|
for _, noise := range []string{",", ".", "-", "(", ")", "+", "&", "x2", "x3", "x4"} {
|
||||||
|
s = strings.ReplaceAll(s, noise, " ")
|
||||||
|
}
|
||||||
|
words := strings.Fields(s)
|
||||||
|
// Filter short/common words
|
||||||
|
var result []string
|
||||||
|
for _, w := range words {
|
||||||
|
if len(w) > 2 {
|
||||||
|
result = append(result, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
wordsA := normalize(a)
|
||||||
|
wordsB := normalize(b)
|
||||||
|
if len(wordsA) == 0 || len(wordsB) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use shorter as reference
|
||||||
|
shorter, longer := wordsA, wordsB
|
||||||
|
if len(wordsA) > len(wordsB) {
|
||||||
|
shorter, longer = wordsB, wordsA
|
||||||
|
}
|
||||||
|
|
||||||
|
longerSet := make(map[string]bool)
|
||||||
|
for _, w := range longer {
|
||||||
|
longerSet[w] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
matches := 0
|
||||||
|
for _, w := range shorter {
|
||||||
|
if longerSet[w] {
|
||||||
|
matches++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return float64(matches)/float64(len(shorter)) >= 0.5
|
||||||
|
}
|
||||||
|
|
||||||
func (s *DeliveryStore) Get(id string) (Delivery, bool) {
|
func (s *DeliveryStore) Get(id string) (Delivery, bool) {
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
defer s.mu.RUnlock()
|
defer s.mu.RUnlock()
|
||||||
|
|
@ -1175,6 +1304,46 @@ func main() {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Delivery upsert — match by tracking_number or description, update or create
|
||||||
|
mux.HandleFunc("/api/deliveries/upsert", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
cors(w)
|
||||||
|
if r.Method == "OPTIONS" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method != "PUT" && r.Method != "POST" {
|
||||||
|
http.Error(w, `{"error": "method not allowed"}`, http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var d Delivery
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&d); err != nil {
|
||||||
|
http.Error(w, `{"error": "invalid JSON"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if d.Description == "" {
|
||||||
|
http.Error(w, `{"error": "description required"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, wasUpdate, err := deliveryStore.Upsert(d)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, `{"error": "failed to save"}`, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
status := http.StatusCreated
|
||||||
|
action := "created"
|
||||||
|
if wasUpdate {
|
||||||
|
status = http.StatusOK
|
||||||
|
action = "updated"
|
||||||
|
}
|
||||||
|
w.WriteHeader(status)
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"delivery": result,
|
||||||
|
"action": action,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// Delivery by ID
|
// Delivery by ID
|
||||||
mux.HandleFunc("/api/deliveries/", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/api/deliveries/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
cors(w)
|
cors(w)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue