refactor(db): replace sqlc with handwritten pgx repos

- Finish Paddle-to-Stripe billing migration; drop paddle/stripe libs
- Renumber migrations; add 00012 memberships+manage_token,
  00013 location_type_capacity
- Add tests: admin, auth, bookings customer service
- Remove graphify-out artifacts and stale env files

Authored By: TDvorak <[email protected]>

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-17 20:21:34 +02:00
1 parent 2039669e2c
commit f2f7439050
93 files changed
+6987 -69060

No files matched your search

+80 -19
View File
@@ -2,6 +2,8 @@ package db
import (
"context"
"errors"
"strings"
"github.com/jackc/pgx/v5"
)
@@ -49,9 +51,20 @@ func (r *PGRepository) GetTenantByBillingCustomerID(ctx context.Context, custome
}
func (r *PGRepository) EnsureUserIdentity(ctx context.Context, subject string, email string, displayName string) error {
// Link an existing password-auth account to this Neon subject when the
// email already exists but has no subject attached yet.
if trimmed := strings.TrimSpace(email); trimmed != "" {
if _, err := r.pool.Exec(ctx, `
UPDATE users SET neon_subject = $1
WHERE email = $2 AND neon_subject IS NULL
`, subject, trimmed); err != nil {
return err
}
}
_, err := r.pool.Exec(ctx, `
INSERT INTO users (id, neon_subject, email, display_name)
VALUES (gen_random_uuid(), $1, COALESCE(NULLIF($2, ''), $1 || '@users.bookra.invalid'), NULLIF($3, ''))
VALUES (gen_random_uuid(), $1, COALESCE(NULLIF($2, ''), $1 || '@users.bookra.invalid'), COALESCE(NULLIF($3, ''), ''))
ON CONFLICT (neon_subject) DO UPDATE SET email = EXCLUDED.email, display_name = COALESCE(NULLIF($3, ''), users.display_name)
`, subject, email, displayName)
return err
@@ -123,21 +136,22 @@ func (r *PGRepository) CreateTenantForUser(ctx context.Context, params CreateTen
_, err = tx.Exec(ctx, `
INSERT INTO users (id, neon_subject, email, display_name)
VALUES (gen_random_uuid(), $1, $2, $3)
ON CONFLICT (neon_subject) DO NOTHING
`, params.Subject, params.Subject+"@users.bookra.invalid", "")
SELECT gen_random_uuid(), $1, $2, ''
WHERE NOT EXISTS (
SELECT 1 FROM users WHERE neon_subject = $1 OR id::text = $1
)
`, params.Subject, params.Subject+"@users.bookra.invalid")
if err != nil {
return TenantMembershipRecord{}, err
}
var userID string
var membershipID string
err = tx.QueryRow(ctx, `
INSERT INTO tenant_memberships (id, tenant_id, user_neon_subject, role, joined_at)
SELECT gen_random_uuid(), $1, u.neon_subject, 'owner', now()
FROM users u WHERE u.neon_subject = $2
VALUES (gen_random_uuid(), $1, $2, 'owner', now())
ON CONFLICT (tenant_id, user_neon_subject) DO UPDATE SET role = 'owner'
RETURNING id
`, tenantID, params.Subject).Scan(&userID)
`, tenantID, params.Subject).Scan(&membershipID)
if err != nil {
return TenantMembershipRecord{}, err
}
@@ -156,7 +170,7 @@ func (r *PGRepository) CreateTenantForUser(ctx context.Context, params CreateTen
Locale: params.Locale,
Timezone: params.Timezone,
},
UserID: userID,
UserID: membershipID,
Role: "owner",
}, nil
}
@@ -170,7 +184,7 @@ func (r *PGRepository) GetBrandProfile(ctx context.Context, tenantID string) (Br
return record, err
}
func (r *PGRepository) GetTenantMembershipByUserID(ctx context.Context, userID string) (TenantMembershipRecord, error) {
func (r *PGRepository) GetTenantMembershipBySubject(ctx context.Context, subject string) (TenantMembershipRecord, error) {
var record TenantMembershipRecord
err := r.pool.QueryRow(ctx, `
SELECT t.id, t.slug, t.name, t.preset, t.locale, t.timezone, t.plan_code, t.subscription_status,
@@ -178,11 +192,10 @@ func (r *PGRepository) GetTenantMembershipByUserID(ctx context.Context, userID s
tm.user_neon_subject, tm.role
FROM tenant_memberships tm
JOIN tenants t ON t.id = tm.tenant_id
JOIN users u ON u.neon_subject = tm.user_neon_subject
WHERE u.id = $1
WHERE tm.user_neon_subject = $1
ORDER BY tm.joined_at DESC
LIMIT 1
`, userID).Scan(
`, subject).Scan(
&record.Tenant.ID, &record.Tenant.Slug, &record.Tenant.Name, &record.Tenant.Preset,
&record.Tenant.Locale, &record.Tenant.Timezone, &record.Tenant.PlanCode, &record.Tenant.SubscriptionStatus,
&record.Tenant.BillingProvider, &record.Tenant.BillingCustomerID, &record.Tenant.BillingSubscription,
@@ -191,20 +204,68 @@ func (r *PGRepository) GetTenantMembershipByUserID(ctx context.Context, userID s
return record, err
}
func (r *PGRepository) UpdateTenantBillingCustomerID(ctx context.Context, tenantID string, customerID string) error {
func (r *PGRepository) IsAdminSubject(ctx context.Context, subject string) (bool, error) {
var isAdmin bool
err := r.pool.QueryRow(ctx, `
SELECT EXISTS (
SELECT 1 FROM users
WHERE (neon_subject = $1 OR id::text = $1)
AND role IN ('admin', 'superadmin')
)
`, subject).Scan(&isAdmin)
return isAdmin, err
}
func (r *PGRepository) GetTenantOwnerEmail(ctx context.Context, tenantID string) (string, error) {
var email string
err := r.pool.QueryRow(ctx, `
SELECT u.email
FROM tenant_memberships tm
JOIN users u ON u.neon_subject = tm.user_neon_subject OR u.id::text = tm.user_neon_subject
WHERE tm.tenant_id = $1 AND tm.role = 'owner'
ORDER BY tm.joined_at ASC
LIMIT 1
`, tenantID).Scan(&email)
return email, err
}
func (r *PGRepository) UpdateBrandProfile(ctx context.Context, tenantID string, params UpdateBrandProfileParams) (BrandProfileRecord, error) {
var record BrandProfileRecord
err := r.pool.QueryRow(ctx, `
INSERT INTO brand_profiles (tenant_id, name, site_url, logo_url, primary_color)
VALUES ($1, '', NULL, NULL, NULL)
ON CONFLICT (tenant_id) DO NOTHING
`, tenantID).Scan()
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return BrandProfileRecord{}, err
}
err = r.pool.QueryRow(ctx, `
UPDATE brand_profiles
SET name = COALESCE($2, name),
site_url = COALESCE($3, site_url),
logo_url = COALESCE($4, logo_url),
primary_color = COALESCE($5, primary_color)
WHERE tenant_id = $1
RETURNING tenant_id, name, COALESCE(site_url, ''), COALESCE(logo_url, ''), COALESCE(primary_color, ''), COALESCE(umami_site_id, '')
`, tenantID, params.Name, params.SiteURL, params.LogoURL, params.PrimaryColor).Scan(
&record.TenantID, &record.Name, &record.SiteURL, &record.LogoURL, &record.PrimaryColor, &record.UmamiSiteID)
return record, err
}
func (r *PGRepository) UpdateTenantBillingCustomerID(ctx context.Context, tenantID string, provider string, customerID string) error {
_, err := r.pool.Exec(ctx, `
UPDATE tenants
SET billing_provider = 'paddle', billing_customer_id = $2, updated_at = now()
SET billing_provider = $2, billing_customer_id = $3, updated_at = now()
WHERE id = $1
`, tenantID, customerID)
`, tenantID, provider, customerID)
return err
}
func (r *PGRepository) UpdateTenantBillingState(ctx context.Context, tenantID string, planCode string, subscriptionStatus string, subscriptionID string) error {
func (r *PGRepository) UpdateTenantBillingState(ctx context.Context, tenantID string, provider string, planCode string, subscriptionStatus string, subscriptionID string) error {
_, err := r.pool.Exec(ctx, `
UPDATE tenants
SET billing_provider = 'paddle', plan_code = $2, subscription_status = $3, billing_subscription_id = $4, updated_at = now()
SET billing_provider = $2, plan_code = $3, subscription_status = $4, billing_subscription_id = $5, updated_at = now()
WHERE id = $1
`, tenantID, planCode, subscriptionStatus, subscriptionID)
`, tenantID, provider, planCode, subscriptionStatus, subscriptionID)
return err
}