161 lines
4.3 KiB
Go
161 lines
4.3 KiB
Go
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")
|
|
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)
|
|
}
|
|
|
|
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 {
|
|
CustomerID *uuid.UUID
|
|
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(),
|
|
CustomerID: in.CustomerID,
|
|
CustomerName: in.CustomerName,
|
|
CustomerEmail: in.CustomerEmail,
|
|
CustomerPhone: in.CustomerPhone,
|
|
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) ([]*Order, error) {
|
|
return s.repo.List(ctx)
|
|
}
|
|
|
|
func (s *Service) ListByCustomer(ctx context.Context, customerID uuid.UUID) ([]*Order, error) {
|
|
return s.repo.ListByCustomer(ctx, customerID)
|
|
}
|
|
|
|
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
|
|
}
|