mirror of
https://github.com/marcusDenslow/terminalShop.git
synced 2026-10-25 02:52:20 +02:00
feat(orders): customer-facking tracking labels and webhook response body
the big colored status on the order detail page used to just say
"Shipped" the moment i bought the label, which didnt really feel right
since the carrier hadnt actually picked anything up yet. now it walks
through "Label created" -> "In transit" -> "Delivered" as real scans
come in, and keeps the "Delivery Issue" / "Returned to sender" labels
for unhappy paths
also wired up the existing tracking_status_details column to the tui. i
skipped it before but the response actually includes a description of
the delivery, that is now subtext under the colored statu.
and the shippo webhook now returns a small json body instead of an empty
200. this is just for the operator that runs the curls commands to
update the shipment, which makes debugging a lot less guesswork
This commit is contained in:
@@ -279,7 +279,11 @@ func (h *WebhookHandler) HandleShippo(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
if payload.Event != "track_updated" {
|
if payload.Event != "track_updated" {
|
||||||
webhookLog().Info("shippo webhook: ignoring no-tracking event", "event", payload.Event)
|
webhookLog().Info("shippo webhook: ignoring no-tracking event", "event", payload.Event)
|
||||||
w.WriteHeader(http.StatusOK)
|
writeShippoResponse(w, map[string]any{
|
||||||
|
"received": true,
|
||||||
|
"skipped": "non-tracking event",
|
||||||
|
"event": payload.Event,
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,7 +292,11 @@ func (h *WebhookHandler) HandleShippo(w http.ResponseWriter, r *http.Request) {
|
|||||||
webhookLog().Warn("shippo webhook: unrecognized status, skipping",
|
webhookLog().Warn("shippo webhook: unrecognized status, skipping",
|
||||||
"status", payload.Data.TrackingStatus.Status,
|
"status", payload.Data.TrackingStatus.Status,
|
||||||
"tracking_number", payload.Data.TrackingNumber)
|
"tracking_number", payload.Data.TrackingNumber)
|
||||||
w.WriteHeader(http.StatusOK)
|
writeShippoResponse(w, map[string]any{
|
||||||
|
"received": true,
|
||||||
|
"skipped": "unrecognized status",
|
||||||
|
"raw_status": payload.Data.TrackingStatus.Status,
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,12 +305,21 @@ func (h *WebhookHandler) HandleShippo(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err := db.Where("tracking_number = ?", payload.Data.TrackingNumber).First(&order).Error; err != nil {
|
if err := db.Where("tracking_number = ?", payload.Data.TrackingNumber).First(&order).Error; err != nil {
|
||||||
webhookLog().Warn("shippo webhook: order not found",
|
webhookLog().Warn("shippo webhook: order not found",
|
||||||
"tracking_number", payload.Data.TrackingNumber)
|
"tracking_number", payload.Data.TrackingNumber)
|
||||||
w.WriteHeader(http.StatusOK)
|
writeShippoResponse(w, map[string]any{
|
||||||
|
"received": true,
|
||||||
|
"skipped": "order not found",
|
||||||
|
"tracking_number": payload.Data.TrackingNumber,
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if order.TrackingStatusUpdatedAt != nil && !payload.Data.TrackingStatus.StatusDate.After(*order.TrackingStatusUpdatedAt) {
|
if order.TrackingStatusUpdatedAt != nil && !payload.Data.TrackingStatus.StatusDate.After(*order.TrackingStatusUpdatedAt) {
|
||||||
w.WriteHeader(http.StatusOK)
|
writeShippoResponse(w, map[string]any{
|
||||||
|
"received": true,
|
||||||
|
"skipped": "stale status_date",
|
||||||
|
"order_id": order.ID,
|
||||||
|
"current_tracking_status": string(order.TrackingStatus),
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,7 +349,15 @@ func (h *WebhookHandler) HandleShippo(w http.ResponseWriter, r *http.Request) {
|
|||||||
if deliveredTransition {
|
if deliveredTransition {
|
||||||
audit.OrderDelivered(order.ID, payload.Data.TrackingNumber)
|
audit.OrderDelivered(order.ID, payload.Data.TrackingNumber)
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusOK)
|
writeShippoResponse(w, map[string]any{
|
||||||
|
"received": true,
|
||||||
|
"order_id": order.ID,
|
||||||
|
"tracking_status": string(newStatus),
|
||||||
|
"previous_tracking_status": string(previousTrackingStatus),
|
||||||
|
"lifecycle_status": string(order.Status),
|
||||||
|
"delivered_transition": deliveredTransition,
|
||||||
|
"status_details": payload.Data.TrackingStatus.StatusDetails,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func mapShippoStatus(s string) (models.TrackingStatus, bool) {
|
func mapShippoStatus(s string) (models.TrackingStatus, bool) {
|
||||||
@@ -352,3 +377,9 @@ func mapShippoStatus(s string) (models.TrackingStatus, bool) {
|
|||||||
}
|
}
|
||||||
return models.TrackingStatusUnknown, false
|
return models.TrackingStatusUnknown, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func writeShippoResponse(w http.ResponseWriter, payload map[string]any) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_ = json.NewEncoder(w).Encode(payload)
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,16 +31,16 @@ func (order *Order) DisplayState() string {
|
|||||||
case OrderStatusShipped:
|
case OrderStatusShipped:
|
||||||
switch order.TrackingStatus {
|
switch order.TrackingStatus {
|
||||||
case TrackingStatusTransit:
|
case TrackingStatusTransit:
|
||||||
return "Shipped * in transit"
|
return "In Transit"
|
||||||
case TrackingStatusFailure:
|
case TrackingStatusFailure:
|
||||||
return "Shipped * delivery issue"
|
return "Delivery Issue"
|
||||||
case TrackingStatusReturned:
|
case TrackingStatusReturned:
|
||||||
return "Shipped * returned to sender"
|
return "Returned to Sender"
|
||||||
case TrackingStatusDelivered:
|
case TrackingStatusDelivered:
|
||||||
// Webhook hasn't promoted Status yet, trust tracking
|
// Webhook hasn't promoted Status yet, trust tracking
|
||||||
return "Delivered"
|
return "Delivered"
|
||||||
}
|
}
|
||||||
return "Shipped"
|
return "Label Created"
|
||||||
case OrderStatusDelivered:
|
case OrderStatusDelivered:
|
||||||
return "Delivered"
|
return "Delivered"
|
||||||
case OrderStatusCancelled:
|
case OrderStatusCancelled:
|
||||||
|
|||||||
@@ -13,10 +13,11 @@ func TestOrderDisplayState(t *testing.T) {
|
|||||||
{"paid no tracking", Order{Status: OrderStatusPaid}, "Paid * awaiting label", DisplayKindAccent},
|
{"paid no tracking", Order{Status: OrderStatusPaid}, "Paid * awaiting label", DisplayKindAccent},
|
||||||
{"paid with tracking, no event", Order{Status: OrderStatusPaid, TrackingNumber: "12999"}, "Paid", DisplayKindAccent},
|
{"paid with tracking, no event", Order{Status: OrderStatusPaid, TrackingNumber: "12999"}, "Paid", DisplayKindAccent},
|
||||||
{"Paid pre-transit", Order{Status: OrderStatusPaid, TrackingNumber: "12999", TrackingStatus: TrackingStatusPreTransit}, "Paid * ready to ship", DisplayKindAccent},
|
{"Paid pre-transit", Order{Status: OrderStatusPaid, TrackingNumber: "12999", TrackingStatus: TrackingStatusPreTransit}, "Paid * ready to ship", DisplayKindAccent},
|
||||||
{"shipped no tracking event", Order{Status: OrderStatusShipped}, "Shipped", DisplayKindBrand},
|
{"shipped no tracking event", Order{Status: OrderStatusShipped}, "Label Created", DisplayKindBrand},
|
||||||
{"shipped in transit", Order{Status: OrderStatusShipped, TrackingStatus: TrackingStatusTransit}, "Shipped * in transit", DisplayKindBrand},
|
{"shipped pre-transit", Order{Status: OrderStatusShipped, TrackingStatus: TrackingStatusPreTransit}, "Label Created", DisplayKindBrand},
|
||||||
{"shipped failure", Order{Status: OrderStatusShipped, TrackingStatus: TrackingStatusFailure}, "Shipped * delivery issue", DisplayKindWarning},
|
{"shipped in transit", Order{Status: OrderStatusShipped, TrackingStatus: TrackingStatusTransit}, "In Transit", DisplayKindBrand},
|
||||||
{"shipped returned", Order{Status: OrderStatusShipped, TrackingStatus: TrackingStatusReturned}, "Shipped * returned to sender", DisplayKindWarning},
|
{"shipped failure", Order{Status: OrderStatusShipped, TrackingStatus: TrackingStatusFailure}, "Delivery Issue", DisplayKindWarning},
|
||||||
|
{"shipped returned", Order{Status: OrderStatusShipped, TrackingStatus: TrackingStatusReturned}, "Returned to Sender", DisplayKindWarning},
|
||||||
{"shipped tracking-delivered (webhook lag)", Order{Status: OrderStatusShipped, TrackingStatus: TrackingStatusDelivered}, "Delivered", DisplayKindSuccess},
|
{"shipped tracking-delivered (webhook lag)", Order{Status: OrderStatusShipped, TrackingStatus: TrackingStatusDelivered}, "Delivered", DisplayKindSuccess},
|
||||||
{"delivered", Order{Status: OrderStatusDelivered}, "Delivered", DisplayKindSuccess},
|
{"delivered", Order{Status: OrderStatusDelivered}, "Delivered", DisplayKindSuccess},
|
||||||
{"cancelled", Order{Status: OrderStatusCancelled}, "Cancelled", DisplayKindError},
|
{"cancelled", Order{Status: OrderStatusCancelled}, "Cancelled", DisplayKindError},
|
||||||
|
|||||||
@@ -104,6 +104,10 @@ func (m Model) buildOrderDetailView(order models.Order, _ int) string {
|
|||||||
b.WriteString(labelStyle.Render("Status: "))
|
b.WriteString(labelStyle.Render("Status: "))
|
||||||
b.WriteString(m.displayStyle(order.DisplayKind()).Render(order.DisplayState()))
|
b.WriteString(m.displayStyle(order.DisplayKind()).Render(order.DisplayState()))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
|
if order.TrackingStatusDetails != "" {
|
||||||
|
b.WriteString(dimStyle.Render(" " + order.TrackingStatusDetails))
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
b.WriteString(labelStyle.Render("Date: "))
|
b.WriteString(labelStyle.Render("Date: "))
|
||||||
b.WriteString(valueStyle.Render(order.CreatedAt.Format("Jan 02 2006 3:04 PM")))
|
b.WriteString(valueStyle.Render(order.CreatedAt.Format("Jan 02 2006 3:04 PM")))
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
|
|||||||
Reference in New Issue
Block a user