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

+100 -3
View File
@@ -32,11 +32,11 @@ func NewService(repo db.Repository) *Service {
}
func (s *Service) GetTenantMembership(ctx context.Context, principal domain.Principal) (db.TenantMembershipRecord, error) {
return s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
return s.repo.GetTenantMembershipBySubject(ctx, principal.Subject)
}
func (s *Service) Bootstrap(ctx context.Context, principal domain.Principal) (domain.TenantBootstrap, error) {
membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
membership, err := s.repo.GetTenantMembershipBySubject(ctx, principal.Subject)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return domain.TenantBootstrap{
@@ -76,7 +76,7 @@ func (s *Service) Bootstrap(ctx context.Context, principal domain.Principal) (do
}
func (s *Service) Onboard(ctx context.Context, principal domain.Principal, request domain.OnboardTenantRequest) (domain.TenantBootstrap, error) {
if _, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject); err == nil {
if _, err := s.repo.GetTenantMembershipBySubject(ctx, principal.Subject); err == nil {
return domain.TenantBootstrap{}, ErrTenantAlreadyProvisioned
} else if !errors.Is(err, pgx.ErrNoRows) {
return domain.TenantBootstrap{}, err
@@ -184,6 +184,103 @@ func (s *Service) Onboard(ctx context.Context, principal domain.Principal, reque
}, nil
}
// UpdateBrand persists brand profile fields for the caller's tenant.
func (s *Service) UpdateBrand(ctx context.Context, principal domain.Principal, req domain.UpdateBrandRequest) (domain.BrandProfile, error) {
membership, err := s.repo.GetTenantMembershipBySubject(ctx, principal.Subject)
if err != nil {
return domain.BrandProfile{}, err
}
params := db.UpdateBrandProfileParams{
Name: optionalTrimmed(req.Name),
SiteURL: optionalTrimmed(req.SiteURL),
LogoURL: optionalTrimmed(req.LogoURL),
PrimaryColor: optionalTrimmed(req.PrimaryColor),
}
if _, err := s.repo.UpdateBrandProfile(ctx, membership.Tenant.ID, params); err != nil {
return domain.BrandProfile{}, err
}
return s.brandProfile(ctx, membership.Tenant), nil
}
func optionalTrimmed(value string) *string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return nil
}
return &trimmed
}
var emailTemplateTypes = map[string]struct{}{
"confirmation": {},
"reminder": {},
"reschedule": {},
"cancellation": {},
}
// ListEmailTemplates returns all custom templates stored for the caller's tenant.
func (s *Service) ListEmailTemplates(ctx context.Context, principal domain.Principal) ([]domain.EmailTemplate, error) {
membership, err := s.repo.GetTenantMembershipBySubject(ctx, principal.Subject)
if err != nil {
return nil, err
}
records, err := s.repo.ListEmailTemplates(ctx, membership.Tenant.ID)
if err != nil {
return nil, err
}
out := make([]domain.EmailTemplate, len(records))
for i, rec := range records {
out[i] = emailTemplateToDomain(rec)
}
return out, nil
}
// UpdateEmailTemplate stores a subject/body override for a booking email type.
// Supported placeholders: {{customerName}}, {{service}}, {{location}},
// {{reference}}, {{startsAt}}, {{tenantName}}, {{manageUrl}}.
func (s *Service) UpdateEmailTemplate(ctx context.Context, principal domain.Principal, req domain.UpdateEmailTemplateRequest) (domain.EmailTemplate, error) {
membership, err := s.repo.GetTenantMembershipBySubject(ctx, principal.Subject)
if err != nil {
return domain.EmailTemplate{}, err
}
templateType := strings.ToLower(strings.TrimSpace(req.Type))
if _, ok := emailTemplateTypes[templateType]; !ok {
return domain.EmailTemplate{}, ErrInvalidOnboarding
}
if len(req.Subject) > 200 || len(req.Body) > 20000 {
return domain.EmailTemplate{}, ErrInvalidOnboarding
}
isEnabled := true
if req.IsEnabled != nil {
isEnabled = *req.IsEnabled
}
rec, err := s.repo.UpsertEmailTemplate(ctx, db.EmailTemplateRecord{
TenantID: membership.Tenant.ID,
Type: templateType,
Subject: strings.TrimSpace(req.Subject),
BodyText: strings.TrimSpace(req.Body),
IsEnabled: isEnabled,
})
if err != nil {
return domain.EmailTemplate{}, err
}
return emailTemplateToDomain(rec), nil
}
func emailTemplateToDomain(rec db.EmailTemplateRecord) domain.EmailTemplate {
return domain.EmailTemplate{
ID: rec.ID,
TenantID: rec.TenantID,
Type: rec.Type,
Subject: rec.Subject,
BodyHTML: rec.BodyHTML,
BodyText: rec.BodyText,
IsEnabled: rec.IsEnabled,
}
}
func (s *Service) brandProfile(ctx context.Context, tenant db.TenantRecord) domain.BrandProfile {
brand, err := s.repo.GetBrandProfile(ctx, tenant.ID)
if err != nil {