2
0
Files
Royal-Pop-Accessory/Backend/internal/database/postgres.go
T
2026-06-23 18:50:37 +01:00

135 lines
3.8 KiB
Go

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()
}