feat(membership): enforce tenant subscription plans with blocked UI
Add configurable membership plans (free/plus/pro) synced to DB on boot, a subscription guard middleware that blocks tenant endpoints on expired or missing plans, and a MembershipBlockedView that surfaces the reason so the admin can contact the tenant owner. Quota and brand-library reads now honor the active plan's policy JSON and expired subscriptions.
This commit is contained in:
@@ -75,6 +75,11 @@ func New(configPath string) (*App, error) {
|
||||
return nil, fmt.Errorf("init postgres: %w", err)
|
||||
}
|
||||
|
||||
if err := syncMembershipPlans(ctx, pool, cfg.Membership); err != nil {
|
||||
pool.Close()
|
||||
return nil, fmt.Errorf("sync membership plans: %w", err)
|
||||
}
|
||||
|
||||
monitoringPool, err := postgres.NewPool(ctx, cfg.MonitoringDatabase)
|
||||
if err != nil {
|
||||
pool.Close()
|
||||
@@ -175,6 +180,24 @@ func New(configPath string) (*App, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// syncMembershipPlans upserts all configured plans in a single transaction so
|
||||
// that a partial failure (or concurrent startup) never leaves the `plans` table
|
||||
// in a half-synced state.
|
||||
func syncMembershipPlans(ctx context.Context, pool *pgxpool.Pool, membership config.MembershipConfig) error {
|
||||
tx, err := pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = tx.Rollback(ctx)
|
||||
}()
|
||||
|
||||
if err := repository.NewTenantPlanRepository(tx).UpsertConfiguredPlans(ctx, membership); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (a *App) Close() {
|
||||
if a.AuditLogs != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
|
||||
Reference in New Issue
Block a user