Files
Bookra/apps/backend/internal/bookings/customer_service.go
T
Tomas Dvorak 48c3e15a38 cleanup
2026-05-05 09:48:07 +02:00

201 lines
5.5 KiB
Go

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)
}