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 }