mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 15:51:15 +00:00
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:
1 parent
2039669e2c
commit
f2f7439050
93 files changed
+6987
-69060
No files matched your search
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user