removed intent_ref from frontend

This commit is contained in:
Stephan D
2026-02-26 22:20:54 +01:00
parent 4949c4ffe0
commit e8d763eb15
25 changed files with 174 additions and 750 deletions

View File

@@ -107,9 +107,7 @@ func (r InitiatePayment) Validate() error {
type InitiatePayments struct {
PaymentBase `json:",inline"`
QuoteRef string `json:"quoteRef,omitempty"`
IntentRef string `json:"intentRef,omitempty"`
IntentRefs []string `json:"intentRefs,omitempty"`
QuoteRef string `json:"quoteRef,omitempty"`
}
func (r *InitiatePayments) Validate() error {
@@ -120,35 +118,9 @@ func (r *InitiatePayments) Validate() error {
return err
}
r.QuoteRef = strings.TrimSpace(r.QuoteRef)
r.IntentRef = strings.TrimSpace(r.IntentRef)
hasIntentRefsField := r.IntentRefs != nil
normalizedIntentRefs := make([]string, 0, len(r.IntentRefs))
seen := make(map[string]struct{}, len(r.IntentRefs))
for _, value := range r.IntentRefs {
intentRef := strings.TrimSpace(value)
if intentRef == "" {
return merrors.InvalidArgument("intentRefs must not contain empty values", "intentRefs")
}
if _, exists := seen[intentRef]; exists {
return merrors.InvalidArgument("intentRefs must contain unique values", "intentRefs")
}
seen[intentRef] = struct{}{}
normalizedIntentRefs = append(normalizedIntentRefs, intentRef)
}
if hasIntentRefsField && len(normalizedIntentRefs) == 0 {
return merrors.InvalidArgument("intentRefs must not be empty", "intentRefs")
}
r.IntentRefs = normalizedIntentRefs
if len(r.IntentRefs) == 0 {
r.IntentRefs = nil
}
if r.QuoteRef == "" {
return merrors.InvalidArgument("quoteRef is required", "quoteRef")
}
if r.IntentRef != "" && len(r.IntentRefs) > 0 {
return merrors.DataConflict("intentRef and intentRefs are mutually exclusive")
}
return nil
}

View File

@@ -28,29 +28,11 @@ func TestValidateQuoteIdempotency(t *testing.T) {
})
}
func TestInitiatePaymentsValidateIntentSelectors(t *testing.T) {
t.Run("accepts explicit intentRef", func(t *testing.T) {
req := &InitiatePayments{
PaymentBase: PaymentBase{IdempotencyKey: "idem-1"},
QuoteRef: "quote-1",
IntentRef: " intent-a ",
}
if err := req.Validate(); err != nil {
t.Fatalf("expected no error, got %v", err)
}
if got, want := req.IntentRef, "intent-a"; got != want {
t.Fatalf("intentRef mismatch: got=%q want=%q", got, want)
}
if req.IntentRefs != nil {
t.Fatalf("expected nil intentRefs, got %#v", req.IntentRefs)
}
})
t.Run("accepts explicit intentRefs", func(t *testing.T) {
func TestInitiatePaymentsValidate(t *testing.T) {
t.Run("accepts quoteRef", func(t *testing.T) {
req := &InitiatePayments{
PaymentBase: PaymentBase{IdempotencyKey: "idem-1"},
QuoteRef: " quote-1 ",
IntentRefs: []string{" intent-a ", "intent-b"},
}
if err := req.Validate(); err != nil {
t.Fatalf("expected no error, got %v", err)
@@ -58,66 +40,14 @@ func TestInitiatePaymentsValidateIntentSelectors(t *testing.T) {
if got, want := req.QuoteRef, "quote-1"; got != want {
t.Fatalf("quoteRef mismatch: got=%q want=%q", got, want)
}
if got, want := len(req.IntentRefs), 2; got != want {
t.Fatalf("intentRefs length mismatch: got=%d want=%d", got, want)
}
if got, want := req.IntentRefs[0], "intent-a"; got != want {
t.Fatalf("intentRefs[0] mismatch: got=%q want=%q", got, want)
}
})
t.Run("rejects both intentRef and intentRefs", func(t *testing.T) {
t.Run("rejects missing quoteRef", func(t *testing.T) {
req := &InitiatePayments{
PaymentBase: PaymentBase{IdempotencyKey: "idem-1"},
QuoteRef: "quote-1",
IntentRef: "intent-a",
IntentRefs: []string{"intent-b"},
}
if err := req.Validate(); err == nil {
t.Fatal("expected error")
}
})
t.Run("rejects empty intentRefs item", func(t *testing.T) {
req := &InitiatePayments{
PaymentBase: PaymentBase{IdempotencyKey: "idem-1"},
QuoteRef: "quote-1",
IntentRefs: []string{"intent-a", " "},
}
if err := req.Validate(); err == nil {
t.Fatal("expected error")
}
})
t.Run("rejects empty intentRefs list", func(t *testing.T) {
req := &InitiatePayments{
PaymentBase: PaymentBase{IdempotencyKey: "idem-1"},
QuoteRef: "quote-1",
IntentRefs: []string{},
}
if err := req.Validate(); err == nil {
t.Fatal("expected error")
}
})
t.Run("rejects duplicate intentRefs", func(t *testing.T) {
req := &InitiatePayments{
PaymentBase: PaymentBase{IdempotencyKey: "idem-1"},
QuoteRef: "quote-1",
IntentRefs: []string{"intent-a", " intent-a "},
}
if err := req.Validate(); err == nil {
t.Fatal("expected error")
}
})
t.Run("accepts no selectors for backward compatibility", func(t *testing.T) {
req := &InitiatePayments{
PaymentBase: PaymentBase{IdempotencyKey: "idem-1"},
QuoteRef: "quote-1",
}
if err := req.Validate(); err != nil {
t.Fatalf("expected no error, got %v", err)
}
})
}

View File

@@ -61,7 +61,9 @@ func (a *PaymentAPI) initiatePayment(r *http.Request, account *model.Account, to
}
quotationRef := strings.TrimSpace(payload.QuoteRef)
intentRef := metadataValue(payload.Metadata, "intent_ref")
if metadataValue(payload.Metadata, "intent_ref") != "" {
return response.BadPayload(a.logger, a.Name(), merrors.InvalidArgument("metadata.intent_ref is no longer supported", "metadata.intent_ref"))
}
if payload.Intent != nil {
applyCustomerIP(payload.Intent, r.RemoteAddr)
intent, err := mapQuoteIntent(payload.Intent)
@@ -82,15 +84,11 @@ func (a *PaymentAPI) initiatePayment(r *http.Request, account *model.Account, to
if quotationRef == "" {
return response.Auto(a.logger, a.Name(), merrors.DataConflict("quotation service returned empty quote_ref"))
}
if derived := strings.TrimSpace(quoteResp.GetQuote().GetIntentRef()); derived != "" {
intentRef = derived
}
}
req := &orchestrationv2.ExecutePaymentRequest{
Meta: requestMeta(orgRef.Hex(), payload.IdempotencyKey),
QuotationRef: quotationRef,
IntentRef: intentRef,
ClientPaymentRef: metadataValue(payload.Metadata, "client_payment_ref"),
}

View File

@@ -0,0 +1,68 @@
package paymentapiimp
import (
"bytes"
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/go-chi/chi/v5"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/server/interface/api/sresponse"
"go.mongodb.org/mongo-driver/v2/bson"
)
func TestInitiateByQuote_DoesNotUseIntentRef(t *testing.T) {
orgRef := bson.NewObjectID()
exec := &fakeExecutionClientForBatch{}
api := newBatchAPI(exec)
body := `{"idempotencyKey":"idem-by-quote","quoteRef":"quote-1","metadata":{"client_payment_ref":"client-ref-1"}}`
rr := invokeInitiateByQuote(t, api, orgRef, body)
if got, want := rr.Code, http.StatusOK; got != want {
t.Fatalf("status mismatch: got=%d want=%d body=%s", got, want, rr.Body.String())
}
if got, want := len(exec.executeReqs), 1; got != want {
t.Fatalf("execute calls mismatch: got=%d want=%d", got, want)
}
if got, want := exec.executeReqs[0].GetIntentRef(), ""; got != want {
t.Fatalf("intent_ref mismatch: got=%q want=%q", got, want)
}
if got, want := exec.executeReqs[0].GetClientPaymentRef(), "client-ref-1"; got != want {
t.Fatalf("client_payment_ref mismatch: got=%q want=%q", got, want)
}
}
func TestInitiateByQuote_RejectsMetadataIntentRef(t *testing.T) {
orgRef := bson.NewObjectID()
exec := &fakeExecutionClientForBatch{}
api := newBatchAPI(exec)
body := `{"idempotencyKey":"idem-by-quote","quoteRef":"quote-1","metadata":{"intent_ref":"legacy-intent"}}`
rr := invokeInitiateByQuote(t, api, orgRef, body)
if got, want := rr.Code, http.StatusBadRequest; got != want {
t.Fatalf("status mismatch: got=%d want=%d body=%s", got, want, rr.Body.String())
}
if got := len(exec.executeReqs); got != 0 {
t.Fatalf("expected no execute calls, got=%d", got)
}
}
func invokeInitiateByQuote(t *testing.T, api *PaymentAPI, orgRef bson.ObjectID, body string) *httptest.ResponseRecorder {
t.Helper()
req := httptest.NewRequest(http.MethodPost, "/by-quote", bytes.NewBufferString(body))
routeCtx := chi.NewRouteContext()
routeCtx.URLParams.Add("organizations_ref", orgRef.Hex())
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, routeCtx))
rr := httptest.NewRecorder()
handler := api.initiateByQuote(req, &model.Account{}, &sresponse.TokenData{
Token: "token",
Expiration: time.Now().UTC().Add(time.Hour),
})
handler.ServeHTTP(rr, req)
return rr
}

View File

@@ -39,100 +39,39 @@ func (a *PaymentAPI) initiatePaymentsByQuote(r *http.Request, account *model.Acc
return response.BadPayload(a.logger, a.Name(), err)
}
intentSelectors, err := resolveExecutionIntentSelectors(payload, a.isLegacyMetadataIntentRefFallbackAllowed())
if err != nil {
return response.BadPayload(a.logger, a.Name(), err)
}
clientPaymentRef := metadataValue(payload.Metadata, "client_payment_ref")
baseIdempotencyKey := strings.TrimSpace(payload.IdempotencyKey)
idempotencyKey := strings.TrimSpace(payload.IdempotencyKey)
quotationRef := strings.TrimSpace(payload.QuoteRef)
executeOne := func(idempotencyKey, intentRef string) (*orchestrationv2.Payment, error) {
req := &orchestrationv2.ExecutePaymentRequest{
Meta: requestMeta(orgRef.Hex(), idempotencyKey),
QuotationRef: quotationRef,
IntentRef: strings.TrimSpace(intentRef),
ClientPaymentRef: clientPaymentRef,
}
resp, executeErr := a.execution.ExecutePayment(ctx, req)
if executeErr != nil {
return nil, executeErr
}
return resp.GetPayment(), nil
req := &orchestrationv2.ExecuteBatchPaymentRequest{
Meta: requestMeta(orgRef.Hex(), idempotencyKey),
QuotationRef: quotationRef,
ClientPaymentRef: clientPaymentRef,
}
executeBatch := func(idempotencyKey string) ([]*orchestrationv2.Payment, error) {
req := &orchestrationv2.ExecuteBatchPaymentRequest{
Meta: requestMeta(orgRef.Hex(), idempotencyKey),
QuotationRef: quotationRef,
ClientPaymentRef: clientPaymentRef,
}
resp, executeErr := a.execution.ExecuteBatchPayment(ctx, req)
if executeErr != nil {
return nil, executeErr
}
if resp == nil {
return nil, nil
}
return resp.GetPayments(), nil
}
payments := make([]*orchestrationv2.Payment, 0, max(1, len(intentSelectors)))
if len(payload.IntentRefs) > 0 {
executed, executeErr := executeBatch(baseIdempotencyKey)
if executeErr != nil {
a.logger.Warn("Failed to initiate batch payments", zap.Error(executeErr), zap.String("organization_ref", orgRef.Hex()))
return grpcErrorResponse(a.logger, a.Name(), executeErr)
}
payments = append(payments, executed...)
return sresponse.PaymentsResponse(a.logger, payments, token)
}
intentRef := ""
if len(intentSelectors) > 0 {
intentRef = intentSelectors[0]
}
payment, err := executeOne(baseIdempotencyKey, intentRef)
resp, err := a.execution.ExecuteBatchPayment(ctx, req)
if err != nil {
a.logger.Warn("Failed to initiate batch payments", zap.Error(err), zap.String("organization_ref", orgRef.Hex()))
return grpcErrorResponse(a.logger, a.Name(), err)
}
if payment != nil {
payments = append(payments, payment)
payments := make([]*orchestrationv2.Payment, 0)
if resp != nil {
payments = append(payments, resp.GetPayments()...)
}
return sresponse.PaymentsResponse(a.logger, payments, token)
}
func resolveExecutionIntentSelectors(payload *srequest.InitiatePayments, allowLegacyMetadataIntentRef bool) ([]string, error) {
if payload == nil {
return nil, nil
}
if len(payload.IntentRefs) > 0 {
return append([]string(nil), payload.IntentRefs...), nil
}
if intentRef := strings.TrimSpace(payload.IntentRef); intentRef != "" {
return []string{intentRef}, nil
}
legacy := metadataValue(payload.Metadata, "intent_ref")
if legacy == "" {
return nil, nil
}
if allowLegacyMetadataIntentRef {
return []string{legacy}, nil
}
return nil, merrors.InvalidArgument("metadata.intent_ref is no longer supported; use intentRef or intentRefs", "metadata.intent_ref")
}
func decodeInitiatePaymentsPayload(r *http.Request) (*srequest.InitiatePayments, error) {
defer r.Body.Close()
payload := &srequest.InitiatePayments{}
if err := json.NewDecoder(r.Body).Decode(payload); err != nil {
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
if err := decoder.Decode(payload); err != nil {
return nil, merrors.InvalidArgument("invalid payload: " + err.Error())
}
payload.IdempotencyKey = strings.TrimSpace(payload.IdempotencyKey)
payload.QuoteRef = strings.TrimSpace(payload.QuoteRef)
payload.IntentRef = strings.TrimSpace(payload.IntentRef)
if err := payload.Validate(); err != nil {
return nil, err

View File

@@ -3,7 +3,6 @@ package paymentapiimp
import (
"bytes"
"context"
"errors"
"net/http"
"net/http/httptest"
"testing"
@@ -11,24 +10,22 @@ import (
"github.com/go-chi/chi/v5"
"github.com/tech/sendico/pkg/auth"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
"github.com/tech/sendico/server/interface/api/srequest"
"github.com/tech/sendico/server/interface/api/sresponse"
mutil "github.com/tech/sendico/server/internal/mutil/param"
"go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap"
)
func TestInitiatePaymentsByQuote_PassesIntentRefsInSingleExecuteCall(t *testing.T) {
func TestInitiatePaymentsByQuote_ExecutesBatchPayment(t *testing.T) {
orgRef := bson.NewObjectID()
exec := &fakeExecutionClientForBatch{}
api := newBatchAPI(exec)
body := `{"idempotencyKey":"idem-batch","quoteRef":"quote-1","intentRefs":["intent-a","intent-b"]}`
body := `{"idempotencyKey":"idem-batch","quoteRef":"quote-1"}`
rr := invokeInitiatePaymentsByQuote(t, api, orgRef, body)
if got, want := rr.Code, http.StatusOK; got != want {
t.Fatalf("status mismatch: got=%d want=%d body=%s", got, want, rr.Body.String())
@@ -48,53 +45,34 @@ func TestInitiatePaymentsByQuote_PassesIntentRefsInSingleExecuteCall(t *testing.
}
}
func TestInitiatePaymentsByQuote_UsesExplicitIntentRef(t *testing.T) {
func TestInitiatePaymentsByQuote_ForwardsClientPaymentRef(t *testing.T) {
orgRef := bson.NewObjectID()
exec := &fakeExecutionClientForBatch{}
api := newBatchAPI(exec)
body := `{"idempotencyKey":"idem-single","quoteRef":"quote-1","intentRef":"intent-x"}`
body := `{"idempotencyKey":"idem-batch","quoteRef":"quote-1","metadata":{"client_payment_ref":"client-ref-1"}}`
rr := invokeInitiatePaymentsByQuote(t, api, orgRef, body)
if got, want := rr.Code, http.StatusOK; got != want {
t.Fatalf("status mismatch: got=%d want=%d body=%s", got, want, rr.Body.String())
}
if got, want := len(exec.executeReqs), 1; got != want {
t.Fatalf("execute calls mismatch: got=%d want=%d", got, want)
if got, want := len(exec.executeBatchReqs), 1; got != want {
t.Fatalf("execute batch calls mismatch: got=%d want=%d", got, want)
}
if got, want := exec.executeReqs[0].GetIntentRef(), "intent-x"; got != want {
t.Fatalf("intent_ref mismatch: got=%q want=%q", got, want)
if got, want := exec.executeBatchReqs[0].GetClientPaymentRef(), "client-ref-1"; got != want {
t.Fatalf("client_payment_ref mismatch: got=%q want=%q", got, want)
}
if got, want := exec.executeReqs[0].GetMeta().GetTrace().GetIdempotencyKey(), "idem-single"; got != want {
t.Fatalf("idempotency mismatch: got=%q want=%q", got, want)
if got := len(exec.executeReqs); got != 0 {
t.Fatalf("expected no execute calls, got=%d", got)
}
}
func TestInitiatePaymentsByQuote_UsesLegacyMetadataIntentRefFallback(t *testing.T) {
orgRef := bson.NewObjectID()
exec := &fakeExecutionClientForBatch{}
api := newBatchAPIWithLegacyFallback(exec, true, time.Time{})
body := `{"idempotencyKey":"idem-legacy","quoteRef":"quote-1","metadata":{"intent_ref":"intent-legacy"}}`
rr := invokeInitiatePaymentsByQuote(t, api, orgRef, body)
if got, want := rr.Code, http.StatusOK; got != want {
t.Fatalf("status mismatch: got=%d want=%d body=%s", got, want, rr.Body.String())
}
if got, want := len(exec.executeReqs), 1; got != want {
t.Fatalf("execute calls mismatch: got=%d want=%d", got, want)
}
if got, want := exec.executeReqs[0].GetIntentRef(), "intent-legacy"; got != want {
t.Fatalf("intent_ref mismatch: got=%q want=%q", got, want)
}
}
func TestInitiatePaymentsByQuote_RejectsLegacyMetadataIntentRefFallbackByDefault(t *testing.T) {
func TestInitiatePaymentsByQuote_RejectsDeprecatedIntentRefField(t *testing.T) {
orgRef := bson.NewObjectID()
exec := &fakeExecutionClientForBatch{}
api := newBatchAPI(exec)
body := `{"idempotencyKey":"idem-legacy","quoteRef":"quote-1","metadata":{"intent_ref":"intent-legacy"}}`
body := `{"idempotencyKey":"idem-batch","quoteRef":"quote-1","intentRef":"intent-legacy"}`
rr := invokeInitiatePaymentsByQuote(t, api, orgRef, body)
if got, want := rr.Code, http.StatusBadRequest; got != want {
t.Fatalf("status mismatch: got=%d want=%d body=%s", got, want, rr.Body.String())
@@ -104,14 +82,12 @@ func TestInitiatePaymentsByQuote_RejectsLegacyMetadataIntentRefFallbackByDefault
}
}
func TestInitiatePaymentsByQuote_RejectsLegacyMetadataIntentRefWhenDateGateExpired(t *testing.T) {
func TestInitiatePaymentsByQuote_RejectsDeprecatedIntentRefsField(t *testing.T) {
orgRef := bson.NewObjectID()
exec := &fakeExecutionClientForBatch{}
now := time.Date(2026, time.January, 10, 12, 0, 0, 0, time.UTC)
api := newBatchAPIWithLegacyFallback(exec, true, now.Add(-time.Minute))
api.clock = func() time.Time { return now }
api := newBatchAPI(exec)
body := `{"idempotencyKey":"idem-legacy","quoteRef":"quote-1","metadata":{"intent_ref":"intent-legacy"}}`
body := `{"idempotencyKey":"idem-batch","quoteRef":"quote-1","intentRefs":["intent-a","intent-b"]}`
rr := invokeInitiatePaymentsByQuote(t, api, orgRef, body)
if got, want := rr.Code, http.StatusBadRequest; got != want {
t.Fatalf("status mismatch: got=%d want=%d body=%s", got, want, rr.Body.String())
@@ -121,67 +97,13 @@ func TestInitiatePaymentsByQuote_RejectsLegacyMetadataIntentRefWhenDateGateExpir
}
}
func TestResolveExecutionIntentSelectors_PrefersExplicitSelectors(t *testing.T) {
payload := &srequest.InitiatePayments{
IntentRefs: []string{"intent-a", "intent-b"},
IntentRef: "intent-single",
PaymentBase: srequest.PaymentBase{
Metadata: map[string]string{"intent_ref": "intent-legacy"},
},
}
got, err := resolveExecutionIntentSelectors(payload, false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got == nil || len(got) != 2 {
t.Fatalf("unexpected selectors: %#v", got)
}
if got[0] != "intent-a" || got[1] != "intent-b" {
t.Fatalf("unexpected selectors order/value: %#v", got)
}
}
func TestResolveExecutionIntentSelectors_RejectsLegacyMetadataSelectorWhenDisabled(t *testing.T) {
payload := &srequest.InitiatePayments{
PaymentBase: srequest.PaymentBase{
Metadata: map[string]string{"intent_ref": "intent-legacy"},
},
}
_, err := resolveExecutionIntentSelectors(payload, false)
if !errors.Is(err, merrors.ErrInvalidArg) {
t.Fatalf("expected invalid argument, got %v", err)
}
}
func TestResolveExecutionIntentSelectors_UsesLegacyMetadataSelectorWhenEnabled(t *testing.T) {
payload := &srequest.InitiatePayments{
PaymentBase: srequest.PaymentBase{
Metadata: map[string]string{"intent_ref": "intent-legacy"},
},
}
got, err := resolveExecutionIntentSelectors(payload, true)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got == nil || len(got) != 1 || got[0] != "intent-legacy" {
t.Fatalf("unexpected selectors: %#v", got)
}
}
func newBatchAPI(exec executionClient) *PaymentAPI {
return newBatchAPIWithLegacyFallback(exec, false, time.Time{})
}
func newBatchAPIWithLegacyFallback(exec executionClient, enabled bool, until time.Time) *PaymentAPI {
return &PaymentAPI{
logger: mlogger.Logger(zap.NewNop()),
execution: exec,
enf: fakeEnforcerForBatch{allowed: true},
oph: mutil.CreatePH(mservice.Organizations),
permissionRef: bson.NewObjectID(),
legacyMetadataIntentRefFallbackEnabled: enabled,
legacyMetadataIntentRefFallbackUntil: until,
clock: time.Now,
logger: mlogger.Logger(zap.NewNop()),
execution: exec,
enf: fakeEnforcerForBatch{allowed: true},
oph: mutil.CreatePH(mservice.Organizations),
permissionRef: bson.NewObjectID(),
}
}

View File

@@ -5,7 +5,6 @@ import (
"crypto/tls"
"fmt"
"os"
"strconv"
"strings"
"sync"
"time"
@@ -30,11 +29,6 @@ import (
"google.golang.org/grpc/credentials/insecure"
)
const (
envLegacyMetadataIntentRefFallbackEnabled = "PAYMENTS_LEGACY_METADATA_INTENT_REF_FALLBACK"
envLegacyMetadataIntentRefFallbackUntil = "PAYMENTS_LEGACY_METADATA_INTENT_REF_FALLBACK_UNTIL"
)
type executionClient interface {
ExecutePayment(ctx context.Context, req *orchestrationv2.ExecutePaymentRequest) (*orchestrationv2.ExecutePaymentResponse, error)
ExecuteBatchPayment(ctx context.Context, req *orchestrationv2.ExecuteBatchPaymentRequest) (*orchestrationv2.ExecuteBatchPaymentResponse, error)
@@ -59,10 +53,6 @@ type PaymentAPI struct {
refreshMu sync.RWMutex
refreshEvent *discovery.RefreshEvent
legacyMetadataIntentRefFallbackEnabled bool
legacyMetadataIntentRefFallbackUntil time.Time
clock func() time.Time
permissionRef bson.ObjectID
}
@@ -93,7 +83,6 @@ func CreateAPI(apiCtx eapi.API) (*PaymentAPI, error) {
logger: apiCtx.Logger().Named(mservice.Payments),
enf: apiCtx.Permissions().Enforcer(),
oph: mutil.CreatePH(mservice.Organizations),
clock: time.Now,
}
desc, err := apiCtx.Permissions().GetPolicyDescription(context.Background(), mservice.Payments)
@@ -107,7 +96,6 @@ func CreateAPI(apiCtx eapi.API) (*PaymentAPI, error) {
p.logger.Error("Failed to initialize payment orchestrator client", zap.Error(err))
return nil, err
}
p.configureLegacyMetadataIntentRefFallback()
if err := p.initDiscoveryClient(apiCtx.Config()); err != nil {
p.logger.Warn("Failed to initialize discovery client", zap.Error(err))
}
@@ -303,79 +291,3 @@ func (a *PaymentAPI) initDiscoveryClient(cfg *eapi.Config) error {
}()
return nil
}
func (a *PaymentAPI) configureLegacyMetadataIntentRefFallback() {
enabled := false
enabledRaw := strings.TrimSpace(os.Getenv(envLegacyMetadataIntentRefFallbackEnabled))
if enabledRaw != "" {
parsed, err := strconv.ParseBool(enabledRaw)
if err != nil {
a.logger.Warn("Invalid legacy metadata intent_ref fallback flag, disabling fallback",
zap.String("env", envLegacyMetadataIntentRefFallbackEnabled),
zap.String("value", enabledRaw),
zap.Error(err),
)
} else {
enabled = parsed
}
}
until := time.Time{}
untilRaw := strings.TrimSpace(os.Getenv(envLegacyMetadataIntentRefFallbackUntil))
if untilRaw != "" {
parsed, err := parseLegacyMetadataIntentRefFallbackDeadline(untilRaw)
if err != nil {
a.logger.Warn("Invalid legacy metadata intent_ref fallback deadline, ignoring deadline",
zap.String("env", envLegacyMetadataIntentRefFallbackUntil),
zap.String("value", untilRaw),
zap.Error(err),
)
} else {
until = parsed
}
}
a.legacyMetadataIntentRefFallbackEnabled = enabled
a.legacyMetadataIntentRefFallbackUntil = until
if !enabled {
return
}
fields := []zap.Field{
zap.String("env_flag", envLegacyMetadataIntentRefFallbackEnabled),
zap.String("env_until", envLegacyMetadataIntentRefFallbackUntil),
}
if !until.IsZero() {
fields = append(fields, zap.Time("until_utc", until.UTC()))
}
a.logger.Warn("Legacy metadata.intent_ref fallback is enabled for /by-multiquote", fields...)
}
func (a *PaymentAPI) isLegacyMetadataIntentRefFallbackAllowed() bool {
if a == nil || !a.legacyMetadataIntentRefFallbackEnabled {
return false
}
if a.legacyMetadataIntentRefFallbackUntil.IsZero() {
return true
}
now := time.Now().UTC()
if a.clock != nil {
now = a.clock().UTC()
}
return now.Before(a.legacyMetadataIntentRefFallbackUntil.UTC())
}
func parseLegacyMetadataIntentRefFallbackDeadline(value string) (time.Time, error) {
raw := strings.TrimSpace(value)
if raw == "" {
return time.Time{}, merrors.InvalidArgument("deadline is required")
}
if ts, err := time.Parse(time.RFC3339, raw); err == nil {
return ts.UTC(), nil
}
if date, err := time.Parse("2006-01-02", raw); err == nil {
// Date-only values are treated as inclusive; disable fallback at the next UTC midnight.
return date.UTC().Add(24 * time.Hour), nil
}
return time.Time{}, merrors.InvalidArgument("deadline must be RFC3339 or YYYY-MM-DD")
}

View File

@@ -1,82 +0,0 @@
package paymentapiimp
import (
"testing"
"time"
)
func TestParseLegacyMetadataIntentRefFallbackDeadline(t *testing.T) {
t.Run("parses RFC3339", func(t *testing.T) {
got, err := parseLegacyMetadataIntentRefFallbackDeadline("2026-02-26T12:00:00Z")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
want := time.Date(2026, time.February, 26, 12, 0, 0, 0, time.UTC)
if !got.Equal(want) {
t.Fatalf("deadline mismatch: got=%s want=%s", got.UTC(), want.UTC())
}
})
t.Run("parses date-only as inclusive UTC day", func(t *testing.T) {
got, err := parseLegacyMetadataIntentRefFallbackDeadline("2026-02-26")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
want := time.Date(2026, time.February, 27, 0, 0, 0, 0, time.UTC)
if !got.Equal(want) {
t.Fatalf("deadline mismatch: got=%s want=%s", got.UTC(), want.UTC())
}
})
t.Run("rejects invalid format", func(t *testing.T) {
if _, err := parseLegacyMetadataIntentRefFallbackDeadline("26-02-2026"); err == nil {
t.Fatal("expected error")
}
})
}
func TestIsLegacyMetadataIntentRefFallbackAllowed(t *testing.T) {
now := time.Date(2026, time.February, 26, 12, 0, 0, 0, time.UTC)
t.Run("disabled", func(t *testing.T) {
api := &PaymentAPI{
legacyMetadataIntentRefFallbackEnabled: false,
clock: func() time.Time { return now },
}
if api.isLegacyMetadataIntentRefFallbackAllowed() {
t.Fatal("expected disabled fallback")
}
})
t.Run("enabled without deadline", func(t *testing.T) {
api := &PaymentAPI{
legacyMetadataIntentRefFallbackEnabled: true,
clock: func() time.Time { return now },
}
if !api.isLegacyMetadataIntentRefFallbackAllowed() {
t.Fatal("expected enabled fallback")
}
})
t.Run("enabled with future deadline", func(t *testing.T) {
api := &PaymentAPI{
legacyMetadataIntentRefFallbackEnabled: true,
legacyMetadataIntentRefFallbackUntil: now.Add(time.Minute),
clock: func() time.Time { return now },
}
if !api.isLegacyMetadataIntentRefFallbackAllowed() {
t.Fatal("expected enabled fallback before deadline")
}
})
t.Run("enabled with past deadline", func(t *testing.T) {
api := &PaymentAPI{
legacyMetadataIntentRefFallbackEnabled: true,
legacyMetadataIntentRefFallbackUntil: now.Add(-time.Minute),
clock: func() time.Time { return now },
}
if api.isLegacyMetadataIntentRefFallbackAllowed() {
t.Fatal("expected disabled fallback after deadline")
}
})
}