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) +}