mirror of
https://github.com/marcusDenslow/terminalShop.git
synced 2026-10-25 02:52:20 +02:00
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.
148 lines
3.7 KiB
Go
148 lines
3.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"terminalShop/api/handlers"
|
|
"terminalShop/api/middleware"
|
|
"terminalShop/api/routes"
|
|
"terminalShop/pkg/audit"
|
|
"terminalShop/pkg/auth"
|
|
"terminalShop/pkg/config"
|
|
"terminalShop/pkg/database"
|
|
"terminalShop/pkg/observability"
|
|
"terminalShop/pkg/stripeclient"
|
|
)
|
|
|
|
const version = "v0.1.0"
|
|
|
|
func main() {
|
|
observability.InitLogger()
|
|
middleware.SetBuildInfo(version)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
// Load configuration
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
log.Fatalf("Failed to load config: %v", err)
|
|
}
|
|
|
|
// Initialize database connection
|
|
db, err := database.Connect(cfg.DatabaseURL)
|
|
if err != nil {
|
|
log.Fatalf("Failed to connect to database: %v", err)
|
|
}
|
|
|
|
defer func() { _ = database.Close() }()
|
|
|
|
// Run migrations
|
|
if err := database.Migrate(db); err != nil {
|
|
log.Fatalf("Failed to run migrations: %v", err)
|
|
}
|
|
audit.SetDB(db)
|
|
|
|
// Seed database with initial data
|
|
if err := database.Seed(db); err != nil {
|
|
log.Fatalf("Failed to seed database: %v", err)
|
|
}
|
|
|
|
otlpEndpoint := os.Getenv("OTLP_ENDPOINT")
|
|
if otlpEndpoint == "" {
|
|
otlpEndpoint = "tempo:4318"
|
|
}
|
|
shutdown, err := observability.InitTracing(context.Background(), "terminalshop-api", otlpEndpoint)
|
|
if err != nil {
|
|
log.Printf("tracing init failed (non-fatal): %v", err)
|
|
} else {
|
|
defer func() { _ = shutdown(context.Background()) }()
|
|
}
|
|
|
|
stripeclient.InitOTel()
|
|
|
|
// Init JWT manager (same secret + duration as SSH server)
|
|
jwtManager := auth.NewJWTManager(cfg.JWTSecret, 30*time.Minute)
|
|
|
|
// Setup routes
|
|
router := routes.SetupRoutes(version, cfg.StripeSecretKey, cfg.StripeWebhookSecret, jwtManager, cfg.AuthFingerprintKey, cfg.ShippoAPIKey, cfg.BringAPIUID, cfg.BringAPIKey, cfg.BringCustomerNumber, cfg.ShippoWebhookSecret, cfg.AppURL, cfg.SlackSigningSecret, cfg.AdminAPIKey, cfg.APIPort)
|
|
|
|
// Create HTTP server
|
|
server := &http.Server{
|
|
Addr: fmt.Sprintf(":%s", cfg.APIPort),
|
|
Handler: router,
|
|
ReadTimeout: 15 * time.Second,
|
|
WriteTimeout: 15 * time.Second,
|
|
IdleTimeout: 60 * time.Second,
|
|
}
|
|
|
|
// Channel to listen for interrupt signals
|
|
done := make(chan os.Signal, 1)
|
|
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
|
|
|
// Start server in a goroutine
|
|
go func() {
|
|
log.Printf("Starting API server on port %s", cfg.APIPort)
|
|
log.Printf("Environment: %s", cfg.Environment)
|
|
log.Printf("Database: %s", cfg.SafeDatabaseURL())
|
|
log.Printf("Health check: http://localhost:%s/api/v1/health", cfg.APIPort)
|
|
|
|
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
log.Fatalf("Server failed to start: %v", err)
|
|
}
|
|
}()
|
|
|
|
// Start reconciliation job. runs every 5 minutes to catch any orders
|
|
// that were charged but not recorded due to a crash.
|
|
go func() {
|
|
ticker := time.NewTicker(5 * time.Minute)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
handlers.ReconcileOrders(cfg.StripeSecretKey)
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
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
|
|
<-done
|
|
cancel()
|
|
log.Println("Server is shutting down...")
|
|
|
|
// Graceful shutdown with 30 second timeout
|
|
ctx, cancel = context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
if err := server.Shutdown(ctx); err != nil {
|
|
log.Fatalf("Server forced to shutdown: %v", err)
|
|
}
|
|
|
|
log.Println("Server exited gracefully")
|
|
}
|