package order
import (
"context"
"database/sql"
"errors"
"log/slog"
"github.com/google/uuid"
)
type PlaceOrder struct {1
CustomerID string
Lines []Line
}
type Line struct {
SKU string
Qty int
Price int64 // VND
}
type Publisher interface{ Publish(ctx context.Context, event any) error }
type CommandHandler struct {
DB *sql.DB // primary (write) database
Bus Publisher
}
func (h *CommandHandler) PlaceOrder(ctx context.Context, cmd PlaceOrder) (string, error) {
if len(cmd.Lines) == 0 {2
return "", errors.New("order needs at least one line")
}
id := uuid.NewString()
var total int64
for _, l := range cmd.Lines {
total += l.Price * int64(l.Qty)
}
tx, err := h.DB.BeginTx(ctx, nil)
if err != nil {
return "", err
}
defer tx.Rollback() // no-op after Commit
if _, err := tx.ExecContext(ctx, "INSERT INTO orders (id, customer_id, total, status) VALUES ($1, $2, $3, 'PLACED')", id, cmd.CustomerID, total); err != nil {
return "", err
}
for _, l := range cmd.Lines {
if _, err := tx.ExecContext(ctx, "INSERT INTO order_lines (order_id, sku, qty, price) VALUES ($1, $2, $3, $4)", id, l.SKU, l.Qty, l.Price); err != nil {
return "", err
}
}
if err := tx.Commit(); err != nil {
return "", err
}
ev := OrderPlaced{OrderID: id, CustomerID: cmd.CustomerID, Total: total, ItemCount: len(cmd.Lines)}
if err := h.Bus.Publish(ctx, ev); err != nil {3
slog.Error("publish failed, read model will lag", "order", id, "err", err) // see Transactional Outbox
}
return id, nil
}