package paymentapiimp import ( "bytes" "context" "net/http" "net/http/httptest" "testing" "time" "github.com/go-chi/chi/v5" "github.com/tech/sendico/pkg/auth" "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/sresponse" mutil "github.com/tech/sendico/server/internal/mutil/param" "go.mongodb.org/mongo-driver/v2/bson" "go.uber.org/zap" ) func TestInitiatePaymentsByQuote_ExecutesBatchPayment(t *testing.T) { orgRef := bson.NewObjectID() exec := &fakeExecutionClientForBatch{} api := newBatchAPI(exec) 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()) } if got, want := len(exec.executeBatchReqs), 1; got != want { t.Fatalf("execute batch calls mismatch: got=%d want=%d", got, want) } if got := len(exec.executeReqs); got != 0 { t.Fatalf("expected no execute calls, got=%d", got) } 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.executeBatchReqs[0].GetMeta().GetTrace().GetIdempotencyKey(), "idem-batch"; got != want { t.Fatalf("idempotency mismatch: got=%q want=%q", got, want) } } func TestInitiatePaymentsByQuote_ForwardsClientPaymentRef(t *testing.T) { orgRef := bson.NewObjectID() exec := &fakeExecutionClientForBatch{} api := newBatchAPI(exec) body := `{"idempotencyKey":"idem-batch","quoteRef":"quote-1","clientPaymentRef":"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.executeBatchReqs), 1; got != want { t.Fatalf("execute batch calls mismatch: got=%d want=%d", 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 := len(exec.executeReqs); got != 0 { t.Fatalf("expected no execute calls, got=%d", got) } } func TestInitiatePaymentsByQuote_DoesNotForwardLegacyClientPaymentRefFromMetadata(t *testing.T) { orgRef := bson.NewObjectID() exec := &fakeExecutionClientForBatch{} api := newBatchAPI(exec) body := `{"idempotencyKey":"idem-batch","quoteRef":"quote-1","metadata":{"client_payment_ref":"legacy-client-ref"}}` 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.executeBatchReqs), 1; got != want { t.Fatalf("execute batch calls mismatch: got=%d want=%d", got, want) } if got := exec.executeBatchReqs[0].GetClientPaymentRef(); got != "" { t.Fatalf("expected empty client_payment_ref, got=%q", got) } } func TestInitiatePaymentsByQuote_RejectsDeprecatedIntentRefField(t *testing.T) { orgRef := bson.NewObjectID() exec := &fakeExecutionClientForBatch{} api := newBatchAPI(exec) 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()) } if got := len(exec.executeReqs); got != 0 { t.Fatalf("expected no execute calls, got=%d", got) } } func TestInitiatePaymentsByQuote_RejectsDeprecatedIntentRefsField(t *testing.T) { orgRef := bson.NewObjectID() exec := &fakeExecutionClientForBatch{} api := newBatchAPI(exec) 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()) } if got := len(exec.executeReqs); got != 0 { t.Fatalf("expected no execute calls, got=%d", got) } } func newBatchAPI(exec executionClient) *PaymentAPI { return &PaymentAPI{ logger: mlogger.Logger(zap.NewNop()), execution: exec, enf: fakeEnforcerForBatch{allowed: true}, oph: mutil.CreatePH(mservice.Organizations), permissionRef: bson.NewObjectID(), } } func invokeInitiatePaymentsByQuote(t *testing.T, api *PaymentAPI, orgRef bson.ObjectID, body string) *httptest.ResponseRecorder { t.Helper() req := httptest.NewRequest(http.MethodPost, "/by-multiquote", 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.initiatePaymentsByQuote(req, &model.Account{}, &sresponse.TokenData{ Token: "token", Expiration: time.Now().UTC().Add(time.Hour), }) handler.ServeHTTP(rr, req) return rr } type fakeExecutionClientForBatch struct { executeReqs []*orchestrationv2.ExecutePaymentRequest executeBatchReqs []*orchestrationv2.ExecuteBatchPaymentRequest } func (f *fakeExecutionClientForBatch) ExecutePayment(_ context.Context, req *orchestrationv2.ExecutePaymentRequest) (*orchestrationv2.ExecutePaymentResponse, error) { f.executeReqs = append(f.executeReqs, req) return &orchestrationv2.ExecutePaymentResponse{ Payment: &orchestrationv2.Payment{PaymentRef: bson.NewObjectID().Hex()}, }, 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 } func (*fakeExecutionClientForBatch) Close() error { return nil } type fakeEnforcerForBatch struct { allowed bool } func (f fakeEnforcerForBatch) Enforce(context.Context, bson.ObjectID, bson.ObjectID, bson.ObjectID, bson.ObjectID, model.Action) (bool, error) { return f.allowed, nil } func (fakeEnforcerForBatch) EnforceBatch(context.Context, []model.PermissionBoundStorable, bson.ObjectID, model.Action) (map[bson.ObjectID]bool, error) { return nil, nil } func (fakeEnforcerForBatch) GetRoles(context.Context, bson.ObjectID, bson.ObjectID) ([]model.Role, error) { return nil, nil } func (fakeEnforcerForBatch) GetPermissions(context.Context, bson.ObjectID, bson.ObjectID) ([]model.Role, []model.Permission, error) { return nil, nil, nil } var _ auth.Enforcer = (*fakeEnforcerForBatch)(nil)