uniform
Build and Publish / BuildAndDeployArm64 (push) Failing after 7s
Build and Publish / BuildAndDeployAmd64 (push) Failing after 7s

This commit is contained in:
2026-07-20 00:13:24 +02:00
parent 83ed477aa4
commit e6f69f500a
14 changed files with 457 additions and 471 deletions
+6 -266
View File
@@ -12,12 +12,10 @@ import (
"log"
"log/slog"
"net/http"
"net/mail"
"os"
"path/filepath"
"strconv"
"strings"
"sort"
"sync"
"time"
@@ -56,6 +54,7 @@ type server struct {
idem *idempotency.Store
logger *slog.Logger
inventoryReservations order.InventoryReservationService
statsCache *statsCache
}
func main() {
@@ -229,8 +228,13 @@ func main() {
taxProvider: taxProvider,
defaultFlow: def.Name, dataDir: dataDir, idem: idem, logger: logger,
inventoryReservations: inventoryReservations,
statsCache: newStatsCache(dataDir, pool),
}
// Warm the stats cache before accepting requests so the first dashboard
// hit is instant. Rebuilds asynchronously after every mutation thereafter.
s.statsCache.Rebuild()
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) })
@@ -498,160 +502,6 @@ func (s *server) buildPlaceOrder(id order.OrderId, req *checkoutReq) *messages.P
return po
}
// --- dashboard stats ------------------------------------------------------
type orderAlert struct {
Severity string `json:"severity"` // warn | error
Message string `json:"message"`
OrderId string `json:"orderId,omitempty"`
}
type statusBucket struct {
Status order.Status `json:"status"`
Count int `json:"count"`
Total int64 `json:"total"`
}
type revenueSnapshot struct {
Total int64 `json:"total"`
Captured int64 `json:"captured"`
Refunded int64 `json:"refunded"`
Pending int64 `json:"pending"`
}
type dailyRevenue struct {
Date string `json:"date"` // YYYY-MM-DD
Total int64 `json:"total"`
}
type orderStatsResponse struct {
OrdersTotal int `json:"ordersTotal"`
Revenue revenueSnapshot `json:"revenue"`
ByStatus []statusBucket `json:"byStatus"`
RecentOrders []orderSummary `json:"recentOrders"`
DailyRevenue30 []dailyRevenue `json:"dailyRevenue30,omitempty"`
Alerts []orderAlert `json:"alerts"`
}
// handleStats scans all order logs and returns aggregated dashboard stats.
func (s *server) handleStats(w http.ResponseWriter, r *http.Request) {
matches, _ := filepath.Glob(filepath.Join(s.dataDir, "*.events.log"))
now := time.Now()
var totalOrders int
var totalRevenue, capturedRevenue, refundedRevenue int64
byStatus := map[order.Status]int{}
statusTotal := map[order.Status]int64{} // total amount per status
// Daily revenue for the last 30 days (keys are "2026-06-01" sortable strings).
dailyByDay := map[string]int64{}
for i := 0; i < 30; i++ {
day := now.AddDate(0, 0, -i).Format("2006-01-02")
dailyByDay[day] = 0
}
// Collect all order summaries, then sort by placedAt and take top 10.
var allOrders []orderSummary
var alerts []orderAlert
for _, m := range matches {
base := strings.TrimSuffix(filepath.Base(m), ".events.log")
raw, err := strconv.ParseUint(base, 10, 64)
if err != nil {
continue
}
g, err := s.pool.Get(r.Context(), raw)
if err != nil || g.Status == order.StatusNew {
continue
}
totalOrders++
byStatus[g.Status]++
statusTotal[g.Status] += g.TotalAmount.Int64()
totalRevenue += g.TotalAmount.Int64()
capturedRevenue += g.CapturedAmount.Int64()
refundedRevenue += g.RefundedAmount.Int64()
// Daily revenue — parse placedAt to extract date.
if g.PlacedAt != "" {
if placed, parseErr := time.Parse(time.RFC3339, g.PlacedAt); parseErr == nil {
dayKey := placed.Format("2006-01-02")
if _, ok := dailyByDay[dayKey]; ok {
dailyByDay[dayKey] += g.TotalAmount.Int64()
}
}
}
allOrders = append(allOrders, orderSummary{
OrderId: order.OrderId(raw).String(),
Reference: g.OrderReference,
Status: g.Status,
TotalAmount: g.TotalAmount.Int64(),
Currency: g.Currency,
PlacedAt: g.PlacedAt,
})
// Alerts.
if g.Status == order.StatusPending {
alerts = append(alerts, orderAlert{
Severity: "warn",
Message: "Payment still pending",
OrderId: order.OrderId(raw).String(),
})
}
if g.Status == order.StatusCancelled {
alerts = append(alerts, orderAlert{
Severity: "warn",
Message: "Order was cancelled",
OrderId: order.OrderId(raw).String(),
})
}
}
// Sort ALL orders by placedAt descending, take top 10 as "recent".
sort.Slice(allOrders, func(i, j int) bool {
return allOrders[i].PlacedAt > allOrders[j].PlacedAt
})
recent := allOrders
if len(recent) > 10 {
recent = recent[:10]
}
// Cap alerts
if len(alerts) > 50 {
alerts = alerts[:50]
}
// Build status buckets array with totals.
buckets := make([]statusBucket, 0, len(byStatus))
for st, cnt := range byStatus {
buckets = append(buckets, statusBucket{Status: st, Count: cnt, Total: statusTotal[st]})
}
sort.Slice(buckets, func(i, j int) bool {
return buckets[i].Count > buckets[j].Count
})
// Daily revenue as an ordered slice.
var dailyRev []dailyRevenue
for i := 29; i >= 0; i-- {
day := now.AddDate(0, 0, -i).Format("2006-01-02")
dailyRev = append(dailyRev, dailyRevenue{Date: day, Total: dailyByDay[day]})
}
writeJSON(w, http.StatusOK, orderStatsResponse{
OrdersTotal: totalOrders,
Revenue: revenueSnapshot{
Total: totalRevenue,
Captured: capturedRevenue,
Refunded: refundedRevenue,
Pending: totalRevenue - capturedRevenue - refundedRevenue,
},
ByStatus: buckets,
RecentOrders: recent,
DailyRevenue30: dailyRev,
Alerts: alerts,
})
}
// --- reads ----------------------------------------------------------------
func (s *server) handleGet(w http.ResponseWriter, r *http.Request) {
@@ -743,116 +593,6 @@ func (s *server) handleList(w http.ResponseWriter, r *http.Request) {
// --- helpers --------------------------------------------------------------
// selectTaxProvider picks the tax provider from the environment. Default is
// NordicTaxProvider with SE as the default country (matching the current
// Nordic-only footprint). Set TAX_PROVIDER=static for dev and tests
// (no country awareness).
func selectTaxProvider() tax.Provider {
provider := os.Getenv("TAX_PROVIDER")
switch provider {
case "static":
return tax.NewStatic()
case "nordic":
return tax.NewNordic(config.EnvString("TAX_DEFAULT_COUNTRY", "SE"))
default:
return tax.NewNordic(config.EnvString("TAX_DEFAULT_COUNTRY", "SE"))
}
}
// selectProvider picks the payment provider from the environment. Default is
// the dependency-free mock; set PAYMENT_PROVIDER=stripe + STRIPE_SECRET_KEY for
// real Stripe (STRIPE_API_BASE / STRIPE_PAYMENT_METHOD optionally override).
func selectProvider(logger *slog.Logger) order.PaymentProvider {
if os.Getenv("PAYMENT_PROVIDER") == "stripe" {
key := os.Getenv("STRIPE_SECRET_KEY")
if key == "" {
logger.Warn("PAYMENT_PROVIDER=stripe but STRIPE_SECRET_KEY is empty; falling back to mock")
return order.NewMockProvider()
}
logger.Info("using stripe payment provider")
return order.NewStripeProvider(key, os.Getenv("STRIPE_API_BASE"), os.Getenv("STRIPE_PAYMENT_METHOD"))
}
return order.NewMockProvider()
}
// selectEmailSender picks the flow-email transport. Supports SMTP and
// MailerSend. When no sender is configured the hook still exists in
// capabilities but errors at run time if a flow tries to use it.
func selectEmailSender(logger *slog.Logger) (order.EmailSender, error) {
kind := config.EnvString("ORDER_EMAIL_SENDER", "")
if kind == "" && os.Getenv("ORDER_EMAIL_SMTP_HOST") == "" {
return nil, nil
}
if kind == "" {
kind = "smtp"
}
fromAddr := strings.TrimSpace(os.Getenv("ORDER_EMAIL_FROM_ADDRESS"))
if fromAddr == "" {
return nil, fmt.Errorf("ORDER_EMAIL_FROM_ADDRESS is required when email sender is configured")
}
from := mail.Address{
Name: strings.TrimSpace(os.Getenv("ORDER_EMAIL_FROM_NAME")),
Address: fromAddr,
}
switch kind {
case "smtp":
sender, err := order.NewSMTPEmailSender(order.SMTPEmailSenderConfig{
Host: config.EnvString("ORDER_EMAIL_SMTP_HOST", ""),
Port: config.EnvInt("ORDER_EMAIL_SMTP_PORT", 587, func(n int) bool { return n > 0 }),
Username: os.Getenv("ORDER_EMAIL_SMTP_USERNAME"),
Password: os.Getenv("ORDER_EMAIL_SMTP_PASSWORD"),
DefaultFrom: from,
Timeout: config.EnvDuration("ORDER_EMAIL_SMTP_TIMEOUT", 10*time.Second),
})
if err != nil {
return nil, err
}
logger.Info("email sender enabled", "kind", "smtp", "host", config.EnvString("ORDER_EMAIL_SMTP_HOST", ""), "from", from.Address)
return sender, nil
case "mailersend":
apiKey := os.Getenv("MAILERSEND_API_KEY")
if apiKey == "" {
return nil, fmt.Errorf("MAILERSEND_API_KEY is required for mailersend sender")
}
sender, err := order.NewMailerSendEmailSender(apiKey, from)
if err != nil {
return nil, err
}
logger.Info("email sender enabled", "kind", "mailersend", "from", from.Address)
return sender, nil
default:
return nil, fmt.Errorf("unsupported ORDER_EMAIL_SENDER %q (supported: smtp, mailersend)", kind)
}
}
func selectInventoryReservationService() (order.InventoryReservationService, error) {
return order.NewHTTPInventoryReservationService(config.EnvString("INVENTORY_URL", "http://localhost:8080"), nil)
}
// loadUCPOrderSigner loads the UCP ECDSA signing key from the path specified in
// the UCP_SIGNING_KEY_PATH environment variable. Returns nil if unset or unreadable.
func loadUCPOrderSigner() *ucp.SigningConfig {
path := os.Getenv("UCP_SIGNING_KEY_PATH")
if path == "" {
return nil
}
pemData, err := os.ReadFile(path)
if err != nil {
log.Printf("ucp signing: cannot read key %s: %v", path, err)
return nil
}
cfg, err := ucp.NewSigningConfigFromPEM(pemData, "k6n-ecdsa-2026")
if err != nil {
log.Printf("ucp signing: invalid key at %s: %v", path, err)
return nil
}
return cfg
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)