Files
go-cart-actor/cmd/checkout/order_create.go
T
mats 716a66562e
Build and Publish / BuildAndDeployArm64 (push) Failing after 6s
Build and Publish / BuildAndDeployAmd64 (push) Has been cancelled
update orderflow
2026-06-21 23:26:37 +02:00

111 lines
3.5 KiB
Go

package main
import (
"context"
"fmt"
"git.k6n.net/mats/go-cart-actor/pkg/checkout"
messages "git.k6n.net/mats/go-cart-actor/proto/checkout"
"google.golang.org/protobuf/types/known/timestamppb"
)
// settledPayment carries the provider-agnostic facts about an already-settled
// payment. Each payment integration (Klarna push, Adyen CAPTURE notification,
// …) fills this in from its own callback and hands it to
// createOrderFromSettledCheckout, so the from-checkout request is identical
// regardless of which provider settled the payment.
type settledPayment struct {
Provider string // "klarna" | "adyen"
Reference string // processor / PSP reference
Amount int64 // minor units, as charged
Currency string
Country string
Locale string
}
// buildOrderLinesFromGrain converts the checkout grain's cart items and delivery
// 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 {
lines := make([]OrderLine, 0, len(grain.CartState.Items)+len(grain.Deliveries))
for _, it := range grain.CartState.Items {
if it == nil {
continue
}
lines = append(lines, OrderLine{
Reference: it.Sku,
Sku: it.Sku,
Name: it.Meta.Name,
Quantity: int32(it.Quantity),
UnitPrice: it.Price.IncVat,
TaxRate: int32(it.Tax),
})
}
for _, d := range grain.Deliveries {
if d == nil || d.Price.IncVat <= 0 {
continue
}
lines = append(lines, OrderLine{
Reference: d.Provider,
Sku: d.Provider,
Name: "Delivery",
Quantity: 1,
UnitPrice: d.Price.IncVat,
TaxRate: 2500,
})
}
return lines
}
// createOrderFromSettledCheckout sends the from-checkout request for a settled
// checkout (any provider) and, on success, records the returned order id on the
// checkout grain via the OrderCreated mutation so the backoffice can link
// checkout → order.
//
// 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.
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)
}
var customerEmail, customerName string
if grain.ContactDetails != nil {
if grain.ContactDetails.Email != nil {
customerEmail = *grain.ContactDetails.Email
}
if grain.ContactDetails.Name != nil {
customerName = *grain.ContactDetails.Name
}
}
req := CreateOrderReq{
CheckoutId: grain.Id.String(),
IdempotencyKey: "checkout-" + grain.Id.String(),
CartId: grain.CartId.String(),
Currency: pay.Currency,
Locale: pay.Locale,
Country: pay.Country,
CustomerEmail: customerEmail,
CustomerName: customerName,
Lines: buildOrderLinesFromGrain(grain),
}
req.Payment.Provider = pay.Provider
req.Payment.Reference = pay.Reference
req.Payment.Amount = pay.Amount
result, err := s.orderClient.CreateOrder(ctx, req, "")
if err != nil {
return nil, err
}
_ = s.ApplyAnywhere(ctx, grain.Id, &messages.OrderCreated{
OrderId: result.OrderId,
Status: "completed",
CreatedAt: timestamppb.Now(),
})
return result, nil
}