cart and checkout
This commit is contained in:
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user