more checkout
Build and Publish / BuildAndDeployArm64 (push) Failing after 6s
Build and Publish / BuildAndDeployAmd64 (push) Has been cancelled

This commit is contained in:
2026-06-21 23:43:48 +02:00
parent 716a66562e
commit 9d420c43fa
7 changed files with 684 additions and 31 deletions
+15 -19
View File
@@ -4,7 +4,6 @@ import (
"encoding/json"
"fmt"
"net/http"
"sync"
"time"
"git.k6n.net/mats/go-cart-actor/pkg/flow"
@@ -38,14 +37,6 @@ type fromCheckoutReq struct {
} `json:"payment"`
}
// idempotencyStore guards against duplicate order creation for the same
// idempotency key. In-memory for step 1; a durable store (Redis, bolt) would
// survive restarts. The map is keyed by idempotencyKey → orderId.
var (
idempotencyMu sync.Mutex
idempotencyKeys = map[string]uint64{}
)
// handleFromCheckout creates an event-sourced order from a settled checkout.
// It runs the place-and-pay flow with a passthrough provider that records the
// already-completed authorization and capture, bypassing any external payment
@@ -65,10 +56,13 @@ func (s *server) handleFromCheckout(w http.ResponseWriter, r *http.Request) {
}
// ── Idempotency check ─────────────────────────────────────────────
// Lock the key for the whole check→create→record sequence so a concurrent
// retry with the same key can't race past the check and create a second
// order. The store is durable, so this also holds across a restart.
if req.IdempotencyKey != "" {
idempotencyMu.Lock()
if existingID, ok := idempotencyKeys[req.IdempotencyKey]; ok {
idempotencyMu.Unlock()
unlock := s.idem.Lock(req.IdempotencyKey)
defer unlock()
if existingID, ok := s.idem.Get(req.IdempotencyKey); ok {
// The order already exists — return it.
g, err := s.applier.Get(r.Context(), existingID)
if err != nil {
@@ -76,13 +70,12 @@ func (s *server) handleFromCheckout(w http.ResponseWriter, r *http.Request) {
return
}
writeJSON(w, http.StatusConflict, map[string]any{
"orderId": order.OrderId(existingID).String(),
"order": g,
"orderId": order.OrderId(existingID).String(),
"order": g,
"existing": true,
})
return
}
idempotencyMu.Unlock()
}
// ── Create the order grain ────────────────────────────────────────
@@ -141,11 +134,14 @@ func (s *server) handleFromCheckout(w http.ResponseWriter, r *http.Request) {
s.logger.Warn("from-checkout flow failed", "orderId", id.String(), "err", runErr)
}
// Record the idempotency key so retries are safe.
// Record the idempotency key durably so retries (incl. after a restart) are
// safe. We record even on a failed flow: the failed order grain exists, and a
// blind retry with the same key should return it rather than place a second
// order — the caller is told to retry with a fresh key (402 contract).
if req.IdempotencyKey != "" {
idempotencyMu.Lock()
idempotencyKeys[req.IdempotencyKey] = ordID
idempotencyMu.Unlock()
if err := s.idem.Put(req.IdempotencyKey, ordID); err != nil {
s.logger.Error("idempotency record failed", "key", req.IdempotencyKey, "orderId", id.String(), "err", err)
}
}
writeJSON(w, status, map[string]any{