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

This commit is contained in:
2026-06-27 19:49:00 +02:00
parent 492f54ff45
commit 528c59bfd3
67 changed files with 3618 additions and 1031 deletions
+87 -15
View File
@@ -2,6 +2,7 @@ package main
import (
"context"
"encoding/json"
"fmt"
"git.k6n.net/mats/go-cart-actor/pkg/checkout"
@@ -27,6 +28,16 @@ type settledPayment struct {
// selections into from-checkout order lines. Shared by every provider so the
// order payload is built the same way no matter who settled the payment.
func buildOrderLinesFromGrain(grain *checkout.CheckoutGrain) []OrderLine {
hasFreeShipping := false
if grain.CartState != nil {
for _, ap := range grain.CartState.AppliedPromotions {
if ap.Type == "free_shipping" && !ap.Pending {
hasFreeShipping = true
break
}
}
}
lines := make([]OrderLine, 0, len(grain.CartState.Items)+len(grain.Deliveries))
for _, it := range grain.CartState.Items {
if it == nil {
@@ -38,11 +49,19 @@ func buildOrderLinesFromGrain(grain *checkout.CheckoutGrain) []OrderLine {
Name: it.Meta.Name,
Quantity: int32(it.Quantity),
UnitPrice: it.Price.IncVat,
TaxRate: int32(it.Tax),
// CartItem.Tax is percentage x 100 (e.g. 2500 = 25.00%). Convert to raw percent (25).
TaxRate: int32(it.Tax / 100),
})
}
for _, d := range grain.Deliveries {
if d == nil || d.Price.IncVat <= 0 {
if d == nil {
continue
}
unitPrice := d.Price.IncVat
if hasFreeShipping {
unitPrice = 0
}
if unitPrice <= 0 && !hasFreeShipping {
continue
}
lines = append(lines, OrderLine{
@@ -50,10 +69,35 @@ func buildOrderLinesFromGrain(grain *checkout.CheckoutGrain) []OrderLine {
Sku: d.Provider,
Name: "Delivery",
Quantity: 1,
UnitPrice: d.Price.IncVat,
TaxRate: 2500,
UnitPrice: unitPrice,
TaxRate: 25,
})
}
// Reflected applied promotions as negative discount lines
if grain.CartState != nil {
for _, ap := range grain.CartState.AppliedPromotions {
if ap.Pending {
continue
}
discountVal := int64(0)
if ap.Discount != nil {
discountVal = ap.Discount.IncVat
}
if discountVal <= 0 {
continue
}
lines = append(lines, OrderLine{
Reference: "promo-" + ap.PromotionId,
Sku: "promo-" + ap.PromotionId,
Name: "Promotion: " + ap.Name,
Quantity: 1,
UnitPrice: -discountVal,
TaxRate: 25, // default standard tax rate for promotion discount
})
}
}
return lines
}
@@ -64,8 +108,10 @@ func buildOrderLinesFromGrain(grain *checkout.CheckoutGrain) []OrderLine {
//
// It is idempotent: the order service dedupes on "checkout-{checkoutId}", so
// repeated provider callbacks (Klarna re-push, retried Adyen CAPTURE) return the
// existing order rather than creating a duplicate. Callers should treat a
// non-nil error as non-fatal and retry on the next callback.
// existing order rather than creating a duplicate.
//
// If the HTTP call fails or is not configured, it falls back to publishing the
// exact same payload to RabbitMQ via s.orderHandler.OrderCompleted(body).
func createOrderFromSettledCheckout(ctx context.Context, s *CheckoutPoolServer, grain *checkout.CheckoutGrain, pay settledPayment) (*CreateOrderResult, error) {
if grain.CartState == nil {
return nil, fmt.Errorf("from-checkout: checkout %s has no cart state", grain.Id)
@@ -96,15 +142,41 @@ func createOrderFromSettledCheckout(ctx context.Context, s *CheckoutPoolServer,
req.Payment.Reference = pay.Reference
req.Payment.Amount = pay.Amount
result, err := s.orderClient.CreateOrder(ctx, req, "")
if err != nil {
return nil, err
var createErr error
if s.orderClient != nil {
result, err := s.orderClient.CreateOrder(ctx, req, "")
if err == nil {
_ = s.ApplyAnywhere(ctx, grain.Id, &messages.OrderCreated{
OrderId: result.OrderId,
Status: "completed",
CreatedAt: timestamppb.Now(),
})
return result, nil
}
createErr = err
logger.WarnContext(ctx, "from-checkout HTTP call failed; falling back to AMQP", "err", err, "checkoutId", grain.Id.String())
}
_ = s.ApplyAnywhere(ctx, grain.Id, &messages.OrderCreated{
OrderId: result.OrderId,
Status: "completed",
CreatedAt: timestamppb.Now(),
})
return result, nil
// Fallback to AMQP
if s.orderHandler != nil {
body, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("failed to marshal order fallback: %w", err)
}
if pubErr := s.orderHandler.OrderCompleted(body); pubErr != nil {
if createErr != nil {
return nil, fmt.Errorf("AMQP fallback failed: %w (HTTP error: %v)", pubErr, createErr)
}
return nil, fmt.Errorf("AMQP fallback failed: %w", pubErr)
}
logger.InfoContext(ctx, "order request published to AMQP successfully as fallback", "checkoutId", grain.Id.String())
return &CreateOrderResult{
OrderId: "queued-" + grain.Id.String(),
}, nil
}
if createErr != nil {
return nil, createErr
}
return nil, fmt.Errorf("neither order client nor AMQP order handler configured")
}