package database import ( "context" "time" "github.com/jackc/pgx/v5/pgxpool" ) type DB struct { Pool *pgxpool.Pool } func NewPostgres(databaseURL string) (*DB, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() config, err := pgxpool.ParseConfig(databaseURL) if err != nil { return nil, err } config.MaxConns = 10 config.MinConns = 1 config.MaxConnLifetime = time.Hour config.MaxConnIdleTime = 30 * time.Minute pool, err := pgxpool.NewWithConfig(ctx, config) if err != nil { return nil, err } if err := pool.Ping(ctx); err != nil { pool.Close() return nil, err } return &DB{Pool: pool}, nil } func (d *DB) EnsureSchema(ctx context.Context) error { if d == nil || d.Pool == nil { return nil } statements := []string{ `CREATE TABLE IF NOT EXISTS orders ( id TEXT PRIMARY KEY, status TEXT NOT NULL, fulfillment_status TEXT NOT NULL DEFAULT 'pending', email TEXT NOT NULL, phone TEXT NOT NULL, first_name TEXT NOT NULL, last_name TEXT NOT NULL, address_line_1 TEXT NOT NULL, address_line_2 TEXT NOT NULL DEFAULT '', city TEXT NOT NULL, region TEXT NOT NULL, postal_code TEXT NOT NULL, country TEXT NOT NULL, notes TEXT NOT NULL DEFAULT '', currency TEXT NOT NULL, amount BIGINT NOT NULL, stripe_price_id TEXT NOT NULL, stripe_payment_intent_id TEXT NOT NULL UNIQUE, shipping_carrier TEXT NOT NULL DEFAULT '', tracking_number TEXT NOT NULL DEFAULT '', fulfillment_notes TEXT NOT NULL DEFAULT '', shipped_at TIMESTAMPTZ NULL, webhook_status TEXT NOT NULL DEFAULT 'awaiting_webhook', webhook_event_id TEXT NOT NULL DEFAULT '', webhook_event_type TEXT NOT NULL DEFAULT '', webhook_message TEXT NOT NULL DEFAULT 'Waiting for Stripe webhook verification.', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() )`, `ALTER TABLE orders ADD COLUMN IF NOT EXISTS fulfillment_status TEXT NOT NULL DEFAULT 'pending'`, `ALTER TABLE orders ADD COLUMN IF NOT EXISTS shipping_carrier TEXT NOT NULL DEFAULT ''`, `ALTER TABLE orders ADD COLUMN IF NOT EXISTS tracking_number TEXT NOT NULL DEFAULT ''`, `ALTER TABLE orders ADD COLUMN IF NOT EXISTS fulfillment_notes TEXT NOT NULL DEFAULT ''`, `ALTER TABLE orders ADD COLUMN IF NOT EXISTS shipped_at TIMESTAMPTZ NULL`, `CREATE TABLE IF NOT EXISTS order_items ( id BIGSERIAL PRIMARY KEY, order_id TEXT NOT NULL REFERENCES orders(id) ON DELETE CASCADE, item_id TEXT NOT NULL, style TEXT NOT NULL, colorway_id TEXT NOT NULL, finish_id TEXT NOT NULL, quantity BIGINT NOT NULL, unit_amount BIGINT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() )`, `CREATE INDEX IF NOT EXISTS idx_order_items_order_id ON order_items(order_id)`, `CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status)`, `CREATE INDEX IF NOT EXISTS idx_orders_fulfillment_status ON orders(fulfillment_status)`, `CREATE INDEX IF NOT EXISTS idx_orders_tracking_number ON orders(tracking_number)`, `CREATE TABLE IF NOT EXISTS inventory_levels ( id BIGSERIAL PRIMARY KEY, style TEXT NOT NULL, colorway_id TEXT NOT NULL, finish_id TEXT NOT NULL, quantity_on_hand BIGINT NOT NULL DEFAULT 0, notes TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(style, colorway_id, finish_id) )`, `CREATE INDEX IF NOT EXISTS idx_inventory_levels_colorway ON inventory_levels(colorway_id)`, } for _, statement := range statements { if _, err := d.Pool.Exec(ctx, statement); err != nil { return err } } return nil } func (d *DB) Health(ctx context.Context) error { if d == nil || d.Pool == nil { return nil } return d.Pool.Ping(ctx) } func (d *DB) Close() { if d == nil || d.Pool == nil { return } d.Pool.Close() }