mirror of
https://github.com/marcusDenslow/terminalShop.git
synced 2026-10-25 18:08:45 +01:00
Compare commits
1 Commits
master
...
ca46544518
| Author | SHA1 | Date | |
|---|---|---|---|
| ca46544518 |
@@ -5,6 +5,7 @@ import (
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"terminalShop/api/middleware"
|
||||
"terminalShop/pkg/audit"
|
||||
"terminalShop/pkg/database"
|
||||
"terminalShop/pkg/models"
|
||||
@@ -116,12 +117,14 @@ func ReconcileStale3DSOrders(threshold time.Duration) {
|
||||
// one UPDATE so this should be unreachable; guard anyway so a
|
||||
// future regression noisily logs instead of 4xx'ing Stripe.
|
||||
reconcileLog().Warn("requires_action row missing PI id; skipping", "order_id", order.ID)
|
||||
middleware.RecordAbandoned3DSSweep("missing_pi")
|
||||
continue
|
||||
}
|
||||
actual, err := paymentIntentGetFn(order.StripePaymentID, nil)
|
||||
if err != nil {
|
||||
reconcileLog().Error("stripe get failed",
|
||||
"order_id", order.ID, "pi", order.StripePaymentID, "error", err)
|
||||
middleware.RecordAbandoned3DSSweep("stripe_error")
|
||||
continue
|
||||
}
|
||||
switch actual.Status {
|
||||
@@ -133,10 +136,12 @@ func ReconcileStale3DSOrders(threshold time.Duration) {
|
||||
// tick re-checks and surfaces the dropped webhook if it persists
|
||||
reconcileLog().Warn("stripe reports succeeded for requires_action row; deferring to webhook",
|
||||
"order_id", order.ID, "pi", order.StripePaymentID)
|
||||
middleware.RecordAbandoned3DSSweep("succeeded_skip")
|
||||
case stripe.PaymentIntentStatusProcessing,
|
||||
stripe.PaymentIntentStatusRequiresCapture:
|
||||
reconcileLog().Info("payment intent still transitioning; skipping",
|
||||
"order_id", order.ID, "pi", order.StripePaymentID, "stripe_status", string(actual.Status))
|
||||
middleware.RecordAbandoned3DSSweep("processing_skip")
|
||||
case stripe.PaymentIntentStatusRequiresAction,
|
||||
stripe.PaymentIntentStatusRequiresPaymentMethod,
|
||||
stripe.PaymentIntentStatusRequiresConfirmation,
|
||||
@@ -145,6 +150,7 @@ func ReconcileStale3DSOrders(threshold time.Duration) {
|
||||
default:
|
||||
reconcileLog().Warn("unrecognized stripe status; skipping",
|
||||
"order_id", order.ID, "pi", order.StripePaymentID, "stripe_status", string(actual.Status))
|
||||
middleware.RecordAbandoned3DSSweep("unrecognized_status")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,16 +168,19 @@ func flipStale3DSToFailed(db *gorm.DB, order *models.Order, stripeStatus string)
|
||||
Updates(map[string]any{"status": models.OrderStatusFailed})
|
||||
if res.Error != nil {
|
||||
reconcileLog().Error("flip stale 3ds failed", "order_id", order.ID, "error", res.Error)
|
||||
middleware.RecordAbandoned3DSSweep("flip_failed")
|
||||
return
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
reconcileLog().Info("stale 3ds row already transitioned; sweep no-op",
|
||||
"order_id", order.ID)
|
||||
middleware.RecordAbandoned3DSSweep("flip_noop")
|
||||
return
|
||||
}
|
||||
audit.Order3DSAbandoned(order.UserID, order.ID, order.StripePaymentID)
|
||||
reconcileLog().Info("3ds abandoned order flipped to failed",
|
||||
"order_id", order.ID, "pi", order.StripePaymentID, "stripe_status", stripeStatus)
|
||||
middleware.RecordAbandoned3DSSweep("flipped")
|
||||
}
|
||||
|
||||
// ReconcileExpiredCards removes saved cards whose retention deadline elapsed.
|
||||
|
||||
@@ -6,11 +6,13 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stripe/stripe-go/v78"
|
||||
|
||||
"terminalShop/api/middleware"
|
||||
"terminalShop/pkg/audit"
|
||||
"terminalShop/pkg/database"
|
||||
"terminalShop/pkg/models"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/testutil"
|
||||
"github.com/stripe/stripe-go/v78"
|
||||
)
|
||||
|
||||
// TestReconcileStale3DSOrders_FlipsAbandonedToFailed verifies the predicate
|
||||
@@ -39,6 +41,9 @@ func TestReconcileStale3DSOrders_FlipsAbandonedToFailed(t *testing.T) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
flipped := middleware.Abandoned3DSSweepCounter().WithLabelValues("flipped")
|
||||
before := testutil.ToFloat64(flipped)
|
||||
|
||||
ReconcileStale3DSOrders(30 * time.Minute)
|
||||
|
||||
var reloaded models.Order
|
||||
@@ -57,6 +62,9 @@ func TestReconcileStale3DSOrders_FlipsAbandonedToFailed(t *testing.T) {
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("want 1 order_3ds_abandoned audit row, got %d", len(events))
|
||||
}
|
||||
if got := testutil.ToFloat64(flipped) - before; got != 1 {
|
||||
t.Fatalf("want 1 flipped sweep counter increment, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestReconcileStale3DSOrders_SkipsRecent verifies the threshold predicate
|
||||
@@ -124,6 +132,9 @@ func TestReconcileStale3DSOrders_SkipsWhenStripeSaysSucceeded(t *testing.T) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
skip := middleware.Abandoned3DSSweepCounter().WithLabelValues("succeeded_skip")
|
||||
before := testutil.ToFloat64(skip)
|
||||
|
||||
ReconcileStale3DSOrders(30 * time.Minute)
|
||||
|
||||
var reloaded models.Order
|
||||
@@ -133,6 +144,9 @@ func TestReconcileStale3DSOrders_SkipsWhenStripeSaysSucceeded(t *testing.T) {
|
||||
if reloaded.Status != models.OrderStatusRequiresAction {
|
||||
t.Fatalf("must defer to webhook on succeeded; got %q", reloaded.Status)
|
||||
}
|
||||
if got := testutil.ToFloat64(skip) - before; got != 1 {
|
||||
t.Fatalf("want 1 succeeded_skip sweep counter increment, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestReconcileStale3DSOrders_GuardsAgainstWebhookRace covers the conditional
|
||||
@@ -161,6 +175,9 @@ func TestReconcileStale3DSOrders_GuardsAgainstWebhookRace(t *testing.T) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
noop := middleware.Abandoned3DSSweepCounter().WithLabelValues("flip_noop")
|
||||
before := testutil.ToFloat64(noop)
|
||||
|
||||
ReconcileStale3DSOrders(30 * time.Minute)
|
||||
|
||||
var reloaded models.Order
|
||||
@@ -170,6 +187,9 @@ func TestReconcileStale3DSOrders_GuardsAgainstWebhookRace(t *testing.T) {
|
||||
if reloaded.Status != models.OrderStatusPaid {
|
||||
t.Fatalf("conditional update must preserve paid; got %q", reloaded.Status)
|
||||
}
|
||||
if got := testutil.ToFloat64(noop) - before; got != 1 {
|
||||
t.Fatalf("want 1 flip_noop sweep counter increment, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestReconcileStale3DSOrders_FlipsCanceledPI covers the canceled-PI branch:
|
||||
@@ -344,6 +364,9 @@ func TestReconcileStale3DSOrders_SkipsOnStripeError(t *testing.T) {
|
||||
return nil, fmt.Errorf("simulated stripe outage")
|
||||
}
|
||||
|
||||
stripeErr := middleware.Abandoned3DSSweepCounter().WithLabelValues("stripe_error")
|
||||
before := testutil.ToFloat64(stripeErr)
|
||||
|
||||
ReconcileStale3DSOrders(30 * time.Minute)
|
||||
|
||||
var reloaded models.Order
|
||||
@@ -353,6 +376,9 @@ func TestReconcileStale3DSOrders_SkipsOnStripeError(t *testing.T) {
|
||||
if reloaded.Status != models.OrderStatusRequiresAction {
|
||||
t.Fatalf("stripe error must not flip; got %q", reloaded.Status)
|
||||
}
|
||||
if got := testutil.ToFloat64(stripeErr) - before; got != 1 {
|
||||
t.Fatalf("want 1 stripe_error sweep counter increment, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestReconcileStale3DSOrders_SkipsEmptyPaymentIntentID guards the
|
||||
|
||||
@@ -70,6 +70,14 @@ var (
|
||||
[]string{"outcome"},
|
||||
)
|
||||
|
||||
abandoned3DSSweepTotal = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "abandoned_3ds_sweep_total",
|
||||
Help: "ReconcileStale3DSOrders outcomes per row processed (flipped, flip_noop, flip_failed, succeeded_skip, processing_skip, stripe_error, missing_pi, unrecognized_status)",
|
||||
},
|
||||
[]string{"outcome"},
|
||||
)
|
||||
|
||||
orderValueDollars = promauto.NewHistogram(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "order_value_dollars",
|
||||
@@ -173,6 +181,19 @@ func ObserveOrderValueCents(cents int) {
|
||||
orderValueDollars.Observe(float64(cents) / 100.0)
|
||||
}
|
||||
|
||||
// RecordAbandoned3DSSweep records the outcome of one row processed by
|
||||
// ReconcileStale3DSOrders. Bounded enumeration documented on the counter's
|
||||
// Help string.
|
||||
func RecordAbandoned3DSSweep(outcome string) {
|
||||
abandoned3DSSweepTotal.WithLabelValues(outcome).Inc()
|
||||
}
|
||||
|
||||
// Abandoned3DSSweepCounter exposes the sweep counter so tests can snapshot
|
||||
// per-label values via prometheus testutil. Not used in production code.
|
||||
func Abandoned3DSSweepCounter() *prometheus.CounterVec {
|
||||
return abandoned3DSSweepTotal
|
||||
}
|
||||
|
||||
// ObserveStripeRequest records latency of a Stripe API call.
|
||||
func ObserveStripeRequest(op, outcome string, durSec float64) {
|
||||
stripeRequestDuration.WithLabelValues(op, outcome).Observe(durSec)
|
||||
|
||||
1
go.mod
1
go.mod
@@ -81,6 +81,7 @@ require (
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
|
||||
Reference in New Issue
Block a user