mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 07:41:16 +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
@@ -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
|
||||
// ============================================
|
||||
|
||||
Reference in new issue
Block a user