package checkout
import (
"context"
"database/sql"
"errors"
"fmt"
)
var ErrEmptyCart = errors.New("cart is empty")
// DBTX is satisfied by both *sql.DB and *sql.Tx.
type DBTX interface {1
ExecContext(ctx context.Context, q string, args ...any) (sql.Result, error)
QueryRowContext(ctx context.Context, q string, args ...any) *sql.Row
}
type Inventory interface {
Reserve(ctx context.Context, db DBTX, sku string, qty int) error
}
type Orders interface {
Create(ctx context.Context, db DBTX, o *Order) error
}
type Service struct {
db *sql.DB
inventory Inventory
orders Orders
}
func NewService(db *sql.DB, inv Inventory, o Orders) *Service {
return &Service{db: db, inventory: inv, orders: o}
}
func (s *Service) PlaceOrder(ctx context.Context, cmd PlaceOrderCmd) (Order, error) {
if len(cmd.Items) == 0 {
return Order{}, ErrEmptyCart
}
tx, err := s.db.BeginTx(ctx, nil)2
if err != nil {
return Order{}, fmt.Errorf("begin: %w", err)
}
defer tx.Rollback() // no-op once committed3
for _, it := range cmd.Items {
if err := s.inventory.Reserve(ctx, tx, it.SKU, it.Qty); err != nil {4
return Order{}, fmt.Errorf("reserve %s: %w", it.SKU, err)
}
}
o := Order{CustomerID: cmd.CustomerID, Items: cmd.Items, Status: "PENDING"}
if err := s.orders.Create(ctx, tx, &o); err != nil {
return Order{}, fmt.Errorf("create order: %w", err)
}
if err := tx.Commit(); err != nil {
return Order{}, fmt.Errorf("commit: %w", err)
}
return o, nil
}