From b404303f84f160534e252a1af116dcc048743700 Mon Sep 17 00:00:00 2001 From: marcus06 Date: Mon, 25 May 2026 16:45:15 +0200 Subject: [PATCH] feat(notify): daily slack reminder for paid orders awaiting labels (#8) so the ain was that older paid orders were getting buried in the #orders channel in slack and i would just forget about them. Pin/unpin from earlier helped, but if i missed pinning one or the channel got noisy, it was still easy to lose track. this adds a 24h cron that queries any paid order older than 24h with no label yet and dumps them all into one #orders message. Each line shows the order id, how old it is, total, where it is going, and a permalink back to the orders slack thread so i can hop in and hit Buy Label Ports the idea from terminal.shops cron/unshipped.ts but out stuch state is different( we auto-promote paid->shipped on label-buy so PRE_TRANSIT-stuck can not happen), so the query is "paid + no tracking number + >24h" instead. NO orders get a "Bring blocked" tag since i dont have the norwegian org set up yet and can not actually ship those today. Also fires once 40s after startup so a deploy does not black out the reminder for a full day. --- api/handlers/reconcile.go | 24 ++++++++++++++++++++++ api/main.go | 15 ++++++++++++++ pkg/notify/slack.go | 42 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/api/handlers/reconcile.go b/api/handlers/reconcile.go index 7708842..d34c566 100644 --- a/api/handlers/reconcile.go +++ b/api/handlers/reconcile.go @@ -7,6 +7,7 @@ import ( "terminalShop/pkg/database" "terminalShop/pkg/models" + "terminalShop/pkg/notify" "github.com/stripe/stripe-go/v78" "github.com/stripe/stripe-go/v78/paymentintent" @@ -75,3 +76,26 @@ func ReconcileOrders(stripeKey string) { } } } + +// ReconcileUnshipped finds paid order orlder than 24h with no tracking +// number and posts single Slack reminder. Pure read + notif +func ReconcileUnshipped() { + db := database.GetDB() + + var orders []models.Order + cutoff := time.Now().Add(-24 * time.Hour) + err := db.Where( + "status = ? AND (tracking_number = '' OR tracking_number IS NULL) AND created_at < ?", + models.OrderStatusPaid, cutoff, + ).Order("created_at ASC").Find(&orders).Error + if err != nil { + reconcileLog().Error("unshipped query failed", "error", err) + return + } + if len(orders) == 0 { + return + } + + reconcileLog().Info("unshipped orders found", "count", len(orders)) + notify.SlackUnshippedReminder(orders) +} diff --git a/api/main.go b/api/main.go index 731f5e5..cbbc50b 100644 --- a/api/main.go +++ b/api/main.go @@ -113,6 +113,21 @@ func main() { } }() + go func() { + time.Sleep(30 * time.Second) + handlers.ReconcileUnshipped() + ticker := time.NewTicker(24 * time.Hour) + defer ticker.Stop() + for { + select { + case <-ticker.C: + handlers.ReconcileUnshipped() + case <-ctx.Done(): + return + } + } + }() + log.Println("API server started. Press Ctrl+C to shutdown.") // Block until signal received diff --git a/pkg/notify/slack.go b/pkg/notify/slack.go index 5ac2537..aa80957 100644 --- a/pkg/notify/slack.go +++ b/pkg/notify/slack.go @@ -264,4 +264,44 @@ func markStatusActionBlock(orderID uint) map[string]any { } } -var _ = time.Second +func SlackUnshippedReminder(orders []models.Order) { + token := os.Getenv("SLACK_BOT_TOKEN") + channel := os.Getenv("SLACK_CHANNEL") + if token == "" || channel == "" || len(orders) == 0 { + return + } + + workspace := os.Getenv("SLACK_WORKSPACE_SUBDOMAIN") + + lines := []string{fmt.Sprintf(":hourglass_flowing_sand: %d order(s) awaiting label", len(orders))} + for _, order := range orders { + lines = append(lines, " "+unshippedOrderLine(order, channel, workspace)) + } + + _, _ = postSlackMessage(token, map[string]any{ + "channel": channel, + "text": strings.Join(lines, "\n"), + }) +} + +func unshippedOrderLine(order models.Order, channel, workspace string) string { + age := time.Since(order.CreatedAt).Round(time.Hour) + location := order.ShippingCountry + if order.ShippingCity != "" { + location = order.ShippingCity + ", " + order.ShippingCountry + } + + label := fmt.Sprintf("Order #%d", order.ID) + if order.SlackThreadTS != nil && *order.SlackThreadTS != "" && workspace != "" { + ts := strings.ReplaceAll(*order.SlackThreadTS, ".", "") + permalink := fmt.Sprintf("https://%s.slack.com/archives/%s/p%s", workspace, channel, ts) + label = fmt.Sprintf("<%s|Order #%d>", permalink, order.ID) + } + + tag := "" + if order.ShippingCountry == "NO" { + tag = " _(NO * Bring blocked)_" + } + + return fmt.Sprintf("%s * %s old * %s * %s%s", label, age, dollars(order.Total), location, tag) +}