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

470 lines
12 KiB
Go

package catalog
import (
"context"
"errors"
"time"
"bookra/apps/backend/internal/db"
"bookra/apps/backend/internal/domain"
)
var (
ErrLocationNotFound = errors.New("location not found")
ErrBlockedDayNotFound = errors.New("blocked day not found")
ErrCustomerNotFound = errors.New("customer not found")
ErrBookingNotFound = errors.New("booking not found")
ErrInvalidBooking = errors.New("invalid booking request")
ErrTenantNotFound = errors.New("tenant not found")
ErrTenantMembership = errors.New("tenant membership not found")
)
type Service struct {
repo db.Repository
}
func NewService(repo db.Repository) *Service {
return &Service{repo: repo}
}
// ============================================
// LOCATION / ZONE MANAGEMENT
// ============================================
func (s *Service) ListLocations(ctx context.Context, principal domain.Principal) ([]domain.Location, error) {
membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
if err != nil {
return nil, ErrTenantMembership
}
records, err := s.repo.ListLocationsByTenant(ctx, membership.Tenant.ID)
if err != nil {
return nil, err
}
locations := make([]domain.Location, len(records))
for i, rec := range records {
locations[i] = domain.Location{
ID: rec.ID,
TenantID: rec.TenantID,
Name: rec.Name,
Type: "room", // Default type
Capacity: 10, // Default capacity
Timezone: rec.Timezone,
CreatedAt: rec.CreatedAt,
}
}
return locations, nil
}
func (s *Service) CreateLocation(ctx context.Context, principal domain.Principal, req domain.CreateLocationRequest) (domain.Location, error) {
membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
if err != nil {
return domain.Location{}, ErrTenantMembership
}
params := db.CreateLocationParams{
TenantID: membership.Tenant.ID,
Name: req.Name,
Timezone: membership.Tenant.Timezone,
}
rec, err := s.repo.CreateLocation(ctx, params)
if err != nil {
return domain.Location{}, err
}
return domain.Location{
ID: rec.ID,
TenantID: rec.TenantID,
Name: rec.Name,
Type: req.Type,
Capacity: 10,
Timezone: rec.Timezone,
CreatedAt: rec.CreatedAt,
}, nil
}
func (s *Service) UpdateLocation(ctx context.Context, principal domain.Principal, locationID string, req domain.UpdateLocationRequest) (domain.Location, error) {
membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
if err != nil {
return domain.Location{}, ErrTenantMembership
}
// Verify location belongs to tenant
loc, err := s.repo.GetLocationByID(ctx, locationID)
if err != nil {
return domain.Location{}, ErrLocationNotFound
}
if loc.TenantID != membership.Tenant.ID {
return domain.Location{}, ErrLocationNotFound
}
params := db.UpdateLocationParams{}
if req.Name != "" {
params.Name = &req.Name
}
rec, err := s.repo.UpdateLocation(ctx, locationID, params)
if err != nil {
return domain.Location{}, err
}
return domain.Location{
ID: rec.ID,
TenantID: rec.TenantID,
Name: rec.Name,
Type: req.Type,
Capacity: 10,
Timezone: rec.Timezone,
CreatedAt: rec.CreatedAt,
}, nil
}
func (s *Service) DeleteLocation(ctx context.Context, principal domain.Principal, locationID string) error {
membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
if err != nil {
return ErrTenantMembership
}
loc, err := s.repo.GetLocationByID(ctx, locationID)
if err != nil {
return ErrLocationNotFound
}
if loc.TenantID != membership.Tenant.ID {
return ErrLocationNotFound
}
return s.repo.DeleteLocation(ctx, locationID)
}
// ============================================
// BLOCKED DAYS MANAGEMENT
// ============================================
func (s *Service) ListBlockedDays(ctx context.Context, principal domain.Principal, from time.Time, to time.Time) ([]domain.BlockedDay, error) {
membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
if err != nil {
return nil, ErrTenantMembership
}
records, err := s.repo.ListBlockedDaysByTenant(ctx, membership.Tenant.ID, from, to)
if err != nil {
return nil, err
}
blockedDays := make([]domain.BlockedDay, len(records))
for i, rec := range records {
blockedDays[i] = domain.BlockedDay{
ID: rec.ID,
TenantID: rec.TenantID,
Date: rec.StartsAt,
Reason: rec.Reason,
Type: rec.Kind,
StaffID: rec.StaffID,
CreatedAt: rec.CreatedAt,
}
}
return blockedDays, nil
}
func (s *Service) CreateBlockedDay(ctx context.Context, principal domain.Principal, req domain.CreateBlockedDayRequest) (domain.BlockedDay, error) {
membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
if err != nil {
return domain.BlockedDay{}, ErrTenantMembership
}
date, err := time.Parse(time.RFC3339, req.Date)
if err != nil {
return domain.BlockedDay{}, ErrInvalidBooking
}
params := db.CreateBlockedDayParams{
TenantID: membership.Tenant.ID,
StaffID: req.StaffID,
StartsAt: date,
EndsAt: date.Add(24 * time.Hour),
Kind: req.Type,
Reason: req.Reason,
}
rec, err := s.repo.CreateBlockedDay(ctx, params)
if err != nil {
return domain.BlockedDay{}, err
}
return domain.BlockedDay{
ID: rec.ID,
TenantID: rec.TenantID,
Date: rec.StartsAt,
Reason: rec.Reason,
Type: rec.Kind,
StaffID: rec.StaffID,
CreatedAt: rec.CreatedAt,
}, nil
}
func (s *Service) UpdateBlockedDay(ctx context.Context, principal domain.Principal, blockedDayID string, req domain.UpdateBlockedDayRequest) (domain.BlockedDay, error) {
membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
if err != nil {
return domain.BlockedDay{}, ErrTenantMembership
}
// Verify blocked day belongs to tenant
bd, err := s.repo.ListBlockedDaysByTenant(ctx, membership.Tenant.ID, time.Time{}, time.Now().Add(365*24*time.Hour))
if err != nil {
return domain.BlockedDay{}, err
}
found := false
for _, b := range bd {
if b.ID == blockedDayID {
found = true
break
}
}
if !found {
return domain.BlockedDay{}, ErrBlockedDayNotFound
}
params := db.UpdateBlockedDayParams{}
if req.Reason != "" {
params.Reason = &req.Reason
}
if req.Type != "" {
params.Kind = &req.Type
}
rec, err := s.repo.UpdateBlockedDay(ctx, blockedDayID, params)
if err != nil {
return domain.BlockedDay{}, err
}
return domain.BlockedDay{
ID: rec.ID,
TenantID: rec.TenantID,
Date: rec.StartsAt,
Reason: rec.Reason,
Type: rec.Kind,
StaffID: rec.StaffID,
CreatedAt: rec.CreatedAt,
}, nil
}
func (s *Service) DeleteBlockedDay(ctx context.Context, principal domain.Principal, blockedDayID string) error {
membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
if err != nil {
return ErrTenantMembership
}
// Verify blocked day belongs to tenant
bd, err := s.repo.ListBlockedDaysByTenant(ctx, membership.Tenant.ID, time.Time{}, time.Now().Add(365*24*time.Hour))
if err != nil {
return err
}
found := false
for _, b := range bd {
if b.ID == blockedDayID {
found = true
break
}
}
if !found {
return ErrBlockedDayNotFound
}
return s.repo.DeleteBlockedDay(ctx, blockedDayID)
}
// ============================================
// CUSTOMER MANAGEMENT
// ============================================
func (s *Service) ListCustomers(ctx context.Context, principal domain.Principal, limit int, offset int) ([]domain.Customer, error) {
membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
if err != nil {
return nil, ErrTenantMembership
}
records, err := s.repo.ListCustomersByTenant(ctx, membership.Tenant.ID, limit, offset)
if err != nil {
return nil, err
}
customers := make([]domain.Customer, len(records))
for i, rec := range records {
bookingsCount, _ := s.repo.GetCustomerBookingsCount(ctx, rec.ID)
lastBooking, _ := s.repo.GetCustomerLastBooking(ctx, rec.ID)
customers[i] = domain.Customer{
ID: rec.ID,
TenantID: rec.TenantID,
Name: rec.Name,
Email: rec.Email,
Phone: rec.Phone,
Status: rec.Status,
BookingsCount: bookingsCount,
LastBookingAt: lastBooking,
CreatedAt: rec.CreatedAt,
Notes: "",
}
if rec.Notes != nil {
customers[i].Notes = *rec.Notes
}
}
return customers, nil
}
func (s *Service) CreateCustomer(ctx context.Context, principal domain.Principal, req domain.CreateCustomerRequest) (domain.Customer, error) {
membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
if err != nil {
return domain.Customer{}, ErrTenantMembership
}
status := req.Status
if status == "" {
status = "active"
}
params := db.CreateCustomerParams{
TenantID: membership.Tenant.ID,
Name: req.Name,
Email: req.Email,
Phone: req.Phone,
Status: status,
Notes: nil,
}
rec, err := s.repo.CreateCustomer(ctx, params)
if err != nil {
return domain.Customer{}, err
}
return domain.Customer{
ID: rec.ID,
TenantID: rec.TenantID,
Name: rec.Name,
Email: rec.Email,
Phone: rec.Phone,
Status: rec.Status,
CreatedAt: rec.CreatedAt,
Notes: "",
}, nil
}
func (s *Service) UpdateCustomer(ctx context.Context, principal domain.Principal, customerID string, req domain.UpdateCustomerRequest) (domain.Customer, error) {
membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
if err != nil {
return domain.Customer{}, ErrTenantMembership
}
// Verify customer belongs to tenant
cust, err := s.repo.GetCustomerByID(ctx, customerID)
if err != nil {
return domain.Customer{}, ErrCustomerNotFound
}
if cust.TenantID != membership.Tenant.ID {
return domain.Customer{}, ErrCustomerNotFound
}
params := db.UpdateCustomerParams{}
if req.Name != "" {
params.Name = &req.Name
}
if req.Email != "" {
params.Email = &req.Email
}
if req.Phone != nil {
params.Phone = req.Phone
}
if req.Status != "" {
params.Status = &req.Status
}
if req.Notes != "" {
params.Notes = &req.Notes
}
rec, err := s.repo.UpdateCustomer(ctx, customerID, params)
if err != nil {
return domain.Customer{}, err
}
bookingsCount, _ := s.repo.GetCustomerBookingsCount(ctx, rec.ID)
lastBooking, _ := s.repo.GetCustomerLastBooking(ctx, rec.ID)
return domain.Customer{
ID: rec.ID,
TenantID: rec.TenantID,
Name: rec.Name,
Email: rec.Email,
Phone: rec.Phone,
Status: rec.Status,
BookingsCount: bookingsCount,
LastBookingAt: lastBooking,
CreatedAt: rec.CreatedAt,
Notes: "",
}, nil
}
func (s *Service) DeleteCustomer(ctx context.Context, principal domain.Principal, customerID string) error {
membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
if err != nil {
return ErrTenantMembership
}
cust, err := s.repo.GetCustomerByID(ctx, customerID)
if err != nil {
return ErrCustomerNotFound
}
if cust.TenantID != membership.Tenant.ID {
return ErrCustomerNotFound
}
return s.repo.DeleteCustomer(ctx, customerID)
}
// ============================================
// WORKING HOURS MANAGEMENT
// ============================================
func (s *Service) ListWorkingHours(ctx context.Context, principal domain.Principal) ([]domain.WorkingHours, error) {
membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
if err != nil {
return nil, ErrTenantMembership
}
records, err := s.repo.ListWorkingHoursByTenant(ctx, membership.Tenant.ID)
if err != nil {
return nil, err
}
hours := make([]domain.WorkingHours, len(records))
for i, rec := range records {
hours[i] = domain.WorkingHours{
DayOfWeek: rec.DayOfWeek,
Open: rec.StartsLocal,
Close: rec.EndsLocal,
IsOpen: rec.StartsLocal != "" && rec.EndsLocal != "",
}
}
return hours, nil
}
func (s *Service) UpdateWorkingHours(ctx context.Context, principal domain.Principal, dayOfWeek int, req domain.UpdateWorkingHoursRequest) error {
membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
if err != nil {
return ErrTenantMembership
}
params := db.UpdateWorkingHoursParams{}
if req.Open != "" {
params.StartsLocal = &req.Open
}
if req.Close != "" {
params.EndsLocal = &req.Close
}
return s.repo.UpdateWorkingHours(ctx, membership.Tenant.ID, dayOfWeek, params)
}