diff --git a/api/handlers/orders.go b/api/handlers/orders.go index e34001a..2ceb366 100644 --- a/api/handlers/orders.go +++ b/api/handlers/orders.go @@ -6,6 +6,7 @@ import ( "os" "strconv" "terminalShop/pkg/bring" + "terminalShop/pkg/notify" "terminalShop/pkg/shipping" "terminalShop/pkg/shippo" "time" @@ -272,6 +273,7 @@ func (h *OrderHandler) PurchaseLabel(w http.ResponseWriter, r *http.Request) { } audit.LabelPurchased(order.ID, result.Carrier, result.TrackingNumber, result.TransactionID, result.CostCents) + go notify.SlackUnpinOrder(order.ID) utils.RespondSuccess(w, http.StatusOK, map[string]any{ "order_id": order.ID, diff --git a/pkg/notify/slack.go b/pkg/notify/slack.go index fcc1843..d008519 100644 --- a/pkg/notify/slack.go +++ b/pkg/notify/slack.go @@ -46,6 +46,39 @@ func postSlackMessage(token string, payload map[string]any) (string, bool) { return parsed.TS, true } +func pinMessage(token, channel, ts string) bool { + return slackPinCall(token, "https://slack.com/api/pins.add", channel, ts, "already_pinned") +} + +func unpinMessage(token, channel, ts string) bool { + return slackPinCall(token, "https://slack.com/api/pins.remove", channel, ts, "no_pin") +} + +func slackPinCall(token, endpoint, channel, ts, benignErr string) bool { + body, _ := json.Marshal(map[string]any{"channel": channel, "timestamp": ts}) + req, _ := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json; charset=utf-8") + req.Header.Set("Authorization", "Bearer "+token) + + resp, err := slackClient.Do(req) + if err != nil { + slog.Warn("slack pin call failed", "endpoint", endpoint, "error", err) + return false + } + defer resp.Body.Close() + + var parsed struct { + Ok bool `json:"ok"` + Error string `json:"error"` + } + _ = json.NewDecoder(resp.Body).Decode(&parsed) + if !parsed.Ok && parsed.Error != benignErr { + slog.Warn("slack pin call non-ok", "endpoint", endpoint, "error", parsed.Error) + return false + } + return true +} + func buildReceiptBlocks(order *models.Order) []map[string]any { blocks := []map[string]any{ {"type": "divider"}, @@ -121,6 +154,8 @@ func SlackOrderPaid(order *models.Order) { slog.Warn("failed to save slack thread ts", "order_id", order.ID, "error", err) } + pinMessage(token, channel, ts) + _, _ = postSlackMessage(token, map[string]any{ "channel": channel, "thread_ts": ts, @@ -129,6 +164,25 @@ func SlackOrderPaid(order *models.Order) { }) } +func SlackUnpinOrder(orderID uint) { + token := os.Getenv("SLACK_BOT_TOKEN") + channel := os.Getenv("SLACK_CHANNEL") + if token == "" || channel == "" { + return + } + + var order models.Order + if err := database.GetDB().Select("id", "slack_thread_ts").Where("id = ?", orderID).First(&order).Error; err != nil { + slog.Warn("failed to look up order for unpin", "order_id", orderID, "error", err) + return + } + if order.SlackThreadTS == nil || *order.SlackThreadTS == "" { + return + } + + unpinMessage(token, channel, *order.SlackThreadTS) +} + // SlackPostToOrderThread posts a plain-text reply inside the order's thread. // Looks up the thread ts from the database. Silent no-op if the order has no // saved thread (Slack was disabled when it was created, or post failed earlier).