package orders import ( "context" "errors" "fmt" "strings" "github.com/google/uuid" "backend/internal/modules/pricing" "backend/internal/modules/products" "backend/internal/modules/units" ) var ( ErrEmptyOrder = errors.New("order must contain at least one item") ErrInvalidStatus = errors.New("invalid order status") ErrProductMismatch = errors.New("price tier does not belong to the requested product") ) // The dependencies below are the minimal slices of other modules' services // this module needs, defined on the consumer side (Go idiom) so orders // never has to import their concrete handler/repository types. type ProductFinder interface { Get(ctx context.Context, id uuid.UUID) (*products.Product, error) } type UnitFinder interface { Get(ctx context.Context, id uuid.UUID) (*units.Unit, error) } type PriceResolver interface { PriceForQuantity(ctx context.Context, tierID uuid.UUID, multiplier int64) (int64, *pricing.PriceTier, error) } // OrderNotifier is satisfied by the telegram module's Service (and, later, // any other notification channel) without orders ever depending on it // directly. type OrderNotifier interface { NotifyNewOrder(ctx context.Context, summary string) NotifyOrderStatusChange(ctx context.Context, summary string) } type Service struct { repo Repository products ProductFinder units UnitFinder prices PriceResolver notifier OrderNotifier } func NewService(repo Repository, products ProductFinder, units UnitFinder, prices PriceResolver, notifier OrderNotifier) *Service { return &Service{repo: repo, products: products, units: units, prices: prices, notifier: notifier} } type ItemInput struct { ProductID uuid.UUID PriceTierID uuid.UUID Multiplier int64 } type CreateInput struct { CustomerName string CustomerEmail string CustomerPhone string Notes string Items []ItemInput } func (s *Service) Create(ctx context.Context, in CreateInput) (*Order, []*OrderItem, error) { if len(in.Items) == 0 { return nil, nil, ErrEmptyOrder } order := &Order{ ID: uuid.New(), CustomerName: in.CustomerName, CustomerEmail: in.CustomerEmail, CustomerPhone: in.CustomerPhone, Status: StatusPending, Notes: in.Notes, } items := make([]*OrderItem, 0, len(in.Items)) var total int64 for _, in := range in.Items { multiplier := in.Multiplier if multiplier < 1 { multiplier = 1 } lineTotal, tier, err := s.prices.PriceForQuantity(ctx, in.PriceTierID, multiplier) if err != nil { return nil, nil, fmt.Errorf("resolve price tier %s: %w", in.PriceTierID, err) } if tier.ProductID != in.ProductID { return nil, nil, ErrProductMismatch } product, err := s.products.Get(ctx, in.ProductID) if err != nil { return nil, nil, fmt.Errorf("resolve product %s: %w", in.ProductID, err) } unit, err := s.units.Get(ctx, tier.UnitID) if err != nil { return nil, nil, fmt.Errorf("resolve unit %s: %w", tier.UnitID, err) } items = append(items, &OrderItem{ ID: uuid.New(), ProductID: &product.ID, ProductName: product.Name, PriceTierID: &tier.ID, UnitSymbol: unit.Symbol, TierQuantity: tier.Quantity, Multiplier: multiplier, UnitPriceCents: tier.PriceCents, TotalCents: lineTotal, }) total += lineTotal } order.TotalCents = total if err := s.repo.Create(ctx, order, items); err != nil { return nil, nil, err } s.notifier.NotifyNewOrder(context.Background(), summarizeNewOrder(order, items)) return order, items, nil } func (s *Service) Get(ctx context.Context, id uuid.UUID) (*Order, []*OrderItem, error) { return s.repo.FindByID(ctx, id) } func (s *Service) List(ctx context.Context, status string) ([]*Order, error) { return s.repo.List(ctx, status) } func (s *Service) UpdateStatus(ctx context.Context, id uuid.UUID, status string) (*Order, error) { if !ValidStatuses[status] { return nil, ErrInvalidStatus } order, err := s.repo.UpdateStatus(ctx, id, status) if err != nil { return nil, err } s.notifier.NotifyOrderStatusChange(context.Background(), fmt.Sprintf( "Order %s status changed to %q (customer: %s)", order.ID, order.Status, order.CustomerName, )) return order, nil } func summarizeNewOrder(order *Order, items []*OrderItem) string { var b strings.Builder fmt.Fprintf(&b, "New order from %s (%s)\n", order.CustomerName, order.CustomerEmail) for _, item := range items { fmt.Fprintf(&b, "- %dx %s (%.2f %s) = %.2f\n", item.Multiplier, item.ProductName, item.TierQuantity, item.UnitSymbol, centsToUnits(item.TotalCents)) } fmt.Fprintf(&b, "Total: %.2f", centsToUnits(order.TotalCents)) return b.String() } func centsToUnits(cents int64) float64 { return float64(cents) / 100 }