aurganize-backend/backend/internal/repositories/tenant_repository.go

393 lines
10 KiB
Go

package repositories
import (
"context"
"database/sql"
"time"
"github.com/creativenoz/aurganize-v62/backend/internal/models"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/rs/zerolog/log"
)
type TenantRepository struct {
db *sqlx.DB
}
func NewTenantRepository(db *sqlx.DB) *TenantRepository {
log.Info().
Str("repository", "tenant").
Str("component", "repository_init").
Bool("has_db_connection", db != nil).
Msg("tenant repository initialized")
return &TenantRepository{
db: db,
}
}
type Execer interface {
GetContext(ctx context.Context, des interface{}, query string, args ...interface{}) error
ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row
}
func (tr *TenantRepository) CreateTx(ctx context.Context, tx Execer, input *models.CreateTenantInput, slug string) (*models.Tenant, error) {
log.Info().
Str("repository", "tenant").
Str("action", "create_tenant_tx_started").
Str("tenant_name", input.Name).
Str("slug", slug).
Str("email", *input.Email).
Str("timezone", input.Timezone).
Str("subscription_plan", "basic").
Str("subscription_status", "trial").
Bool("in_transaction", true).
Msg("creating tenant within transaction")
tenant := &models.Tenant{}
// trailEndsAt := time.Now().Add(14 * 24 * time.Hour)
loc, err := time.LoadLocation(input.Timezone)
if err != nil {
log.Warn().
Str("repository", "tenant").
Str("action", "invalid_timezone_fallback").
Str("invalid_timezone", input.Timezone).
Str("fallback_timezone", "UTC").
Str("tenant_name", input.Name).
Msg("invalid timezone provided, falling back to UTC")
loc = time.UTC
}
now := time.Now().In(loc)
trialEndsAt := now.Add(14 * 24 * time.Hour)
log.Debug().
Str("repository", "tenant").
Str("action", "trial_period_calculated").
Int("trial_duration_days", 14).
Time("trial_ends_at", trialEndsAt).
Str("tenant_name", input.Name).
Msg("trial period configured for new tenant")
query := `
INSERT INTO tenants (
name,
slug,
email,
timezone,
currency,
locale,
subscription_status,
subscription_plan,
trial_ends_at,
status
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING id, name, slug, email, phone, website,
address_line1, address_line2, city, state, country, postal_code,
timezone, currency, locale, subscription_status, subscription_plan,
subscription_expires_at, trial_ends_at, max_users, max_contracts,
max_storage_mb, status, created_at, updated_at, deleted_at
`
err = tx.GetContext(
ctx,
tenant,
query,
input.Name,
slug,
input.Email,
input.Timezone,
input.Currency,
input.Locale,
"trial",
"basic",
trialEndsAt,
"active",
)
if err != nil {
log.Error().
Str("repository", "tenant").
Str("action", "create_tenant_tx_failed").
Str("tenant_name", input.Name).
Str("slug", slug).
Str("email", *input.Email).
Bool("in_transaction", true).
Err(err).
Msg("CRITICAL: failed to create tenant in transaction - registration will fail")
return nil, err
}
log.Info().
Str("repository", "tenant").
Str("action", "create_tenant_tx_success").
Str("tenant_id", tenant.ID.String()).
Str("tenant_name", tenant.Name).
Str("slug", tenant.Slug).
Str("email", *tenant.Email).
Str("subscription_status", tenant.SubscriptionStatus).
Str("subscription_plan", tenant.SubscriptionPlan).
Time("trial_ends_at", *tenant.TrialEnds).
Str("currency", tenant.Currency).
Str("timezone", tenant.Timezone).
Bool("in_transaction", true).
Msg("tenant created successfully in transaction")
return tenant, nil
}
// Create creates a new tenant
func (tr *TenantRepository) Create(ctx context.Context, input *models.CreateTenantInput, slug string) (*models.Tenant, error) {
log.Info().
Str("repository", "tenant").
Str("action", "create_tenant_started").
Str("tenant_name", input.Name).
Str("slug", slug).
Str("email", *input.Email).
Str("timezone", input.Timezone).
Bool("in_transaction", false).
Msg("creating tenant (standalone, not in transaction)")
tenant := &models.Tenant{}
// trailEndsAt := time.Now().Add(14 * 24 * time.Hour)
loc, err := time.LoadLocation(input.Timezone)
if err != nil {
log.Warn().
Str("repository", "tenant").
Str("action", "invalid_timezone_fallback").
Str("invalid_timezone", input.Timezone).
Str("fallback_timezone", "UTC").
Str("tenant_name", input.Name).
Msg("invalid timezone provided, falling back to UTC")
loc = time.UTC
}
now := time.Now().In(loc)
trialEndsAt := now.Add(14 * 24 * time.Hour)
log.Debug().
Str("repository", "tenant").
Str("action", "trial_period_calculated").
Int("trial_duration_days", 14).
Time("trial_ends_at", trialEndsAt).
Str("tenant_name", input.Name).
Msg("trial period configured for new tenant")
query := `
INSERT INTO tenants (
name,
slug,
email,
timezone,
currency,
locale,
subscription_status,
subscription_plan,
trial_ends_at,
status
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING id, name, slug, email, phone, website,
address_line1, address_line2, city, state, country, postal_code,
timezone, currency, locale, subscription_status, subscription_plan,
subscription_expires_at, trial_ends_at, max_users, max_contracts,
max_storage_mb, status, created_at, updated_at, deleted_at
`
err = tr.db.GetContext(
ctx,
tenant,
query,
input.Name,
slug,
input.Email,
input.Timezone,
input.Currency,
input.Locale,
"trial",
"basic",
trialEndsAt,
"active",
)
if err != nil {
log.Error().
Str("repository", "tenant").
Str("action", "create_tenant_failed").
Str("tenant_name", input.Name).
Str("slug", slug).
Err(err).
Msg("failed to create tenant")
return nil, err
}
log.Info().
Str("repository", "tenant").
Str("action", "create_tenant_success").
Str("tenant_id", tenant.ID.String()).
Str("tenant_name", tenant.Name).
Str("slug", tenant.Slug).
Str("subscription_status", tenant.SubscriptionStatus).
Time("trial_ends_at", *tenant.TrialEnds).
Bool("in_transaction", false).
Msg("tenant created successfully")
return tenant, nil
}
func (tr *TenantRepository) FindByID(ctx context.Context, tenantId uuid.UUID) (*models.Tenant, error) {
log.Debug().
Str("repository", "tenant").
Str("action", "find_tenant_by_id_started").
Str("tenant_id", tenantId.String()).
Msg("looking up tenant by id")
tenant := &models.Tenant{}
query := `
SELECT id, name, slug, email, phone, website,
address_line1, address_line2, city, state, country, postal_code,
timezone, currency, locale, subscription_status, subscription_plan,
subscription_expires_at, trial_ends_at, max_users, max_contracts,
max_storage_mb, status, created_at, updated_at, deleted_at
FROM tenants
WHERE id = $1
AND deleted_at IS NULL
`
err := tr.db.GetContext(ctx, tenant, query, tenantId)
if err == sql.ErrNoRows {
log.Warn().
Str("repository", "tenant").
Str("action", "tenant_not_found_by_id").
Str("tenant_id", tenantId.String()).
Msg("tenant not found - may be deleted or never existed")
return nil, nil
}
if err != nil {
log.Error().
Str("repository", "tenant").
Str("action", "find_tenant_by_id_error").
Str("tenant_id", tenantId.String()).
Err(err).
Msg("database error while looking up tenant by id")
return nil, err
}
log.Debug().
Str("repository", "tenant").
Str("action", "tenant_found_by_id").
Str("tenant_id", tenant.ID.String()).
Str("tenant_name", tenant.Name).
Str("subscription_status", tenant.SubscriptionStatus).
Str("subscription_plan", tenant.SubscriptionPlan).
Str("status", tenant.Status).
Msg("tenant found by id")
return tenant, nil
}
func (tr *TenantRepository) FindBySlug(ctx context.Context, slug string) (*models.Tenant, error) {
log.Debug().
Str("repository", "tenant").
Str("action", "find_tenant_by_slug_started").
Str("slug", slug).
Msg("looking up tenant by slug")
tenant := &models.Tenant{}
query := `
SELECT id, name, slug, email, phone, website,
address_line1, address_line2, city, state, country, postal_code,
timezone, currency, locale, subscription_status, subscription_plan,
subscription_expires_at, trial_ends_at, max_users, max_contracts,
max_storage_mb, status, created_at, updated_at, deleted_at
FROM tenants
WHERE slug = $1
AND deleted_at IS NULL
`
err := tr.db.GetContext(ctx, tenant, query, slug)
if err == sql.ErrNoRows {
log.Warn().
Str("repository", "tenant").
Str("action", "tenant_not_found_by_slug").
Str("slug", slug).
Msg("tenant not found by slug")
return nil, nil
}
if err != nil {
log.Error().
Str("repository", "tenant").
Str("action", "find_tenant_by_slug_error").
Str("slug", slug).
Err(err).
Msg("database error while looking up tenant by slug")
return nil, err
}
log.Debug().
Str("repository", "tenant").
Str("action", "tenant_found_by_slug").
Str("tenant_id", tenant.ID.String()).
Str("tenant_name", tenant.Name).
Str("slug", tenant.Slug).
Str("subscription_status", tenant.SubscriptionStatus).
Msg("tenant found by slug")
return tenant, nil
}
func (tr *TenantRepository) SlugExists(ctx context.Context, slug string) (bool, error) {
log.Debug().
Str("repository", "tenant").
Str("action", "checking_slug_exists").
Str("slug", slug).
Msg("checking if tenant slug is available")
var exists bool = false
query := `
SELECT EXISTS (
SELECT 1 FROM tenants
WHERE slug = $1
AND deleted_at IS NULL
)
`
err := tr.db.GetContext(ctx, &exists, query, slug)
if err != nil {
log.Error().
Str("repository", "tenant").
Str("action", "slug_exists_check_error").
Str("slug", slug).
Err(err).
Msg("database error while checking slug existence")
return true, err
}
if exists {
log.Info().
Str("repository", "tenant").
Str("action", "slug_exists_conflict").
Str("slug", slug).
Bool("exists", true).
Msg("slug already taken - registration will require different name")
} else {
log.Debug().
Str("repository", "tenant").
Str("action", "slug_available").
Str("slug", slug).
Bool("exists", false).
Msg("slug is available for registration")
}
return exists, nil
}
func (tr TenantRepository) Update(ctx context.Context, id uuid.UUID, updates map[string]interface{}) error {
// TODO
return nil
}