Merge pull request #15 from nutcas3/ci-&migrations

Ci &migrations
This commit is contained in:
Nutcase
2026-04-29 03:49:51 +03:00
committed by GitHub
28 changed files with 847 additions and 248 deletions
+61 -3
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
image: postgres:18
env:
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_password
@@ -40,11 +40,17 @@ jobs:
working-directory: apps/api-server
run: go mod download
- name: Install goose
run: go install github.com/pressly/goose/v3/cmd/goose@latest
- name: Add goose to PATH
run: echo "$HOME/go/bin" >> $GITHUB_PATH
- name: Run database migrations
working-directory: apps/api-server
env:
DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db?sslmode=disable
run: go run cmd/migrate/main.go --action=up
working-directory: .
run: goose postgres $DATABASE_URL up migrations
- name: Run linter
working-directory: apps/api-server
@@ -108,6 +114,20 @@ jobs:
charging-engine-test:
name: Charging Engine Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_password
POSTGRES_DB: test_db
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -132,6 +152,18 @@ jobs:
working-directory: apps/charging-engine
run: cargo fetch
- name: Install goose
run: go install github.com/pressly/goose/v3/cmd/goose@latest
- name: Add goose to PATH
run: echo "$HOME/go/bin" >> $GITHUB_PATH
- name: Run database migrations
working-directory: .
env:
DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db?sslmode=disable
run: goose postgres $DATABASE_URL up migrations
- name: Run tests
working-directory: apps/charging-engine
run: cargo test --release || true
@@ -143,6 +175,20 @@ jobs:
packet-gateway-test:
name: Packet Gateway Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_password
POSTGRES_DB: test_db
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -167,6 +213,18 @@ jobs:
working-directory: apps/packet-gateway
run: cargo fetch
- name: Install goose
run: go install github.com/pressly/goose/v3/cmd/goose@latest
- name: Add goose to PATH
run: echo "$HOME/go/bin" >> $GITHUB_PATH
- name: Run database migrations
working-directory: .
env:
DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db?sslmode=disable
run: goose postgres $DATABASE_URL up migrations
- name: Run tests
working-directory: apps/packet-gateway
run: cargo test --release || true
+8 -2
View File
@@ -170,10 +170,13 @@ jobs:
with:
version: 'latest'
- name: Install goose
run: go install github.com/pressly/goose/v3/cmd/goose@latest
- name: Run database migrations
run: |
echo "Running database migrations for staging..."
# kubectl exec -it deployment/api-server -- go run cmd/migrate/main.go --action=up
# goose postgres $DATABASE_URL up migrations
echo "Staging migrations complete"
- name: Deploy to staging cluster
@@ -197,10 +200,13 @@ jobs:
with:
version: 'latest'
- name: Install goose
run: go install github.com/pressly/goose/v3/cmd/goose@latest
- name: Run database migrations
run: |
echo "Running database migrations for production..."
# kubectl exec -it deployment/api-server -- go run cmd/migrate/main.go --action=up
# goose postgres $DATABASE_URL up migrations
echo "Production migrations complete"
- name: Deploy to production cluster
+21 -2
View File
@@ -181,11 +181,30 @@ db-setup: ## Set up PostgreSQL, MongoDB and Redis
@bash scripts/setup-redis.sh
@echo "$(GREEN)Database setup complete!$(RESET)"
db-migrate: ## Run database migrations
db-migrate: ## Run database migrations (Go and Rust)
@echo "$(CYAN)Running database migrations...$(RESET)"
@cd apps/api-server && go run cmd/migrate/main.go
@goose postgres $(DATABASE_URL) up migrations
@echo "$(GREEN)Migrations complete!$(RESET)"
db-migrate-down: ## Rollback database migrations
@echo "$(CYAN)Rolling back database migrations...$(RESET)"
@goose postgres $(DATABASE_URL) down migrations
@echo "$(GREEN)Rollback complete!$(RESET)"
db-migrate-status: ## Show migration status
@echo "$(CYAN)Migration status:$(RESET)"
@goose postgres $(DATABASE_URL) status migrations
db-create-migration: ## Create a new migration file (usage: make db-create-migration NAME=migration_name)
@echo "$(CYAN)Creating new migration...$(RESET)"
@if [ -z "$(NAME)" ]; then echo "Error: NAME parameter is required. Usage: make db-create-migration NAME=migration_name"; exit 1; fi
@goose create $(NAME) sql
db-migrate-rust: ## Run migrations for Rust services (uses goose binary)
@echo "$(CYAN)Running migrations for Rust services...$(RESET)"
@goose postgres $(DATABASE_URL) up migrations
@echo "$(GREEN)Rust migrations complete!$(RESET)"
# Free5GC integration
free5gc-setup: ## Set up free5GC configuration files
@echo "$(CYAN)Setting up free5GC configuration...$(RESET)"
+1
View File
@@ -75,6 +75,7 @@ require (
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/serf v0.10.1 // indirect
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // indirect
github.com/lib/pq v1.12.3 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/microsoft/go-mssqldb v1.9.5 // indirect
+2
View File
@@ -319,6 +319,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+158
View File
@@ -0,0 +1,158 @@
package database
import (
"context"
"github.com/nutcas3/telecom-platform/apps/api-server/internal/models"
"gorm.io/gorm"
)
// Subscriber CRUD
func (d *Database) CreateSubscriber(ctx context.Context, subscriber *models.Subscriber) error {
return d.DB.WithContext(ctx).Create(subscriber).Error
}
func (d *Database) GetSubscriber(ctx context.Context, id uint) (*models.Subscriber, error) {
var subscriber models.Subscriber
err := d.DB.WithContext(ctx).Preload("Plan").First(&subscriber, id).Error
if err != nil {
return nil, err
}
return &subscriber, nil
}
func (d *Database) GetSubscriberByIMSI(ctx context.Context, imsi models.IMSI) (*models.Subscriber, error) {
var subscriber models.Subscriber
err := d.DB.WithContext(ctx).Preload("Plan").Where("imsi = ?", imsi).First(&subscriber).Error
if err != nil {
return nil, err
}
return &subscriber, nil
}
func (d *Database) UpdateSubscriber(ctx context.Context, subscriber *models.Subscriber) error {
return d.DB.WithContext(ctx).Save(subscriber).Error
}
func (d *Database) ListSubscribers(ctx context.Context, req *ListSubscribersRequest) ([]models.Subscriber, int64, error) {
var subscribers []models.Subscriber
var total int64
query := d.DB.WithContext(ctx).Model(&models.Subscriber{})
if req.Status != "" {
query = query.Where("status = ?", req.Status)
}
if req.OrganizationID != "" {
query = query.Where("organization_id = ?", req.OrganizationID)
}
if req.Search != "" {
query = query.Where("first_name ILIKE ? OR last_name ILIKE ? OR email ILIKE ? OR msisdn ILIKE ?",
"%"+req.Search+"%", "%"+req.Search+"%", "%"+req.Search+"%", "%"+req.Search+"%")
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
offset := (req.Page - 1) * req.PageSize
err := query.Preload("Plan").Offset(offset).Limit(req.PageSize).Order("created_at DESC").Find(&subscribers).Error
return subscribers, total, err
}
func (d *Database) GetActiveSessionsByIMSI(ctx context.Context, imsi models.IMSI) ([]models.Session, error) {
var sessions []models.Session
err := d.DB.WithContext(ctx).Where("subscriber_id = ? AND status = ?", imsi, "active").Find(&sessions).Error
return sessions, err
}
func (d *Database) UpdateSession(ctx context.Context, session *models.Session) error {
return d.DB.WithContext(ctx).Save(session).Error
}
func (d *Database) UpdateSubscriberBalance(ctx context.Context, subscriberID uint, amount float64) error {
return d.DB.WithContext(ctx).Model(&models.Subscriber{}).Where("id = ?", subscriberID).
UpdateColumn("balance", gorm.Expr("balance + ?", amount)).Error
}
// Payment methods
func (d *Database) CreatePaymentMethod(ctx context.Context, pm *models.PaymentMethod) error {
return d.DB.WithContext(ctx).Create(pm).Error
}
func (d *Database) GetPaymentMethod(ctx context.Context, id string) (*models.PaymentMethod, error) {
var pm models.PaymentMethod
err := d.DB.WithContext(ctx).Where("id = ?", id).First(&pm).Error
if err != nil {
return nil, err
}
return &pm, nil
}
func (d *Database) ListPaymentMethods(ctx context.Context, subscriberID uint) ([]models.PaymentMethod, error) {
var methods []models.PaymentMethod
err := d.DB.WithContext(ctx).Where("subscriber_id = ?", subscriberID).Find(&methods).Error
return methods, err
}
func (d *Database) DeletePaymentMethod(ctx context.Context, id string) error {
return d.DB.WithContext(ctx).Where("id = ?", id).Delete(&models.PaymentMethod{}).Error
}
// Transactions
func (d *Database) CreateTransaction(ctx context.Context, transaction *models.Transaction) error {
return d.DB.WithContext(ctx).Create(transaction).Error
}
func (d *Database) GetTransaction(ctx context.Context, transactionID string) (*models.Transaction, error) {
var tx models.Transaction
err := d.DB.WithContext(ctx).Where("transaction_id = ?", transactionID).First(&tx).Error
if err != nil {
return nil, err
}
return &tx, nil
}
func (d *Database) GetTransactionByGatewayID(ctx context.Context, gatewayID string) (*models.Transaction, error) {
var tx models.Transaction
err := d.DB.WithContext(ctx).Where("transaction_id = ?", gatewayID).First(&tx).Error
if err != nil {
return nil, err
}
return &tx, nil
}
func (d *Database) GetTransactionByChargeID(ctx context.Context, chargeID string) (*models.Transaction, error) {
var tx models.Transaction
err := d.DB.WithContext(ctx).Where("transaction_id = ?", chargeID).First(&tx).Error
if err != nil {
return nil, err
}
return &tx, nil
}
func (d *Database) UpdateTransaction(ctx context.Context, tx *models.Transaction) error {
return d.DB.WithContext(ctx).Save(tx).Error
}
func (d *Database) ListTransactions(ctx context.Context, subscriberID uint) ([]models.Transaction, error) {
var transactions []models.Transaction
err := d.DB.WithContext(ctx).Where("subscriber_id = ?", subscriberID).Order("created_at DESC").Find(&transactions).Error
return transactions, err
}
// Notifications and alerts
func (d *Database) CreateAlert(ctx context.Context, alert *models.Alert) error {
return d.DB.WithContext(ctx).Create(alert).Error
}
func (d *Database) CreateNotification(ctx context.Context, notification *models.Notification) error {
return d.DB.WithContext(ctx).Create(notification).Error
}
type ListSubscribersRequest struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Status string `json:"status"`
OrganizationID string `json:"organization_id"`
Search string `json:"search"`
}
+17 -211
View File
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"log"
"os/exec"
"time"
"gorm.io/driver/postgres"
@@ -11,7 +12,6 @@ import (
"gorm.io/gorm/logger"
"github.com/nutcas3/telecom-platform/apps/api-server/internal/config"
"github.com/nutcas3/telecom-platform/apps/api-server/internal/models"
)
type Database struct {
@@ -31,36 +31,28 @@ func NewDatabase(cfg *config.DatabaseConfig) (*Database, error) {
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=%s",
cfg.Host, cfg.Username, cfg.Password, cfg.Database, cfg.Port, cfg.SSLMode)
// Configure GORM logger
gormLogger := logger.Default.LogMode(logger.Info)
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: gormLogger,
})
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: gormLogger})
if err != nil {
return nil, fmt.Errorf("failed to connect to database: %w", err)
}
// Get underlying SQL DB to configure connection pool
sqlDB, err := db.DB()
if err != nil {
return nil, fmt.Errorf("failed to get underlying sql.DB: %w", err)
}
// Configure connection pool for optimal performance
sqlDB.SetMaxIdleConns(25) // Minimum idle connections
sqlDB.SetMaxOpenConns(100) // Maximum open connections
sqlDB.SetConnMaxLifetime(time.Hour) // Max connection lifetime
sqlDB.SetConnMaxIdleTime(30 * time.Minute) // Max idle time before closing
sqlDB.SetMaxIdleConns(25)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour)
sqlDB.SetConnMaxIdleTime(30 * time.Minute)
database := &Database{DB: db}
// Auto-migrate schemas
if err := database.AutoMigrate(); err != nil {
return nil, fmt.Errorf("failed to auto-migrate: %w", err)
if err := runMigrations(dsn); err != nil {
return nil, fmt.Errorf("failed to run migrations: %w", err)
}
// Initialize IMSI allocation
if err := database.InitializeIMSIAllocation(); err != nil {
return nil, fmt.Errorf("failed to initialize IMSI allocation: %w", err)
}
@@ -68,20 +60,14 @@ func NewDatabase(cfg *config.DatabaseConfig) (*Database, error) {
return database, nil
}
func (d *Database) AutoMigrate() error {
return d.DB.AutoMigrate(
&models.Subscriber{},
&models.ServicePlan{},
&models.Session{},
&models.UsageRecord{},
&IMSIAllocation{},
&models.Plugin{},
&models.Automation{},
&models.AutomationRun{},
&models.ConfigEntry{},
&models.DeploymentRecord{},
&models.ChaosExperimentRecord{},
)
func runMigrations(dsn string) error {
cmd := exec.Command("goose", "postgres", dsn, "up", "migrations")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("goose migration failed: %w, output: %s", err, string(output))
}
log.Printf("Database migrations completed successfully")
return nil
}
func (d *Database) InitializeIMSIAllocation() error {
@@ -89,17 +75,10 @@ func (d *Database) InitializeIMSIAllocation() error {
result := d.DB.First(&allocation)
if result.Error == gorm.ErrRecordNotFound {
// Create initial allocation record
allocation = IMSIAllocation{
LastIMSI: 0,
MinIMSI: 1,
MaxIMSI: 999999999, // 9-digit subscriber numbers
}
allocation = IMSIAllocation{LastIMSI: 0, MinIMSI: 1, MaxIMSI: 999999999}
if err := d.DB.Create(&allocation).Error; err != nil {
return fmt.Errorf("failed to create IMSI allocation: %w", err)
}
log.Printf("Created IMSI allocation record")
} else if result.Error != nil {
return fmt.Errorf("failed to query IMSI allocation: %w", result.Error)
@@ -116,27 +95,6 @@ func (d *Database) Close() error {
return sqlDB.Close()
}
// PoolStats returns current connection pool statistics
func (d *Database) PoolStats() (map[string]any, error) {
sqlDB, err := d.DB.DB()
if err != nil {
return nil, err
}
stats := sqlDB.Stats()
return map[string]any{
"max_open_connections": stats.MaxOpenConnections,
"open_connections": stats.OpenConnections,
"in_use": stats.InUse,
"idle": stats.Idle,
"wait_count": stats.WaitCount,
"wait_duration": stats.WaitDuration.String(),
"max_idle_closed": stats.MaxIdleClosed,
"max_idle_time_closed": stats.MaxIdleTimeClosed,
"max_lifetime_closed": stats.MaxLifetimeClosed,
}, nil
}
// Ping verifies database connectivity
func (d *Database) Ping(ctx context.Context) error {
sqlDB, err := d.DB.DB()
if err != nil {
@@ -145,62 +103,6 @@ func (d *Database) Ping(ctx context.Context) error {
return sqlDB.PingContext(ctx)
}
func (d *Database) CreateSubscriber(ctx context.Context, subscriber *models.Subscriber) error {
return d.DB.WithContext(ctx).Create(subscriber).Error
}
func (d *Database) GetSubscriber(ctx context.Context, id uint) (*models.Subscriber, error) {
var subscriber models.Subscriber
err := d.DB.WithContext(ctx).Preload("Plan").First(&subscriber, id).Error
if err != nil {
return nil, err
}
return &subscriber, nil
}
func (d *Database) GetSubscriberByIMSI(ctx context.Context, imsi models.IMSI) (*models.Subscriber, error) {
var subscriber models.Subscriber
err := d.DB.WithContext(ctx).Preload("Plan").Where("imsi = ?", imsi).First(&subscriber).Error
if err != nil {
return nil, err
}
return &subscriber, nil
}
func (d *Database) UpdateSubscriber(ctx context.Context, subscriber *models.Subscriber) error {
return d.DB.WithContext(ctx).Save(subscriber).Error
}
func (d *Database) ListSubscribers(ctx context.Context, req *ListSubscribersRequest) ([]models.Subscriber, int64, error) {
var subscribers []models.Subscriber
var total int64
query := d.DB.WithContext(ctx).Model(&models.Subscriber{})
// Apply filters
if req.Status != "" {
query = query.Where("status = ?", req.Status)
}
if req.OrganizationID != "" {
query = query.Where("organization_id = ?", req.OrganizationID)
}
if req.Search != "" {
query = query.Where("first_name ILIKE ? OR last_name ILIKE ? OR email ILIKE ? OR msisdn ILIKE ?",
"%"+req.Search+"%", "%"+req.Search+"%", "%"+req.Search+"%", "%"+req.Search+"%")
}
// Count total records
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// Apply pagination and ordering
offset := (req.Page - 1) * req.PageSize
err := query.Preload("Plan").Offset(offset).Limit(req.PageSize).Order("created_at DESC").Find(&subscribers).Error
return subscribers, total, err
}
func (d *Database) GetIMSIAllocation(ctx context.Context) (*IMSIAllocation, error) {
var allocation IMSIAllocation
err := d.DB.WithContext(ctx).First(&allocation).Error
@@ -213,99 +115,3 @@ func (d *Database) GetIMSIAllocation(ctx context.Context) (*IMSIAllocation, erro
func (d *Database) UpdateIMSIAllocation(ctx context.Context, allocation *IMSIAllocation) error {
return d.DB.WithContext(ctx).Save(allocation).Error
}
func (d *Database) GetActiveSessionsByIMSI(ctx context.Context, imsi models.IMSI) ([]models.Session, error) {
var sessions []models.Session
err := d.DB.WithContext(ctx).Where("subscriber_id = ? AND status = ?", imsi, "active").Find(&sessions).Error
return sessions, err
}
func (d *Database) UpdateSession(ctx context.Context, session *models.Session) error {
return d.DB.WithContext(ctx).Save(session).Error
}
func (d *Database) CreateTransaction(ctx context.Context, transaction *models.Transaction) error {
return d.DB.WithContext(ctx).Create(transaction).Error
}
func (d *Database) UpdateSubscriberBalance(ctx context.Context, subscriberID uint, amount float64) error {
return d.DB.WithContext(ctx).Model(&models.Subscriber{}).Where("id = ?", subscriberID).
UpdateColumn("balance", gorm.Expr("balance + ?", amount)).Error
}
func (d *Database) CreatePaymentMethod(ctx context.Context, pm *models.PaymentMethod) error {
return d.DB.WithContext(ctx).Create(pm).Error
}
func (d *Database) GetPaymentMethod(ctx context.Context, id string) (*models.PaymentMethod, error) {
var pm models.PaymentMethod
err := d.DB.WithContext(ctx).Where("id = ?", id).First(&pm).Error
if err != nil {
return nil, err
}
return &pm, nil
}
func (d *Database) DeletePaymentMethod(ctx context.Context, id string) error {
return d.DB.WithContext(ctx).Where("id = ?", id).Delete(&models.PaymentMethod{}).Error
}
func (d *Database) ListPaymentMethods(ctx context.Context, subscriberID uint) ([]models.PaymentMethod, error) {
var methods []models.PaymentMethod
err := d.DB.WithContext(ctx).Where("subscriber_id = ?", subscriberID).Find(&methods).Error
return methods, err
}
func (d *Database) GetTransaction(ctx context.Context, transactionID string) (*models.Transaction, error) {
var tx models.Transaction
err := d.DB.WithContext(ctx).Where("transaction_id = ?", transactionID).First(&tx).Error
if err != nil {
return nil, err
}
return &tx, nil
}
func (d *Database) GetTransactionByGatewayID(ctx context.Context, gatewayID string) (*models.Transaction, error) {
var tx models.Transaction
err := d.DB.WithContext(ctx).Where("transaction_id = ?", gatewayID).First(&tx).Error
if err != nil {
return nil, err
}
return &tx, nil
}
func (d *Database) GetTransactionByChargeID(ctx context.Context, chargeID string) (*models.Transaction, error) {
var tx models.Transaction
err := d.DB.WithContext(ctx).Where("transaction_id = ?", chargeID).First(&tx).Error
if err != nil {
return nil, err
}
return &tx, nil
}
func (d *Database) UpdateTransaction(ctx context.Context, tx *models.Transaction) error {
return d.DB.WithContext(ctx).Save(tx).Error
}
func (d *Database) ListTransactions(ctx context.Context, subscriberID uint) ([]models.Transaction, error) {
var transactions []models.Transaction
err := d.DB.WithContext(ctx).Where("subscriber_id = ?", subscriberID).Order("created_at DESC").Find(&transactions).Error
return transactions, err
}
func (d *Database) CreateAlert(ctx context.Context, alert *models.Alert) error {
return d.DB.WithContext(ctx).Create(alert).Error
}
func (d *Database) CreateNotification(ctx context.Context, notification *models.Notification) error {
return d.DB.WithContext(ctx).Create(notification).Error
}
// Request types for database operations
type ListSubscribersRequest struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Status models.SubscriberStatus `json:"status"`
OrganizationID string `json:"organization_id"`
Search string `json:"search"`
}
@@ -109,7 +109,7 @@ func (s *SubscriberService) ListSubscribers(ctx context.Context, req *ListSubscr
dbReq := &database.ListSubscribersRequest{
Page: req.Page,
PageSize: req.PageSize,
Status: req.Status,
Status: string(req.Status),
OrganizationID: req.OrganizationID,
Search: req.Search,
}
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"os/exec"
"time"
"gorm.io/driver/postgres"
@@ -34,8 +35,9 @@ type PostgresProfileStore struct {
db *gorm.DB
}
// NewPostgresProfileStore connects to Postgres using the given DSN, runs the
// profile schema migration, and returns a repository ready to use.
// NewPostgresProfileStore connects to Postgres using the given DSN and runs migrations.
// Note: Database schema is managed by centralized migration system using Goose.
// Migrations are automatically run on startup.
func NewPostgresProfileStore(dsn string) (*PostgresProfileStore, error) {
if dsn == "" {
return nil, errors.New("postgres DSN is empty")
@@ -46,12 +48,25 @@ func NewPostgresProfileStore(dsn string) (*PostgresProfileStore, error) {
if err != nil {
return nil, fmt.Errorf("open postgres: %w", err)
}
if err := db.AutoMigrate(&profileRow{}); err != nil {
return nil, fmt.Errorf("migrate esim_profiles: %w", err)
// Run Goose migrations on startup
if err := runMigrations(dsn); err != nil {
return nil, fmt.Errorf("migration failed: %w", err)
}
return &PostgresProfileStore{db: db}, nil
}
// runMigrations executes Goose migrations using the goose binary
func runMigrations(dsn string) error {
cmd := exec.Command("goose", "postgres", dsn, "up", "migrations")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("goose migration failed: %w, output: %s", err, string(output))
}
return nil
}
// Create inserts a profile row.
func (s *PostgresProfileStore) Create(ctx context.Context, p *Profile) error {
row := toRow(p)
@@ -4,6 +4,7 @@
//! services read a single source of truth.
use std::collections::HashMap;
use std::process::Command;
use sqlx::postgres::{PgPool, PgPoolOptions};
use sqlx::Row;
@@ -20,7 +21,9 @@ pub struct RatingPlansRepo {
}
impl RatingPlansRepo {
/// Connect to Postgres using the given DSN and ensure the schema exists.
/// Connect to Postgres using the given DSN and run migrations.
/// Note: Database schema is managed by centralized migration system using Goose.
/// Migrations are automatically run on startup.
pub async fn connect(database_url: &str) -> ChargingResult<Self> {
let pool = PgPoolOptions::new()
.max_connections(10)
@@ -30,30 +33,10 @@ impl RatingPlansRepo {
let circuit_breaker = CircuitBreaker::new(5, std::time::Duration::from_secs(60));
// Idempotent schema for rating plans. api-server's GORM migration also
// creates a compatible `rating_plans` table; the column set here is
// intentionally a strict subset that both services agree on.
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS rating_plans (
plan_id TEXT PRIMARY KEY,
name TEXT NOT NULL,
data_rate DOUBLE PRECISION NOT NULL,
voice_rate DOUBLE PRECISION NOT NULL,
sms_rate DOUBLE PRECISION NOT NULL,
monthly_fee DOUBLE PRECISION NOT NULL,
data_limit BIGINT NOT NULL,
voice_limit BIGINT NOT NULL,
sms_limit BIGINT NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
"#,
)
.execute(&pool)
.await
.map_err(|e| crate::errors::ChargingError::DatabaseError(e.to_string()))?;
// Run Goose migrations on startup
if let Err(e) = run_migrations(database_url).await {
return Err(crate::errors::ChargingError::InternalError(format!("Migration failed: {}", e)));
}
Ok(Self { pool, circuit_breaker })
}
@@ -250,3 +233,19 @@ fn row_to_plan(row: sqlx::postgres::PgRow) -> RatingPlan {
sms_limit: row.get::<i64, _>("sms_limit") as u64,
}
}
/// Run Goose migrations by calling the goose binary.
/// This ensures both Go and Rust services use the same migration system.
async fn run_migrations(database_url: &str) -> Result<(), Box<dyn std::error::Error>> {
let output = Command::new("goose")
.args(["postgres", database_url, "up", "migrations"])
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("Goose migration failed: {}", stderr).into());
}
tracing::info!("Database migrations completed successfully");
Ok(())
}
+24
View File
@@ -0,0 +1,24 @@
-- +goose Up
-- Create rating_plans table
CREATE TABLE IF NOT EXISTS rating_plans (
plan_id TEXT PRIMARY KEY,
name TEXT NOT NULL,
data_rate DOUBLE PRECISION NOT NULL,
voice_rate DOUBLE PRECISION NOT NULL,
sms_rate DOUBLE PRECISION NOT NULL,
monthly_fee DOUBLE PRECISION NOT NULL,
data_limit BIGINT NOT NULL,
voice_limit BIGINT NOT NULL,
sms_limit BIGINT NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Create indexes
CREATE INDEX IF NOT EXISTS idx_rating_plans_is_active ON rating_plans(is_active);
CREATE INDEX IF NOT EXISTS idx_rating_plans_created_at ON rating_plans(created_at);
-- +goose Down
-- Drop rating_plans table
DROP TABLE IF EXISTS rating_plans CASCADE;
+26
View File
@@ -0,0 +1,26 @@
-- +goose Up
-- Create esim_profiles table
CREATE TABLE IF NOT EXISTS esim_profiles (
iccid TEXT PRIMARY KEY,
eid TEXT,
imsi TEXT,
mcc TEXT,
mnc TEXT,
profile_type TEXT,
state TEXT,
tenant_id TEXT,
activation_code TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Create indexes
CREATE INDEX IF NOT EXISTS idx_esim_profiles_eid ON esim_profiles(eid);
CREATE INDEX IF NOT EXISTS idx_esim_profiles_imsi ON esim_profiles(imsi);
CREATE INDEX IF NOT EXISTS idx_esim_profiles_state ON esim_profiles(state);
CREATE INDEX IF NOT EXISTS idx_esim_profiles_tenant_id ON esim_profiles(tenant_id);
CREATE INDEX IF NOT EXISTS idx_esim_profiles_created_at ON esim_profiles(created_at);
-- +goose Down
-- Drop esim_profiles table
DROP TABLE IF EXISTS esim_profiles CASCADE;
+38
View File
@@ -0,0 +1,38 @@
-- +goose Up
-- Create subscribers table
CREATE TABLE IF NOT EXISTS subscribers (
id SERIAL PRIMARY KEY,
imsi TEXT NOT NULL UNIQUE,
msisdn TEXT UNIQUE,
imei TEXT,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
email TEXT UNIQUE,
organization_id TEXT,
status TEXT NOT NULL DEFAULT 'active',
plan_id INTEGER,
balance DOUBLE PRECISION NOT NULL DEFAULT 0,
auth_key TEXT NOT NULL,
opc TEXT NOT NULL,
serving_plmn_mcc TEXT NOT NULL,
serving_plmn_mnc TEXT NOT NULL,
euicc_id TEXT,
profile_id TEXT,
profile_status TEXT NOT NULL DEFAULT 'inactive',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- Create indexes
CREATE INDEX IF NOT EXISTS idx_subscribers_imsi ON subscribers(imsi);
CREATE INDEX IF NOT EXISTS idx_subscribers_msisdn ON subscribers(msisdn);
CREATE INDEX IF NOT EXISTS idx_subscribers_email ON subscribers(email);
CREATE INDEX IF NOT EXISTS idx_subscribers_status ON subscribers(status);
CREATE INDEX IF NOT EXISTS idx_subscribers_organization_id ON subscribers(organization_id);
CREATE INDEX IF NOT EXISTS idx_subscribers_plan_id ON subscribers(plan_id);
CREATE INDEX IF NOT EXISTS idx_subscribers_deleted_at ON subscribers(deleted_at);
-- +goose Down
-- Drop subscribers table
DROP TABLE IF EXISTS subscribers CASCADE;
+28
View File
@@ -0,0 +1,28 @@
-- +goose Up
-- Create service_plans table
CREATE TABLE IF NOT EXISTS service_plans (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
data_limit BIGINT,
voice_limit INTEGER,
sms_limit INTEGER,
monthly_fee DOUBLE PRECISION,
data_rate DOUBLE PRECISION,
voice_rate DOUBLE PRECISION,
sms_rate DOUBLE PRECISION,
arpa INTEGER,
ambr_uplink INTEGER,
ambr_downlink INTEGER,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- Create indexes
CREATE INDEX IF NOT EXISTS idx_service_plans_name ON service_plans(name);
CREATE INDEX IF NOT EXISTS idx_service_plans_deleted_at ON service_plans(deleted_at);
-- +goose Down
-- Drop service_plans table
DROP TABLE IF EXISTS service_plans CASCADE;
+37
View File
@@ -0,0 +1,37 @@
-- +goose Up
-- Create sessions table
CREATE TABLE IF NOT EXISTS sessions (
id SERIAL PRIMARY KEY,
subscriber_id INTEGER NOT NULL,
session_id TEXT NOT NULL UNIQUE,
pdu_address TEXT,
dnn TEXT,
snssai_sst SMALLINT,
snssai_sd INTEGER,
qos_qci SMALLINT,
qos_arpa INTEGER,
qos_mbr_uplink INTEGER,
qos_mbr_downlink INTEGER,
qos_gbr_uplink INTEGER,
qos_gbr_downlink INTEGER,
qos_priority SMALLINT,
status TEXT NOT NULL DEFAULT 'active',
start_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
end_time TIMESTAMPTZ,
data_used BIGINT NOT NULL DEFAULT 0,
voice_used INTEGER NOT NULL DEFAULT 0,
sms_used INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- Create indexes
CREATE INDEX IF NOT EXISTS idx_sessions_subscriber_id ON sessions(subscriber_id);
CREATE INDEX IF NOT EXISTS idx_sessions_session_id ON sessions(session_id);
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
CREATE INDEX IF NOT EXISTS idx_sessions_deleted_at ON sessions(deleted_at);
-- +goose Down
-- Drop sessions table
DROP TABLE IF EXISTS sessions CASCADE;
+27
View File
@@ -0,0 +1,27 @@
-- +goose Up
-- Create usage_records table
CREATE TABLE IF NOT EXISTS usage_records (
id SERIAL PRIMARY KEY,
subscriber_id INTEGER NOT NULL,
session_id INTEGER,
usage_type TEXT NOT NULL,
start_time TIMESTAMPTZ NOT NULL,
end_time TIMESTAMPTZ NOT NULL,
volume BIGINT NOT NULL,
rate DOUBLE PRECISION,
cost DOUBLE PRECISION,
billing_cycle TEXT,
invoice_id INTEGER,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Create indexes
CREATE INDEX IF NOT EXISTS idx_usage_records_subscriber_id ON usage_records(subscriber_id);
CREATE INDEX IF NOT EXISTS idx_usage_records_session_id ON usage_records(session_id);
CREATE INDEX IF NOT EXISTS idx_usage_records_usage_type ON usage_records(usage_type);
CREATE INDEX IF NOT EXISTS idx_usage_records_billing_cycle ON usage_records(billing_cycle);
-- +goose Down
-- Drop usage_records table
DROP TABLE IF EXISTS usage_records CASCADE;
+26
View File
@@ -0,0 +1,26 @@
-- +goose Up
-- Create users table
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'viewer',
is_active BOOLEAN NOT NULL DEFAULT true,
last_login TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- Create indexes
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_role ON users(role);
CREATE INDEX IF NOT EXISTS idx_users_deleted_at ON users(deleted_at);
-- +goose Down
-- Drop users table
DROP TABLE IF EXISTS users CASCADE;
@@ -0,0 +1,37 @@
-- +goose Up
-- Create roles table
CREATE TABLE IF NOT EXISTS roles (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Create permissions table
CREATE TABLE IF NOT EXISTS permissions (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
resource TEXT NOT NULL,
action TEXT NOT NULL,
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Create role_permissions junction table
CREATE TABLE IF NOT EXISTS role_permissions (
role_id INTEGER NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
permission_id INTEGER NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,
PRIMARY KEY (role_id, permission_id)
);
-- Create indexes
CREATE INDEX IF NOT EXISTS idx_role_permissions_role_id ON role_permissions(role_id);
CREATE INDEX IF NOT EXISTS idx_role_permissions_permission_id ON role_permissions(permission_id);
-- +goose Down
-- Drop role_permissions, permissions, and roles tables
DROP TABLE IF EXISTS role_permissions CASCADE;
DROP TABLE IF EXISTS permissions CASCADE;
DROP TABLE IF EXISTS roles CASCADE;
@@ -0,0 +1,40 @@
-- +goose Up
-- Create auth_sessions table
CREATE TABLE IF NOT EXISTS auth_sessions (
id TEXT PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token TEXT NOT NULL,
refresh_token TEXT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
ip_address TEXT,
user_agent TEXT,
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Create api_keys table
CREATE TABLE IF NOT EXISTS api_keys (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
key TEXT NOT NULL UNIQUE,
secret TEXT NOT NULL,
permissions JSONB,
is_active BOOLEAN NOT NULL DEFAULT true,
expires_at TIMESTAMPTZ,
last_used TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Create indexes
CREATE INDEX IF NOT EXISTS idx_auth_sessions_user_id ON auth_sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_auth_sessions_token ON auth_sessions(token);
CREATE INDEX IF NOT EXISTS idx_auth_sessions_expires_at ON auth_sessions(expires_at);
CREATE INDEX IF NOT EXISTS idx_api_keys_key ON api_keys(key);
CREATE INDEX IF NOT EXISTS idx_api_keys_is_active ON api_keys(is_active);
-- +goose Down
-- Drop auth_sessions and api_keys tables
DROP TABLE IF EXISTS auth_sessions CASCADE;
DROP TABLE IF EXISTS api_keys CASCADE;
+30
View File
@@ -0,0 +1,30 @@
-- +goose Up
-- Create plugins table
CREATE TABLE IF NOT EXISTS plugins (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
version TEXT NOT NULL,
description TEXT,
author TEXT,
type TEXT,
category TEXT,
license TEXT,
homepage TEXT,
repository TEXT,
enabled BOOLEAN NOT NULL DEFAULT false,
status TEXT NOT NULL DEFAULT 'installed',
config JSONB,
installed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- Create indexes
CREATE INDEX IF NOT EXISTS idx_plugins_name ON plugins(name);
CREATE INDEX IF NOT EXISTS idx_plugins_enabled ON plugins(enabled);
CREATE INDEX IF NOT EXISTS idx_plugins_deleted_at ON plugins(deleted_at);
-- +goose Down
-- Drop plugins table
DROP TABLE IF EXISTS plugins CASCADE;
+46
View File
@@ -0,0 +1,46 @@
-- +goose Up
-- Create automations table
CREATE TABLE IF NOT EXISTS automations (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
description TEXT,
type TEXT,
enabled BOOLEAN NOT NULL DEFAULT true,
schedule_type TEXT,
schedule_cron TEXT,
schedule_interval_sec INTEGER,
timezone TEXT NOT NULL DEFAULT 'UTC',
definition JSONB,
last_run_at TIMESTAMPTZ,
next_run_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
-- Create automation_runs table
CREATE TABLE IF NOT EXISTS automation_runs (
id SERIAL PRIMARY KEY,
automation_id INTEGER NOT NULL REFERENCES automations(id) ON DELETE CASCADE,
status TEXT NOT NULL,
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
ended_at TIMESTAMPTZ,
duration_ms BIGINT NOT NULL DEFAULT 0,
output TEXT,
error TEXT,
details JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Create indexes
CREATE INDEX IF NOT EXISTS idx_automations_name ON automations(name);
CREATE INDEX IF NOT EXISTS idx_automations_enabled ON automations(enabled);
CREATE INDEX IF NOT EXISTS idx_automations_deleted_at ON automations(deleted_at);
CREATE INDEX IF NOT EXISTS idx_automation_runs_automation_id ON automation_runs(automation_id);
CREATE INDEX IF NOT EXISTS idx_automation_runs_status ON automation_runs(status);
CREATE INDEX IF NOT EXISTS idx_automation_runs_started_at ON automation_runs(started_at);
-- +goose Down
-- Drop automation_runs and automations tables
DROP TABLE IF EXISTS automation_runs CASCADE;
DROP TABLE IF EXISTS automations CASCADE;
+23
View File
@@ -0,0 +1,23 @@
-- +goose Up
-- Create config_entries table
CREATE TABLE IF NOT EXISTS config_entries (
id SERIAL PRIMARY KEY,
section TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT,
type TEXT,
sensitive BOOLEAN NOT NULL DEFAULT false,
description TEXT,
updated_by TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(section, key)
);
-- Create indexes
CREATE INDEX IF NOT EXISTS idx_config_entries_section ON config_entries(section);
CREATE INDEX IF NOT EXISTS idx_config_entries_key ON config_entries(key);
-- +goose Down
-- Drop config_entries table
DROP TABLE IF EXISTS config_entries CASCADE;
@@ -0,0 +1,28 @@
-- +goose Up
-- Create deployment_records table
CREATE TABLE IF NOT EXISTS deployment_records (
id SERIAL PRIMARY KEY,
service TEXT NOT NULL,
version TEXT NOT NULL,
environment TEXT,
status TEXT NOT NULL,
strategy TEXT,
replicas INTEGER NOT NULL DEFAULT 1,
triggered_by TEXT,
reason TEXT,
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
rollback_to TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Create indexes
CREATE INDEX IF NOT EXISTS idx_deployment_records_service ON deployment_records(service);
CREATE INDEX IF NOT EXISTS idx_deployment_records_environment ON deployment_records(environment);
CREATE INDEX IF NOT EXISTS idx_deployment_records_status ON deployment_records(status);
CREATE INDEX IF NOT EXISTS idx_deployment_records_created_at ON deployment_records(created_at);
-- +goose Down
-- Drop deployment_records table
DROP TABLE IF EXISTS deployment_records CASCADE;
@@ -0,0 +1,28 @@
-- +goose Up
-- Create chaos_experiment_records table
CREATE TABLE IF NOT EXISTS chaos_experiment_records (
id SERIAL PRIMARY KEY,
external_id TEXT UNIQUE,
name TEXT,
type TEXT,
target TEXT,
status TEXT,
duration_ms BIGINT NOT NULL DEFAULT 0,
probability DOUBLE PRECISION,
amount INTEGER NOT NULL DEFAULT 0,
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
finished_at TIMESTAMPTZ,
error TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Create indexes
CREATE INDEX IF NOT EXISTS idx_chaos_experiment_records_external_id ON chaos_experiment_records(external_id);
CREATE INDEX IF NOT EXISTS idx_chaos_experiment_records_status ON chaos_experiment_records(status);
CREATE INDEX IF NOT EXISTS idx_chaos_experiment_records_target ON chaos_experiment_records(target);
CREATE INDEX IF NOT EXISTS idx_chaos_experiment_records_started_at ON chaos_experiment_records(started_at);
-- +goose Down
-- Drop chaos_experiment_records table
DROP TABLE IF EXISTS chaos_experiment_records CASCADE;
@@ -0,0 +1,14 @@
-- +goose Up
-- Create imsi_allocations table
CREATE TABLE IF NOT EXISTS imsi_allocations (
id SERIAL PRIMARY KEY,
last_imsi BIGINT NOT NULL DEFAULT 0,
min_imsi BIGINT NOT NULL DEFAULT 1,
max_imsi BIGINT NOT NULL DEFAULT 999999999,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- +goose Down
-- Drop imsi_allocations table
DROP TABLE IF EXISTS imsi_allocations CASCADE;
+23
View File
@@ -0,0 +1,23 @@
-- +goose Up
-- Create payment_methods table
CREATE TABLE IF NOT EXISTS payment_methods (
id TEXT PRIMARY KEY,
subscriber_id INTEGER NOT NULL,
gateway_id TEXT,
type TEXT NOT NULL,
customer_id TEXT,
last4 TEXT,
brand TEXT,
expiry_month INTEGER,
expiry_year INTEGER,
is_default BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Create indexes
CREATE INDEX IF NOT EXISTS idx_payment_methods_subscriber_id ON payment_methods(subscriber_id);
CREATE INDEX IF NOT EXISTS idx_payment_methods_gateway_id ON payment_methods(gateway_id);
-- +goose Down
-- Drop payment_methods table
DROP TABLE IF EXISTS payment_methods CASCADE;
+25
View File
@@ -0,0 +1,25 @@
-- +goose Up
-- Create transactions table
CREATE TABLE IF NOT EXISTS transactions (
id SERIAL PRIMARY KEY,
subscriber_id INTEGER NOT NULL,
transaction_id TEXT NOT NULL,
type TEXT NOT NULL,
amount DOUBLE PRECISION NOT NULL,
currency TEXT NOT NULL,
status TEXT NOT NULL,
description TEXT,
parent_id INTEGER,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Create indexes
CREATE INDEX IF NOT EXISTS idx_transactions_subscriber_id ON transactions(subscriber_id);
CREATE INDEX IF NOT EXISTS idx_transactions_transaction_id ON transactions(transaction_id);
CREATE INDEX IF NOT EXISTS idx_transactions_status ON transactions(status);
CREATE INDEX IF NOT EXISTS idx_transactions_created_at ON transactions(created_at);
-- +goose Down
-- Drop transactions table
DROP TABLE IF EXISTS transactions CASCADE;
@@ -0,0 +1,35 @@
-- +goose Up
-- Create notifications table
CREATE TABLE IF NOT EXISTS notifications (
id SERIAL PRIMARY KEY,
subscriber_id INTEGER NOT NULL,
type TEXT NOT NULL,
title TEXT NOT NULL,
message TEXT NOT NULL,
read BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Create alerts table
CREATE TABLE IF NOT EXISTS alerts (
id SERIAL PRIMARY KEY,
type TEXT NOT NULL,
severity TEXT NOT NULL,
message TEXT NOT NULL,
subscriber_id INTEGER,
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
resolved BOOLEAN NOT NULL DEFAULT false
);
-- Create indexes
CREATE INDEX IF NOT EXISTS idx_notifications_subscriber_id ON notifications(subscriber_id);
CREATE INDEX IF NOT EXISTS idx_notifications_read ON notifications(read);
CREATE INDEX IF NOT EXISTS idx_alerts_subscriber_id ON alerts(subscriber_id);
CREATE INDEX IF NOT EXISTS idx_alerts_type ON alerts(type);
CREATE INDEX IF NOT EXISTS idx_alerts_severity ON alerts(severity);
CREATE INDEX IF NOT EXISTS idx_alerts_resolved ON alerts(resolved);
-- +goose Down
-- Drop notifications and alerts tables
DROP TABLE IF EXISTS notifications CASCADE;
DROP TABLE IF EXISTS alerts CASCADE;