package main import ( "bytes" "context" "net/http" "net/http/httptest" "testing" "git.k6n.net/mats/go-cart-actor/pkg/order" ) func TestLifecycleIdempotencyRefund(t *testing.T) { s := testServer(t) s.provider = order.NewMockProvider() ctx := context.Background() // Ingest an order to get it into Captured status body := []byte(`{ "checkoutId":"K-1","idempotencyKey":"checkout-K-1","cartId":"cart-1", "currency":"SEK","country":"SE","customerEmail":"customer@example.com", "lines":[{"reference":"l1","sku":"l1","name":"Widget","quantity":1,"unitPrice":15000,"taxRate":25}], "payment":{"provider":"klarna","reference":"ref-123","amount":15000} }`) if err := ingestOrderFromQueue(ctx, s, body); err != nil { t.Fatalf("ingest: %v", err) } orderID, ok := s.idem.Get("checkout-K-1") if !ok { t.Fatalf("expected order ID to be stored in idempotency cache") } // ── First refund request (with idempotency key) ──────────────────────── reqBody := []byte(`{"amount": 5000}`) req1 := httptest.NewRequest("POST", "/api/orders/"+order.OrderId(orderID).String()+"/refunds", bytes.NewReader(reqBody)) req1.Header.Set("Idempotency-Key", "refund-key-123") req1.SetPathValue("id", order.OrderId(orderID).String()) w1 := httptest.NewRecorder() s.handleRefund(w1, req1) if w1.Code != http.StatusOK { t.Fatalf("first refund status = %d, want 200", w1.Code) } g1, err := s.applier.Get(ctx, orderID) if err != nil { t.Fatal(err) } if g1.RefundedAmount != 5000 { t.Fatalf("refunded amount after first call = %d, want 5000", g1.RefundedAmount) } // ── Second refund request (with the same idempotency key) ────────────── req2 := httptest.NewRequest("POST", "/api/orders/"+order.OrderId(orderID).String()+"/refunds", bytes.NewReader(reqBody)) req2.Header.Set("Idempotency-Key", "refund-key-123") req2.SetPathValue("id", order.OrderId(orderID).String()) w2 := httptest.NewRecorder() s.handleRefund(w2, req2) if w2.Code != http.StatusOK { t.Fatalf("second refund status = %d, want 200", w2.Code) } g2, err := s.applier.Get(ctx, orderID) if err != nil { t.Fatal(err) } // It should NOT have refunded another 5000 (total refunded remains 5000) if g2.RefundedAmount != 5000 { t.Fatalf("refunded amount after second call = %d, want 5000 (idempotency hit failed, did double refund)", g2.RefundedAmount) } } func TestLifecycleWithoutIdempotencyRefund(t *testing.T) { s := testServer(t) s.provider = order.NewMockProvider() ctx := context.Background() // Ingest an order to get it into Captured status body := []byte(`{ "checkoutId":"K-1","idempotencyKey":"checkout-K-1","cartId":"cart-1", "currency":"SEK","country":"SE","customerEmail":"customer@example.com", "lines":[{"reference":"l1","sku":"l1","name":"Widget","quantity":1,"unitPrice":15000,"taxRate":25}], "payment":{"provider":"klarna","reference":"ref-123","amount":15000} }`) if err := ingestOrderFromQueue(ctx, s, body); err != nil { t.Fatalf("ingest: %v", err) } orderID, ok := s.idem.Get("checkout-K-1") if !ok { t.Fatalf("expected order ID to be stored in idempotency cache") } // First refund request (no idempotency key) reqBody := []byte(`{"amount": 5000}`) req1 := httptest.NewRequest("POST", "/api/orders/"+order.OrderId(orderID).String()+"/refunds", bytes.NewReader(reqBody)) req1.SetPathValue("id", order.OrderId(orderID).String()) w1 := httptest.NewRecorder() s.handleRefund(w1, req1) if w1.Code != http.StatusOK { t.Fatalf("first refund status = %d, want 200", w1.Code) } // Second refund request (no idempotency key) req2 := httptest.NewRequest("POST", "/api/orders/"+order.OrderId(orderID).String()+"/refunds", bytes.NewReader(reqBody)) req2.SetPathValue("id", order.OrderId(orderID).String()) w2 := httptest.NewRecorder() s.handleRefund(w2, req2) if w2.Code != http.StatusOK { t.Fatalf("second refund status = %d, want 200", w2.Code) } g2, err := s.applier.Get(ctx, orderID) if err != nil { t.Fatal(err) } // Without idempotency, it SHOULD have refunded twice (total 10000) if g2.RefundedAmount != 10000 { t.Fatalf("refunded amount after second call without idempotency = %d, want 10000", g2.RefundedAmount) } }