mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-06-03 20:13:00 +00:00
1341 lines
40 KiB
Go
1341 lines
40 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
type Repository interface {
|
|
GetTenantBySlug(ctx context.Context, slug string) (TenantRecord, error)
|
|
GetTenantByID(ctx context.Context, tenantID string) (TenantRecord, error)
|
|
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)
|
|
GetBrandProfile(ctx context.Context, tenantID string) (BrandProfileRecord, error)
|
|
ListServicesByTenant(ctx context.Context, tenantID string) ([]ServiceRecord, error)
|
|
ListAvailabilityRulesByTenant(ctx context.Context, tenantID string) ([]AvailabilityRuleRecord, error)
|
|
ListClassSessionsByTenant(ctx context.Context, tenantID string, from time.Time, limit int) ([]ClassSessionRecord, error)
|
|
ListBookingsByTenantBetween(ctx context.Context, tenantID string, from time.Time, to time.Time) ([]BookingRecord, error)
|
|
CreateBooking(ctx context.Context, params CreateBookingParams) (CreatedBooking, error)
|
|
AppendWaitlistEntry(ctx context.Context, params WaitlistEntryParams) error
|
|
CreateReminderJob(ctx context.Context, params ReminderJobParams) error
|
|
ListDueReminderJobs(ctx context.Context, dueBefore time.Time, limit int) ([]ReminderJobRecord, error)
|
|
MarkReminderJobDispatched(ctx context.Context, reminderJobID string, status string, dispatchedAt time.Time) error
|
|
CreateNotificationDeliveryLog(ctx context.Context, params NotificationDeliveryLogParams) error
|
|
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
|
|
RecordBillingEvent(ctx context.Context, tenantID string, provider string, eventID string, eventType string, payload []byte) (bool, error)
|
|
|
|
// Location / Zone Management
|
|
ListLocationsByTenant(ctx context.Context, tenantID string) ([]LocationRecord, error)
|
|
GetLocationByID(ctx context.Context, locationID string) (LocationRecord, error)
|
|
CreateLocation(ctx context.Context, params CreateLocationParams) (LocationRecord, error)
|
|
UpdateLocation(ctx context.Context, locationID string, params UpdateLocationParams) (LocationRecord, error)
|
|
DeleteLocation(ctx context.Context, locationID string) error
|
|
|
|
// Blocked Days / Availability Exceptions
|
|
ListBlockedDaysByTenant(ctx context.Context, tenantID string, from time.Time, to time.Time) ([]BlockedDayRecord, error)
|
|
CreateBlockedDay(ctx context.Context, params CreateBlockedDayParams) (BlockedDayRecord, error)
|
|
UpdateBlockedDay(ctx context.Context, blockedDayID string, params UpdateBlockedDayParams) (BlockedDayRecord, error)
|
|
DeleteBlockedDay(ctx context.Context, blockedDayID string) error
|
|
|
|
// Customer Management
|
|
ListCustomersByTenant(ctx context.Context, tenantID string, limit int, offset int) ([]CustomerRecord, error)
|
|
GetCustomerByID(ctx context.Context, customerID string) (CustomerRecord, error)
|
|
GetCustomerByEmail(ctx context.Context, tenantID string, email string) (CustomerRecord, error)
|
|
CreateCustomer(ctx context.Context, params CreateCustomerParams) (CustomerRecord, error)
|
|
UpdateCustomer(ctx context.Context, customerID string, params UpdateCustomerParams) (CustomerRecord, error)
|
|
DeleteCustomer(ctx context.Context, customerID string) error
|
|
GetCustomerBookingsCount(ctx context.Context, customerID string) (int, error)
|
|
GetCustomerLastBooking(ctx context.Context, customerID string) (*time.Time, error)
|
|
|
|
// Customer Booking Management
|
|
GetBookingByReference(ctx context.Context, reference string) (BookingRecord, error)
|
|
UpdateBookingStatus(ctx context.Context, bookingID string, status string) error
|
|
RescheduleBooking(ctx context.Context, bookingID string, startsAt time.Time, endsAt time.Time) error
|
|
|
|
// Working Hours
|
|
ListWorkingHoursByTenant(ctx context.Context, tenantID string) ([]WorkingHoursRecord, error)
|
|
UpdateWorkingHours(ctx context.Context, tenantID string, dayOfWeek int, params UpdateWorkingHoursParams) error
|
|
}
|
|
|
|
type TenantRecord struct {
|
|
ID string
|
|
Slug string
|
|
Name string
|
|
Preset string
|
|
Locale string
|
|
Timezone string
|
|
PlanCode string
|
|
SubscriptionStatus string
|
|
BillingProvider string
|
|
BillingCustomerID *string
|
|
BillingSubscription *string
|
|
}
|
|
|
|
type TenantMembershipRecord struct {
|
|
Tenant TenantRecord
|
|
UserID string
|
|
Role string
|
|
}
|
|
|
|
type CreateTenantForUserParams struct {
|
|
Subject string
|
|
Name string
|
|
Slug string
|
|
Preset string
|
|
Locale string
|
|
Timezone string
|
|
BrandName string
|
|
SiteURL string
|
|
LogoURL string
|
|
PrimaryColor string
|
|
LocationName string
|
|
ServiceName string
|
|
DurationMinutes int
|
|
BufferBeforeMinutes int
|
|
BufferAfterMinutes int
|
|
CancelWindowHours int
|
|
AvailabilityBlocks []AvailabilityBlockRecord
|
|
TeamInvites []TeamInviteRecord
|
|
}
|
|
|
|
type BrandProfileRecord struct {
|
|
TenantID string
|
|
Name string
|
|
SiteURL string
|
|
LogoURL string
|
|
PrimaryColor string
|
|
UmamiSiteID string
|
|
}
|
|
|
|
type AvailabilityBlockRecord struct {
|
|
DayOfWeek int
|
|
StartsLocal string
|
|
EndsLocal string
|
|
Busy bool
|
|
}
|
|
|
|
type TeamInviteRecord struct {
|
|
Email string
|
|
Role string
|
|
}
|
|
|
|
type ServiceRecord struct {
|
|
ID string
|
|
TenantID string
|
|
Name string
|
|
DurationMinutes int
|
|
BufferBeforeMinutes int
|
|
BufferAfterMinutes int
|
|
PriceCents int
|
|
}
|
|
|
|
type AvailabilityRuleRecord struct {
|
|
ID string
|
|
TenantID string
|
|
StaffID *string
|
|
DayOfWeek int
|
|
StartsLocal string
|
|
EndsLocal string
|
|
}
|
|
|
|
type ClassSessionRecord struct {
|
|
ID string
|
|
TenantID string
|
|
TemplateID string
|
|
LocationID *string
|
|
Title string
|
|
StartsAt time.Time
|
|
EndsAt time.Time
|
|
Capacity int32
|
|
}
|
|
|
|
type BookingRecord struct {
|
|
ID string
|
|
TenantID string
|
|
ServiceID *string
|
|
ClassSessionID *string
|
|
StaffID *string
|
|
LocationID *string
|
|
CustomerName string
|
|
CustomerEmail string
|
|
StartsAt time.Time
|
|
EndsAt time.Time
|
|
Status string
|
|
Reference string
|
|
}
|
|
|
|
type CreateBookingParams struct {
|
|
TenantID string
|
|
ServiceID *string
|
|
ClassSessionID *string
|
|
StaffID *string
|
|
LocationID *string
|
|
BookingMode string
|
|
CustomerName string
|
|
CustomerEmail string
|
|
StartsAt time.Time
|
|
EndsAt time.Time
|
|
Status string
|
|
Reference string
|
|
Notes string
|
|
}
|
|
|
|
type CreatedBooking struct {
|
|
ID string
|
|
Reference string
|
|
Status string
|
|
}
|
|
|
|
type WaitlistEntryParams struct {
|
|
TenantID string
|
|
ClassSessionID string
|
|
CustomerName string
|
|
CustomerEmail string
|
|
Position int
|
|
}
|
|
|
|
type DashboardMetrics struct {
|
|
BookingsCount int
|
|
CancellationsCount int
|
|
UtilizationPercent int
|
|
}
|
|
|
|
type ReminderJobParams struct {
|
|
TenantID string
|
|
BookingID string
|
|
Channel string
|
|
ScheduledFor time.Time
|
|
}
|
|
|
|
type ReminderJobRecord struct {
|
|
ID string
|
|
TenantID string
|
|
TenantName string
|
|
Locale string
|
|
Timezone string
|
|
BookingID string
|
|
Channel string
|
|
ScheduledFor time.Time
|
|
CustomerName string
|
|
CustomerEmail string
|
|
Reference string
|
|
StartsAt time.Time
|
|
Status string
|
|
}
|
|
|
|
type NotificationDeliveryLogParams struct {
|
|
TenantID string
|
|
ReminderJobID string
|
|
Channel string
|
|
Provider string
|
|
Recipient string
|
|
Status string
|
|
ExternalID string
|
|
ErrorMessage string
|
|
}
|
|
|
|
type BillingSnapshotRecord struct {
|
|
TenantID string
|
|
BillingProvider string
|
|
BillingCustomerID string
|
|
BillingSubscriptionID string
|
|
Status string
|
|
PlanCode string
|
|
Currency string
|
|
PriceID string
|
|
CancelAtPeriodEnd bool
|
|
CurrentPeriodStart *time.Time
|
|
CurrentPeriodEnd *time.Time
|
|
PaymentMethodBrand string
|
|
PaymentMethodLast4 string
|
|
LastSyncedAt *time.Time
|
|
}
|
|
|
|
// Location / Zone Records
|
|
type LocationRecord struct {
|
|
ID string
|
|
TenantID string
|
|
Name string
|
|
Timezone string
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
type CreateLocationParams struct {
|
|
TenantID string
|
|
Name string
|
|
Timezone string
|
|
}
|
|
|
|
type UpdateLocationParams struct {
|
|
Name *string
|
|
Timezone *string
|
|
}
|
|
|
|
// Blocked Day Records
|
|
type BlockedDayRecord struct {
|
|
ID string
|
|
TenantID string
|
|
StaffID *string
|
|
StartsAt time.Time
|
|
EndsAt time.Time
|
|
Kind string
|
|
Reason string
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
type CreateBlockedDayParams struct {
|
|
TenantID string
|
|
StaffID *string
|
|
StartsAt time.Time
|
|
EndsAt time.Time
|
|
Kind string
|
|
Reason string
|
|
}
|
|
|
|
type UpdateBlockedDayParams struct {
|
|
StartsAt *time.Time
|
|
EndsAt *time.Time
|
|
Kind *string
|
|
Reason *string
|
|
}
|
|
|
|
// Customer Records
|
|
type CustomerRecord struct {
|
|
ID string
|
|
TenantID string
|
|
Name string
|
|
Email string
|
|
Phone *string
|
|
Status string
|
|
CreatedAt time.Time
|
|
Notes *string
|
|
}
|
|
|
|
type CreateCustomerParams struct {
|
|
TenantID string
|
|
Name string
|
|
Email string
|
|
Phone *string
|
|
Status string
|
|
Notes *string
|
|
}
|
|
|
|
type UpdateCustomerParams struct {
|
|
Name *string
|
|
Email *string
|
|
Phone *string
|
|
Status *string
|
|
Notes *string
|
|
}
|
|
|
|
// Working Hours Records
|
|
type WorkingHoursRecord struct {
|
|
TenantID string
|
|
StaffID *string
|
|
DayOfWeek int
|
|
StartsLocal string
|
|
EndsLocal string
|
|
}
|
|
|
|
type UpdateWorkingHoursParams struct {
|
|
StartsLocal *string
|
|
EndsLocal *string
|
|
IsOpen *bool
|
|
}
|
|
|
|
type PGRepository struct {
|
|
pool *pgxpool.Pool
|
|
}
|
|
|
|
func NewRepository(pools *Pools, demoMode bool) Repository {
|
|
if demoMode {
|
|
return NewMemoryRepository()
|
|
}
|
|
if pools != nil && pools.App != nil {
|
|
return &PGRepository{pool: pools.App}
|
|
}
|
|
return NewMemoryRepository()
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
// LOCATION / ZONE METHODS - PG REPOSITORY (STUBS)
|
|
// ============================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
// BLOCKED DAYS METHODS - PG REPOSITORY (STUBS)
|
|
// ============================================
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
// CUSTOMER METHODS - PG REPOSITORY (STUBS)
|
|
// ============================================
|
|
|
|
func (r *PGRepository) ListCustomersByTenant(ctx context.Context, tenantID string, limit int, offset int) ([]CustomerRecord, error) {
|
|
rows, err := r.pool.Query(ctx, `
|
|
SELECT id, tenant_id, name, email, phone, status, created_at, notes
|
|
FROM customers
|
|
WHERE tenant_id = $1
|
|
ORDER BY name
|
|
LIMIT $2 OFFSET $3
|
|
`, tenantID, limit, offset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var records []CustomerRecord
|
|
for rows.Next() {
|
|
var rec CustomerRecord
|
|
if err := rows.Scan(&rec.ID, &rec.TenantID, &rec.Name, &rec.Email, &rec.Phone, &rec.Status, &rec.CreatedAt, &rec.Notes); err != nil {
|
|
return nil, err
|
|
}
|
|
records = append(records, rec)
|
|
}
|
|
return records, rows.Err()
|
|
}
|
|
|
|
func (r *PGRepository) GetCustomerByID(ctx context.Context, customerID string) (CustomerRecord, error) {
|
|
var rec CustomerRecord
|
|
err := r.pool.QueryRow(ctx, `
|
|
SELECT id, tenant_id, name, email, phone, status, created_at, notes
|
|
FROM customers
|
|
WHERE id = $1
|
|
`, customerID).Scan(&rec.ID, &rec.TenantID, &rec.Name, &rec.Email, &rec.Phone, &rec.Status, &rec.CreatedAt, &rec.Notes)
|
|
return rec, err
|
|
}
|
|
|
|
func (r *PGRepository) GetCustomerByEmail(ctx context.Context, tenantID string, email string) (CustomerRecord, error) {
|
|
var rec CustomerRecord
|
|
err := r.pool.QueryRow(ctx, `
|
|
SELECT id, tenant_id, name, email, phone, status, created_at, notes
|
|
FROM customers
|
|
WHERE tenant_id = $1 AND email = $2
|
|
`, tenantID, email).Scan(&rec.ID, &rec.TenantID, &rec.Name, &rec.Email, &rec.Phone, &rec.Status, &rec.CreatedAt, &rec.Notes)
|
|
return rec, err
|
|
}
|
|
|
|
func (r *PGRepository) CreateCustomer(ctx context.Context, params CreateCustomerParams) (CustomerRecord, error) {
|
|
var rec CustomerRecord
|
|
err := r.pool.QueryRow(ctx, `
|
|
INSERT INTO customers (tenant_id, name, email, phone, status, notes)
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
RETURNING id, tenant_id, name, email, phone, status, created_at, notes
|
|
`, params.TenantID, params.Name, params.Email, params.Phone, params.Status, params.Notes).Scan(&rec.ID, &rec.TenantID, &rec.Name, &rec.Email, &rec.Phone, &rec.Status, &rec.CreatedAt, &rec.Notes)
|
|
return rec, err
|
|
}
|
|
|
|
func (r *PGRepository) UpdateCustomer(ctx context.Context, customerID string, params UpdateCustomerParams) (CustomerRecord, error) {
|
|
var rec CustomerRecord
|
|
err := r.pool.QueryRow(ctx, `
|
|
UPDATE customers
|
|
SET name = COALESCE($2, name),
|
|
email = COALESCE($3, email),
|
|
phone = COALESCE($4, phone),
|
|
status = COALESCE($5, status),
|
|
notes = COALESCE($6, notes)
|
|
WHERE id = $1
|
|
RETURNING id, tenant_id, name, email, phone, status, created_at, notes
|
|
`, customerID, params.Name, params.Email, params.Phone, params.Status, params.Notes).Scan(&rec.ID, &rec.TenantID, &rec.Name, &rec.Email, &rec.Phone, &rec.Status, &rec.CreatedAt, &rec.Notes)
|
|
return rec, err
|
|
}
|
|
|
|
func (r *PGRepository) DeleteCustomer(ctx context.Context, customerID string) error {
|
|
_, err := r.pool.Exec(ctx, `DELETE FROM customers WHERE id = $1`, customerID)
|
|
return err
|
|
}
|
|
|
|
func (r *PGRepository) GetCustomerBookingsCount(ctx context.Context, customerID string) (int, error) {
|
|
var count int
|
|
err := r.pool.QueryRow(ctx, `
|
|
SELECT COUNT(*) FROM bookings
|
|
WHERE customer_email = (SELECT email FROM customers WHERE id = $1)
|
|
`, customerID).Scan(&count)
|
|
return count, err
|
|
}
|
|
|
|
func (r *PGRepository) GetCustomerLastBooking(ctx context.Context, customerID string) (*time.Time, error) {
|
|
var lastTime *time.Time
|
|
err := r.pool.QueryRow(ctx, `
|
|
SELECT MAX(starts_at) FROM bookings
|
|
WHERE customer_email = (SELECT email FROM customers WHERE id = $1)
|
|
`, customerID).Scan(&lastTime)
|
|
return lastTime, err
|
|
}
|
|
|
|
// ============================================
|
|
// BOOKING MANAGEMENT METHODS - PG REPOSITORY (STUBS)
|
|
// ============================================
|
|
|
|
func (r *PGRepository) GetBookingByReference(ctx context.Context, reference string) (BookingRecord, error) {
|
|
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, starts_at, ends_at, status, reference
|
|
FROM bookings
|
|
WHERE reference = $1
|
|
`, reference).Scan(&rec.ID, &rec.TenantID, &rec.ServiceID, &rec.ClassSessionID, &rec.StaffID, &rec.LocationID,
|
|
&rec.CustomerName, &rec.CustomerEmail, &rec.StartsAt, &rec.EndsAt, &rec.Status, &rec.Reference)
|
|
return rec, err
|
|
}
|
|
|
|
func (r *PGRepository) UpdateBookingStatus(ctx context.Context, bookingID string, status string) error {
|
|
_, err := r.pool.Exec(ctx, `
|
|
UPDATE bookings SET status = $2, updated_at = now() WHERE id = $1
|
|
`, bookingID, status)
|
|
return err
|
|
}
|
|
|
|
func (r *PGRepository) RescheduleBooking(ctx context.Context, bookingID string, startsAt time.Time, endsAt time.Time) error {
|
|
_, err := r.pool.Exec(ctx, `
|
|
UPDATE bookings SET starts_at = $2, ends_at = $3, updated_at = now() WHERE id = $1
|
|
`, bookingID, startsAt, endsAt)
|
|
return err
|
|
}
|
|
|
|
// ============================================
|
|
// WORKING HOURS METHODS - PG REPOSITORY (STUBS)
|
|
// ============================================
|
|
|
|
|
|
|
|
type MemoryRepository struct {
|
|
tenant TenantRecord
|
|
membership TenantMembershipRecord
|
|
brand BrandProfileRecord
|
|
services []ServiceRecord
|
|
rules []AvailabilityRuleRecord
|
|
classSessions []ClassSessionRecord
|
|
bookings []BookingRecord
|
|
waitlistSize int
|
|
reminderJobs []ReminderJobRecord
|
|
deliveryLogs []NotificationDeliveryLogParams
|
|
billingSnapshot BillingSnapshotRecord
|
|
recordedEvents map[string]struct{}
|
|
locations []LocationRecord
|
|
blockedDays []BlockedDayRecord
|
|
customers []CustomerRecord
|
|
workingHours []WorkingHoursRecord
|
|
}
|
|
|
|
func NewMemoryRepository() *MemoryRepository {
|
|
tenantID := "5d6b3551-0a3e-4b86-bdf0-e9df20a47148"
|
|
locationID := "659f1cc0-a850-46d6-b3b8-cb15d55d8daf"
|
|
staffID := "6936c444-c7d0-4a7d-b596-a9b72d2f4fc0"
|
|
serviceID := "d5d76a61-3d49-467c-8dd4-bf61ee754e39"
|
|
templateID := "d13fe5fd-727f-4d69-bfd8-47f1b92a2cf7"
|
|
sessionID := "4bf74c12-44dd-45ca-86bb-b104f16f2435"
|
|
now := time.Now().UTC()
|
|
|
|
return &MemoryRepository{
|
|
tenant: TenantRecord{
|
|
ID: tenantID,
|
|
Slug: "studio-atelier",
|
|
Name: "Studio Atelier",
|
|
Preset: "studio",
|
|
Locale: "cs",
|
|
Timezone: "Europe/Prague",
|
|
PlanCode: "pro",
|
|
SubscriptionStatus: "active",
|
|
BillingProvider: "paddle",
|
|
BillingCustomerID: stringPtr("ctm_demo_bookra"),
|
|
},
|
|
membership: TenantMembershipRecord{
|
|
Tenant: TenantRecord{
|
|
ID: tenantID,
|
|
Slug: "studio-atelier",
|
|
Name: "Studio Atelier",
|
|
Preset: "studio",
|
|
Locale: "cs",
|
|
Timezone: "Europe/Prague",
|
|
PlanCode: "pro",
|
|
SubscriptionStatus: "active",
|
|
BillingProvider: "paddle",
|
|
BillingCustomerID: stringPtr("ctm_demo_bookra"),
|
|
},
|
|
UserID: "demo-owner",
|
|
Role: "owner",
|
|
},
|
|
brand: BrandProfileRecord{
|
|
TenantID: tenantID,
|
|
Name: "Studio Atelier",
|
|
SiteURL: "https://studio-atelier.example",
|
|
PrimaryColor: "#a65c3e",
|
|
},
|
|
services: []ServiceRecord{
|
|
{
|
|
ID: serviceID,
|
|
TenantID: tenantID,
|
|
Name: "Signature treatment",
|
|
DurationMinutes: 60,
|
|
BufferBeforeMinutes: 0,
|
|
BufferAfterMinutes: 15,
|
|
PriceCents: 120000,
|
|
},
|
|
},
|
|
rules: []AvailabilityRuleRecord{
|
|
{ID: uuid.NewString(), TenantID: tenantID, StaffID: &staffID, DayOfWeek: 1, StartsLocal: "09:00:00", EndsLocal: "17:00:00"},
|
|
{ID: uuid.NewString(), TenantID: tenantID, StaffID: &staffID, DayOfWeek: 2, StartsLocal: "09:00:00", EndsLocal: "17:00:00"},
|
|
{ID: uuid.NewString(), TenantID: tenantID, StaffID: &staffID, DayOfWeek: 3, StartsLocal: "09:00:00", EndsLocal: "17:00:00"},
|
|
{ID: uuid.NewString(), TenantID: tenantID, StaffID: &staffID, DayOfWeek: 4, StartsLocal: "09:00:00", EndsLocal: "17:00:00"},
|
|
{ID: uuid.NewString(), TenantID: tenantID, StaffID: &staffID, DayOfWeek: 5, StartsLocal: "09:00:00", EndsLocal: "17:00:00"},
|
|
},
|
|
classSessions: []ClassSessionRecord{
|
|
{
|
|
ID: sessionID,
|
|
TenantID: tenantID,
|
|
TemplateID: templateID,
|
|
LocationID: &locationID,
|
|
Title: "Small group mobility class",
|
|
StartsAt: now.Add(48 * time.Hour),
|
|
EndsAt: now.Add(49 * time.Hour),
|
|
Capacity: 4,
|
|
},
|
|
},
|
|
bookings: []BookingRecord{},
|
|
reminderJobs: []ReminderJobRecord{},
|
|
deliveryLogs: []NotificationDeliveryLogParams{},
|
|
billingSnapshot: BillingSnapshotRecord{
|
|
TenantID: tenantID,
|
|
BillingProvider: "paddle",
|
|
BillingCustomerID: "ctm_demo_bookra",
|
|
BillingSubscriptionID: "",
|
|
Status: "active",
|
|
PlanCode: "pro",
|
|
Currency: "czk",
|
|
PriceID: "",
|
|
},
|
|
recordedEvents: map[string]struct{}{},
|
|
locations: []LocationRecord{
|
|
{ID: locationID, TenantID: tenantID, Name: "Main Studio", Timezone: "Europe/Prague", CreatedAt: now},
|
|
},
|
|
blockedDays: []BlockedDayRecord{},
|
|
customers: []CustomerRecord{
|
|
{ID: uuid.NewString(), TenantID: tenantID, Name: "Alice Johnson", Email: "alice@example.com", Phone: stringPtr("+420123456789"), Status: "vip", CreatedAt: now},
|
|
{ID: uuid.NewString(), TenantID: tenantID, Name: "Bob Smith", Email: "bob@example.com", Status: "active", CreatedAt: now},
|
|
{ID: uuid.NewString(), TenantID: tenantID, Name: "Carol White", Email: "carol@example.com", Status: "active", CreatedAt: now},
|
|
},
|
|
workingHours: []WorkingHoursRecord{
|
|
{TenantID: tenantID, DayOfWeek: 1, StartsLocal: "09:00", EndsLocal: "17:00"},
|
|
{TenantID: tenantID, DayOfWeek: 2, StartsLocal: "09:00", EndsLocal: "17:00"},
|
|
{TenantID: tenantID, DayOfWeek: 3, StartsLocal: "09:00", EndsLocal: "17:00"},
|
|
{TenantID: tenantID, DayOfWeek: 4, StartsLocal: "09:00", EndsLocal: "17:00"},
|
|
{TenantID: tenantID, DayOfWeek: 5, StartsLocal: "09:00", EndsLocal: "17:00"},
|
|
{TenantID: tenantID, DayOfWeek: 6, StartsLocal: "10:00", EndsLocal: "14:00"},
|
|
{TenantID: tenantID, DayOfWeek: 0, StartsLocal: "", EndsLocal: ""},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (r *MemoryRepository) GetTenantBySlug(_ context.Context, slug string) (TenantRecord, error) {
|
|
if slug != r.tenant.Slug {
|
|
return TenantRecord{}, pgx.ErrNoRows
|
|
}
|
|
return r.tenant, nil
|
|
}
|
|
|
|
func (r *MemoryRepository) GetTenantByID(_ context.Context, tenantID string) (TenantRecord, error) {
|
|
if tenantID == r.tenant.ID {
|
|
return r.tenant, nil
|
|
}
|
|
return TenantRecord{}, pgx.ErrNoRows
|
|
}
|
|
|
|
func (r *MemoryRepository) GetTenantByBillingCustomerID(_ context.Context, customerID string) (TenantRecord, error) {
|
|
if r.billingSnapshot.BillingCustomerID == customerID {
|
|
return r.tenant, nil
|
|
}
|
|
return TenantRecord{}, pgx.ErrNoRows
|
|
}
|
|
|
|
func (r *MemoryRepository) EnsureUserIdentity(_ context.Context, subject string, email string, displayName string) error {
|
|
if strings.TrimSpace(subject) == "" {
|
|
return nil
|
|
}
|
|
r.membership.UserID = subject
|
|
_ = email
|
|
_ = displayName
|
|
return nil
|
|
}
|
|
|
|
func (r *MemoryRepository) CreateTenantForUser(_ context.Context, params CreateTenantForUserParams) (TenantMembershipRecord, error) {
|
|
tenantID := uuid.NewString()
|
|
r.tenant = TenantRecord{
|
|
ID: tenantID,
|
|
Slug: params.Slug,
|
|
Name: params.Name,
|
|
Preset: params.Preset,
|
|
Locale: params.Locale,
|
|
Timezone: params.Timezone,
|
|
PlanCode: "starter",
|
|
SubscriptionStatus: "trialing",
|
|
}
|
|
r.membership = TenantMembershipRecord{
|
|
Tenant: r.tenant,
|
|
UserID: params.Subject,
|
|
Role: "owner",
|
|
}
|
|
r.brand = BrandProfileRecord{
|
|
TenantID: tenantID,
|
|
Name: firstNonEmpty(params.BrandName, params.Name),
|
|
SiteURL: params.SiteURL,
|
|
LogoURL: params.LogoURL,
|
|
PrimaryColor: params.PrimaryColor,
|
|
}
|
|
serviceName := firstNonEmpty(params.ServiceName, "First appointment")
|
|
duration := params.DurationMinutes
|
|
if duration <= 0 {
|
|
duration = 60
|
|
}
|
|
r.services = []ServiceRecord{{
|
|
ID: uuid.NewString(),
|
|
TenantID: tenantID,
|
|
Name: serviceName,
|
|
DurationMinutes: duration,
|
|
BufferBeforeMinutes: maxInt(params.BufferBeforeMinutes, 0),
|
|
BufferAfterMinutes: maxInt(params.BufferAfterMinutes, 0),
|
|
PriceCents: 0,
|
|
}}
|
|
r.rules = nil
|
|
blocks := params.AvailabilityBlocks
|
|
if len(blocks) == 0 {
|
|
blocks = defaultAvailabilityBlocks()
|
|
}
|
|
for _, block := range blocks {
|
|
if block.Busy {
|
|
continue
|
|
}
|
|
r.rules = append(r.rules, AvailabilityRuleRecord{
|
|
ID: uuid.NewString(),
|
|
TenantID: tenantID,
|
|
DayOfWeek: block.DayOfWeek,
|
|
StartsLocal: block.StartsLocal,
|
|
EndsLocal: block.EndsLocal,
|
|
})
|
|
}
|
|
r.classSessions = nil
|
|
r.bookings = nil
|
|
r.reminderJobs = nil
|
|
return r.membership, nil
|
|
}
|
|
|
|
func (r *MemoryRepository) GetTenantMembershipByUserID(_ context.Context, userID string) (TenantMembershipRecord, error) {
|
|
if userID == "" || userID == "demo-owner" {
|
|
return r.membership, nil
|
|
}
|
|
if userID == r.membership.UserID {
|
|
return r.membership, nil
|
|
}
|
|
return TenantMembershipRecord{}, pgx.ErrNoRows
|
|
}
|
|
|
|
func (r *MemoryRepository) GetBrandProfile(_ context.Context, tenantID string) (BrandProfileRecord, error) {
|
|
if tenantID != r.tenant.ID {
|
|
return BrandProfileRecord{}, pgx.ErrNoRows
|
|
}
|
|
if r.brand.Name == "" {
|
|
r.brand = BrandProfileRecord{TenantID: tenantID, Name: r.tenant.Name}
|
|
}
|
|
return r.brand, nil
|
|
}
|
|
|
|
func (r *MemoryRepository) ListServicesByTenant(_ context.Context, tenantID string) ([]ServiceRecord, error) {
|
|
if tenantID != r.tenant.ID {
|
|
return nil, nil
|
|
}
|
|
return append([]ServiceRecord(nil), r.services...), nil
|
|
}
|
|
|
|
func (r *MemoryRepository) ListAvailabilityRulesByTenant(_ context.Context, tenantID string) ([]AvailabilityRuleRecord, error) {
|
|
if tenantID != r.tenant.ID {
|
|
return nil, nil
|
|
}
|
|
return append([]AvailabilityRuleRecord(nil), r.rules...), nil
|
|
}
|
|
|
|
func (r *MemoryRepository) ListClassSessionsByTenant(_ context.Context, tenantID string, from time.Time, limit int) ([]ClassSessionRecord, error) {
|
|
if tenantID != r.tenant.ID {
|
|
return nil, nil
|
|
}
|
|
var out []ClassSessionRecord
|
|
for _, session := range r.classSessions {
|
|
if session.StartsAt.Before(from) {
|
|
continue
|
|
}
|
|
out = append(out, session)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].StartsAt.Before(out[j].StartsAt) })
|
|
if len(out) > limit {
|
|
out = out[:limit]
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (r *MemoryRepository) ListBookingsByTenantBetween(_ context.Context, tenantID string, from time.Time, to time.Time) ([]BookingRecord, error) {
|
|
var out []BookingRecord
|
|
for _, booking := range r.bookings {
|
|
if booking.TenantID != tenantID {
|
|
continue
|
|
}
|
|
if booking.StartsAt.Before(to) && booking.EndsAt.After(from) {
|
|
out = append(out, booking)
|
|
}
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].StartsAt.Before(out[j].StartsAt) })
|
|
return out, nil
|
|
}
|
|
|
|
func (r *MemoryRepository) CreateBooking(_ context.Context, params CreateBookingParams) (CreatedBooking, error) {
|
|
created := CreatedBooking{
|
|
ID: uuid.NewString(),
|
|
Reference: params.Reference,
|
|
Status: params.Status,
|
|
}
|
|
r.bookings = append(r.bookings, BookingRecord{
|
|
ID: created.ID,
|
|
TenantID: params.TenantID,
|
|
ServiceID: params.ServiceID,
|
|
ClassSessionID: params.ClassSessionID,
|
|
StaffID: params.StaffID,
|
|
LocationID: params.LocationID,
|
|
CustomerName: params.CustomerName,
|
|
CustomerEmail: params.CustomerEmail,
|
|
StartsAt: params.StartsAt,
|
|
EndsAt: params.EndsAt,
|
|
Status: params.Status,
|
|
Reference: params.Reference,
|
|
})
|
|
return created, nil
|
|
}
|
|
|
|
func (r *MemoryRepository) AppendWaitlistEntry(_ context.Context, _ WaitlistEntryParams) error {
|
|
r.waitlistSize++
|
|
return nil
|
|
}
|
|
|
|
func (r *MemoryRepository) CreateReminderJob(_ context.Context, params ReminderJobParams) error {
|
|
reference := params.BookingID
|
|
startsAt := time.Now().UTC()
|
|
customerName := "Booking Customer"
|
|
customerEmail := "customer@example.com"
|
|
for _, booking := range r.bookings {
|
|
if booking.ID == params.BookingID {
|
|
reference = booking.Reference
|
|
startsAt = booking.StartsAt
|
|
customerName = booking.CustomerName
|
|
customerEmail = booking.CustomerEmail
|
|
break
|
|
}
|
|
}
|
|
r.reminderJobs = append(r.reminderJobs, ReminderJobRecord{
|
|
ID: uuid.NewString(),
|
|
TenantID: params.TenantID,
|
|
TenantName: r.tenant.Name,
|
|
Locale: r.tenant.Locale,
|
|
Timezone: r.tenant.Timezone,
|
|
BookingID: params.BookingID,
|
|
Channel: params.Channel,
|
|
ScheduledFor: params.ScheduledFor,
|
|
CustomerName: customerName,
|
|
CustomerEmail: customerEmail,
|
|
Reference: reference,
|
|
StartsAt: startsAt,
|
|
Status: "pending",
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func (r *MemoryRepository) ListDueReminderJobs(_ context.Context, dueBefore time.Time, limit int) ([]ReminderJobRecord, error) {
|
|
var out []ReminderJobRecord
|
|
for _, job := range r.reminderJobs {
|
|
if job.Status == "pending" && !job.ScheduledFor.After(dueBefore) {
|
|
out = append(out, job)
|
|
}
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].ScheduledFor.Before(out[j].ScheduledFor) })
|
|
if len(out) > limit {
|
|
out = out[:limit]
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (r *MemoryRepository) MarkReminderJobDispatched(_ context.Context, reminderJobID string, status string, _ time.Time) error {
|
|
for index := range r.reminderJobs {
|
|
if r.reminderJobs[index].ID == reminderJobID {
|
|
r.reminderJobs[index].Status = status
|
|
return nil
|
|
}
|
|
}
|
|
return pgx.ErrNoRows
|
|
}
|
|
|
|
func (r *MemoryRepository) CreateNotificationDeliveryLog(_ context.Context, params NotificationDeliveryLogParams) error {
|
|
r.deliveryLogs = append(r.deliveryLogs, params)
|
|
return nil
|
|
}
|
|
|
|
func (r *MemoryRepository) GetDashboardMetrics(_ context.Context, tenantID string, startsAt time.Time, endsAt time.Time) (DashboardMetrics, error) {
|
|
metrics := DashboardMetrics{}
|
|
for _, booking := range r.bookings {
|
|
if booking.TenantID != tenantID {
|
|
continue
|
|
}
|
|
if booking.StartsAt.Before(startsAt) || !booking.StartsAt.Before(endsAt) {
|
|
continue
|
|
}
|
|
metrics.BookingsCount++
|
|
if booking.Status == "cancelled" {
|
|
metrics.CancellationsCount++
|
|
}
|
|
if booking.Status == "confirmed" {
|
|
metrics.UtilizationPercent++
|
|
}
|
|
}
|
|
if metrics.BookingsCount == 0 {
|
|
metrics.UtilizationPercent = 0
|
|
} else {
|
|
metrics.UtilizationPercent = int(float64(metrics.UtilizationPercent) / float64(metrics.BookingsCount) * 100)
|
|
}
|
|
return metrics, nil
|
|
}
|
|
|
|
func (r *MemoryRepository) GetSubscriptionSnapshot(_ context.Context, tenantID string) (BillingSnapshotRecord, error) {
|
|
if tenantID != r.tenant.ID {
|
|
return BillingSnapshotRecord{}, pgx.ErrNoRows
|
|
}
|
|
return r.billingSnapshot, nil
|
|
}
|
|
|
|
func (r *MemoryRepository) UpsertSubscriptionSnapshot(_ context.Context, params BillingSnapshotRecord) error {
|
|
r.billingSnapshot = params
|
|
return nil
|
|
}
|
|
|
|
func (r *MemoryRepository) UpdateTenantBillingCustomerID(_ context.Context, tenantID string, customerID string) error {
|
|
if tenantID != r.tenant.ID {
|
|
return pgx.ErrNoRows
|
|
}
|
|
r.tenant.BillingProvider = "paddle"
|
|
r.tenant.BillingCustomerID = &customerID
|
|
r.membership.Tenant.BillingProvider = "paddle"
|
|
r.membership.Tenant.BillingCustomerID = &customerID
|
|
r.billingSnapshot.BillingProvider = "paddle"
|
|
r.billingSnapshot.BillingCustomerID = customerID
|
|
return nil
|
|
}
|
|
|
|
func (r *MemoryRepository) UpdateTenantBillingState(_ context.Context, tenantID 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.membership.Tenant.PlanCode = planCode
|
|
r.membership.Tenant.SubscriptionStatus = subscriptionStatus
|
|
r.membership.Tenant.BillingProvider = "paddle"
|
|
r.billingSnapshot.PlanCode = planCode
|
|
r.billingSnapshot.Status = subscriptionStatus
|
|
r.billingSnapshot.BillingProvider = "paddle"
|
|
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
|
|
if _, exists := r.recordedEvents[key]; exists {
|
|
return false, nil
|
|
}
|
|
if len(payload) > 0 {
|
|
var raw json.RawMessage
|
|
if err := json.Unmarshal(payload, &raw); err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
r.recordedEvents[key] = struct{}{}
|
|
return true, nil
|
|
}
|
|
|
|
// ============================================
|
|
// LOCATION / ZONE METHODS - MEMORY REPOSITORY
|
|
// ============================================
|
|
|
|
func (r *MemoryRepository) ListLocationsByTenant(_ context.Context, tenantID string) ([]LocationRecord, error) {
|
|
if tenantID != r.tenant.ID {
|
|
return nil, pgx.ErrNoRows
|
|
}
|
|
return r.locations, nil
|
|
}
|
|
|
|
func (r *MemoryRepository) GetLocationByID(_ context.Context, locationID string) (LocationRecord, error) {
|
|
for _, loc := range r.locations {
|
|
if loc.ID == locationID {
|
|
return loc, nil
|
|
}
|
|
}
|
|
return LocationRecord{}, pgx.ErrNoRows
|
|
}
|
|
|
|
func (r *MemoryRepository) CreateLocation(_ context.Context, params CreateLocationParams) (LocationRecord, error) {
|
|
loc := LocationRecord{
|
|
ID: uuid.NewString(),
|
|
TenantID: params.TenantID,
|
|
Name: params.Name,
|
|
Timezone: params.Timezone,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
r.locations = append(r.locations, loc)
|
|
return loc, nil
|
|
}
|
|
|
|
func (r *MemoryRepository) UpdateLocation(_ context.Context, locationID string, params UpdateLocationParams) (LocationRecord, error) {
|
|
for i, loc := range r.locations {
|
|
if loc.ID == locationID {
|
|
if params.Name != nil {
|
|
r.locations[i].Name = *params.Name
|
|
}
|
|
if params.Timezone != nil {
|
|
r.locations[i].Timezone = *params.Timezone
|
|
}
|
|
return r.locations[i], nil
|
|
}
|
|
}
|
|
return LocationRecord{}, pgx.ErrNoRows
|
|
}
|
|
|
|
func (r *MemoryRepository) DeleteLocation(_ context.Context, locationID string) error {
|
|
for i, loc := range r.locations {
|
|
if loc.ID == locationID {
|
|
r.locations = append(r.locations[:i], r.locations[i+1:]...)
|
|
return nil
|
|
}
|
|
}
|
|
return pgx.ErrNoRows
|
|
}
|
|
|
|
// ============================================
|
|
// BLOCKED DAYS METHODS - MEMORY REPOSITORY
|
|
// ============================================
|
|
|
|
func (r *MemoryRepository) ListBlockedDaysByTenant(_ context.Context, tenantID string, from time.Time, to time.Time) ([]BlockedDayRecord, error) {
|
|
if tenantID != r.tenant.ID {
|
|
return nil, pgx.ErrNoRows
|
|
}
|
|
var out []BlockedDayRecord
|
|
for _, bd := range r.blockedDays {
|
|
if bd.TenantID == tenantID && !bd.StartsAt.After(to) && !bd.EndsAt.Before(from) {
|
|
out = append(out, bd)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (r *MemoryRepository) CreateBlockedDay(_ context.Context, params CreateBlockedDayParams) (BlockedDayRecord, error) {
|
|
bd := BlockedDayRecord{
|
|
ID: uuid.NewString(),
|
|
TenantID: params.TenantID,
|
|
StaffID: params.StaffID,
|
|
StartsAt: params.StartsAt,
|
|
EndsAt: params.EndsAt,
|
|
Kind: params.Kind,
|
|
Reason: params.Reason,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
r.blockedDays = append(r.blockedDays, bd)
|
|
return bd, nil
|
|
}
|
|
|
|
func (r *MemoryRepository) UpdateBlockedDay(_ context.Context, blockedDayID string, params UpdateBlockedDayParams) (BlockedDayRecord, error) {
|
|
for i, bd := range r.blockedDays {
|
|
if bd.ID == blockedDayID {
|
|
if params.StartsAt != nil {
|
|
r.blockedDays[i].StartsAt = *params.StartsAt
|
|
}
|
|
if params.EndsAt != nil {
|
|
r.blockedDays[i].EndsAt = *params.EndsAt
|
|
}
|
|
if params.Kind != nil {
|
|
r.blockedDays[i].Kind = *params.Kind
|
|
}
|
|
if params.Reason != nil {
|
|
r.blockedDays[i].Reason = *params.Reason
|
|
}
|
|
return r.blockedDays[i], nil
|
|
}
|
|
}
|
|
return BlockedDayRecord{}, pgx.ErrNoRows
|
|
}
|
|
|
|
func (r *MemoryRepository) DeleteBlockedDay(_ context.Context, blockedDayID string) error {
|
|
for i, bd := range r.blockedDays {
|
|
if bd.ID == blockedDayID {
|
|
r.blockedDays = append(r.blockedDays[:i], r.blockedDays[i+1:]...)
|
|
return nil
|
|
}
|
|
}
|
|
return pgx.ErrNoRows
|
|
}
|
|
|
|
// ============================================
|
|
// CUSTOMER METHODS - MEMORY REPOSITORY
|
|
// ============================================
|
|
|
|
func (r *MemoryRepository) ListCustomersByTenant(_ context.Context, tenantID string, limit int, offset int) ([]CustomerRecord, error) {
|
|
if tenantID != r.tenant.ID {
|
|
return nil, pgx.ErrNoRows
|
|
}
|
|
start := offset
|
|
if start > len(r.customers) {
|
|
return []CustomerRecord{}, nil
|
|
}
|
|
end := start + limit
|
|
if end > len(r.customers) {
|
|
end = len(r.customers)
|
|
}
|
|
return r.customers[start:end], nil
|
|
}
|
|
|
|
func (r *MemoryRepository) GetCustomerByID(_ context.Context, customerID string) (CustomerRecord, error) {
|
|
for _, c := range r.customers {
|
|
if c.ID == customerID {
|
|
return c, nil
|
|
}
|
|
}
|
|
return CustomerRecord{}, pgx.ErrNoRows
|
|
}
|
|
|
|
func (r *MemoryRepository) GetCustomerByEmail(_ context.Context, tenantID string, email string) (CustomerRecord, error) {
|
|
for _, c := range r.customers {
|
|
if c.TenantID == tenantID && c.Email == email {
|
|
return c, nil
|
|
}
|
|
}
|
|
return CustomerRecord{}, pgx.ErrNoRows
|
|
}
|
|
|
|
func (r *MemoryRepository) CreateCustomer(_ context.Context, params CreateCustomerParams) (CustomerRecord, error) {
|
|
c := CustomerRecord{
|
|
ID: uuid.NewString(),
|
|
TenantID: params.TenantID,
|
|
Name: params.Name,
|
|
Email: params.Email,
|
|
Phone: params.Phone,
|
|
Status: params.Status,
|
|
CreatedAt: time.Now(),
|
|
Notes: params.Notes,
|
|
}
|
|
r.customers = append(r.customers, c)
|
|
return c, nil
|
|
}
|
|
|
|
func (r *MemoryRepository) UpdateCustomer(_ context.Context, customerID string, params UpdateCustomerParams) (CustomerRecord, error) {
|
|
for i, c := range r.customers {
|
|
if c.ID == customerID {
|
|
if params.Name != nil {
|
|
r.customers[i].Name = *params.Name
|
|
}
|
|
if params.Email != nil {
|
|
r.customers[i].Email = *params.Email
|
|
}
|
|
if params.Phone != nil {
|
|
r.customers[i].Phone = params.Phone
|
|
}
|
|
if params.Status != nil {
|
|
r.customers[i].Status = *params.Status
|
|
}
|
|
if params.Notes != nil {
|
|
r.customers[i].Notes = params.Notes
|
|
}
|
|
return r.customers[i], nil
|
|
}
|
|
}
|
|
return CustomerRecord{}, pgx.ErrNoRows
|
|
}
|
|
|
|
func (r *MemoryRepository) DeleteCustomer(_ context.Context, customerID string) error {
|
|
for i, c := range r.customers {
|
|
if c.ID == customerID {
|
|
r.customers = append(r.customers[:i], r.customers[i+1:]...)
|
|
return nil
|
|
}
|
|
}
|
|
return pgx.ErrNoRows
|
|
}
|
|
|
|
func (r *MemoryRepository) GetCustomerBookingsCount(_ context.Context, customerID string) (int, error) {
|
|
count := 0
|
|
for _, b := range r.bookings {
|
|
for _, c := range r.customers {
|
|
if c.ID == customerID && b.CustomerEmail == c.Email {
|
|
count++
|
|
}
|
|
}
|
|
}
|
|
return count, nil
|
|
}
|
|
|
|
func (r *MemoryRepository) GetCustomerLastBooking(_ context.Context, customerID string) (*time.Time, error) {
|
|
var lastTime *time.Time
|
|
var customerEmail string
|
|
for _, c := range r.customers {
|
|
if c.ID == customerID {
|
|
customerEmail = c.Email
|
|
break
|
|
}
|
|
}
|
|
if customerEmail == "" {
|
|
return nil, nil
|
|
}
|
|
for _, b := range r.bookings {
|
|
if b.CustomerEmail == customerEmail {
|
|
if lastTime == nil || b.StartsAt.After(*lastTime) {
|
|
lastTime = &b.StartsAt
|
|
}
|
|
}
|
|
}
|
|
return lastTime, nil
|
|
}
|
|
|
|
// ============================================
|
|
// BOOKING MANAGEMENT METHODS - MEMORY REPOSITORY
|
|
// ============================================
|
|
|
|
func (r *MemoryRepository) GetBookingByReference(_ context.Context, reference string) (BookingRecord, error) {
|
|
for _, b := range r.bookings {
|
|
if b.Reference == reference {
|
|
return b, nil
|
|
}
|
|
}
|
|
return BookingRecord{}, pgx.ErrNoRows
|
|
}
|
|
|
|
func (r *MemoryRepository) UpdateBookingStatus(_ context.Context, bookingID string, status string) error {
|
|
for i, b := range r.bookings {
|
|
if b.ID == bookingID {
|
|
r.bookings[i].Status = status
|
|
return nil
|
|
}
|
|
}
|
|
return pgx.ErrNoRows
|
|
}
|
|
|
|
func (r *MemoryRepository) RescheduleBooking(_ context.Context, bookingID string, startsAt time.Time, endsAt time.Time) error {
|
|
for i, b := range r.bookings {
|
|
if b.ID == bookingID {
|
|
r.bookings[i].StartsAt = startsAt
|
|
r.bookings[i].EndsAt = endsAt
|
|
return nil
|
|
}
|
|
}
|
|
return pgx.ErrNoRows
|
|
}
|
|
|
|
// ============================================
|
|
// WORKING HOURS METHODS - MEMORY REPOSITORY
|
|
// ============================================
|
|
|
|
func (r *MemoryRepository) ListWorkingHoursByTenant(_ context.Context, tenantID string) ([]WorkingHoursRecord, error) {
|
|
if tenantID != r.tenant.ID {
|
|
return nil, pgx.ErrNoRows
|
|
}
|
|
return r.workingHours, nil
|
|
}
|
|
|
|
func (r *MemoryRepository) UpdateWorkingHours(_ context.Context, tenantID string, dayOfWeek int, params UpdateWorkingHoursParams) error {
|
|
if tenantID != r.tenant.ID {
|
|
return pgx.ErrNoRows
|
|
}
|
|
for i, wh := range r.workingHours {
|
|
if wh.TenantID == tenantID && wh.DayOfWeek == dayOfWeek {
|
|
if params.StartsLocal != nil {
|
|
r.workingHours[i].StartsLocal = *params.StartsLocal
|
|
}
|
|
if params.EndsLocal != nil {
|
|
r.workingHours[i].EndsLocal = *params.EndsLocal
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
return pgx.ErrNoRows
|
|
}
|
|
|
|
func Reference(prefix string, at time.Time) string {
|
|
return fmt.Sprintf("%s-%s-%s", prefix, at.UTC().Format("20060102150405"), strings.Split(uuid.NewString(), "-")[0])
|
|
}
|
|
|
|
func stringPtr(value string) *string {
|
|
return &value
|
|
}
|
|
|
|
func maxInt(left int, right int) int {
|
|
if left > right {
|
|
return left
|
|
}
|
|
return right
|
|
}
|
|
|
|
func firstNonEmpty(values ...string) string {
|
|
for _, value := range values {
|
|
if strings.TrimSpace(value) != "" {
|
|
return strings.TrimSpace(value)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func defaultAvailabilityBlocks() []AvailabilityBlockRecord {
|
|
blocks := make([]AvailabilityBlockRecord, 0, 5)
|
|
for day := 1; day <= 5; day++ {
|
|
blocks = append(blocks, AvailabilityBlockRecord{
|
|
DayOfWeek: day,
|
|
StartsLocal: "09:00:00",
|
|
EndsLocal: "17:00:00",
|
|
})
|
|
}
|
|
return blocks
|
|
}
|