refactor
Build and Publish / BuildAndDeployArm64 (push) Failing after 6s
Build and Publish / BuildAndDeployAmd64 (push) Failing after 4s

This commit is contained in:
2026-06-29 12:34:58 +02:00
parent d711348863
commit 46be260fcc
35 changed files with 1765 additions and 198 deletions
+75 -23
View File
@@ -12,6 +12,7 @@ import (
"log"
"log/slog"
"net/http"
"net/mail"
"os"
"path/filepath"
"strconv"
@@ -36,19 +37,20 @@ import (
)
type server struct {
pool *actor.SimpleGrainPool[order.OrderGrain]
applier *orderedApplier
storage *actor.DiskStorage[order.OrderGrain]
reg actor.MutationRegistry
engine *flow.Engine
freg *flow.Registry
flows *flowStore
provider order.PaymentProvider
taxProvider tax.Provider
defaultFlow string
dataDir string
idem *idempotency.Store
logger *slog.Logger
pool *actor.SimpleGrainPool[order.OrderGrain]
applier *orderedApplier
storage *actor.DiskStorage[order.OrderGrain]
reg actor.MutationRegistry
engine *flow.Engine
freg *flow.Registry
flows *flowStore
provider order.PaymentProvider
taxProvider tax.Provider
defaultFlow string
dataDir string
idem *idempotency.Store
logger *slog.Logger
inventoryReservations order.InventoryReservationService
}
func main() {
@@ -101,6 +103,18 @@ func main() {
// the broker (publisher is nil without AMQP — the hook then errors at run
// time, which is logged, not fatal).
amqpURL := os.Getenv("AMQP_URL")
emailTemplates, err := order.LoadEmailTemplates(config.EnvString("ORDER_EMAIL_TEMPLATES", "data/order-email-templates"))
if err != nil {
log.Fatalf("load email templates: %v", err)
}
emailSender, err := selectEmailSender(logger)
if err != nil {
log.Fatalf("configure email sender: %v", err)
}
inventoryReservations, err := selectInventoryReservationService()
if err != nil {
log.Fatalf("configure inventory reservation service: %v", err)
}
var emitPub order.Publisher
if amqpURL != "" {
// The amqp_emit hook writes to a durable outbox rather than the broker
@@ -138,13 +152,7 @@ func main() {
}
}
}
freg := flow.NewRegistry()
flow.RegisterBuiltinHooks(freg)
order.RegisterFlowActions(freg, applier, provider)
order.RegisterEmitHook(freg, emitPub)
// order.created domain event → durable outbox → "order" topic exchange.
// Reactors: inventory commit, fulfillment allocation.
order.RegisterOrderCreatedEmit(freg, applier, emitPub, "order", "order")
freg := buildFlowRegistry(applier, provider, inventoryReservations, emitPub, emailSender, emailTemplates, logger)
engine := flow.NewEngine(freg, logger)
def, err := order.EmbeddedFlow("place-and-pay")
@@ -171,6 +179,7 @@ func main() {
engine: engine, freg: freg, flows: flows, provider: provider,
taxProvider: taxProvider,
defaultFlow: def.Name, dataDir: dataDir, idem: idem, logger: logger,
inventoryReservations: inventoryReservations,
}
mux := http.NewServeMux()
@@ -276,8 +285,9 @@ type lineReq struct {
Sku string `json:"sku"`
Name string `json:"name"`
Quantity int32 `json:"quantity"`
UnitPrice int64 `json:"unitPrice"` // inc-vat, minor units
TaxRate int32 `json:"taxRate"` // basis points (2500 = 25%)
UnitPrice int64 `json:"unitPrice"` // inc-vat, minor units
TaxRate int32 `json:"taxRate"` // basis points (2500 = 25%)
DropShip bool `json:"drop_ship,omitempty"`
Location string `json:"location,omitempty"` // inventory commit location / store id
}
@@ -384,6 +394,7 @@ func (s *server) buildPlaceOrder(id order.OrderId, req *checkoutReq) *messages.P
TaxRate: l.TaxRate,
TotalAmount: lineTotal,
TotalTax: lineTax,
DropShip: l.DropShip,
})
}
po.TotalAmount = total
@@ -502,7 +513,6 @@ func selectTaxProvider() tax.Provider {
// the dependency-free mock; set PAYMENT_PROVIDER=stripe + STRIPE_SECRET_KEY for
// real Stripe (STRIPE_API_BASE / STRIPE_PAYMENT_METHOD optionally override).
func selectProvider(logger *slog.Logger) order.PaymentProvider {
if os.Getenv("PAYMENT_PROVIDER") == "stripe" {
key := os.Getenv("STRIPE_SECRET_KEY")
if key == "" {
@@ -515,6 +525,48 @@ func selectProvider(logger *slog.Logger) order.PaymentProvider {
return order.NewMockProvider()
}
// selectEmailSender picks the flow-email transport. SMTP is the only sender
// implementation for now; when no SMTP host is configured the hook still exists
// in capabilities but errors at run time if a flow tries to use it.
func selectEmailSender(logger *slog.Logger) (order.EmailSender, error) {
kind := config.EnvString("ORDER_EMAIL_SENDER", "")
if kind == "" && os.Getenv("ORDER_EMAIL_SMTP_HOST") == "" {
return nil, nil
}
if kind == "" {
kind = "smtp"
}
if kind != "smtp" {
return nil, fmt.Errorf("unsupported ORDER_EMAIL_SENDER %q", kind)
}
fromAddr := strings.TrimSpace(os.Getenv("ORDER_EMAIL_FROM_ADDRESS"))
if fromAddr == "" {
return nil, fmt.Errorf("ORDER_EMAIL_FROM_ADDRESS is required when email sender is configured")
}
from := mail.Address{
Name: strings.TrimSpace(os.Getenv("ORDER_EMAIL_FROM_NAME")),
Address: fromAddr,
}
sender, err := order.NewSMTPEmailSender(order.SMTPEmailSenderConfig{
Host: config.EnvString("ORDER_EMAIL_SMTP_HOST", ""),
Port: config.EnvInt("ORDER_EMAIL_SMTP_PORT", 587, func(n int) bool { return n > 0 }),
Username: os.Getenv("ORDER_EMAIL_SMTP_USERNAME"),
Password: os.Getenv("ORDER_EMAIL_SMTP_PASSWORD"),
DefaultFrom: from,
Timeout: config.EnvDuration("ORDER_EMAIL_SMTP_TIMEOUT", 10*time.Second),
})
if err != nil {
return nil, err
}
logger.Info("email sender enabled", "kind", kind, "host", config.EnvString("ORDER_EMAIL_SMTP_HOST", ""), "from", from.Address)
return sender, nil
}
func selectInventoryReservationService() (order.InventoryReservationService, error) {
return order.NewHTTPInventoryReservationService(config.EnvString("INVENTORY_URL", "http://localhost:8080"), nil)
}
// loadUCPOrderSigner loads the UCP ECDSA signing key from the path specified in
// the UCP_SIGNING_KEY_PATH environment variable. Returns nil if unset or unreadable.
func loadUCPOrderSigner() *ucp.SigningConfig {