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

+152 -26
View File
@@ -19,7 +19,9 @@ type Repository interface {
GetTenantByBillingCustomerID(ctx context.Context, customerID string) (TenantRecord, error)
EnsureUserIdentity(ctx context.Context, subject string, email string, displayName string) error
CreateTenantForUser(ctx context.Context, params CreateTenantForUserParams) (TenantMembershipRecord, error)
GetTenantMembershipByUserID(ctx context.Context, userID string) (TenantMembershipRecord, error)
GetTenantMembershipBySubject(ctx context.Context, subject string) (TenantMembershipRecord, error)
IsAdminSubject(ctx context.Context, subject string) (bool, error)
GetTenantOwnerEmail(ctx context.Context, tenantID string) (string, error)
GetBrandProfile(ctx context.Context, tenantID string) (BrandProfileRecord, error)
ListServicesByTenant(ctx context.Context, tenantID string) ([]ServiceRecord, error)
ListAvailabilityRulesByTenant(ctx context.Context, tenantID string) ([]AvailabilityRuleRecord, error)
@@ -34,8 +36,9 @@ type Repository interface {
GetDashboardMetrics(ctx context.Context, tenantID string, startsAt time.Time, endsAt time.Time) (DashboardMetrics, error)
GetSubscriptionSnapshot(ctx context.Context, tenantID string) (BillingSnapshotRecord, error)
UpsertSubscriptionSnapshot(ctx context.Context, params BillingSnapshotRecord) error
UpdateTenantBillingCustomerID(ctx context.Context, tenantID string, customerID string) error
UpdateTenantBillingState(ctx context.Context, tenantID string, planCode string, subscriptionStatus string, subscriptionID string) error
UpdateTenantBillingCustomerID(ctx context.Context, tenantID string, provider string, customerID string) error
UpdateTenantBillingState(ctx context.Context, tenantID string, provider string, planCode string, subscriptionStatus string, subscriptionID string) error
UpdateBrandProfile(ctx context.Context, tenantID string, params UpdateBrandProfileParams) (BrandProfileRecord, error)
RecordBillingEvent(ctx context.Context, tenantID string, provider string, eventID string, eventType string, payload []byte) (bool, error)
// Auth methods
@@ -87,6 +90,11 @@ type Repository interface {
ListWorkingHoursByTenant(ctx context.Context, tenantID string) ([]WorkingHoursRecord, error)
UpdateWorkingHours(ctx context.Context, tenantID string, dayOfWeek int, params UpdateWorkingHoursParams) error
// Email templates
ListEmailTemplates(ctx context.Context, tenantID string) ([]EmailTemplateRecord, error)
GetEmailTemplate(ctx context.Context, tenantID string, templateType string) (EmailTemplateRecord, error)
UpsertEmailTemplate(ctx context.Context, params EmailTemplateRecord) (EmailTemplateRecord, error)
// SMS
GetTenantSMSSettings(ctx context.Context, tenantID string) (TenantSMSSettingsRecord, error)
UpsertTenantSMSSettings(ctx context.Context, params TenantSMSSettingsRecord) error
@@ -190,6 +198,13 @@ type BrandProfileRecord struct {
UmamiSiteID string
}
type UpdateBrandProfileParams struct {
Name *string
SiteURL *string
LogoURL *string
PrimaryColor *string
}
type AvailabilityBlockRecord struct {
DayOfWeek int
StartsLocal string
@@ -246,6 +261,8 @@ type BookingRecord struct {
EndsAt time.Time
Status string
Reference string
ManageToken string
Notes string
}
type CreateBookingParams struct {
@@ -262,13 +279,15 @@ type CreateBookingParams struct {
EndsAt time.Time
Status string
Reference string
ManageToken string
Notes string
}
type CreatedBooking struct {
ID string
Reference string
Status string
ID string
Reference string
Status string
ManageToken string
}
type WaitlistEntryParams struct {
@@ -341,6 +360,8 @@ type LocationRecord struct {
ID string
TenantID string
Name string
Type string
Capacity int
Timezone string
CreatedAt time.Time
}
@@ -348,11 +369,15 @@ type LocationRecord struct {
type CreateLocationParams struct {
TenantID string
Name string
Type string
Capacity int
Timezone string
}
type UpdateLocationParams struct {
Name *string
Type *string
Capacity *int
Timezone *string
}
@@ -428,6 +453,18 @@ type UpdateWorkingHoursParams struct {
IsOpen *bool
}
// Email Template Records
type EmailTemplateRecord struct {
ID string
TenantID string
Type string
Subject string
BodyHTML string
BodyText string
IsEnabled bool
UpdatedAt time.Time
}
// SMS Records
type TenantSMSSettingsRecord struct {
TenantID string
@@ -592,11 +629,12 @@ func (r *PGRepository) GetBookingByReference(ctx context.Context, reference stri
var rec BookingRecord
err := r.pool.QueryRow(ctx, `
SELECT id, tenant_id, service_id, class_session_id, staff_id, location_id,
customer_name, customer_email, customer_phone, starts_at, ends_at, status, reference
customer_name, customer_email, customer_phone, starts_at, ends_at, status, reference,
COALESCE(manage_token, ''), COALESCE(notes, '')
FROM bookings
WHERE reference = $1
`, reference).Scan(&rec.ID, &rec.TenantID, &rec.ServiceID, &rec.ClassSessionID, &rec.StaffID, &rec.LocationID,
&rec.CustomerName, &rec.CustomerEmail, &rec.CustomerPhone, &rec.StartsAt, &rec.EndsAt, &rec.Status, &rec.Reference)
&rec.CustomerName, &rec.CustomerEmail, &rec.CustomerPhone, &rec.StartsAt, &rec.EndsAt, &rec.Status, &rec.Reference, &rec.ManageToken, &rec.Notes)
return rec, err
}
@@ -638,6 +676,7 @@ type MemoryRepository struct {
smsSettings TenantSMSSettingsRecord
smsLogs []SMSUsageLogRecord
smsReports []SMSMonthlyReportRecord
emailTemplates []EmailTemplateRecord
}
func NewMemoryRepository() *MemoryRepository {
@@ -659,7 +698,7 @@ func NewMemoryRepository() *MemoryRepository {
Timezone: "Europe/Prague",
PlanCode: "pro",
SubscriptionStatus: "active",
BillingProvider: "paddle",
BillingProvider: "stripe",
BillingCustomerID: stringPtr("ctm_demo_bookra"),
},
membership: TenantMembershipRecord{
@@ -672,7 +711,7 @@ func NewMemoryRepository() *MemoryRepository {
Timezone: "Europe/Prague",
PlanCode: "pro",
SubscriptionStatus: "active",
BillingProvider: "paddle",
BillingProvider: "stripe",
BillingCustomerID: stringPtr("ctm_demo_bookra"),
},
UserID: "demo-owner",
@@ -719,7 +758,7 @@ func NewMemoryRepository() *MemoryRepository {
deliveryLogs: []NotificationDeliveryLogParams{},
billingSnapshot: BillingSnapshotRecord{
TenantID: tenantID,
BillingProvider: "paddle",
BillingProvider: "stripe",
BillingCustomerID: "ctm_demo_bookra",
BillingSubscriptionID: "",
Status: "active",
@@ -841,16 +880,46 @@ func (r *MemoryRepository) CreateTenantForUser(_ context.Context, params CreateT
return r.membership, nil
}
func (r *MemoryRepository) GetTenantMembershipByUserID(_ context.Context, userID string) (TenantMembershipRecord, error) {
if userID == "" || userID == "demo-owner" {
func (r *MemoryRepository) GetTenantMembershipBySubject(_ context.Context, subject string) (TenantMembershipRecord, error) {
if subject == "" || subject == "demo-owner" {
return r.membership, nil
}
if userID == r.membership.UserID {
if subject == r.membership.UserID {
return r.membership, nil
}
return TenantMembershipRecord{}, pgx.ErrNoRows
}
func (r *MemoryRepository) IsAdminSubject(_ context.Context, subject string) (bool, error) {
return subject == "demo-owner" || subject == r.membership.UserID, nil
}
func (r *MemoryRepository) GetTenantOwnerEmail(_ context.Context, tenantID string) (string, error) {
if tenantID != r.tenant.ID {
return "", pgx.ErrNoRows
}
return "[email protected]", nil
}
func (r *MemoryRepository) UpdateBrandProfile(_ context.Context, tenantID string, params UpdateBrandProfileParams) (BrandProfileRecord, error) {
if tenantID != r.tenant.ID {
return BrandProfileRecord{}, pgx.ErrNoRows
}
if params.Name != nil {
r.brand.Name = *params.Name
}
if params.SiteURL != nil {
r.brand.SiteURL = *params.SiteURL
}
if params.LogoURL != nil {
r.brand.LogoURL = *params.LogoURL
}
if params.PrimaryColor != nil {
r.brand.PrimaryColor = *params.PrimaryColor
}
return r.brand, nil
}
func (r *MemoryRepository) GetBrandProfile(_ context.Context, tenantID string) (BrandProfileRecord, error) {
if tenantID != r.tenant.ID {
return BrandProfileRecord{}, pgx.ErrNoRows
@@ -909,9 +978,10 @@ func (r *MemoryRepository) ListBookingsByTenantBetween(_ context.Context, tenant
func (r *MemoryRepository) CreateBooking(_ context.Context, params CreateBookingParams) (CreatedBooking, error) {
created := CreatedBooking{
ID: uuid.NewString(),
Reference: params.Reference,
Status: params.Status,
ID: uuid.NewString(),
Reference: params.Reference,
Status: params.Status,
ManageToken: params.ManageToken,
}
r.bookings = append(r.bookings, BookingRecord{
ID: created.ID,
@@ -922,10 +992,13 @@ func (r *MemoryRepository) CreateBooking(_ context.Context, params CreateBooking
LocationID: params.LocationID,
CustomerName: params.CustomerName,
CustomerEmail: params.CustomerEmail,
CustomerPhone: params.CustomerPhone,
StartsAt: params.StartsAt,
EndsAt: params.EndsAt,
Status: params.Status,
Reference: params.Reference,
ManageToken: params.ManageToken,
Notes: params.Notes,
})
return created, nil
}
@@ -1033,38 +1106,38 @@ func (r *MemoryRepository) UpsertSubscriptionSnapshot(_ context.Context, params
return nil
}
func (r *MemoryRepository) UpdateTenantBillingCustomerID(_ context.Context, tenantID string, customerID string) error {
func (r *MemoryRepository) UpdateTenantBillingCustomerID(_ context.Context, tenantID string, provider string, customerID string) error {
if tenantID != r.tenant.ID {
return pgx.ErrNoRows
}
r.tenant.BillingProvider = "paddle"
r.tenant.BillingProvider = provider
r.tenant.BillingCustomerID = &customerID
r.membership.Tenant.BillingProvider = "paddle"
r.membership.Tenant.BillingProvider = provider
r.membership.Tenant.BillingCustomerID = &customerID
r.billingSnapshot.BillingProvider = "paddle"
r.billingSnapshot.BillingProvider = provider
r.billingSnapshot.BillingCustomerID = customerID
return nil
}
func (r *MemoryRepository) UpdateTenantBillingState(_ context.Context, tenantID string, planCode string, subscriptionStatus string, subscriptionID string) error {
func (r *MemoryRepository) UpdateTenantBillingState(_ context.Context, tenantID string, provider string, planCode string, subscriptionStatus string, subscriptionID string) error {
if tenantID != r.tenant.ID {
return pgx.ErrNoRows
}
r.tenant.PlanCode = planCode
r.tenant.SubscriptionStatus = subscriptionStatus
r.tenant.BillingProvider = "paddle"
r.tenant.BillingProvider = provider
r.membership.Tenant.PlanCode = planCode
r.membership.Tenant.SubscriptionStatus = subscriptionStatus
r.membership.Tenant.BillingProvider = "paddle"
r.membership.Tenant.BillingProvider = provider
r.billingSnapshot.PlanCode = planCode
r.billingSnapshot.Status = subscriptionStatus
r.billingSnapshot.BillingProvider = "paddle"
r.billingSnapshot.BillingProvider = provider
r.billingSnapshot.BillingSubscriptionID = subscriptionID
return nil
}
func (r *MemoryRepository) RecordBillingEvent(_ context.Context, _ string, provider string, eventID string, _ string, payload []byte) (bool, error) {
key := firstNonEmpty(provider, "paddle") + ":" + eventID
key := firstNonEmpty(provider, "stripe") + ":" + eventID
if _, exists := r.recordedEvents[key]; exists {
return false, nil
}
@@ -1103,6 +1176,8 @@ func (r *MemoryRepository) CreateLocation(_ context.Context, params CreateLocati
ID: uuid.NewString(),
TenantID: params.TenantID,
Name: params.Name,
Type: params.Type,
Capacity: params.Capacity,
Timezone: params.Timezone,
CreatedAt: time.Now(),
}
@@ -1116,6 +1191,12 @@ func (r *MemoryRepository) UpdateLocation(_ context.Context, locationID string,
if params.Name != nil {
r.locations[i].Name = *params.Name
}
if params.Type != nil {
r.locations[i].Type = *params.Type
}
if params.Capacity != nil {
r.locations[i].Capacity = *params.Capacity
}
if params.Timezone != nil {
r.locations[i].Timezone = *params.Timezone
}
@@ -1352,6 +1433,51 @@ func (r *MemoryRepository) RescheduleBooking(_ context.Context, bookingID string
return pgx.ErrNoRows
}
// ============================================
// EMAIL TEMPLATE METHODS - MEMORY REPOSITORY
// ============================================
func (r *MemoryRepository) ListEmailTemplates(_ context.Context, tenantID string) ([]EmailTemplateRecord, error) {
if tenantID != r.tenant.ID {
return nil, pgx.ErrNoRows
}
return r.emailTemplates, nil
}
func (r *MemoryRepository) GetEmailTemplate(_ context.Context, tenantID string, templateType string) (EmailTemplateRecord, error) {
for _, t := range r.emailTemplates {
if t.TenantID == tenantID && t.Type == templateType {
return t, nil
}
}
return EmailTemplateRecord{}, pgx.ErrNoRows
}
func (r *MemoryRepository) UpsertEmailTemplate(_ context.Context, params EmailTemplateRecord) (EmailTemplateRecord, error) {
for i, t := range r.emailTemplates {
if t.TenantID == params.TenantID && t.Type == params.Type {
r.emailTemplates[i].Subject = params.Subject
r.emailTemplates[i].BodyHTML = params.BodyHTML
r.emailTemplates[i].BodyText = params.BodyText
r.emailTemplates[i].IsEnabled = params.IsEnabled
r.emailTemplates[i].UpdatedAt = time.Now()
return r.emailTemplates[i], nil
}
}
rec := EmailTemplateRecord{
ID: uuid.NewString(),
TenantID: params.TenantID,
Type: params.Type,
Subject: params.Subject,
BodyHTML: params.BodyHTML,
BodyText: params.BodyText,
IsEnabled: params.IsEnabled,
UpdatedAt: time.Now(),
}
r.emailTemplates = append(r.emailTemplates, rec)
return rec, nil
}
// ============================================
// WORKING HOURS METHODS - MEMORY REPOSITORY
// ============================================