Files
template-vitrine/backend/internal/modules/orders/service.go
T
2026-09-14 20:50:19 +02:00

172 lines
4.7 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")
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
}