393 lines
10 KiB
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
|
|
}
|