package bookings import ( "context" "errors" "time" "bookra/apps/backend/internal/db" "bookra/apps/backend/internal/domain" "bookra/apps/backend/internal/notifications" ) var ( ErrInvalidReschedule = errors.New("invalid reschedule request") ErrBookingCancelled = errors.New("booking already cancelled") ErrUnauthorized = errors.New("unauthorized access") ) type CustomerService struct { repo db.Repository notifier CustomerNotifier } type CustomerNotifier interface { SendBookingReschedule(ctx context.Context, data notifications.BookingEmailData) error SendBookingCancellation(ctx context.Context, data notifications.BookingEmailData) error } func NewCustomerService(repo db.Repository, notifier CustomerNotifier) *CustomerService { if notifier == nil { notifier = &customerNoopNotifier{} } return &CustomerService{repo: repo, notifier: notifier} } type customerNoopNotifier struct{} func (n *customerNoopNotifier) SendBookingReschedule(ctx context.Context, data notifications.BookingEmailData) error { return nil } func (n *customerNoopNotifier) SendBookingCancellation(ctx context.Context, data notifications.BookingEmailData) error { return nil } // GetBookingByReference returns booking details for customer management link func (s *CustomerService) GetBookingByReference(ctx context.Context, reference string, token string) (domain.CustomerBookingView, error) { booking, err := s.repo.GetBookingByReference(ctx, reference) if err != nil { return domain.CustomerBookingView{}, ErrBookingNotFound } // Get tenant details for business name tenant, err := s.repo.GetTenantByID(ctx, booking.TenantID) if err != nil { return domain.CustomerBookingView{}, err } return domain.CustomerBookingView{ Reference: booking.Reference, CustomerName: booking.CustomerName, CustomerEmail: booking.CustomerEmail, Service: "Service", // Would get from service ID in full implementation BusinessName: tenant.Name, StartsAt: booking.StartsAt, EndsAt: booking.EndsAt, Location: "Location", // Would get from location ID in full implementation Status: booking.Status, }, nil } // RescheduleBooking allows customers to reschedule their booking func (s *CustomerService) RescheduleBooking(ctx context.Context, reference string, req domain.RescheduleBookingRequest, token string) error { booking, err := s.repo.GetBookingByReference(ctx, reference) if err != nil { return ErrBookingNotFound } if booking.Status == "cancelled" { return ErrBookingCancelled } newStartsAt, err := time.Parse(time.RFC3339, req.NewStartsAt) if err != nil { return ErrInvalidReschedule } newEndsAt, err := time.Parse(time.RFC3339, req.NewEndsAt) if err != nil { return ErrInvalidReschedule } if !newEndsAt.After(newStartsAt) { return ErrInvalidReschedule } if newStartsAt.Before(time.Now().UTC()) { return ErrInvalidReschedule } if err := s.repo.RescheduleBooking(ctx, booking.ID, newStartsAt, newEndsAt); err != nil { return err } // Send reschedule email (async) go s.sendRescheduleEmail(booking, newStartsAt, newEndsAt) return nil } // CancelBooking allows customers to cancel their booking func (s *CustomerService) CancelBooking(ctx context.Context, reference string, token string) error { booking, err := s.repo.GetBookingByReference(ctx, reference) if err != nil { return ErrBookingNotFound } if booking.Status == "cancelled" { return ErrBookingCancelled } if err := s.repo.UpdateBookingStatus(ctx, booking.ID, "cancelled"); err != nil { return err } // Send cancellation email (async) go s.sendCancellationEmail(booking) return nil } func (s *CustomerService) sendRescheduleEmail(booking db.BookingRecord, newStartsAt, newEndsAt time.Time) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() tenant, err := s.repo.GetTenantByID(ctx, booking.TenantID) if err != nil { return } brand, _ := s.repo.GetBrandProfile(ctx, tenant.ID) brandColor := brand.PrimaryColor if brandColor == "" { brandColor = "#a65c3e" } managementURL := "https://bookra.eu/manage/" + booking.Reference + "?token=" + booking.Reference emailData := notifications.BookingEmailData{ TenantName: tenant.Name, TenantSlug: tenant.Slug, BrandColor: brandColor, CustomerName: booking.CustomerName, CustomerEmail: booking.CustomerEmail, Service: "Service", Location: "Location", Reference: booking.Reference, StartsAt: newStartsAt, EndsAt: newEndsAt, Timezone: tenant.Timezone, Locale: tenant.Locale, ManagementURL: managementURL, } s.notifier.SendBookingReschedule(ctx, emailData) } func (s *CustomerService) sendCancellationEmail(booking db.BookingRecord) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() tenant, err := s.repo.GetTenantByID(ctx, booking.TenantID) if err != nil { return } brand, _ := s.repo.GetBrandProfile(ctx, tenant.ID) brandColor := brand.PrimaryColor if brandColor == "" { brandColor = "#a65c3e" } emailData := notifications.BookingEmailData{ TenantName: tenant.Name, TenantSlug: tenant.Slug, BrandColor: brandColor, CustomerName: booking.CustomerName, CustomerEmail: booking.CustomerEmail, Service: "Service", Location: "Location", Reference: booking.Reference, StartsAt: booking.StartsAt, EndsAt: booking.EndsAt, Timezone: tenant.Timezone, Locale: tenant.Locale, ManagementURL: "", } s.notifier.SendBookingCancellation(ctx, emailData) }