Files
go-cart-actor/pkg/order/emit_created.go
T
mats 83ed477aa4
Build and Publish / BuildAndDeployAmd64 (push) Failing after 4s
Build and Publish / BuildAndDeployArm64 (push) Failing after 7s
flow
2026-07-19 00:36:01 +02:00

101 lines
3.7 KiB
Go

package order
import (
"context"
"encoding/json"
"time"
"git.k6n.net/mats/go-cart-actor/pkg/flow"
"git.k6n.net/mats/platform/event"
contract "git.k6n.net/mats/platform/order"
)
// RegisterOrderCreatedEmit registers the "emit_order_created" flow hook: it
// publishes an event.OrderCreated envelope (payload contract.Created) for the
// order, reading the grain via app and publishing durably via pub.
//
// It is a HOOK, not an action, on purpose: hook errors are logged, never abort
// the flow. The order is already placed and paid by the time this fires, so a
// transient publish hiccup must not roll it back — and pub is the durable outbox
// (a local fsync'd append), so "failure" here means disk error, then the relay
// retries delivery on its own. With a nil publisher (dev without AMQP) it is a
// no-op. Attach it to the "after" hooks of the final paid step, e.g. capture.
func RegisterOrderCreatedEmit(reg *flow.Registry, app Applier, pub Publisher, exchange, source string) {
reg.HookWithMeta("emit_order_created", func(ctx context.Context, st *flow.State, _ flow.HookInfo, _ json.RawMessage) error {
if pub == nil {
return nil // no broker configured (dev)
}
o, err := app.Get(ctx, st.ID)
if err != nil {
return err
}
exchange, routingKey, body, err := BuildOrderCreatedPayload(o, source)
if err != nil {
return err
}
if body == nil {
return nil // nothing for a reactor to act on
}
return pub.Publish(exchange, routingKey, body)
}, flow.CapabilityMeta{
Description: "Emit the order.created domain event after a paid order is finalized.",
})
}
// BuildOrderCreatedPayload builds the full order.created outbox entry
// (exchange, routingKey, body) for the order grain. Returns ("", "", nil, nil)
// when there are no lines (nothing for a reactor to act on) or on encode failure.
// This is the same logic as the emit_order_created hook, extracted for reuse by
// the applier's atomic emitOnApply callback.
//
// The body is a marshalled event.Event{Type: event.OrderCreated} envelope whose
// payload is the versioned contract.Created wire shape. The routing key is the
// string form of event.OrderCreated ("order.created").
func BuildOrderCreatedPayload(o *OrderGrain, source string) (exchange, routingKey string, body []byte, err error) {
created := buildOrderCreated(o)
if len(created.Lines) == 0 {
return "", "", nil, nil // nothing for a reactor to act on
}
payload, err := created.Encode()
if err != nil {
return "", "", nil, err
}
ev := event.Event{
ID: "order-created-" + created.OrderID,
Type: event.OrderCreated,
Source: source,
Subject: created.OrderID,
Payload: payload,
Time: time.Now(),
Meta: map[string]string{"country": created.Country},
}
body, err = json.Marshal(ev)
if err != nil {
return "", "", nil, err
}
return "order", string(event.OrderCreated), body, nil
}
// buildOrderCreated projects the grain onto the exported order.created contract.
// Lines carry SKU, quantity, the inventory commit location, and the drop-ship
// flag so the inventory reactor can skip the stock decrement for supplier-fulfilled items.
func buildOrderCreated(o *OrderGrain) contract.Created {
lines := make([]contract.Line, 0, len(o.Lines))
for _, l := range o.Lines {
if l.Sku == "" || l.Quantity <= 0 {
continue
}
lines = append(lines, contract.Line{SKU: l.Sku, Quantity: l.Quantity, Location: l.Location, DropShip: l.DropShip})
}
return contract.Created{
OrderID: OrderId(o.Id).String(),
OrderReference: o.OrderReference,
CartID: o.CartId,
Country: o.Country,
Currency: o.Currency,
CustomerEmail: o.CustomerEmail,
Lines: lines,
PlacedAtMs: nowMs(),
}
}