diff --git a/pkg/audit/audit.go b/pkg/audit/audit.go index c2412ca..c95d723 100644 --- a/pkg/audit/audit.go +++ b/pkg/audit/audit.go @@ -5,6 +5,7 @@ package audit import ( "log/slog" + "terminalShop/pkg/notify" ) type eventType string @@ -116,6 +117,7 @@ func OrderPaid(userID, orderID uint, amountCents int, stripePaymentIntentID stri Amount: amountCents, StripeID: stripePaymentIntentID, }.attrs()...) + go notify.SlackOrderPaid(orderID, amountCents) } // OrderShipped records that an order has been marked as shipped with carrier metadata diff --git a/pkg/notify/slack.go b/pkg/notify/slack.go new file mode 100644 index 0000000..c9ddaaf --- /dev/null +++ b/pkg/notify/slack.go @@ -0,0 +1,33 @@ +package notify + +import ( + "bytes" + "encoding/json" + "fmt" + "log/slog" + "net/http" + "os" + "time" +) + +var slackClient = &http.Client{Timeout: 5 * time.Second} + +func SlackOrderPaid(orderID uint, amountCents int) { + url := os.Getenv("SLACK_WEBHOOK_URL") + if url == "" { + return + } + + text := fmt.Sprintf(":coffee: Order #%d paid - $%.2f", orderID, float64(amountCents)/100) + body, _ := json.Marshal(map[string]string{"text": text}) + + resp, err := slackClient.Post(url, "application/json", bytes.NewReader(body)) + if err != nil { + slog.Warn("slack notify failed", "error", err, "order_id", orderID) + return + } + defer resp.Body.Close() + if resp.StatusCode >= 300 { + slog.Warn("slack notify non-2xx", "status", resp.StatusCode, "order_id", orderID) + } +} diff --git a/scripts/ops/buy-label.sh b/scripts/ops/buy-label.sh new file mode 100755 index 0000000..e7390d3 --- /dev/null +++ b/scripts/ops/buy-label.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# Buy a shipping label via the admin endpoint. +# Usage: scripts/ops/buy-label.sh +set -euo pipefail + +ORDER_ID="${1:?usage: $0 }" +: "${ADMIN_KEY:?ADMIN_KEY not set; source scripts/ops/env.sh first}" +: "${BASE_URL:=https://api.sshops.uk}" + +curl -sS -X POST "$BASE_URL/api/v1/orders/$ORDER_ID/label" \ + -H "X-Admin-Key: $ADMIN_KEY" \ + -H "Content-Type: application/json" | jq . diff --git a/scripts/ops/env.sh b/scripts/ops/env.sh new file mode 100755 index 0000000..61f6992 --- /dev/null +++ b/scripts/ops/env.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Source once per shell session: `source scripts/ops/env.sh` +# Fetches ADMIN_API_KEY + SHIPPO_WEBHOOK_SECRET from prod's .env.prod +# and exports them so the other ops scripts can use them. + +export BASE_URL="${BASE_URL:-https://api.sshops.uk}" + +ADMIN_KEY_FETCHED=$(ssh ubuntu-helsinki "grep '^ADMIN_API_KEY=' /root/terminalShop/.env.prod | cut -d= -f2-") +SECRET_FETCHED=$(ssh ubuntu-helsinki "grep '^SHIPPO_WEBHOOK_SECRET=' /root/terminalShop/.env.prod | cut -d= -f2-") + +if [[ -z "$ADMIN_KEY_FETCHED" || -z "$SECRET_FETCHED" ]]; then + echo "failed to fetch secrets from prod; aborting" >&2 + return 1 2>/dev/null || exit 1 +fi + +export ADMIN_KEY="$ADMIN_KEY_FETCHED" +export SECRET="$SECRET_FETCHED" + +echo "ops env loaded (BASE_URL=$BASE_URL)" diff --git a/scripts/ops/list-paid.sh b/scripts/ops/list-paid.sh new file mode 100755 index 0000000..02039ee --- /dev/null +++ b/scripts/ops/list-paid.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# List recent paid orders awaiting label purchase. +# Usage: scripts/ops/list-paid.sh +set -euo pipefail + +ssh ubuntu-helsinki "sqlite3 -header -column /root/terminalShop/data/terminalshop.db \"SELECT id, status, total, shipping_name, shipping_city, shipping_country FROM orders WHERE status='paid' ORDER BY id DESC LIMIT 10;\"" diff --git a/scripts/ops/order-state.sh b/scripts/ops/order-state.sh new file mode 100755 index 0000000..a6a488a --- /dev/null +++ b/scripts/ops/order-state.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# Show the current DB row state for an order (lifecycle + tracking columns). +# Usage: scripts/ops/order-state.sh +set -euo pipefail + +ORDER_ID="${1:?usage: $0 }" + +ssh ubuntu-helsinki "sqlite3 -header -column /root/terminalShop/data/terminalshop.db \"SELECT id, status, total, carrier, tracking_number, tracking_status, tracking_status_details, tracking_status_updated_at FROM orders WHERE id=$ORDER_ID;\"" diff --git a/scripts/ops/scan.sh b/scripts/ops/scan.sh new file mode 100755 index 0000000..760010a --- /dev/null +++ b/scripts/ops/scan.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Fire a Shippo-shaped tracking webhook against an order's tracking number. +# Usage: scripts/ops/scan.sh [--detail "free text"] +set -euo pipefail + +ORDER_ID="${1:?usage: $0 [--detail \"text\"]}" +STATUS_LOWER="${2:?missing status; one of: pre_transit transit delivered failure returned}" +DETAIL="" +if [[ "${3:-}" == "--detail" ]]; then + DETAIL="${4:?--detail requires a value}" +fi + +: "${SECRET:?SECRET not set; source scripts/ops/env.sh first}" +: "${BASE_URL:=https://api.sshops.uk}" + +case "$STATUS_LOWER" in + pre_transit) STATUS="PRE_TRANSIT" ;; + transit) STATUS="TRANSIT" ;; + delivered) STATUS="DELIVERED" ;; + failure) STATUS="FAILURE" ;; + returned) STATUS="RETURNED" ;; + *) echo "unknown status: $STATUS_LOWER" >&2; exit 1 ;; +esac + +if [[ -z "$DETAIL" ]]; then + case "$STATUS" in + PRE_TRANSIT) DETAIL="Shipping label created, USPS awaiting item" ;; + TRANSIT) DETAIL="In transit to destination" ;; + DELIVERED) DETAIL="Delivered, front porch" ;; + FAILURE) DETAIL="Delivery exception" ;; + RETURNED) DETAIL="Returned to sender" ;; + esac +fi + +TRACKING=$(ssh ubuntu-helsinki "sqlite3 /root/terminalShop/data/terminalshop.db \"SELECT tracking_number FROM orders WHERE id=$ORDER_ID;\"") +if [[ -z "$TRACKING" ]]; then + echo "order $ORDER_ID has no tracking number; buy a label first" >&2 + exit 1 +fi + +STATUS_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + +curl -sS -X POST "$BASE_URL/api/v1/webhooks/shippo?token=$SECRET" \ + -H "Content-Type: application/json" \ + --data @- <