Batch payment execution + got rid of intent references

This commit is contained in:
Stephan D
2026-02-26 22:12:32 +01:00
parent 7661038868
commit 4949c4ffe0
16 changed files with 891 additions and 78 deletions

View File

@@ -1,8 +1,6 @@
package paymentapiimp
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"net/http"
"strings"
@@ -18,11 +16,6 @@ import (
"go.uber.org/zap"
)
const (
fanoutIdempotencyHashLen = 16
maxExecuteIdempotencyKey = 256
)
func (a *PaymentAPI) initiatePaymentsByQuote(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc {
orgRef, err := a.oph.GetRef(r)
if err != nil {
@@ -68,18 +61,30 @@ func (a *PaymentAPI) initiatePaymentsByQuote(r *http.Request, account *model.Acc
return resp.GetPayment(), nil
}
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 {
for _, intentRef := range payload.IntentRefs {
payment, executeErr := executeOne(deriveFanoutIdempotencyKey(baseIdempotencyKey, intentRef), intentRef)
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)
}
if payment != nil {
payments = append(payments, payment)
}
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)
}
@@ -118,28 +123,6 @@ func resolveExecutionIntentSelectors(payload *srequest.InitiatePayments, allowLe
return nil, merrors.InvalidArgument("metadata.intent_ref is no longer supported; use intentRef or intentRefs", "metadata.intent_ref")
}
func deriveFanoutIdempotencyKey(baseIdempotencyKey, intentRef string) string {
baseIdempotencyKey = strings.TrimSpace(baseIdempotencyKey)
intentRef = strings.TrimSpace(intentRef)
if baseIdempotencyKey == "" || intentRef == "" {
return baseIdempotencyKey
}
sum := sha256.Sum256([]byte(intentRef))
hash := hex.EncodeToString(sum[:])
if len(hash) > fanoutIdempotencyHashLen {
hash = hash[:fanoutIdempotencyHashLen]
}
suffix := ":i:" + hash
if len(baseIdempotencyKey)+len(suffix) <= maxExecuteIdempotencyKey {
return baseIdempotencyKey + suffix
}
if len(suffix) >= maxExecuteIdempotencyKey {
return suffix[:maxExecuteIdempotencyKey]
}
prefixLen := maxExecuteIdempotencyKey - len(suffix)
return baseIdempotencyKey[:prefixLen] + suffix
}
func decodeInitiatePaymentsPayload(r *http.Request) (*srequest.InitiatePayments, error) {
defer r.Body.Close()

View File

@@ -6,7 +6,6 @@ import (
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
@@ -24,7 +23,7 @@ import (
"go.uber.org/zap"
)
func TestInitiatePaymentsByQuote_FansOutByIntentRefs(t *testing.T) {
func TestInitiatePaymentsByQuote_PassesIntentRefsInSingleExecuteCall(t *testing.T) {
orgRef := bson.NewObjectID()
exec := &fakeExecutionClientForBatch{}
api := newBatchAPI(exec)
@@ -35,20 +34,17 @@ func TestInitiatePaymentsByQuote_FansOutByIntentRefs(t *testing.T) {
t.Fatalf("status mismatch: got=%d want=%d body=%s", got, want, rr.Body.String())
}
if got, want := len(exec.executeReqs), 2; 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-a"; got != want {
t.Fatalf("intent_ref[0] mismatch: got=%q want=%q", got, want)
if got := len(exec.executeReqs); got != 0 {
t.Fatalf("expected no execute calls, got=%d", got)
}
if got, want := exec.executeReqs[1].GetIntentRef(), "intent-b"; got != want {
t.Fatalf("intent_ref[1] mismatch: got=%q want=%q", got, want)
if got, want := exec.executeBatchReqs[0].GetQuotationRef(), "quote-1"; got != want {
t.Fatalf("quotation_ref mismatch: got=%q want=%q", got, want)
}
if got, want := exec.executeReqs[0].GetMeta().GetTrace().GetIdempotencyKey(), deriveFanoutIdempotencyKey("idem-batch", "intent-a"); got != want {
t.Fatalf("idempotency[0] mismatch: got=%q want=%q", got, want)
}
if got, want := exec.executeReqs[1].GetMeta().GetTrace().GetIdempotencyKey(), deriveFanoutIdempotencyKey("idem-batch", "intent-b"); got != want {
t.Fatalf("idempotency[1] mismatch: got=%q want=%q", got, want)
if got, want := exec.executeBatchReqs[0].GetMeta().GetTrace().GetIdempotencyKey(), "idem-batch"; got != want {
t.Fatalf("idempotency mismatch: got=%q want=%q", got, want)
}
}
@@ -125,28 +121,6 @@ func TestInitiatePaymentsByQuote_RejectsLegacyMetadataIntentRefWhenDateGateExpir
}
}
func TestDeriveFanoutIdempotencyKey_IsDeterministicAndBounded(t *testing.T) {
a := deriveFanoutIdempotencyKey("idem-1", "intent-a")
b := deriveFanoutIdempotencyKey("idem-1", "intent-a")
if got, want := a, b; got != want {
t.Fatalf("determinism mismatch: got=%q want=%q", got, want)
}
if a == "idem-1" {
t.Fatalf("expected derived key to differ from base")
}
c := deriveFanoutIdempotencyKey("idem-1", "intent-b")
if c == a {
t.Fatalf("expected different derived keys for different intents")
}
longBase := strings.Repeat("a", 400)
long := deriveFanoutIdempotencyKey(longBase, "intent-a")
if got, want := len(long), maxExecuteIdempotencyKey; got != want {
t.Fatalf("length mismatch: got=%d want=%d", got, want)
}
}
func TestResolveExecutionIntentSelectors_PrefersExplicitSelectors(t *testing.T) {
payload := &srequest.InitiatePayments{
IntentRefs: []string{"intent-a", "intent-b"},
@@ -229,7 +203,8 @@ func invokeInitiatePaymentsByQuote(t *testing.T, api *PaymentAPI, orgRef bson.Ob
}
type fakeExecutionClientForBatch struct {
executeReqs []*orchestrationv2.ExecutePaymentRequest
executeReqs []*orchestrationv2.ExecutePaymentRequest
executeBatchReqs []*orchestrationv2.ExecuteBatchPaymentRequest
}
func (f *fakeExecutionClientForBatch) ExecutePayment(_ context.Context, req *orchestrationv2.ExecutePaymentRequest) (*orchestrationv2.ExecutePaymentResponse, error) {
@@ -239,6 +214,13 @@ func (f *fakeExecutionClientForBatch) ExecutePayment(_ context.Context, req *orc
}, nil
}
func (f *fakeExecutionClientForBatch) ExecuteBatchPayment(_ context.Context, req *orchestrationv2.ExecuteBatchPaymentRequest) (*orchestrationv2.ExecuteBatchPaymentResponse, error) {
f.executeBatchReqs = append(f.executeBatchReqs, req)
return &orchestrationv2.ExecuteBatchPaymentResponse{
Payments: []*orchestrationv2.Payment{{PaymentRef: bson.NewObjectID().Hex()}},
}, nil
}
func (*fakeExecutionClientForBatch) ListPayments(context.Context, *orchestrationv2.ListPaymentsRequest) (*orchestrationv2.ListPaymentsResponse, error) {
return &orchestrationv2.ListPaymentsResponse{}, nil
}

View File

@@ -37,6 +37,7 @@ const (
type executionClient interface {
ExecutePayment(ctx context.Context, req *orchestrationv2.ExecutePaymentRequest) (*orchestrationv2.ExecutePaymentResponse, error)
ExecuteBatchPayment(ctx context.Context, req *orchestrationv2.ExecuteBatchPaymentRequest) (*orchestrationv2.ExecuteBatchPaymentResponse, error)
ListPayments(ctx context.Context, req *orchestrationv2.ListPaymentsRequest) (*orchestrationv2.ListPaymentsResponse, error)
Close() error
}