all the refactor
Build and Publish / BuildAndDeployAmd64 (push) Failing after 5s
Build and Publish / BuildAndDeployArm64 (push) Failing after 6s

This commit is contained in:
2026-06-28 17:51:52 +02:00
parent 7db0d236c7
commit aa8b2bdedc
84 changed files with 4328 additions and 2344 deletions
+3 -22
View File
@@ -12,7 +12,6 @@ import (
"git.k6n.net/mats/go-cart-actor/pkg/cart"
messages "git.k6n.net/mats/go-cart-actor/proto/checkout"
"git.k6n.net/mats/go-redis-inventory/pkg/inventory"
adyenCheckout "github.com/adyen/adyen-go-api-library/v21/src/checkout"
"github.com/adyen/adyen-go-api-library/v21/src/common"
"github.com/adyen/adyen-go-api-library/v21/src/hmacvalidator"
@@ -232,27 +231,9 @@ func (s *CheckoutPoolServer) createAdyenOrder(ctx context.Context, checkoutId ca
return
}
if s.inventoryService != nil && grain.CartState != nil && !grain.InventoryReserved {
if rerr := s.inventoryService.ReserveInventory(ctx, getInventoryRequests(grain.CartState.Items)...); rerr != nil {
log.Printf("from-checkout: inventory reservation failed for %s: %v", checkoutId.String(), rerr)
} else {
// Commit step: release cart reservation since we permanently decremented stock
if s.reservationService != nil {
for _, item := range grain.CartState.Items {
if item == nil || !shouldTrackInventory(item) {
continue
}
if err := s.reservationService.ReleaseForCart(ctx, inventory.SKU(item.Sku), getLocationId(item), inventory.CartID(grain.CartId.String())); err != nil {
log.Printf("from-checkout: failed to release cart reservation for %s: %v", item.Sku, err)
}
}
}
s.Apply(ctx, uint64(grain.Id), &messages.InventoryReserved{
Id: grain.Id.String(),
Status: "success",
})
}
}
// Inventory is NOT committed here — see the note in KlarnaPushHandler. The
// order saga emits order.created and the inventory service reacts
// (commit + release + level crossing), idempotently and off the revenue path.
country := getCountryFromCurrency(item.Amount.Currency)
if _, oerr := createOrderFromSettledCheckout(ctx, s, grain, settledPayment{
+18 -14
View File
@@ -9,6 +9,7 @@ import (
"git.k6n.net/mats/go-cart-actor/pkg/cart"
"git.k6n.net/mats/go-cart-actor/pkg/checkout"
"git.k6n.net/mats/platform/tax"
adyenCheckout "github.com/adyen/adyen-go-api-library/v21/src/checkout"
"github.com/adyen/adyen-go-api-library/v21/src/common"
)
@@ -66,7 +67,7 @@ type CheckoutMeta struct {
// CartItem / Delivery to expose that data and propagate it here.
// tp is an optional TaxProvider; when non-nil, its DefaultTaxRate is used for
// delivery lines instead of the hardcoded 2500 (25 % as CartItem format).
func BuildCheckoutOrderPayload(grain *checkout.CheckoutGrain, meta *CheckoutMeta, tp cart.TaxProvider) ([]byte, *CheckoutOrder, error) {
func BuildCheckoutOrderPayload(grain *checkout.CheckoutGrain, meta *CheckoutMeta, tp tax.Provider) ([]byte, *CheckoutOrder, error) {
if grain == nil {
return nil, nil, fmt.Errorf("nil grain")
}
@@ -177,19 +178,22 @@ func BuildCheckoutOrderPayload(grain *checkout.CheckoutGrain, meta *CheckoutMeta
}
// defaultKlarnaTaxRate returns the default tax rate for the purchase country, in Klarna
// format (percent x 100, e.g. 2500 = 25.00 %). When tp is nil, falls back to 2500 (25 %).
func defaultKlarnaTaxRate(tp cart.TaxProvider, country string) int {
// format (percent x 100, e.g. 2500 = 25.00 %). This matches the platform basis-point
// scale exactly, so DefaultTaxRate is passed through. When tp is nil, falls back to 2500.
func defaultKlarnaTaxRate(tp tax.Provider, country string) int {
if tp == nil {
return 2500
}
return tp.DefaultTaxRate(country) * 100
return tp.DefaultTaxRate(country)
}
// defaultAdyenTaxRate returns the default tax rate for the purchase country, in Adyen
// format (raw percent, e.g. 25 = 25 %). When tp is nil, falls back to 25.
func defaultAdyenTaxRate(tp cart.TaxProvider, country string) int64 {
// format (taxPercentage in basis points, e.g. 2500 = 25 %). This matches the platform
// basis-point scale exactly, so DefaultTaxRate is passed through. When tp is nil, falls
// back to 2500.
func defaultAdyenTaxRate(tp tax.Provider, country string) int64 {
if tp == nil {
return 25
return 2500
}
return int64(tp.DefaultTaxRate(country))
}
@@ -211,7 +215,7 @@ func GetCheckoutMetaFromRequest(r *http.Request) *CheckoutMeta {
}
}
func BuildAdyenCheckoutSession(grain *checkout.CheckoutGrain, meta *CheckoutMeta, tp cart.TaxProvider) (*adyenCheckout.CreateCheckoutSessionRequest, error) {
func BuildAdyenCheckoutSession(grain *checkout.CheckoutGrain, meta *CheckoutMeta, tp tax.Provider) (*adyenCheckout.CreateCheckoutSessionRequest, error) {
if grain == nil {
return nil, fmt.Errorf("nil grain")
}
@@ -237,10 +241,10 @@ func BuildAdyenCheckoutSession(grain *checkout.CheckoutGrain, meta *CheckoutMeta
}
lineItems = append(lineItems, adyenCheckout.LineItem{
Quantity: common.PtrInt64(int64(it.Quantity)),
AmountIncludingTax: common.PtrInt64(it.TotalPrice.IncVat),
AmountIncludingTax: common.PtrInt64(it.TotalPrice.IncVat.Int64()),
Description: common.PtrString(it.Meta.Name),
AmountExcludingTax: common.PtrInt64(it.TotalPrice.ValueExVat()),
TaxAmount: common.PtrInt64(it.TotalPrice.TotalVat()),
AmountExcludingTax: common.PtrInt64(it.TotalPrice.ValueExVat().Int64()),
TaxAmount: common.PtrInt64(it.TotalPrice.TotalVat().Int64()),
TaxPercentage: common.PtrInt64(int64(it.Tax)),
})
}
@@ -263,8 +267,8 @@ func BuildAdyenCheckoutSession(grain *checkout.CheckoutGrain, meta *CheckoutMeta
if d == nil {
continue
}
amountIncTax := d.Price.IncVat
amountExTax := d.Price.ValueExVat()
amountIncTax := d.Price.IncVat.Int64()
amountExTax := d.Price.ValueExVat().Int64()
if hasFreeShipping {
amountIncTax = 0
amountExTax = 0
@@ -284,7 +288,7 @@ func BuildAdyenCheckoutSession(grain *checkout.CheckoutGrain, meta *CheckoutMeta
return &adyenCheckout.CreateCheckoutSessionRequest{
Reference: grain.Id.String(),
Amount: adyenCheckout.Amount{
Value: total.IncVat,
Value: total.IncVat.Int64(),
Currency: currency,
},
CountryCode: common.PtrString(country),
+7 -33
View File
@@ -181,36 +181,12 @@ func (s *CheckoutPoolServer) KlarnaPushHandler(w http.ResponseWriter, r *http.Re
return
}
if s.inventoryService != nil {
inventoryRequests := getInventoryRequests(grain.CartState.Items)
invStatus := "success"
if err = s.inventoryService.ReserveInventory(r.Context(), inventoryRequests...); err != nil {
// The payment is already settled at Klarna (checkout_complete), so the
// order MUST be created. Inventory is best-effort at this point — a
// reservation failure (unstocked / drop-ship / non-tracked items)
// must not block order creation. Log it and flag the grain so
// fulfillment can reconcile.
logger.WarnContext(r.Context(), "klarna push: inventory reservation failed; creating order anyway",
"err", err, "checkoutId", grain.Id.String())
invStatus = "failed"
} else {
// Commit step: successfully decremented stock permanently, now release the temporary cart reservations
if s.reservationService != nil {
for _, item := range grain.CartState.Items {
if item == nil || !shouldTrackInventory(item) {
continue
}
if err := s.reservationService.ReleaseForCart(r.Context(), inventory.SKU(item.Sku), getLocationId(item), inventory.CartID(grain.CartId.String())); err != nil {
logger.WarnContext(r.Context(), "klarna push: failed to release cart reservation", "sku", item.Sku, "err", err)
}
}
}
}
s.Apply(r.Context(), uint64(grain.Id), &messages.InventoryReserved{
Id: grain.Id.String(),
Status: invStatus,
})
}
// Inventory is NOT committed here. Checkout announces a completed sale; the
// order saga emits order.created and the inventory service reacts (commit +
// release the cart hold + emit a level crossing), idempotently. This keeps a
// down inventory service from blocking the revenue path — the payment is
// already settled at Klarna, so the sale is a fact, not a request. See
// docs/inventory-shape.md.
s.ApplyAnywhere(r.Context(), grain.Id, &messages.PaymentCompleted{
PaymentId: orderId,
@@ -266,8 +242,6 @@ func createOrderFromCheckout(ctx context.Context, s *CheckoutPoolServer, grain *
})
}
func getLocationId(item *cart.CartItem) inventory.LocationID {
if item.StoreId == nil || *item.StoreId == "" {
return "se"
@@ -282,7 +256,7 @@ func shouldTrackInventory(item *cart.CartItem) bool {
if item.InventoryTracked {
return true
}
return item.Cgm == "55010"
return false
}
func getInventoryRequests(items []*cart.CartItem) []inventory.ReserveRequest {
+21 -8
View File
@@ -12,17 +12,17 @@ import (
"git.k6n.net/mats/go-cart-actor/internal/ucp"
"git.k6n.net/mats/go-cart-actor/pkg/actor"
"git.k6n.net/mats/go-cart-actor/pkg/cart"
"git.k6n.net/mats/go-cart-actor/pkg/checkout"
"git.k6n.net/mats/go-cart-actor/pkg/proxy"
"git.k6n.net/mats/go-cart-actor/pkg/telemetry"
"git.k6n.net/mats/go-redis-inventory/pkg/inventory"
"git.k6n.net/mats/platform/config"
"git.k6n.net/mats/platform/rabbit"
"git.k6n.net/mats/platform/tax"
"github.com/adyen/adyen-go-api-library/v21/src/adyen"
"github.com/adyen/adyen-go-api-library/v21/src/common"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
amqp "github.com/rabbitmq/amqp091-go"
"github.com/redis/go-redis/v9"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
@@ -55,14 +55,14 @@ var cartInternalUrl = os.Getenv("CART_INTERNAL_URL") // e.g., http://cart-servic
// selectTaxProvider picks the tax provider from the environment.
// Default is NordicTaxProvider with SE as the default country.
// Set TAX_PROVIDER=static for dev and tests (no country awareness).
func selectTaxProvider() cart.TaxProvider {
func selectTaxProvider() tax.Provider {
switch os.Getenv("TAX_PROVIDER") {
case "static":
return cart.NewStaticTaxProvider()
return tax.NewStatic()
case "nordic":
return cart.NewNordicTaxProvider(config.EnvString("TAX_DEFAULT_COUNTRY", "SE"))
return tax.NewNordic(config.EnvString("TAX_DEFAULT_COUNTRY", "SE"))
default:
return cart.NewNordicTaxProvider(config.EnvString("TAX_DEFAULT_COUNTRY", "SE"))
return tax.NewNordic(config.EnvString("TAX_DEFAULT_COUNTRY", "SE"))
}
}
@@ -171,15 +171,28 @@ func main() {
}
var orderHandler *AmqpOrderHandler
conn, err := amqp.Dial(amqpUrl)
conn, err := rabbit.Dial(amqpUrl, "cart-checkout")
if err != nil {
log.Fatalf("failed to connect to RabbitMQ: %v", err)
}
orderHandler = NewAmqpOrderHandler(conn)
orderHandler = NewAmqpOrderHandler(conn.Connection())
if err := orderHandler.DefineQueue(); err != nil {
log.Fatalf("failed to declare order queue: %v", err)
}
// Reconnecting order queue consumer: re-define queue and re-consume on reconnect.
conn.NotifyOnReconnect(func() {
if err := orderHandler.DefineQueue(); err != nil {
log.Printf("checkout: define queue on reconnect: %v", err)
}
})
// Stream applied checkout mutations to the shared mutations exchange
// (routing key mutation.checkout) for the backoffice live feed.
checkoutFeed := actor.NewAmqpListener(conn.Connection(), "checkout", actor.MutationSummary)
checkoutFeed.DefineTopics()
pool.AddListener(checkoutFeed)
syncedServer = NewCheckoutPoolServer(pool, fmt.Sprintf("%s, %s", name, podIp), klarnaClient, cartClient, adyenClient, orderClient, orderHandler)
syncedServer.inventoryService = inventoryService
syncedServer.reservationService = reservationService
+1
View File
@@ -66,6 +66,7 @@ type OrderLine struct {
Quantity int32 `json:"quantity"`
UnitPrice int64 `json:"unitPrice"`
TaxRate int32 `json:"taxRate"`
Location string `json:"location,omitempty"`
}
// CreateOrderResult is the success response from POST /api/orders/from-checkout.
+15 -7
View File
@@ -43,21 +43,29 @@ func buildOrderLinesFromGrain(grain *checkout.CheckoutGrain) []OrderLine {
if it == nil {
continue
}
// Carry the per-item store as the inventory commit location; empty when
// no store (commit then falls back to the order country / central stock).
location := ""
if it.StoreId != nil {
location = *it.StoreId
}
lines = append(lines, OrderLine{
Reference: it.Sku,
Sku: it.Sku,
Name: it.Meta.Name,
Quantity: int32(it.Quantity),
UnitPrice: it.Price.IncVat,
// CartItem.Tax is percentage x 100 (e.g. 2500 = 25.00%). Convert to raw percent (25).
TaxRate: int32(it.Tax / 100),
UnitPrice: it.Price.IncVat.Int64(),
// CartItem.Tax and OrderLine.TaxRate share the platform basis-point scale
// (2500 = 25%), so the rate passes through unchanged.
TaxRate: int32(it.Tax),
Location: location,
})
}
for _, d := range grain.Deliveries {
if d == nil {
continue
}
unitPrice := d.Price.IncVat
unitPrice := d.Price.IncVat.Int64()
if hasFreeShipping {
unitPrice = 0
}
@@ -70,7 +78,7 @@ func buildOrderLinesFromGrain(grain *checkout.CheckoutGrain) []OrderLine {
Name: "Delivery",
Quantity: 1,
UnitPrice: unitPrice,
TaxRate: 25,
TaxRate: 2500, // 25% in basis points
})
}
@@ -82,7 +90,7 @@ func buildOrderLinesFromGrain(grain *checkout.CheckoutGrain) []OrderLine {
}
discountVal := int64(0)
if ap.Discount != nil {
discountVal = ap.Discount.IncVat
discountVal = ap.Discount.IncVat.Int64()
}
if discountVal <= 0 {
continue
@@ -93,7 +101,7 @@ func buildOrderLinesFromGrain(grain *checkout.CheckoutGrain) []OrderLine {
Name: "Promotion: " + ap.Name,
Quantity: 1,
UnitPrice: -discountVal,
TaxRate: 25, // default standard tax rate for promotion discount
TaxRate: 2500, // default standard tax rate (25%) in basis points
})
}
}
+2 -1
View File
@@ -13,6 +13,7 @@ import (
"git.k6n.net/mats/go-cart-actor/pkg/actor"
"git.k6n.net/mats/go-cart-actor/pkg/cart"
"git.k6n.net/mats/go-cart-actor/pkg/checkout"
"git.k6n.net/mats/platform/tax"
messages "git.k6n.net/mats/go-cart-actor/proto/checkout"
adyen "github.com/adyen/adyen-go-api-library/v21/src/adyen"
@@ -53,7 +54,7 @@ type CheckoutPoolServer struct {
orderHandler *AmqpOrderHandler
inventoryService *inventory.RedisInventoryService
reservationService *inventory.RedisCartReservationService
taxProvider cart.TaxProvider
taxProvider tax.Provider
}
func NewCheckoutPoolServer(pool actor.GrainPool[checkout.CheckoutGrain], pod_name string, klarnaClient *KlarnaClient, cartClient *CartClient, adyenClient *adyen.APIClient, orderClient *OrderClient, orderHandler *AmqpOrderHandler) *CheckoutPoolServer {