mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-18 02:52:53 +00:00
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user