mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-06-03 20:13:00 +00:00
470 lines
12 KiB
Go
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)
|
|
}
|