From 77d611240281f4267ac733a09879e857ed24f0c7 Mon Sep 17 00:00:00 2001 From: fabrice guery Date: Thu, 23 Oct 2025 15:13:45 +0200 Subject: [PATCH 01/12] feat(EN-208): enable default & minimum polling period specifically for Stripe --- internal/connectors/config.go | 8 +- internal/connectors/config_test.go | 43 ++++++++ .../plugins/public/stripe/config.go | 47 +++++++- .../plugins/public/stripe/config_test.go | 100 ++++++++++++++++++ 4 files changed, 193 insertions(+), 5 deletions(-) create mode 100644 internal/connectors/config_test.go create mode 100644 internal/connectors/plugins/public/stripe/config_test.go diff --git a/internal/connectors/config.go b/internal/connectors/config.go index 9bcc6f12a..d152305e1 100644 --- a/internal/connectors/config.go +++ b/internal/connectors/config.go @@ -31,9 +31,13 @@ func combineConfigs(baseConfig models.Config, pluginConfig models.PluginInternal pluginMap = make(map[string]interface{}, len(baseMap)) } - // Merge maps (fields from baseConfig take precedence) + //Merge maps giving precedence to pluginConfig values for key, value := range baseMap { - pluginMap[key] = value + _, exists := pluginMap[key] + if !exists { + pluginMap[key] = value + continue + } } return json.Marshal(pluginMap) diff --git a/internal/connectors/config_test.go b/internal/connectors/config_test.go new file mode 100644 index 000000000..ab50737a8 --- /dev/null +++ b/internal/connectors/config_test.go @@ -0,0 +1,43 @@ +package connectors + +import ( + "encoding/json" + "testing" + "time" + + "github.com/formancehq/payments/internal/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCombineConfigs_PluginPrecedence(t *testing.T) { + t.Parallel() + + base := models.Config{ + Name: "conn-name", + PollingPeriod: 10 * time.Minute, // lower than plugin + PageSize: 50, + } + + pluginCfg := map[string]any{ + "apiKey": "sk_test", + "pollingPeriod": "20m0s", // plugin-normalized value should win + } + + b, err := combineConfigs(base, pluginCfg) + require.NoError(t, err) + + var out map[string]any + require.NoError(t, json.Unmarshal(b, &out)) + + // Plugin value should take precedence + assert.Equal(t, "20m0s", out["pollingPeriod"]) + + // Base-only fields should be present + assert.Equal(t, "conn-name", out["name"]) + // pageSize from base since plugin didn't set it + assert.Equal(t, float64(50), out["pageSize"]) // numbers become float64 via json + + // Plugin specific field preserved + assert.Equal(t, "sk_test", out["apiKey"]) +} diff --git a/internal/connectors/plugins/public/stripe/config.go b/internal/connectors/plugins/public/stripe/config.go index 568318bda..670df285a 100644 --- a/internal/connectors/plugins/public/stripe/config.go +++ b/internal/connectors/plugins/public/stripe/config.go @@ -2,21 +2,62 @@ package stripe import ( "encoding/json" + "time" "github.com/formancehq/payments/internal/models" "github.com/go-playground/validator/v10" "github.com/pkg/errors" ) +const ( + minimumPollingInterval = 20 * time.Minute + defaultPollingInterval = 30 * time.Minute +) + type Config struct { - APIKey string `json:"apiKey" validate:"required"` + APIKey string `json:"apiKey" validate:"required"` + PollingPeriod time.Duration `json:"pollingPeriod" validate:"required,gte=0s"` +} + +func (c Config) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + APIKey string `json:"apiKey"` + PollingPeriod string `json:"pollingPeriod"` + }{ + APIKey: c.APIKey, + PollingPeriod: c.PollingPeriod.String(), + }) } func unmarshalAndValidateConfig(payload json.RawMessage) (Config, error) { - var config Config - if err := json.Unmarshal(payload, &config); err != nil { + var raw struct { + APIKey string `json:"apiKey"` + PollingPeriod string `json:"pollingPeriod"` + } + + if err := json.Unmarshal(payload, &raw); err != nil { return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) } + + pollingPeriod := defaultPollingInterval + if raw.PollingPeriod != "" { + var err error + pollingPeriod, err = time.ParseDuration(raw.PollingPeriod) + if err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + } + validate := validator.New(validator.WithRequiredStructEnabled()) + + if pollingPeriod < minimumPollingInterval { + pollingPeriod = minimumPollingInterval + } + + config := Config{ + APIKey: raw.APIKey, + PollingPeriod: pollingPeriod, + } + return config, validate.Struct(config) } diff --git a/internal/connectors/plugins/public/stripe/config_test.go b/internal/connectors/plugins/public/stripe/config_test.go new file mode 100644 index 000000000..d3639563f --- /dev/null +++ b/internal/connectors/plugins/public/stripe/config_test.go @@ -0,0 +1,100 @@ +package stripe + +import ( + "encoding/json" + "time" + + "github.com/formancehq/payments/internal/models" + "github.com/go-playground/validator/v10" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("unmarshalAndValidateConfig", func() { + var ( + payload json.RawMessage + expectedError error + config Config + err error + ) + + JustBeforeEach(func() { + config, err = unmarshalAndValidateConfig(payload) + }) + + Context("with valid configuration and explicit polling period", func() { + BeforeEach(func() { + // 45 minutes in nanoseconds + payload = json.RawMessage(`{"apiKey":"sk_test_123","pollingPeriod":"45m"}`) + expectedError = nil + }) + + It("should successfully unmarshal and validate with given polling period", func() { + Expect(err).To(BeNil()) + Expect(config.APIKey).To(Equal("sk_test_123")) + Expect(config.PollingPeriod).To(Equal(45 * time.Minute)) + }) + }) + + Context("with missing polling period", func() { + BeforeEach(func() { + payload = json.RawMessage(`{"apiKey":"sk_test_123"}`) + expectedError = nil + }) + + It("should default to 30 minutes", func() { + Expect(err).To(BeNil()) + Expect(config.PollingPeriod).To(Equal(30 * time.Minute)) + }) + }) + + Context("with polling period lower than minimum", func() { + BeforeEach(func() { + // 5 minutes in nanoseconds + payload = json.RawMessage(`{"apiKey":"sk_test_123","pollingPeriod":"5m"}`) + expectedError = nil + }) + + It("should coerce to minimum 20 minutes", func() { + Expect(err).To(BeNil()) + Expect(config.PollingPeriod).To(Equal(20 * time.Minute)) + }) + }) + + Context("with missing apiKey", func() { + BeforeEach(func() { + // 30 minutes in nanoseconds + payload = json.RawMessage(`{"pollingPeriod":"30m"}`) + }) + + It("should return a validation error", func() { + Expect(err).To(HaveOccurred()) + Expect(err).To(BeAssignableToTypeOf(validator.ValidationErrors{})) + }) + }) + + Context("with invalid JSON payload", func() { + BeforeEach(func() { + payload = json.RawMessage(`{"apiKey":sk_test_123}`) + expectedError = models.ErrInvalidConfig + }) + + It("should return an unmarshalling error wrapped as invalid config", func() { + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError(ContainSubstring(expectedError.Error()))) + }) + }) +}) + +var _ = Describe("marshall", func() { + It("keeps duration in string format", func() { + config := &Config{ + APIKey: "sk_test_123", + PollingPeriod: 30 * time.Minute, + } + marshaledConfig, err := config.MarshalJSON() + Expect(err).To(BeNil()) + Expect(string(marshaledConfig)).To(ContainSubstring(`"pollingPeriod":"30m0s"`)) + Expect(string(marshaledConfig)).To(ContainSubstring(`"apiKey":"sk_test_123"`)) + }) +}) From 28d1db5dd58c4461c381ef94ed6880bb993a84d6 Mon Sep 17 00:00:00 2001 From: fabrice guery Date: Thu, 23 Oct 2025 15:44:38 +0200 Subject: [PATCH 02/12] feat(EN-208): update openAPI + fix compile-configs tool to support Durations --- docs/api/README.md | 2 +- .../plugins/public/stripe/config.go | 2 +- openapi.yaml | 1 - openapi/v3/v3-connectors-config.yaml | 1 - pkg/client/.speakeasy/gen.lock | 2 +- .../models/components/v3stripeconfig.go | 2 +- tools/compile-configs/main.go | 33 +++++++++++++++++-- 7 files changed, 34 insertions(+), 9 deletions(-) diff --git a/docs/api/README.md b/docs/api/README.md index aae14f2df..0aeca3e57 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -7304,7 +7304,7 @@ xor "apiKey": "string", "name": "string", "pageSize": 25, - "pollingPeriod": "2m", + "pollingPeriod": "string", "provider": "Stripe" } diff --git a/internal/connectors/plugins/public/stripe/config.go b/internal/connectors/plugins/public/stripe/config.go index 670df285a..c0ca5ed07 100644 --- a/internal/connectors/plugins/public/stripe/config.go +++ b/internal/connectors/plugins/public/stripe/config.go @@ -16,7 +16,7 @@ const ( type Config struct { APIKey string `json:"apiKey" validate:"required"` - PollingPeriod time.Duration `json:"pollingPeriod" validate:"required,gte=0s"` + PollingPeriod time.Duration `json:"pollingPeriod"` } func (c Config) MarshalJSON() ([]byte, error) { diff --git a/openapi.yaml b/openapi.yaml index 7dfec73b7..ca00bd6ef 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -6078,7 +6078,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Stripe diff --git a/openapi/v3/v3-connectors-config.yaml b/openapi/v3/v3-connectors-config.yaml index 346eca818..ed8e23694 100644 --- a/openapi/v3/v3-connectors-config.yaml +++ b/openapi/v3/v3-connectors-config.yaml @@ -418,7 +418,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Stripe diff --git a/pkg/client/.speakeasy/gen.lock b/pkg/client/.speakeasy/gen.lock index acbe5986e..13e8dde6e 100644 --- a/pkg/client/.speakeasy/gen.lock +++ b/pkg/client/.speakeasy/gen.lock @@ -1,7 +1,7 @@ lockVersion: 2.0.0 id: 1fa8a26f-45d9-44b7-8b97-fbeebcdcd8b1 management: - docChecksum: 2d54f05b4486002d50d64c242f33daa2 + docChecksum: eef91ffe7da06070422e5df9aad7ca67 docVersion: v1 speakeasyVersion: 1.525.0 generationVersion: 2.562.2 diff --git a/pkg/client/models/components/v3stripeconfig.go b/pkg/client/models/components/v3stripeconfig.go index f26482be2..a09bf1586 100644 --- a/pkg/client/models/components/v3stripeconfig.go +++ b/pkg/client/models/components/v3stripeconfig.go @@ -10,7 +10,7 @@ type V3StripeConfig struct { APIKey string `json:"apiKey"` Name string `json:"name"` PageSize *int64 `default:"25" json:"pageSize"` - PollingPeriod *string `default:"2m" json:"pollingPeriod"` + PollingPeriod *string `json:"pollingPeriod,omitempty"` Provider *string `default:"Stripe" json:"provider"` } diff --git a/tools/compile-configs/main.go b/tools/compile-configs/main.go index a2ae46a54..2914e89da 100644 --- a/tools/compile-configs/main.go +++ b/tools/compile-configs/main.go @@ -159,6 +159,9 @@ func readConfig(name string, caserName string) (V3Config, error) { } name := "" + if field.Tag == nil { + continue + } tagValue := strings.Trim(field.Tag.Value, "`") arr := strings.Split(tagValue, " ") for _, tag := range arr { @@ -170,17 +173,41 @@ func readConfig(name string, caserName string) (V3Config, error) { switch fields[0] { case "json": name = strings.Trim(fields[1], "\"") - typ := field.Type.(*ast.Ident).Name + // Determine the field type name supporting identifiers, selectors (e.g., time.Duration), and pointers + var typName string + switch t := field.Type.(type) { + case *ast.Ident: + typName = t.Name + case *ast.SelectorExpr: + // Qualified type like pkg.Type -> use the selected identifier + typName = t.Sel.Name + case *ast.StarExpr: + // Pointer to something + switch x := t.X.(type) { + case *ast.Ident: + typName = x.Name + case *ast.SelectorExpr: + typName = x.Sel.Name + default: + return V3Config{}, fmt.Errorf("unsupported type expr: %T", field.Type) + } + default: + return V3Config{}, fmt.Errorf("unsupported type expr: %T", field.Type) + } + fieldType := "" - switch typ { + switch typName { case "string": fieldType = "string" case "int", "int32", "int64", "uint32", "uint64": fieldType = "integer" case "bool": fieldType = "boolean" + case "Duration": + // time.Duration is represented as a string in JSON/schema + fieldType = "string" default: - return V3Config{}, fmt.Errorf("invalid type: %s", typ) + return V3Config{}, fmt.Errorf("invalid type: %s", typName) } properties[name] = Property{ Type: fieldType, From 0000b74df2de012a56c3e81dc3d000c613ad76b3 Mon Sep 17 00:00:00 2001 From: fabrice guery Date: Thu, 23 Oct 2025 17:51:09 +0200 Subject: [PATCH 03/12] feat(EN-208): created helper type for period so it's easier to generalize --- .../plugins/public/stripe/config.go | 44 +++++-------------- .../plugins/public/stripe/config_test.go | 15 +++---- .../plugins/sharedconfig/polling_period.go | 37 ++++++++++++++++ .../sharedconfig/polling_period_test.go | 33 ++++++++++++++ tools/compile-configs/main.go | 3 ++ 5 files changed, 92 insertions(+), 40 deletions(-) create mode 100644 internal/connectors/plugins/sharedconfig/polling_period.go create mode 100644 internal/connectors/plugins/sharedconfig/polling_period_test.go diff --git a/internal/connectors/plugins/public/stripe/config.go b/internal/connectors/plugins/public/stripe/config.go index c0ca5ed07..f31862cde 100644 --- a/internal/connectors/plugins/public/stripe/config.go +++ b/internal/connectors/plugins/public/stripe/config.go @@ -2,31 +2,16 @@ package stripe import ( "encoding/json" - "time" + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" "github.com/formancehq/payments/internal/models" "github.com/go-playground/validator/v10" "github.com/pkg/errors" ) -const ( - minimumPollingInterval = 20 * time.Minute - defaultPollingInterval = 30 * time.Minute -) - type Config struct { - APIKey string `json:"apiKey" validate:"required"` - PollingPeriod time.Duration `json:"pollingPeriod"` -} - -func (c Config) MarshalJSON() ([]byte, error) { - return json.Marshal(struct { - APIKey string `json:"apiKey"` - PollingPeriod string `json:"pollingPeriod"` - }{ - APIKey: c.APIKey, - PollingPeriod: c.PollingPeriod.String(), - }) + APIKey string `json:"apiKey" validate:"required"` + PollingPeriod sharedconfig.PollingPeriod `json:"pollingPeriod"` } func unmarshalAndValidateConfig(payload json.RawMessage) (Config, error) { @@ -39,25 +24,20 @@ func unmarshalAndValidateConfig(payload json.RawMessage) (Config, error) { return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) } - pollingPeriod := defaultPollingInterval - if raw.PollingPeriod != "" { - var err error - pollingPeriod, err = time.ParseDuration(raw.PollingPeriod) - if err != nil { - return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) - } - } - - validate := validator.New(validator.WithRequiredStructEnabled()) - - if pollingPeriod < minimumPollingInterval { - pollingPeriod = minimumPollingInterval + pp, err := sharedconfig.NewPollingPeriod( + raw.PollingPeriod, + sharedconfig.DefaultPollingPeriod, + sharedconfig.MinimumPollingPeriod, + ) + if err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) } config := Config{ APIKey: raw.APIKey, - PollingPeriod: pollingPeriod, + PollingPeriod: pp, } + validate := validator.New(validator.WithRequiredStructEnabled()) return config, validate.Struct(config) } diff --git a/internal/connectors/plugins/public/stripe/config_test.go b/internal/connectors/plugins/public/stripe/config_test.go index d3639563f..9b28fa166 100644 --- a/internal/connectors/plugins/public/stripe/config_test.go +++ b/internal/connectors/plugins/public/stripe/config_test.go @@ -32,7 +32,7 @@ var _ = Describe("unmarshalAndValidateConfig", func() { It("should successfully unmarshal and validate with given polling period", func() { Expect(err).To(BeNil()) Expect(config.APIKey).To(Equal("sk_test_123")) - Expect(config.PollingPeriod).To(Equal(45 * time.Minute)) + Expect(config.PollingPeriod.Duration()).To(Equal(45 * time.Minute)) }) }) @@ -44,7 +44,7 @@ var _ = Describe("unmarshalAndValidateConfig", func() { It("should default to 30 minutes", func() { Expect(err).To(BeNil()) - Expect(config.PollingPeriod).To(Equal(30 * time.Minute)) + Expect(config.PollingPeriod.Duration()).To(Equal(30 * time.Minute)) }) }) @@ -57,7 +57,7 @@ var _ = Describe("unmarshalAndValidateConfig", func() { It("should coerce to minimum 20 minutes", func() { Expect(err).To(BeNil()) - Expect(config.PollingPeriod).To(Equal(20 * time.Minute)) + Expect(config.PollingPeriod.Duration()).To(Equal(20 * time.Minute)) }) }) @@ -88,11 +88,10 @@ var _ = Describe("unmarshalAndValidateConfig", func() { var _ = Describe("marshall", func() { It("keeps duration in string format", func() { - config := &Config{ - APIKey: "sk_test_123", - PollingPeriod: 30 * time.Minute, - } - marshaledConfig, err := config.MarshalJSON() + raw := json.RawMessage(`{"apiKey":"sk_test_123", "pollingPeriod":"30m"}`) + config, err := unmarshalAndValidateConfig(raw) + Expect(err).To(BeNil()) + marshaledConfig, err := json.Marshal(config) Expect(err).To(BeNil()) Expect(string(marshaledConfig)).To(ContainSubstring(`"pollingPeriod":"30m0s"`)) Expect(string(marshaledConfig)).To(ContainSubstring(`"apiKey":"sk_test_123"`)) diff --git a/internal/connectors/plugins/sharedconfig/polling_period.go b/internal/connectors/plugins/sharedconfig/polling_period.go new file mode 100644 index 000000000..b4e2625d4 --- /dev/null +++ b/internal/connectors/plugins/sharedconfig/polling_period.go @@ -0,0 +1,37 @@ +package sharedconfig + +import ( + "encoding/json" + "time" +) + +const ( + MinimumPollingPeriod = 20 * time.Minute + DefaultPollingPeriod = 30 * time.Minute +) + +type PollingPeriod time.Duration + +func (p PollingPeriod) Duration() time.Duration { return time.Duration(p) } + +func (p PollingPeriod) MarshalJSON() ([]byte, error) { + return json.Marshal(time.Duration(p).String()) +} + +// Helper to construct the value while applying min/default. +func NewPollingPeriod(raw string, def, min time.Duration) (PollingPeriod, error) { + if raw == "" { + if def < min { + return PollingPeriod(min), nil + } + return PollingPeriod(def), nil + } + v, err := time.ParseDuration(raw) + if err != nil { + return 0, err + } + if v < min { + v = min + } + return PollingPeriod(v), nil +} diff --git a/internal/connectors/plugins/sharedconfig/polling_period_test.go b/internal/connectors/plugins/sharedconfig/polling_period_test.go new file mode 100644 index 000000000..71bf2364f --- /dev/null +++ b/internal/connectors/plugins/sharedconfig/polling_period_test.go @@ -0,0 +1,33 @@ +package sharedconfig_test + +import ( + "testing" + "time" + + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" +) + +func TestParsePollingPeriod(t *testing.T) { + cases := []struct { + raw string + def, min, want time.Duration + wantErr bool + }{ + {"", 30 * time.Minute, 20 * time.Minute, 30 * time.Minute, false}, + {"15m", 30 * time.Minute, 20 * time.Minute, 20 * time.Minute, false}, + {"45m", 30 * time.Minute, 20 * time.Minute, 45 * time.Minute, false}, + {"not-a-duration", 30 * time.Minute, 20 * time.Minute, 0, true}, + } + for _, c := range cases { + got, err := sharedconfig.NewPollingPeriod(c.raw, c.def, c.min) + if c.wantErr && err == nil { + t.Fatalf("expected error for value %s", c.raw) + } + if !c.wantErr && err != nil { + t.Fatalf("unexpected error for value %s: %v", c.raw, err) + } + if !c.wantErr && got.Duration() != c.want { + t.Fatalf("unexpected result for value %s, got %v, want %v", c.raw, got, c.want) + } + } +} diff --git a/tools/compile-configs/main.go b/tools/compile-configs/main.go index 2914e89da..4bff660d6 100644 --- a/tools/compile-configs/main.go +++ b/tools/compile-configs/main.go @@ -206,6 +206,9 @@ func readConfig(name string, caserName string) (V3Config, error) { case "Duration": // time.Duration is represented as a string in JSON/schema fieldType = "string" + case "PollingPeriod": + // sharedconfig.PollingPeriod is represented as a string in JSON/schema + fieldType = "string" default: return V3Config{}, fmt.Errorf("invalid type: %s", typName) } From e1162ae44d19cb7b9ab6d2254e58dad471d86b74 Mon Sep 17 00:00:00 2001 From: fabrice guery Date: Fri, 24 Oct 2025 13:23:01 +0200 Subject: [PATCH 04/12] feat(EN-208): enable pollingPeriod for atlar --- .../connectors/plugins/public/atlar/config.go | 33 +++++++-- .../plugins/public/atlar/config_test.go | 70 +++++++++++++++++++ 2 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 internal/connectors/plugins/public/atlar/config_test.go diff --git a/internal/connectors/plugins/public/atlar/config.go b/internal/connectors/plugins/public/atlar/config.go index 3a7323ecc..2e447a971 100644 --- a/internal/connectors/plugins/public/atlar/config.go +++ b/internal/connectors/plugins/public/atlar/config.go @@ -3,22 +3,45 @@ package atlar import ( "encoding/json" + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" "github.com/formancehq/payments/internal/models" "github.com/go-playground/validator/v10" "github.com/pkg/errors" ) type Config struct { - BaseURL string `json:"baseUrl" validate:"required"` - AccessKey string `json:"accessKey" validate:"required"` - Secret string `json:"secret" validate:"required"` + BaseURL string `json:"baseUrl" validate:"required"` + AccessKey string `json:"accessKey" validate:"required"` + Secret string `json:"secret" validate:"required"` + PollingPeriod sharedconfig.PollingPeriod `json:"pollingPeriod"` } func unmarshalAndValidateConfig(payload []byte) (Config, error) { - var config Config - if err := json.Unmarshal(payload, &config); err != nil { + var raw struct { + BaseURL string `json:"baseUrl"` + AccessKey string `json:"accessKey"` + Secret string `json:"secret"` + PollingPeriod string `json:"pollingPeriod"` + } + if err := json.Unmarshal(payload, &raw); err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + pp, err := sharedconfig.NewPollingPeriod( + raw.PollingPeriod, + sharedconfig.DefaultPollingPeriod, + sharedconfig.MinimumPollingPeriod, + ) + if err != nil { return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) } + + config := Config{ + BaseURL: raw.BaseURL, + AccessKey: raw.AccessKey, + Secret: raw.Secret, + PollingPeriod: pp, + } validate := validator.New(validator.WithRequiredStructEnabled()) return config, validate.Struct(config) } diff --git a/internal/connectors/plugins/public/atlar/config_test.go b/internal/connectors/plugins/public/atlar/config_test.go new file mode 100644 index 000000000..14db8471f --- /dev/null +++ b/internal/connectors/plugins/public/atlar/config_test.go @@ -0,0 +1,70 @@ +package atlar + +import ( + "testing" + + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUnmarshalAndValidateConfig(t *testing.T) { + t.Parallel() + + defaultPollingPeriod, _ := sharedconfig.NewPollingPeriod("", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + longPollingPeriod, _ := sharedconfig.NewPollingPeriod("45m", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + + tests := []struct { + name string + payload []byte + expected Config + expectError bool + }{ + { + name: "Valid Config", + payload: []byte(`{"baseUrl":"https://api.atlar.com","accessKey":"ak_test","secret":"sk_test"}`), + expected: Config{ + BaseURL: "https://api.atlar.com", + AccessKey: "ak_test", + Secret: "sk_test", + PollingPeriod: defaultPollingPeriod, + }, + expectError: false, + }, + { + name: "Missing Required Fields", + payload: []byte(`{"baseUrl":"https://api.atlar.com"}`), + expected: Config{}, + expectError: true, + }, + { + name: "Non default polling period", + payload: []byte(`{"baseUrl":"https://api.atlar.com","accessKey":"ak_test","secret":"sk_test","pollingPeriod":"45m"}`), + expected: Config{ + BaseURL: "https://api.atlar.com", + AccessKey: "ak_test", + Secret: "sk_test", + PollingPeriod: longPollingPeriod, + }, + expectError: false, + }, + { + name: "Invalid polling period", + payload: []byte(`{"baseUrl":"https://api.atlar.com","accessKey":"ak_test","secret":"sk_test","pollingPeriod":"not-a-duration"}`), + expected: Config{}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config, err := unmarshalAndValidateConfig(tt.payload) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, config) + } + }) + } +} From 2fb206ea7657809ea677a08a96bbdd2a41c18e49 Mon Sep 17 00:00:00 2001 From: fabrice guery Date: Fri, 24 Oct 2025 14:43:53 +0200 Subject: [PATCH 05/12] feat(EN-208): same treatment for bankingbridge --- .../plugins/public/bankingcircle/config.go | 45 +++++++++-- .../public/bankingcircle/config_test.go | 76 +++++++++++++++++++ 2 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 internal/connectors/plugins/public/bankingcircle/config_test.go diff --git a/internal/connectors/plugins/public/bankingcircle/config.go b/internal/connectors/plugins/public/bankingcircle/config.go index 0688a43af..800bec0f2 100644 --- a/internal/connectors/plugins/public/bankingcircle/config.go +++ b/internal/connectors/plugins/public/bankingcircle/config.go @@ -3,25 +3,54 @@ package bankingcircle import ( "encoding/json" + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" "github.com/formancehq/payments/internal/models" "github.com/go-playground/validator/v10" "github.com/pkg/errors" ) type Config struct { - Username string `json:"username" yaml:"username" validate:"required"` - Password string `json:"password" yaml:"password" validate:"required"` - Endpoint string `json:"endpoint" yaml:"endpoint" validate:"required"` - AuthorizationEndpoint string `json:"authorizationEndpoint" yaml:"authorizationEndpoint" validate:"required"` - UserCertificate string `json:"userCertificate" yaml:"userCertificate" validate:"required"` - UserCertificateKey string `json:"userCertificateKey" yaml:"userCertificateKey" validate:"required"` + Username string `json:"username" yaml:"username" validate:"required"` + Password string `json:"password" yaml:"password" validate:"required"` + Endpoint string `json:"endpoint" yaml:"endpoint" validate:"required"` + AuthorizationEndpoint string `json:"authorizationEndpoint" yaml:"authorizationEndpoint" validate:"required"` + UserCertificate string `json:"userCertificate" yaml:"userCertificate" validate:"required"` + UserCertificateKey string `json:"userCertificateKey" yaml:"userCertificateKey" validate:"required"` + PollingPeriod sharedconfig.PollingPeriod `json:"pollingPeriod"` } func unmarshalAndValidateConfig(payload []byte) (Config, error) { - var config Config - if err := json.Unmarshal(payload, &config); err != nil { + var raw struct { + Username string `json:"username"` + Password string `json:"password"` + Endpoint string `json:"endpoint"` + AuthorizationEndpoint string `json:"authorizationEndpoint"` + UserCertificate string `json:"userCertificate"` + UserCertificateKey string `json:"userCertificateKey"` + PollingPeriod string `json:"pollingPeriod"` + } + if err := json.Unmarshal(payload, &raw); err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + pp, err := sharedconfig.NewPollingPeriod( + raw.PollingPeriod, + sharedconfig.DefaultPollingPeriod, + sharedconfig.MinimumPollingPeriod, + ) + if err != nil { return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) } + + config := Config{ + Username: raw.Username, + Password: raw.Password, + Endpoint: raw.Endpoint, + AuthorizationEndpoint: raw.AuthorizationEndpoint, + UserCertificate: raw.UserCertificate, + UserCertificateKey: raw.UserCertificateKey, + PollingPeriod: pp, + } validate := validator.New(validator.WithRequiredStructEnabled()) return config, validate.Struct(config) } diff --git a/internal/connectors/plugins/public/bankingcircle/config_test.go b/internal/connectors/plugins/public/bankingcircle/config_test.go new file mode 100644 index 000000000..0cbbe6cc5 --- /dev/null +++ b/internal/connectors/plugins/public/bankingcircle/config_test.go @@ -0,0 +1,76 @@ +package bankingcircle + +import ( + "testing" + + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUnmarshalAndValidateConfig(t *testing.T) { + t.Parallel() + + defaultPollingPeriod, _ := sharedconfig.NewPollingPeriod("", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + longPollingPeriod, _ := sharedconfig.NewPollingPeriod("45m", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + + tests := []struct { + name string + payload []byte + expected Config + expectError bool + }{ + { + name: "Valid Config", + payload: []byte(`{"username":"user","password":"pass","endpoint":"https://api.bankingcircle.com","authorizationEndpoint":"https://auth.bankingcircle.com","userCertificate":"cert","userCertificateKey":"key"}`), + expected: Config{ + Username: "user", + Password: "pass", + Endpoint: "https://api.bankingcircle.com", + AuthorizationEndpoint: "https://auth.bankingcircle.com", + UserCertificate: "cert", + UserCertificateKey: "key", + PollingPeriod: defaultPollingPeriod, + }, + expectError: false, + }, + { + name: "Missing Required Fields", + payload: []byte(`{"username":"user"}`), + expected: Config{}, + expectError: true, + }, + { + name: "Non default polling period", + payload: []byte(`{"username":"user","password":"pass","endpoint":"https://api.bankingcircle.com","authorizationEndpoint":"https://auth.bankingcircle.com","userCertificate":"cert","userCertificateKey":"key","pollingPeriod":"45m"}`), + expected: Config{ + Username: "user", + Password: "pass", + Endpoint: "https://api.bankingcircle.com", + AuthorizationEndpoint: "https://auth.bankingcircle.com", + UserCertificate: "cert", + UserCertificateKey: "key", + PollingPeriod: longPollingPeriod, + }, + expectError: false, + }, + { + name: "Invalid polling period", + payload: []byte(`{"username":"user","password":"pass","endpoint":"https://api.bankingcircle.com","authorizationEndpoint":"https://auth.bankingcircle.com","userCertificate":"cert","userCertificateKey":"key","pollingPeriod":"not-a-duration"}`), + expected: Config{}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config, err := unmarshalAndValidateConfig(tt.payload) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, config) + } + }) + } +} From cf2bd7ee0b2dc54869710288e2ebb152ff166665 Mon Sep 17 00:00:00 2001 From: fabrice guery Date: Fri, 24 Oct 2025 16:05:52 +0200 Subject: [PATCH 06/12] feat(EN-208): change column --- .../plugins/public/column/config.go | 32 +++++++-- .../plugins/public/column/config_test.go | 68 +++++++++++++++++++ 2 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 internal/connectors/plugins/public/column/config_test.go diff --git a/internal/connectors/plugins/public/column/config.go b/internal/connectors/plugins/public/column/config.go index 83736d063..4f37c602d 100644 --- a/internal/connectors/plugins/public/column/config.go +++ b/internal/connectors/plugins/public/column/config.go @@ -2,23 +2,43 @@ package column import ( "encoding/json" - "fmt" + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" "github.com/formancehq/payments/internal/models" "github.com/go-playground/validator/v10" + "github.com/pkg/errors" ) type Config struct { - APIKey string `json:"apiKey" validate:"required"` - Endpoint string `json:"endpoint" validate:"required,url"` + APIKey string `json:"apiKey" validate:"required"` + Endpoint string `json:"endpoint" validate:"required,url"` + PollingPeriod sharedconfig.PollingPeriod `json:"pollingPeriod"` } func unmarshalAndValidateConfig(payload json.RawMessage) (Config, error) { - var config Config - if err := json.Unmarshal(payload, &config); err != nil { - return Config{}, fmt.Errorf("%w: %w", err, models.ErrInvalidConfig) + var raw struct { + APIKey string `json:"apiKey"` + Endpoint string `json:"endpoint"` + PollingPeriod string `json:"pollingPeriod"` + } + if err := json.Unmarshal(payload, &raw); err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + pp, err := sharedconfig.NewPollingPeriod( + raw.PollingPeriod, + sharedconfig.DefaultPollingPeriod, + sharedconfig.MinimumPollingPeriod, + ) + if err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) } + config := Config{ + APIKey: raw.APIKey, + Endpoint: raw.Endpoint, + PollingPeriod: pp, + } validate := validator.New(validator.WithRequiredStructEnabled()) return config, validate.Struct(config) } diff --git a/internal/connectors/plugins/public/column/config_test.go b/internal/connectors/plugins/public/column/config_test.go new file mode 100644 index 000000000..51554f7e0 --- /dev/null +++ b/internal/connectors/plugins/public/column/config_test.go @@ -0,0 +1,68 @@ +package column + +import ( + "testing" + + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUnmarshalAndValidateConfig(t *testing.T) { + t.Parallel() + + defaultPollingPeriod, _ := sharedconfig.NewPollingPeriod("", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + longPollingPeriod, _ := sharedconfig.NewPollingPeriod("45m", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + + tests := []struct { + name string + payload []byte + expected Config + expectError bool + }{ + { + name: "Valid Config", + payload: []byte(`{"apiKey":"sk_test","endpoint":"https://api.column.com"}`), + expected: Config{ + APIKey: "sk_test", + Endpoint: "https://api.column.com", + PollingPeriod: defaultPollingPeriod, + }, + expectError: false, + }, + { + name: "Missing Required Fields", + payload: []byte(`{"apiKey":"sk_test"}`), + expected: Config{}, + expectError: true, + }, + { + name: "Non default polling period", + payload: []byte(`{"apiKey":"sk_test","endpoint":"https://api.column.com","pollingPeriod":"45m"}`), + expected: Config{ + APIKey: "sk_test", + Endpoint: "https://api.column.com", + PollingPeriod: longPollingPeriod, + }, + expectError: false, + }, + { + name: "Invalid polling period", + payload: []byte(`{"apiKey":"sk_test","endpoint":"https://api.column.com","pollingPeriod":"not-a-duration"}`), + expected: Config{}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config, err := unmarshalAndValidateConfig(tt.payload) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, config) + } + }) + } +} From adcda581c58f73e320d910b2461593442a0c69d4 Mon Sep 17 00:00:00 2001 From: fabrice guery Date: Fri, 24 Oct 2025 16:10:35 +0200 Subject: [PATCH 07/12] feat(EN-208): added pollingPeriod for generic. --- .../plugins/public/generic/config.go | 29 ++++++-- .../plugins/public/generic/config_test.go | 68 +++++++++++++++++++ 2 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 internal/connectors/plugins/public/generic/config_test.go diff --git a/internal/connectors/plugins/public/generic/config.go b/internal/connectors/plugins/public/generic/config.go index 7acf12dfa..f949fccb7 100644 --- a/internal/connectors/plugins/public/generic/config.go +++ b/internal/connectors/plugins/public/generic/config.go @@ -3,21 +3,42 @@ package generic import ( "encoding/json" + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" "github.com/formancehq/payments/internal/models" "github.com/go-playground/validator/v10" "github.com/pkg/errors" ) type Config struct { - APIKey string `json:"apiKey" validate:"required"` - Endpoint string `json:"endpoint" validate:"required"` + APIKey string `json:"apiKey" validate:"required"` + Endpoint string `json:"endpoint" validate:"required"` + PollingPeriod sharedconfig.PollingPeriod `json:"pollingPeriod"` } func unmarshalAndValidateConfig(payload json.RawMessage) (Config, error) { - var config Config - if err := json.Unmarshal(payload, &config); err != nil { + var raw struct { + APIKey string `json:"apiKey"` + Endpoint string `json:"endpoint"` + PollingPeriod string `json:"pollingPeriod"` + } + if err := json.Unmarshal(payload, &raw); err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + pp, err := sharedconfig.NewPollingPeriod( + raw.PollingPeriod, + sharedconfig.DefaultPollingPeriod, + sharedconfig.MinimumPollingPeriod, + ) + if err != nil { return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) } + + config := Config{ + APIKey: raw.APIKey, + Endpoint: raw.Endpoint, + PollingPeriod: pp, + } validate := validator.New(validator.WithRequiredStructEnabled()) return config, validate.Struct(config) } diff --git a/internal/connectors/plugins/public/generic/config_test.go b/internal/connectors/plugins/public/generic/config_test.go new file mode 100644 index 000000000..7bf44c911 --- /dev/null +++ b/internal/connectors/plugins/public/generic/config_test.go @@ -0,0 +1,68 @@ +package generic + +import ( + "testing" + + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUnmarshalAndValidateConfig(t *testing.T) { + t.Parallel() + + defaultPollingPeriod, _ := sharedconfig.NewPollingPeriod("", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + longPollingPeriod, _ := sharedconfig.NewPollingPeriod("45m", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + + tests := []struct { + name string + payload []byte + expected Config + expectError bool + }{ + { + name: "Valid Config", + payload: []byte(`{"apiKey":"sk_test","endpoint":"https://api.generic.example"}`), + expected: Config{ + APIKey: "sk_test", + Endpoint: "https://api.generic.example", + PollingPeriod: defaultPollingPeriod, + }, + expectError: false, + }, + { + name: "Missing Required Fields", + payload: []byte(`{"apiKey":"sk_test"}`), + expected: Config{}, + expectError: true, + }, + { + name: "Non default polling period", + payload: []byte(`{"apiKey":"sk_test","endpoint":"https://api.generic.example","pollingPeriod":"45m"}`), + expected: Config{ + APIKey: "sk_test", + Endpoint: "https://api.generic.example", + PollingPeriod: longPollingPeriod, + }, + expectError: false, + }, + { + name: "Invalid polling period", + payload: []byte(`{"apiKey":"sk_test","endpoint":"https://api.generic.example","pollingPeriod":"not-a-duration"}`), + expected: Config{}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config, err := unmarshalAndValidateConfig(tt.payload) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, config) + } + }) + } +} From 55b835a405e49885217bfb2711d8e63be679e723 Mon Sep 17 00:00:00 2001 From: fabrice guery Date: Fri, 24 Oct 2025 16:27:40 +0200 Subject: [PATCH 08/12] feat(EN-208): increase, couldn't connecto properly --- .../plugins/public/increase/config.go | 37 ++++++++-- .../plugins/public/increase/config_test.go | 70 +++++++++++++++++++ 2 files changed, 100 insertions(+), 7 deletions(-) create mode 100644 internal/connectors/plugins/public/increase/config_test.go diff --git a/internal/connectors/plugins/public/increase/config.go b/internal/connectors/plugins/public/increase/config.go index 4723a49de..3d68a3f56 100644 --- a/internal/connectors/plugins/public/increase/config.go +++ b/internal/connectors/plugins/public/increase/config.go @@ -2,22 +2,45 @@ package increase import ( "encoding/json" - "fmt" + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" "github.com/formancehq/payments/internal/models" "github.com/go-playground/validator/v10" + "github.com/pkg/errors" ) type Config struct { - APIKey string `json:"apiKey" validate:"required"` - Endpoint string `json:"endpoint" validate:"required"` - WebhookSharedSecret string `json:"webhookSharedSecret" validate:"required"` + APIKey string `json:"apiKey" validate:"required"` + Endpoint string `json:"endpoint" validate:"required"` + WebhookSharedSecret string `json:"webhookSharedSecret" validate:"required"` + PollingPeriod sharedconfig.PollingPeriod `json:"pollingPeriod"` } func unmarshalAndValidateConfig(payload json.RawMessage) (Config, error) { - var config Config - if err := json.Unmarshal(payload, &config); err != nil { - return Config{}, fmt.Errorf("%w: %w", err, models.ErrInvalidConfig) + var raw struct { + APIKey string `json:"apiKey"` + Endpoint string `json:"endpoint"` + WebhookSharedSecret string `json:"webhookSharedSecret"` + PollingPeriod string `json:"pollingPeriod"` + } + if err := json.Unmarshal(payload, &raw); err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + pp, err := sharedconfig.NewPollingPeriod( + raw.PollingPeriod, + sharedconfig.DefaultPollingPeriod, + sharedconfig.MinimumPollingPeriod, + ) + if err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + config := Config{ + APIKey: raw.APIKey, + Endpoint: raw.Endpoint, + WebhookSharedSecret: raw.WebhookSharedSecret, + PollingPeriod: pp, } validate := validator.New(validator.WithRequiredStructEnabled()) diff --git a/internal/connectors/plugins/public/increase/config_test.go b/internal/connectors/plugins/public/increase/config_test.go new file mode 100644 index 000000000..2d8eb201e --- /dev/null +++ b/internal/connectors/plugins/public/increase/config_test.go @@ -0,0 +1,70 @@ +package increase + +import ( + "testing" + + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUnmarshalAndValidateConfig(t *testing.T) { + t.Parallel() + + defaultPollingPeriod, _ := sharedconfig.NewPollingPeriod("", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + longPollingPeriod, _ := sharedconfig.NewPollingPeriod("45m", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + + tests := []struct { + name string + payload []byte + expected Config + expectError bool + }{ + { + name: "Valid Config", + payload: []byte(`{"apiKey":"sk_test","endpoint":"https://api.increase.com","webhookSharedSecret":"whsec_123"}`), + expected: Config{ + APIKey: "sk_test", + Endpoint: "https://api.increase.com", + WebhookSharedSecret: "whsec_123", + PollingPeriod: defaultPollingPeriod, + }, + expectError: false, + }, + { + name: "Missing Required Fields", + payload: []byte(`{"apiKey":"sk_test"}`), + expected: Config{}, + expectError: true, + }, + { + name: "Non default polling period", + payload: []byte(`{"apiKey":"sk_test","endpoint":"https://api.increase.com","webhookSharedSecret":"whsec_123","pollingPeriod":"45m"}`), + expected: Config{ + APIKey: "sk_test", + Endpoint: "https://api.increase.com", + WebhookSharedSecret: "whsec_123", + PollingPeriod: longPollingPeriod, + }, + expectError: false, + }, + { + name: "Invalid polling period", + payload: []byte(`{"apiKey":"sk_test","endpoint":"https://api.increase.com","webhookSharedSecret":"whsec_123","pollingPeriod":"not-a-duration"}`), + expected: Config{}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config, err := unmarshalAndValidateConfig(tt.payload) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, config) + } + }) + } +} From 36be87d4e87b7b6c354da92c7376d9fce47434ed Mon Sep 17 00:00:00 2001 From: fabrice guery Date: Fri, 24 Oct 2025 17:13:00 +0200 Subject: [PATCH 09/12] feat(EN-208): added pollingPeriod for mangopay, modulr, moneycorp, qonto and wise --- .../plugins/public/mangopay/config.go | 35 +++++-- .../plugins/public/mangopay/config_test.go | 70 ++++++++++++++ .../plugins/public/modulr/config.go | 33 ++++++- .../plugins/public/modulr/config_test.go | 70 ++++++++++++++ .../plugins/public/moneycorp/config.go | 33 ++++++- .../plugins/public/moneycorp/config_test.go | 70 ++++++++++++++ .../connectors/plugins/public/qonto/config.go | 42 +++++++-- .../plugins/public/qonto/config_test.go | 37 ++++++++ .../connectors/plugins/public/wise/config.go | 30 +++++- .../plugins/public/wise/config_test.go | 93 +++++++++++++++++++ 10 files changed, 485 insertions(+), 28 deletions(-) create mode 100644 internal/connectors/plugins/public/mangopay/config_test.go create mode 100644 internal/connectors/plugins/public/modulr/config_test.go create mode 100644 internal/connectors/plugins/public/moneycorp/config_test.go create mode 100644 internal/connectors/plugins/public/wise/config_test.go diff --git a/internal/connectors/plugins/public/mangopay/config.go b/internal/connectors/plugins/public/mangopay/config.go index 49b12cbaf..48eb1fafb 100644 --- a/internal/connectors/plugins/public/mangopay/config.go +++ b/internal/connectors/plugins/public/mangopay/config.go @@ -3,22 +3,45 @@ package mangopay import ( "encoding/json" + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" "github.com/formancehq/payments/internal/models" "github.com/go-playground/validator/v10" "github.com/pkg/errors" ) -type Config struct { - ClientID string `json:"clientID" validate:"required"` - APIKey string `json:"apiKey" validate:"required"` - Endpoint string `json:"endpoint" validate:"required"` + type Config struct { + ClientID string `json:"clientID" validate:"required"` + APIKey string `json:"apiKey" validate:"required"` + Endpoint string `json:"endpoint" validate:"required"` + PollingPeriod sharedconfig.PollingPeriod `json:"pollingPeriod"` } func unmarshalAndValidateConfig(payload json.RawMessage) (Config, error) { - var config Config - if err := json.Unmarshal(payload, &config); err != nil { + var raw struct { + ClientID string `json:"clientID"` + APIKey string `json:"apiKey"` + Endpoint string `json:"endpoint"` + PollingPeriod string `json:"pollingPeriod"` + } + if err := json.Unmarshal(payload, &raw); err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + pp, err := sharedconfig.NewPollingPeriod( + raw.PollingPeriod, + sharedconfig.DefaultPollingPeriod, + sharedconfig.MinimumPollingPeriod, + ) + if err != nil { return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) } + + config := Config{ + ClientID: raw.ClientID, + APIKey: raw.APIKey, + Endpoint: raw.Endpoint, + PollingPeriod: pp, + } validate := validator.New(validator.WithRequiredStructEnabled()) return config, validate.Struct(config) } diff --git a/internal/connectors/plugins/public/mangopay/config_test.go b/internal/connectors/plugins/public/mangopay/config_test.go new file mode 100644 index 000000000..5f8193897 --- /dev/null +++ b/internal/connectors/plugins/public/mangopay/config_test.go @@ -0,0 +1,70 @@ +package mangopay + +import ( + "testing" + + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUnmarshalAndValidateConfig(t *testing.T) { + t.Parallel() + + defaultPollingPeriod, _ := sharedconfig.NewPollingPeriod("", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + longPollingPeriod, _ := sharedconfig.NewPollingPeriod("45m", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + + tests := []struct { + name string + payload []byte + expected Config + expectError bool + }{ + { + name: "Valid Config", + payload: []byte(`{"clientID":"client_123","apiKey":"sk_test","endpoint":"https://api.mangopay.com"}`), + expected: Config{ + ClientID: "client_123", + APIKey: "sk_test", + Endpoint: "https://api.mangopay.com", + PollingPeriod: defaultPollingPeriod, + }, + expectError: false, + }, + { + name: "Missing Required Fields", + payload: []byte(`{"clientID":"client_123"}`), + expected: Config{}, + expectError: true, + }, + { + name: "Non default polling period", + payload: []byte(`{"clientID":"client_123","apiKey":"sk_test","endpoint":"https://api.mangopay.com","pollingPeriod":"45m"}`), + expected: Config{ + ClientID: "client_123", + APIKey: "sk_test", + Endpoint: "https://api.mangopay.com", + PollingPeriod: longPollingPeriod, + }, + expectError: false, + }, + { + name: "Invalid polling period", + payload: []byte(`{"clientID":"client_123","apiKey":"sk_test","endpoint":"https://api.mangopay.com","pollingPeriod":"not-a-duration"}`), + expected: Config{}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config, err := unmarshalAndValidateConfig(tt.payload) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, config) + } + }) + } +} diff --git a/internal/connectors/plugins/public/modulr/config.go b/internal/connectors/plugins/public/modulr/config.go index a835d4732..a1367916f 100644 --- a/internal/connectors/plugins/public/modulr/config.go +++ b/internal/connectors/plugins/public/modulr/config.go @@ -3,22 +3,45 @@ package modulr import ( "encoding/json" + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" "github.com/formancehq/payments/internal/models" "github.com/go-playground/validator/v10" "github.com/pkg/errors" ) type Config struct { - APIKey string `json:"apiKey" validate:"required"` - APISecret string `json:"apiSecret" validate:"required"` - Endpoint string `json:"endpoint" validate:"required"` + APIKey string `json:"apiKey" validate:"required"` + APISecret string `json:"apiSecret" validate:"required"` + Endpoint string `json:"endpoint" validate:"required"` + PollingPeriod sharedconfig.PollingPeriod `json:"pollingPeriod"` } func unmarshalAndValidateConfig(payload []byte) (Config, error) { - var config Config - if err := json.Unmarshal(payload, &config); err != nil { + var raw struct { + APIKey string `json:"apiKey"` + APISecret string `json:"apiSecret"` + Endpoint string `json:"endpoint"` + PollingPeriod string `json:"pollingPeriod"` + } + if err := json.Unmarshal(payload, &raw); err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + pp, err := sharedconfig.NewPollingPeriod( + raw.PollingPeriod, + sharedconfig.DefaultPollingPeriod, + sharedconfig.MinimumPollingPeriod, + ) + if err != nil { return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) } + + config := Config{ + APIKey: raw.APIKey, + APISecret: raw.APISecret, + Endpoint: raw.Endpoint, + PollingPeriod: pp, + } validate := validator.New(validator.WithRequiredStructEnabled()) return config, validate.Struct(config) } diff --git a/internal/connectors/plugins/public/modulr/config_test.go b/internal/connectors/plugins/public/modulr/config_test.go new file mode 100644 index 000000000..b65489909 --- /dev/null +++ b/internal/connectors/plugins/public/modulr/config_test.go @@ -0,0 +1,70 @@ +package modulr + +import ( + "testing" + + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUnmarshalAndValidateConfig(t *testing.T) { + t.Parallel() + + defaultPollingPeriod, _ := sharedconfig.NewPollingPeriod("", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + longPollingPeriod, _ := sharedconfig.NewPollingPeriod("45m", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + + tests := []struct { + name string + payload []byte + expected Config + expectError bool + }{ + { + name: "Valid Config", + payload: []byte(`{"apiKey":"ak_test","apiSecret":"as_test","endpoint":"https://api.modulr.com"}`), + expected: Config{ + APIKey: "ak_test", + APISecret: "as_test", + Endpoint: "https://api.modulr.com", + PollingPeriod: defaultPollingPeriod, + }, + expectError: false, + }, + { + name: "Missing Required Fields", + payload: []byte(`{"apiKey":"ak_test"}`), + expected: Config{}, + expectError: true, + }, + { + name: "Non default polling period", + payload: []byte(`{"apiKey":"ak_test","apiSecret":"as_test","endpoint":"https://api.modulr.com","pollingPeriod":"45m"}`), + expected: Config{ + APIKey: "ak_test", + APISecret: "as_test", + Endpoint: "https://api.modulr.com", + PollingPeriod: longPollingPeriod, + }, + expectError: false, + }, + { + name: "Invalid polling period", + payload: []byte(`{"apiKey":"ak_test","apiSecret":"as_test","endpoint":"https://api.modulr.com","pollingPeriod":"not-a-duration"}`), + expected: Config{}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config, err := unmarshalAndValidateConfig(tt.payload) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, config) + } + }) + } +} diff --git a/internal/connectors/plugins/public/moneycorp/config.go b/internal/connectors/plugins/public/moneycorp/config.go index ecb2d6214..b4049134e 100644 --- a/internal/connectors/plugins/public/moneycorp/config.go +++ b/internal/connectors/plugins/public/moneycorp/config.go @@ -3,22 +3,45 @@ package moneycorp import ( "encoding/json" + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" "github.com/formancehq/payments/internal/models" "github.com/go-playground/validator/v10" "github.com/pkg/errors" ) type Config struct { - ClientID string `json:"clientID" validate:"required"` - APIKey string `json:"apiKey" validate:"required"` - Endpoint string `json:"endpoint" validate:"required"` + ClientID string `json:"clientID" validate:"required"` + APIKey string `json:"apiKey" validate:"required"` + Endpoint string `json:"endpoint" validate:"required"` + PollingPeriod sharedconfig.PollingPeriod `json:"pollingPeriod"` } func unmarshalAndValidateConfig(payload json.RawMessage) (Config, error) { - var config Config - if err := json.Unmarshal(payload, &config); err != nil { + var raw struct { + ClientID string `json:"clientID"` + APIKey string `json:"apiKey"` + Endpoint string `json:"endpoint"` + PollingPeriod string `json:"pollingPeriod"` + } + if err := json.Unmarshal(payload, &raw); err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + pp, err := sharedconfig.NewPollingPeriod( + raw.PollingPeriod, + sharedconfig.DefaultPollingPeriod, + sharedconfig.MinimumPollingPeriod, + ) + if err != nil { return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) } + + config := Config{ + ClientID: raw.ClientID, + APIKey: raw.APIKey, + Endpoint: raw.Endpoint, + PollingPeriod: pp, + } validate := validator.New(validator.WithRequiredStructEnabled()) return config, validate.Struct(config) } diff --git a/internal/connectors/plugins/public/moneycorp/config_test.go b/internal/connectors/plugins/public/moneycorp/config_test.go new file mode 100644 index 000000000..8be0bc4b0 --- /dev/null +++ b/internal/connectors/plugins/public/moneycorp/config_test.go @@ -0,0 +1,70 @@ +package moneycorp + +import ( + "testing" + + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUnmarshalAndValidateConfig(t *testing.T) { + t.Parallel() + + defaultPollingPeriod, _ := sharedconfig.NewPollingPeriod("", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + longPollingPeriod, _ := sharedconfig.NewPollingPeriod("45m", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + + tests := []struct { + name string + payload []byte + expected Config + expectError bool + }{ + { + name: "Valid Config", + payload: []byte(`{"clientID":"client_123","apiKey":"sk_test","endpoint":"https://api.moneycorp.com"}`), + expected: Config{ + ClientID: "client_123", + APIKey: "sk_test", + Endpoint: "https://api.moneycorp.com", + PollingPeriod: defaultPollingPeriod, + }, + expectError: false, + }, + { + name: "Missing Required Fields", + payload: []byte(`{"clientID":"client_123"}`), + expected: Config{}, + expectError: true, + }, + { + name: "Non default polling period", + payload: []byte(`{"clientID":"client_123","apiKey":"sk_test","endpoint":"https://api.moneycorp.com","pollingPeriod":"45m"}`), + expected: Config{ + ClientID: "client_123", + APIKey: "sk_test", + Endpoint: "https://api.moneycorp.com", + PollingPeriod: longPollingPeriod, + }, + expectError: false, + }, + { + name: "Invalid polling period", + payload: []byte(`{"clientID":"client_123","apiKey":"sk_test","endpoint":"https://api.moneycorp.com","pollingPeriod":"not-a-duration"}`), + expected: Config{}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config, err := unmarshalAndValidateConfig(tt.payload) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, config) + } + }) + } +} diff --git a/internal/connectors/plugins/public/qonto/config.go b/internal/connectors/plugins/public/qonto/config.go index ca1f3e509..ba758bcce 100644 --- a/internal/connectors/plugins/public/qonto/config.go +++ b/internal/connectors/plugins/public/qonto/config.go @@ -2,22 +2,48 @@ package qonto import ( "encoding/json" - "fmt" + + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" "github.com/formancehq/payments/internal/models" "github.com/go-playground/validator/v10" + "github.com/pkg/errors" ) type Config struct { - ClientID string `json:"clientID" validate:"required"` - APIKey string `json:"apiKey" validate:"required"` - Endpoint string `json:"endpoint" validate:"required"` - StagingToken string `json:"stagingToken" validate:"omitempty"` + ClientID string `json:"clientID" validate:"required"` + APIKey string `json:"apiKey" validate:"required"` + Endpoint string `json:"endpoint" validate:"required"` + StagingToken string `json:"stagingToken" validate:"omitempty"` + PollingPeriod sharedconfig.PollingPeriod `json:"pollingPeriod"` } func unmarshalAndValidateConfig(payload json.RawMessage) (Config, error) { - var config Config - if err := json.Unmarshal(payload, &config); err != nil { - return Config{}, fmt.Errorf("%w: %w", err, models.ErrInvalidConfig) + var raw struct { + ClientID string `json:"clientID"` + APIKey string `json:"apiKey"` + Endpoint string `json:"endpoint"` + StagingToken string `json:"stagingToken"` + PollingPeriod string `json:"pollingPeriod"` + } + if err := json.Unmarshal(payload, &raw); err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + pp, err := sharedconfig.NewPollingPeriod( + raw.PollingPeriod, + sharedconfig.DefaultPollingPeriod, + sharedconfig.MinimumPollingPeriod, + ) + if err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + config := Config{ + ClientID: raw.ClientID, + APIKey: raw.APIKey, + Endpoint: raw.Endpoint, + StagingToken: raw.StagingToken, + PollingPeriod: pp, } validate := validator.New(validator.WithRequiredStructEnabled()) diff --git a/internal/connectors/plugins/public/qonto/config_test.go b/internal/connectors/plugins/public/qonto/config_test.go index 2a173d6ea..200f96d43 100644 --- a/internal/connectors/plugins/public/qonto/config_test.go +++ b/internal/connectors/plugins/public/qonto/config_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "github.com/go-playground/validator/v10" + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" "github.com/formancehq/payments/internal/models" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -15,8 +16,19 @@ var _ = Describe("unmarshalAndValidateConfig", func() { expectedError error config Config err error + + defaultPollingPeriod sharedconfig.PollingPeriod + longPollingPeriod sharedconfig.PollingPeriod ) + BeforeEach(func() { + var err error + defaultPollingPeriod, err = sharedconfig.NewPollingPeriod("", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + Expect(err).To(BeNil()) + longPollingPeriod, err = sharedconfig.NewPollingPeriod("45m", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + Expect(err).To(BeNil()) + }) + JustBeforeEach(func() { config, err = unmarshalAndValidateConfig(payload) }) @@ -33,6 +45,7 @@ var _ = Describe("unmarshalAndValidateConfig", func() { Expect(config.APIKey).To(Equal("validApiKey")) Expect(config.Endpoint).To(Equal("https://example.com")) Expect(config.StagingToken).To(Equal("token123")) + Expect(config.PollingPeriod).To(Equal(defaultPollingPeriod)) }) }) @@ -83,6 +96,7 @@ var _ = Describe("unmarshalAndValidateConfig", func() { Expect(config.APIKey).To(Equal("validApiKey")) Expect(config.Endpoint).To(Equal("https://example.com")) Expect(config.StagingToken).To(BeEmpty()) + Expect(config.PollingPeriod).To(Equal(defaultPollingPeriod)) }) }) @@ -109,4 +123,27 @@ var _ = Describe("unmarshalAndValidateConfig", func() { Expect(err).To(MatchError(ContainSubstring(expectedError.Error()))) }) }) + + Context("with custom polling period", func() { + BeforeEach(func() { + payload = json.RawMessage(`{"clientID":"validClient","apiKey":"validApiKey","endpoint":"https://example.com","pollingPeriod":"45m"}`) + }) + + It("should parse and set the custom polling period", func() { + Expect(err).To(BeNil()) + Expect(config.PollingPeriod).To(Equal(longPollingPeriod)) + }) + }) + + Context("with invalid polling period", func() { + BeforeEach(func() { + payload = json.RawMessage(`{"clientID":"validClient","apiKey":"validApiKey","endpoint":"https://example.com","pollingPeriod":"not-a-duration"}`) + expectedError = models.ErrInvalidConfig + }) + + It("should return an error about invalid config", func() { + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError(ContainSubstring(expectedError.Error()))) + }) + }) }) diff --git a/internal/connectors/plugins/public/wise/config.go b/internal/connectors/plugins/public/wise/config.go index 681baf8cc..1d861f49f 100644 --- a/internal/connectors/plugins/public/wise/config.go +++ b/internal/connectors/plugins/public/wise/config.go @@ -7,6 +7,7 @@ import ( "encoding/pem" "fmt" + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" "github.com/formancehq/payments/internal/models" errorsutils "github.com/formancehq/payments/internal/utils/errors" "github.com/go-playground/validator/v10" @@ -14,8 +15,9 @@ import ( ) type Config struct { - APIKey string `json:"apiKey" validate:"required"` - WebhookPublicKey string `json:"webhookPublicKey" validate:"required"` + APIKey string `json:"apiKey" validate:"required"` + WebhookPublicKey string `json:"webhookPublicKey" validate:"required"` + PollingPeriod sharedconfig.PollingPeriod `json:"pollingPeriod"` webhookPublicKey *rsa.PublicKey `json:"-"` } @@ -51,10 +53,30 @@ func (c *Config) validate() error { } func unmarshalAndValidateConfig(payload json.RawMessage) (Config, error) { - var config Config - if err := json.Unmarshal(payload, &config); err != nil { + var raw struct { + APIKey string `json:"apiKey"` + WebhookPublicKey string `json:"webhookPublicKey"` + PollingPeriod string `json:"pollingPeriod"` + } + if err := json.Unmarshal(payload, &raw); err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + pp, err := sharedconfig.NewPollingPeriod( + raw.PollingPeriod, + sharedconfig.DefaultPollingPeriod, + sharedconfig.MinimumPollingPeriod, + ) + if err != nil { return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) } + + config := Config{ + APIKey: raw.APIKey, + WebhookPublicKey: raw.WebhookPublicKey, + PollingPeriod: pp, + } + validate := validator.New(validator.WithRequiredStructEnabled()) if err := validate.Struct(config); err != nil { return config, err diff --git a/internal/connectors/plugins/public/wise/config_test.go b/internal/connectors/plugins/public/wise/config_test.go new file mode 100644 index 000000000..feebae1c3 --- /dev/null +++ b/internal/connectors/plugins/public/wise/config_test.go @@ -0,0 +1,93 @@ +package wise + +import ( + "encoding/json" + "testing" + + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" + "github.com/formancehq/payments/internal/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const validRSAPublicKeyPEM = `-----BEGIN PUBLIC KEY----- +MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALiJyoSgGJE0E7E5Wdl66iRS0LlwM651 +01qmPvvrLpzjAU6YewsGmmKzBSSMSmc5QwDFi1Cdm42Hcps225y7sKsCAwEAAQ== +-----END PUBLIC KEY-----` + +func makePayload(t *testing.T, v any) []byte { + b, err := json.Marshal(v) + require.NoError(t, err) + return b +} + +func TestUnmarshalAndValidateConfig(t *testing.T) { + t.Parallel() + + defaultPollingPeriod, _ := sharedconfig.NewPollingPeriod("", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + longPollingPeriod, _ := sharedconfig.NewPollingPeriod("45m", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + + tests := []struct { + name string + payload []byte + expected Config + expectError bool + }{ + { + name: "Valid Config", + payload: makePayload(t, map[string]string{"apiKey": "sk_test", "webhookPublicKey": validRSAPublicKeyPEM}), + expected: Config{ + APIKey: "sk_test", + WebhookPublicKey: validRSAPublicKeyPEM, + PollingPeriod: defaultPollingPeriod, + }, + expectError: false, + }, + { + name: "Missing Required Fields", + payload: makePayload(t, map[string]string{"apiKey": "sk_test"}), + expected: Config{}, + expectError: true, + }, + { + name: "Non default polling period", + payload: makePayload(t, map[string]string{"apiKey": "sk_test", "webhookPublicKey": validRSAPublicKeyPEM, "pollingPeriod": "45m"}), + expected: Config{ + APIKey: "sk_test", + WebhookPublicKey: validRSAPublicKeyPEM, + PollingPeriod: longPollingPeriod, + }, + expectError: false, + }, + { + name: "Invalid polling period", + payload: makePayload(t, map[string]string{"apiKey": "sk_test", "webhookPublicKey": validRSAPublicKeyPEM, "pollingPeriod": "not-a-duration"}), + expected: Config{}, + expectError: true, + }, + { + name: "Invalid public key", + payload: makePayload(t, map[string]string{"apiKey": "sk_test", "webhookPublicKey": "not-a-valid-pem"}), + expected: Config{}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config, err := unmarshalAndValidateConfig(tt.payload) + if tt.expectError { + require.Error(t, err) + // For invalid polling period and invalid key, ensure error is marked as invalid config + if tt.name == "Invalid polling period" || tt.name == "Invalid public key" { + assert.ErrorContains(t, err, models.ErrInvalidConfig.Error()) + } + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected.APIKey, config.APIKey) + assert.Equal(t, tt.expected.WebhookPublicKey, config.WebhookPublicKey) + assert.Equal(t, tt.expected.PollingPeriod, config.PollingPeriod) + } + }) + } +} From 2b5146e035409803133d05cef1daaec1fff14847 Mon Sep 17 00:00:00 2001 From: fabrice guery Date: Fri, 24 Oct 2025 17:25:27 +0200 Subject: [PATCH 10/12] feat(EN-208): fixed openapi and sdk --- docs/api/README.md | 20 +++++++++---------- openapi.yaml | 10 ---------- openapi/v3/v3-connectors-config.yaml | 10 ---------- pkg/client/.speakeasy/gen.lock | 2 +- pkg/client/models/components/v3atlarconfig.go | 2 +- .../components/v3bankingcircleconfig.go | 2 +- .../models/components/v3columnconfig.go | 2 +- .../models/components/v3genericconfig.go | 2 +- .../models/components/v3increaseconfig.go | 2 +- .../models/components/v3mangopayconfig.go | 2 +- .../models/components/v3modulrconfig.go | 2 +- .../models/components/v3moneycorpconfig.go | 2 +- pkg/client/models/components/v3qontoconfig.go | 2 +- pkg/client/models/components/v3wiseconfig.go | 2 +- 14 files changed, 21 insertions(+), 41 deletions(-) diff --git a/docs/api/README.md b/docs/api/README.md index 0aeca3e57..4e7fcd02d 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -6879,7 +6879,7 @@ xor "baseUrl": "string", "name": "string", "pageSize": 25, - "pollingPeriod": "2m", + "pollingPeriod": "string", "provider": "Atlar", "secret": "string" } @@ -6912,7 +6912,7 @@ xor "name": "string", "pageSize": 25, "password": "string", - "pollingPeriod": "2m", + "pollingPeriod": "string", "provider": "Bankingcircle", "userCertificate": "string", "userCertificateKey": "string", @@ -6949,7 +6949,7 @@ xor "endpoint": "string", "name": "string", "pageSize": 25, - "pollingPeriod": "2m", + "pollingPeriod": "string", "provider": "Column" } @@ -7043,7 +7043,7 @@ xor "endpoint": "string", "name": "string", "pageSize": 25, - "pollingPeriod": "2m", + "pollingPeriod": "string", "provider": "Generic" } @@ -7073,7 +7073,7 @@ xor "endpoint": "string", "name": "string", "pageSize": 25, - "pollingPeriod": "2m", + "pollingPeriod": "string", "provider": "Increase", "webhookSharedSecret": "string" } @@ -7106,7 +7106,7 @@ xor "endpoint": "string", "name": "string", "pageSize": 25, - "pollingPeriod": "2m", + "pollingPeriod": "string", "provider": "Mangopay" } @@ -7138,7 +7138,7 @@ xor "endpoint": "string", "name": "string", "pageSize": 25, - "pollingPeriod": "2m", + "pollingPeriod": "string", "provider": "Modulr" } @@ -7170,7 +7170,7 @@ xor "endpoint": "string", "name": "string", "pageSize": 25, - "pollingPeriod": "2m", + "pollingPeriod": "string", "provider": "Moneycorp" } @@ -7272,7 +7272,7 @@ xor "endpoint": "string", "name": "string", "pageSize": 25, - "pollingPeriod": "2m", + "pollingPeriod": "string", "provider": "Qonto", "stagingToken": "string" } @@ -7364,7 +7364,7 @@ xor "apiKey": "string", "name": "string", "pageSize": 25, - "pollingPeriod": "2m", + "pollingPeriod": "string", "provider": "Wise", "webhookPublicKey": "string" } diff --git a/openapi.yaml b/openapi.yaml index ca00bd6ef..8a46d156c 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -5746,7 +5746,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Atlar @@ -5776,7 +5775,6 @@ components: type: string pollingPeriod: type: string - default: 2m provider: type: string default: Bankingcircle @@ -5804,7 +5802,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Column @@ -5874,7 +5871,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Generic @@ -5897,7 +5893,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Increase @@ -5924,7 +5919,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Mangopay @@ -5949,7 +5943,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Modulr @@ -5974,7 +5967,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Moneycorp @@ -6057,7 +6049,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Qonto @@ -6122,7 +6113,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Wise diff --git a/openapi/v3/v3-connectors-config.yaml b/openapi/v3/v3-connectors-config.yaml index ed8e23694..a8e32cef8 100644 --- a/openapi/v3/v3-connectors-config.yaml +++ b/openapi/v3/v3-connectors-config.yaml @@ -86,7 +86,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Atlar @@ -116,7 +115,6 @@ components: type: string pollingPeriod: type: string - default: 2m provider: type: string default: Bankingcircle @@ -144,7 +142,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Column @@ -214,7 +211,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Generic @@ -237,7 +233,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Increase @@ -264,7 +259,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Mangopay @@ -289,7 +283,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Modulr @@ -314,7 +307,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Moneycorp @@ -397,7 +389,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Qonto @@ -462,7 +453,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Wise diff --git a/pkg/client/.speakeasy/gen.lock b/pkg/client/.speakeasy/gen.lock index 13e8dde6e..bd80eebd0 100644 --- a/pkg/client/.speakeasy/gen.lock +++ b/pkg/client/.speakeasy/gen.lock @@ -1,7 +1,7 @@ lockVersion: 2.0.0 id: 1fa8a26f-45d9-44b7-8b97-fbeebcdcd8b1 management: - docChecksum: eef91ffe7da06070422e5df9aad7ca67 + docChecksum: 71facf2180596f678f821f4bef836873 docVersion: v1 speakeasyVersion: 1.525.0 generationVersion: 2.562.2 diff --git a/pkg/client/models/components/v3atlarconfig.go b/pkg/client/models/components/v3atlarconfig.go index 029c60172..c39825509 100644 --- a/pkg/client/models/components/v3atlarconfig.go +++ b/pkg/client/models/components/v3atlarconfig.go @@ -11,7 +11,7 @@ type V3AtlarConfig struct { BaseURL string `json:"baseUrl"` Name string `json:"name"` PageSize *int64 `default:"25" json:"pageSize"` - PollingPeriod *string `default:"2m" json:"pollingPeriod"` + PollingPeriod *string `json:"pollingPeriod,omitempty"` Provider *string `default:"Atlar" json:"provider"` Secret string `json:"secret"` } diff --git a/pkg/client/models/components/v3bankingcircleconfig.go b/pkg/client/models/components/v3bankingcircleconfig.go index 320351127..b0a400d32 100644 --- a/pkg/client/models/components/v3bankingcircleconfig.go +++ b/pkg/client/models/components/v3bankingcircleconfig.go @@ -12,7 +12,7 @@ type V3BankingcircleConfig struct { Name string `json:"name"` PageSize *int64 `default:"25" json:"pageSize"` Password string `json:"password"` - PollingPeriod *string `default:"2m" json:"pollingPeriod"` + PollingPeriod *string `json:"pollingPeriod,omitempty"` Provider *string `default:"Bankingcircle" json:"provider"` UserCertificate string `json:"userCertificate"` UserCertificateKey string `json:"userCertificateKey"` diff --git a/pkg/client/models/components/v3columnconfig.go b/pkg/client/models/components/v3columnconfig.go index 7961cac05..0245e4b17 100644 --- a/pkg/client/models/components/v3columnconfig.go +++ b/pkg/client/models/components/v3columnconfig.go @@ -11,7 +11,7 @@ type V3ColumnConfig struct { Endpoint string `json:"endpoint"` Name string `json:"name"` PageSize *int64 `default:"25" json:"pageSize"` - PollingPeriod *string `default:"2m" json:"pollingPeriod"` + PollingPeriod *string `json:"pollingPeriod,omitempty"` Provider *string `default:"Column" json:"provider"` } diff --git a/pkg/client/models/components/v3genericconfig.go b/pkg/client/models/components/v3genericconfig.go index 3d6cc5953..a36284ec2 100644 --- a/pkg/client/models/components/v3genericconfig.go +++ b/pkg/client/models/components/v3genericconfig.go @@ -11,7 +11,7 @@ type V3GenericConfig struct { Endpoint string `json:"endpoint"` Name string `json:"name"` PageSize *int64 `default:"25" json:"pageSize"` - PollingPeriod *string `default:"2m" json:"pollingPeriod"` + PollingPeriod *string `json:"pollingPeriod,omitempty"` Provider *string `default:"Generic" json:"provider"` } diff --git a/pkg/client/models/components/v3increaseconfig.go b/pkg/client/models/components/v3increaseconfig.go index 0a8ab9571..d6dbed6c5 100644 --- a/pkg/client/models/components/v3increaseconfig.go +++ b/pkg/client/models/components/v3increaseconfig.go @@ -11,7 +11,7 @@ type V3IncreaseConfig struct { Endpoint string `json:"endpoint"` Name string `json:"name"` PageSize *int64 `default:"25" json:"pageSize"` - PollingPeriod *string `default:"2m" json:"pollingPeriod"` + PollingPeriod *string `json:"pollingPeriod,omitempty"` Provider *string `default:"Increase" json:"provider"` WebhookSharedSecret string `json:"webhookSharedSecret"` } diff --git a/pkg/client/models/components/v3mangopayconfig.go b/pkg/client/models/components/v3mangopayconfig.go index 381fd08da..3957ea326 100644 --- a/pkg/client/models/components/v3mangopayconfig.go +++ b/pkg/client/models/components/v3mangopayconfig.go @@ -12,7 +12,7 @@ type V3MangopayConfig struct { Endpoint string `json:"endpoint"` Name string `json:"name"` PageSize *int64 `default:"25" json:"pageSize"` - PollingPeriod *string `default:"2m" json:"pollingPeriod"` + PollingPeriod *string `json:"pollingPeriod,omitempty"` Provider *string `default:"Mangopay" json:"provider"` } diff --git a/pkg/client/models/components/v3modulrconfig.go b/pkg/client/models/components/v3modulrconfig.go index 82065ee90..2a7c18a1a 100644 --- a/pkg/client/models/components/v3modulrconfig.go +++ b/pkg/client/models/components/v3modulrconfig.go @@ -12,7 +12,7 @@ type V3ModulrConfig struct { Endpoint string `json:"endpoint"` Name string `json:"name"` PageSize *int64 `default:"25" json:"pageSize"` - PollingPeriod *string `default:"2m" json:"pollingPeriod"` + PollingPeriod *string `json:"pollingPeriod,omitempty"` Provider *string `default:"Modulr" json:"provider"` } diff --git a/pkg/client/models/components/v3moneycorpconfig.go b/pkg/client/models/components/v3moneycorpconfig.go index 5c2246a92..b29a82b8e 100644 --- a/pkg/client/models/components/v3moneycorpconfig.go +++ b/pkg/client/models/components/v3moneycorpconfig.go @@ -12,7 +12,7 @@ type V3MoneycorpConfig struct { Endpoint string `json:"endpoint"` Name string `json:"name"` PageSize *int64 `default:"25" json:"pageSize"` - PollingPeriod *string `default:"2m" json:"pollingPeriod"` + PollingPeriod *string `json:"pollingPeriod,omitempty"` Provider *string `default:"Moneycorp" json:"provider"` } diff --git a/pkg/client/models/components/v3qontoconfig.go b/pkg/client/models/components/v3qontoconfig.go index 94c74d009..b752890a7 100644 --- a/pkg/client/models/components/v3qontoconfig.go +++ b/pkg/client/models/components/v3qontoconfig.go @@ -12,7 +12,7 @@ type V3QontoConfig struct { Endpoint string `json:"endpoint"` Name string `json:"name"` PageSize *int64 `default:"25" json:"pageSize"` - PollingPeriod *string `default:"2m" json:"pollingPeriod"` + PollingPeriod *string `json:"pollingPeriod,omitempty"` Provider *string `default:"Qonto" json:"provider"` StagingToken *string `json:"stagingToken,omitempty"` } diff --git a/pkg/client/models/components/v3wiseconfig.go b/pkg/client/models/components/v3wiseconfig.go index 198505c00..ee2f88733 100644 --- a/pkg/client/models/components/v3wiseconfig.go +++ b/pkg/client/models/components/v3wiseconfig.go @@ -10,7 +10,7 @@ type V3WiseConfig struct { APIKey string `json:"apiKey"` Name string `json:"name"` PageSize *int64 `default:"25" json:"pageSize"` - PollingPeriod *string `default:"2m" json:"pollingPeriod"` + PollingPeriod *string `json:"pollingPeriod,omitempty"` Provider *string `default:"Wise" json:"provider"` WebhookPublicKey string `json:"webhookPublicKey"` } From 66ce32f11d07e277da3bdb3780e29e6d6c9bfa7d Mon Sep 17 00:00:00 2001 From: fabrice guery Date: Mon, 3 Nov 2025 16:45:29 +0100 Subject: [PATCH 11/12] fix: change global default/min pollingPeriod --- internal/models/config.go | 5 +++-- internal/models/config_test.go | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/internal/models/config.go b/internal/models/config.go index 116cabfd4..5d6991eca 100644 --- a/internal/models/config.go +++ b/internal/models/config.go @@ -8,7 +8,7 @@ import ( ) const ( - defaultPollingPeriod = 2 * time.Minute + defaultPollingPeriod = 30 * time.Minute defaultPageSize = 25 ) @@ -17,9 +17,10 @@ const ( type PluginInternalConfig interface{} // Config is the generic configuration that all connectors share +// Note that the PollingPeriod defined here is often overwritten by the connector-specific configuration type Config struct { Name string `json:"name" validate:"required,gte=3,lte=500"` - PollingPeriod time.Duration `json:"pollingPeriod" validate:"required,gte=30000000000,lte=86400000000000"` // gte=30s lte=1d in ns + PollingPeriod time.Duration `json:"pollingPeriod" validate:"required,gte=1200000000000,lte=86400000000000"` // gte=20mn lte=1d in ns PageSize int `json:"pageSize" validate:"lte=150"` } diff --git a/internal/models/config_test.go b/internal/models/config_test.go index 51c9027cb..730a081b1 100644 --- a/internal/models/config_test.go +++ b/internal/models/config_test.go @@ -16,7 +16,7 @@ func TestValidate(t *testing.T) { t.Run("missing name", func(t *testing.T) { config := models.Config{ - PollingPeriod: 40 * time.Second, + PollingPeriod: 40 * time.Minute, PageSize: 30, } err := config.Validate() @@ -76,7 +76,7 @@ func TestValidate(t *testing.T) { t.Run(name, func(t *testing.T) { config := models.Config{ Name: "test", - PollingPeriod: time.Minute, + PollingPeriod: 20 * time.Minute, PageSize: c.val, } err := config.Validate() @@ -93,7 +93,7 @@ func TestValidate(t *testing.T) { t.Run("valid config", func(t *testing.T) { config := models.Config{ Name: "test", - PollingPeriod: 30 * time.Second, + PollingPeriod: 30 * time.Minute, } err := config.Validate() // Then @@ -164,7 +164,7 @@ func TestConfigUnmarshalJSON(t *testing.T) { require.NoError(t, err) assert.Equal(t, "test-config", config.Name) - assert.Equal(t, 2*time.Minute, config.PollingPeriod) // Default value + assert.Equal(t, 30*time.Minute, config.PollingPeriod) // Default value assert.Equal(t, 50, config.PageSize) }) @@ -227,7 +227,7 @@ func TestDefaultConfig(t *testing.T) { config := models.DefaultConfig() - assert.Equal(t, 2*time.Minute, config.PollingPeriod) + assert.Equal(t, 30*time.Minute, config.PollingPeriod) assert.Equal(t, 25, config.PageSize) assert.Empty(t, config.Name) } From 88ba09a70fd1fcbeae7563c4452a799d47c24dcd Mon Sep 17 00:00:00 2001 From: fabrice guery Date: Mon, 3 Nov 2025 18:08:57 +0100 Subject: [PATCH 12/12] fix: e2e tests --- internal/connectors/engine/engine_test.go | 6 +++--- test/e2e/api_connectors_test.go | 24 +++++++++++++++++------ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/internal/connectors/engine/engine_test.go b/internal/connectors/engine/engine_test.go index 29314a5de..de5619e8a 100644 --- a/internal/connectors/engine/engine_test.go +++ b/internal/connectors/engine/engine_test.go @@ -134,7 +134,7 @@ var _ = Describe("Engine Tests", func() { config json.RawMessage ) BeforeEach(func() { - config = json.RawMessage(`{"name":"somename","pollingPeriod":"30s"}`) + config = json.RawMessage(`{"name":"somename","pollingPeriod":"30m"}`) }) It("should return validation error when config is not a valid json", func(ctx SpecContext) { @@ -399,7 +399,7 @@ var _ = Describe("Engine Tests", func() { connectorID models.ConnectorID ) BeforeEach(func() { - config = json.RawMessage(`{"name":"somename","pollingPeriod":"30s"}`) + config = json.RawMessage(`{"name":"somename","pollingPeriod":"30m"}`) connectorID = models.ConnectorID{Provider: "dummypay", Reference: uuid.New()} }) @@ -464,7 +464,7 @@ var _ = Describe("Engine Tests", func() { It("should store the updated config", func(ctx SpecContext) { newName := "new-name" - inputJson := json.RawMessage(fmt.Sprintf(`{"name":"%s","pollingPeriod":"2m","pageSize":25}`, newName)) + inputJson := json.RawMessage(fmt.Sprintf(`{"name":"%s","pollingPeriod":"20m","pageSize":25}`, newName)) connector := &models.Connector{ ConnectorBase: models.ConnectorBase{ ID: connectorID, diff --git a/test/e2e/api_connectors_test.go b/test/e2e/api_connectors_test.go index 70f00a41a..cc839d8be 100644 --- a/test/e2e/api_connectors_test.go +++ b/test/e2e/api_connectors_test.go @@ -77,8 +77,20 @@ var _ = Context("Payments API Connectors", Serial, func() { getRes, err := app.GetValue().SDK().Payments.V3.GetConnectorConfig(ctx, connectorID) Expect(err).To(BeNil()) Expect(getRes.V3GetConnectorConfigResponse).NotTo(BeNil()) - Expect(getRes.V3GetConnectorConfigResponse.Data.V3DummypayConfig).To(Equal(connectorConf)) + Expect(getRes.V3GetConnectorConfigResponse.Data.V3DummypayConfig.Name).To(Equal(connectorConf.Name)) + Expect(getRes.V3GetConnectorConfigResponse.Data.V3DummypayConfig.Provider).To(Equal(connectorConf.Provider)) + Expect(getRes.V3GetConnectorConfigResponse.Data.V3DummypayConfig.Directory).To(Equal(connectorConf.Directory)) + Expect(getRes.V3GetConnectorConfigResponse.Data.V3DummypayConfig.PageSize).To(Equal(connectorConf.PageSize)) Expect(getRes.V3GetConnectorConfigResponse.Data.Type).To(Equal(components.V3ConnectorConfigTypeDummypay)) + + getResPollingPeriod, err := time.ParseDuration( + *getRes.V3GetConnectorConfigResponse.Data.V3DummypayConfig.PollingPeriod, + ) + Expect(err).To(BeNil()) + configPollingPeriod, err := time.ParseDuration(*connectorConf.PollingPeriod) + Expect(err).To(BeNil()) + + Expect(getResPollingPeriod).To(Equal(configPollingPeriod)) }) It("can install a connector with v2 API", func() { @@ -91,7 +103,7 @@ var _ = Context("Payments API Connectors", Serial, func() { connectorConf := components.ConnectorConfig{ DummyPayConfig: &components.DummyPayConfig{ Name: fmt.Sprintf("connector-%s", id.String()), - FilePollingPeriod: pointer.For("30s"), + FilePollingPeriod: pointer.For("30m"), Provider: pointer.For("Dummypay"), Directory: dir, }, @@ -163,7 +175,7 @@ var _ = Context("Payments API Connectors", Serial, func() { config := components.ConnectorConfig{ DummyPayConfig: &components.DummyPayConfig{ Name: "some name", - FilePollingPeriod: pointer.For("2m"), + FilePollingPeriod: pointer.For("30m"), Provider: pointer.For("Dummypay"), Directory: dir, }, @@ -186,7 +198,7 @@ var _ = Context("Payments API Connectors", Serial, func() { Expect(err).To(BeNil()) blockTillWorkflowComplete(ctx, connectorID, "run-tasks-") - config.PollingPeriod = pointer.For("2m0s") + config.PollingPeriod = pointer.For("30m0s") _, err = app.GetValue().SDK().Payments.V3.V3UpdateConnectorConfig(ctx, connectorID, &components.V3UpdateConnectorRequest{ V3DummypayConfig: config, }) @@ -569,7 +581,7 @@ func newV3ConnectorConfigFn() func(id uuid.UUID) *components.V3DummypayConfig { LinkFlowError: pointer.For(false), Name: fmt.Sprintf("connector-%s", id.String()), PageSize: pointer.For(int64(30)), - PollingPeriod: pointer.For("30s"), + PollingPeriod: pointer.For("30m"), Provider: pointer.For("Dummypay"), UpdateLinkFlowError: pointer.For(false), } @@ -586,7 +598,7 @@ func newV2ConnectorConfigFn() func(id uuid.UUID) *components.DummyPayConfig { return &components.DummyPayConfig{ Name: fmt.Sprintf("connector-%s", id.String()), - FilePollingPeriod: pointer.For("30s"), + FilePollingPeriod: pointer.For("30m"), Provider: pointer.For("Dummypay"), Directory: dir, }