diff --git a/.gitignore b/.gitignore index c37041316e..4d6c65acb2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea +.vscode openapi/build/generate.json-e openapi/build/generate.json.bak coverage.out diff --git a/components/operator/go.mod b/components/operator/go.mod index 5dd704e874..fb2d45816f 100644 --- a/components/operator/go.mod +++ b/components/operator/go.mod @@ -41,7 +41,7 @@ require ( github.com/go-openapi/swag v0.22.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect diff --git a/components/operator/go.sum b/components/operator/go.sum index d80959b870..b987ab8b36 100644 --- a/components/operator/go.sum +++ b/components/operator/go.sum @@ -34,12 +34,10 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -166,8 +164,6 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/components/operator/internal/resources/payments/deployments.go b/components/operator/internal/resources/payments/deployments.go index c86f7a7ae0..387cb0623e 100644 --- a/components/operator/internal/resources/payments/deployments.go +++ b/components/operator/internal/resources/payments/deployments.go @@ -1,10 +1,15 @@ package payments import ( + "fmt" + "strings" + + "github.com/formancehq/go-libs/pointer" "github.com/formancehq/operator/internal/resources/brokers" "github.com/formancehq/operator/internal/resources/brokertopics" "github.com/formancehq/operator/internal/resources/caddy" "github.com/formancehq/operator/internal/resources/registries" + "github.com/formancehq/operator/internal/resources/resourcereferences" "github.com/formancehq/operator/internal/resources/settings" appsv1 "k8s.io/api/apps/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -28,6 +33,58 @@ func getEncryptionKey(ctx core.Context, payments *v1beta1.Payments) (string, err return "", nil } +func temporalEnvVars(ctx core.Context, stack *v1beta1.Stack, payments *v1beta1.Payments) ([]v1.EnvVar, error) { + + temporalURI, err := settings.RequireURL(ctx, stack.Name, "temporal", "dsn") + if err != nil { + return nil, err + } + + if err := validateTemporalURI(temporalURI); err != nil { + return nil, err + } + + if secret := temporalURI.Query().Get("secret"); secret != "" { + _, err = resourcereferences.Create(ctx, payments, "payments-temporal", secret, &v1.Secret{}) + } else { + err = resourcereferences.Delete(ctx, payments, "payments-temporal") + } + if err != nil { + return nil, err + } + + env := make([]v1.EnvVar, 0) + env = append(env, + core.Env("TEMPORAL_TASK_QUEUE", stack.Name), + core.Env("TEMPORAL_ADDRESS", temporalURI.Host), + core.Env("TEMPORAL_NAMESPACE", temporalURI.Path[1:]), + ) + + if secret := temporalURI.Query().Get("secret"); secret == "" { + temporalTLSCrt, err := settings.GetStringOrEmpty(ctx, stack.Name, "temporal", "tls", "crt") + if err != nil { + return nil, err + } + + temporalTLSKey, err := settings.GetStringOrEmpty(ctx, stack.Name, "temporal", "tls", "key") + if err != nil { + return nil, err + } + + env = append(env, + core.Env("TEMPORAL_SSL_CLIENT_KEY", temporalTLSKey), + core.Env("TEMPORAL_SSL_CLIENT_CERT", temporalTLSCrt), + ) + } else { + env = append(env, + core.EnvFromSecret("TEMPORAL_SSL_CLIENT_KEY", secret, "tls.key"), + core.EnvFromSecret("TEMPORAL_SSL_CLIENT_CERT", secret, "tls.crt"), + ) + } + + return env, nil +} + func commonEnvVars(ctx core.Context, stack *v1beta1.Stack, payments *v1beta1.Payments, database *v1beta1.Database) ([]v1.EnvVar, error) { env := make([]v1.EnvVar, 0) otlpEnv, err := settings.GetOTELEnvVars(ctx, stack.Name, core.LowerCamelCaseKind(ctx, payments)) @@ -57,13 +114,15 @@ func commonEnvVars(ctx core.Context, stack *v1beta1.Stack, payments *v1beta1.Pay env = append(env, core.Env("POSTGRES_DATABASE_NAME", "$(POSTGRES_DATABASE)"), core.Env("CONFIG_ENCRYPTION_KEY", encryptionKey), + core.Env("PLUGIN_DIRECTORY_PATH", "/plugins"), + core.Env("PLUGIN_MAGIC_COOKIE", "magic-value"), // TODO(polo): change value ) return env, nil } func createFullDeployment(ctx core.Context, stack *v1beta1.Stack, - payments *v1beta1.Payments, database *v1beta1.Database, image string) error { + payments *v1beta1.Payments, database *v1beta1.Database, image string, v3 bool) error { env, err := commonEnvVars(ctx, stack, payments, database) if err != nil { @@ -101,11 +160,25 @@ func createFullDeployment(ctx core.Context, stack *v1beta1.Stack, env = append(env, brokers.GetPublisherEnvVars(stack, broker, "payments", "")...) } + if v3 { + temporalEnvVars, err := temporalEnvVars(ctx, stack, payments) + if err != nil { + return err + } + + env = append(env, temporalEnvVars...) + } + serviceAccountName, err := settings.GetAWSServiceAccount(ctx, stack.Name) if err != nil { return err } + appOpts := applications.WithProbePath("/_health") + if v3 { + appOpts = applications.WithProbePath("/_healthcheck") + } + err = applications. New(payments, &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ @@ -120,8 +193,12 @@ func createFullDeployment(ctx core.Context, stack *v1beta1.Stack, Args: []string{"serve"}, Env: env, Image: image, - LivenessProbe: applications.DefaultLiveness("http", applications.WithProbePath("/_health")), + LivenessProbe: applications.DefaultLiveness("http", appOpts), Ports: []v1.ContainerPort{applications.StandardHTTPPort()}, + // TODO(polo): only for v3 + SecurityContext: &v1.SecurityContext{ + ReadOnlyRootFilesystem: pointer.For(false), + }, }}, // Ensure empty InitContainers: []v1.Container{}, @@ -298,3 +375,19 @@ func createGateway(ctx core.Context, stack *v1beta1.Stack, p *v1beta1.Payments) New(p, deploymentTemplate). Install(ctx) } + +func validateTemporalURI(temporalURI *v1beta1.URI) error { + if temporalURI.Scheme != "temporal" { + return fmt.Errorf("invalid temporal uri: %s", temporalURI.String()) + } + + if temporalURI.Path == "" { + return fmt.Errorf("invalid temporal uri: %s", temporalURI.String()) + } + + if !strings.HasPrefix(temporalURI.Path, "/") { + return fmt.Errorf("invalid temporal uri: %s", temporalURI.String()) + } + + return nil +} diff --git a/components/operator/internal/resources/payments/init.go b/components/operator/internal/resources/payments/init.go index 0996302bdc..5c33917b59 100644 --- a/components/operator/internal/resources/payments/init.go +++ b/components/operator/internal/resources/payments/init.go @@ -92,11 +92,13 @@ func Reconcile(ctx Context, stack *v1beta1.Stack, p *v1beta1.Payments, version s } } - if semver.IsValid(version) && semver.Compare(version, "v1.0.0-alpha") < 0 { - if err := createFullDeployment(ctx, stack, p, database, image); err != nil { + switch { + case semver.IsValid(version) && semver.Compare(version, "v1.0.0-alpha") < 0: + if err := createFullDeployment(ctx, stack, p, database, image, false); err != nil { return err } - } else { + case semver.IsValid(version) && semver.Compare(version, "v1.0.0-alpha") >= 0 && + semver.Compare(version, "v3.0.0") < 0: if err := createReadDeployment(ctx, stack, p, database, image); err != nil { return err } @@ -107,6 +109,10 @@ func Reconcile(ctx Context, stack *v1beta1.Stack, p *v1beta1.Payments, version s if err := createGateway(ctx, stack, p); err != nil { return err } + case !semver.IsValid(version) || semver.Compare(version, "v3.0.0") >= 0: + if err := createFullDeployment(ctx, stack, p, database, image, true); err != nil { + return err + } } if err := benthosstreams.LoadFromFileSystem(ctx, benthos.Streams, p, "streams/payments", "ingestion"); err != nil { diff --git a/components/operator/internal/tests/payments_controller_test.go b/components/operator/internal/tests/payments_controller_test.go index 27a0e5f155..90d28f9c96 100644 --- a/components/operator/internal/tests/payments_controller_test.go +++ b/components/operator/internal/tests/payments_controller_test.go @@ -10,16 +10,16 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" ) var _ = Describe("PaymentsController", func() { Context("When creating a Payments object", func() { var ( - stack *v1beta1.Stack - payments *v1beta1.Payments - databaseSettings *v1beta1.Settings + stack *v1beta1.Stack + payments *v1beta1.Payments + databaseSettings *v1beta1.Settings + temporalDSNSettings *v1beta1.Settings ) BeforeEach(func() { stack = &v1beta1.Stack{ @@ -35,10 +35,12 @@ var _ = Describe("PaymentsController", func() { }, } databaseSettings = settings.New(uuid.NewString(), "postgres.*.uri", "postgresql://localhost", stack.Name) + temporalDSNSettings = settings.New(uuid.NewString(), "temporal.dsn", "temporal://localhost/namespace", stack.Name) }) JustBeforeEach(func() { Expect(Create(stack)).To(Succeed()) Expect(Create(databaseSettings)).To(Succeed()) + Expect(Create(temporalDSNSettings)).To(Succeed()) Expect(Create(payments)).To(Succeed()) }) AfterEach(func() { @@ -55,45 +57,13 @@ var _ = Describe("PaymentsController", func() { return reference }).Should(BeTrue()) }) - By("Should create a read deployment with a service", func() { - deployment := &appsv1.Deployment{} - Eventually(func() error { - return LoadResource(stack.Name, "payments-read", deployment) - }).Should(Succeed()) - Expect(deployment).To(BeControlledBy(payments)) - - service := &corev1.Service{} - Eventually(func() error { - return LoadResource(stack.Name, "payments-read", service) - }).Should(Succeed()) - Expect(service).To(BeControlledBy(payments)) - }) - By("Should create a connectors deployment with a service", func() { - deployment := &appsv1.Deployment{} - Eventually(func() error { - return LoadResource(stack.Name, "payments-connectors", deployment) - }).Should(Succeed()) - Expect(deployment).To(BeControlledBy(payments)) - - service := &corev1.Service{} - Eventually(func() error { - return LoadResource(stack.Name, "payments-connectors", service) - }).Should(Succeed()) - Expect(service).To(BeControlledBy(payments)) - }) - By("Should create a gateway", func() { + By("Should create a deployment", func() { deployment := &appsv1.Deployment{} Eventually(func() error { return LoadResource(stack.Name, "payments", deployment) }).Should(Succeed()) Expect(deployment).To(BeControlledBy(payments)) }) - By("Should create a new GatewayHTTPAPI object", func() { - httpService := &v1beta1.GatewayHTTPAPI{} - Eventually(func() error { - return LoadResource("", core.GetObjectName(stack.Name, "payments"), httpService) - }).Should(Succeed()) - }) }) Context("With Search enabled", func() { var search *v1beta1.Search diff --git a/components/operator/tools/kubectl-stacks/go.mod b/components/operator/tools/kubectl-stacks/go.mod index 1b5af2d4e9..9a49342221 100644 --- a/components/operator/tools/kubectl-stacks/go.mod +++ b/components/operator/tools/kubectl-stacks/go.mod @@ -31,7 +31,7 @@ require ( github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.0.1 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/gofuzz v1.2.0 // indirect diff --git a/components/operator/tools/kubectl-stacks/go.sum b/components/operator/tools/kubectl-stacks/go.sum index 5f774ccc88..60fe5623a3 100644 --- a/components/operator/tools/kubectl-stacks/go.sum +++ b/components/operator/tools/kubectl-stacks/go.sum @@ -73,9 +73,8 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= @@ -86,7 +85,6 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -316,8 +314,6 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/components/operator/tools/utils/go.mod b/components/operator/tools/utils/go.mod index cd34b560fe..e75b1ad739 100644 --- a/components/operator/tools/utils/go.mod +++ b/components/operator/tools/utils/go.mod @@ -5,7 +5,7 @@ go 1.22 toolchain go1.22.6 require ( - github.com/formancehq/go-libs v1.5.0 + github.com/formancehq/go-libs v1.6.0 github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.8.1 ) @@ -34,7 +34,7 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/lib/pq v1.10.9 // indirect @@ -70,10 +70,10 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/net v0.28.0 // indirect - golang.org/x/sys v0.23.0 // indirect + golang.org/x/sys v0.24.0 // indirect golang.org/x/text v0.17.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect - google.golang.org/grpc v1.64.1 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed // indirect + google.golang.org/grpc v1.66.0 // indirect google.golang.org/protobuf v1.34.2 // indirect ) diff --git a/components/operator/tools/utils/go.sum b/components/operator/tools/utils/go.sum index feca11c73b..bd5c516ff7 100644 --- a/components/operator/tools/utils/go.sum +++ b/components/operator/tools/utils/go.sum @@ -57,8 +57,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/formancehq/go-libs v1.5.0 h1:jOVET378uquKvQBnSaS8ClLFbk7fB1Lhy4xM0+RK74o= -github.com/formancehq/go-libs v1.5.0/go.mod h1:rnvi4Au1DCNephaBEVzcv64BZEEksXiyKjwo/wHG51Y= +github.com/formancehq/go-libs v1.6.0 h1:q7vmx5YKxFQEBgCJ+pUVQXOEWcuRMLo/vWd84eNQNn4= +github.com/formancehq/go-libs v1.6.0/go.mod h1:EvSv9hcZ2+e+2x4FwRNyr5QXwN08g+d8pdopWJVvVQw= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -81,8 +81,8 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3 github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= @@ -190,18 +190,18 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= -google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0= -google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= -google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= -google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= +google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed h1:3RgNmBoI9MZhsj3QxC+AP/qQhNwpCLOvYDYYsFrhFt0= +google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed h1:J6izYgfBXAI3xTKLgxzTmUltdYaLsuBxFCgDHWJ/eXg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= +google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/components/payments/Earthfile b/components/payments/Earthfile index 49ad37a620..3a3cabee19 100644 --- a/components/payments/Earthfile +++ b/components/payments/Earthfile @@ -12,13 +12,38 @@ sources: COPY --pass-args (releases+sdk-generate/go) /src/releases/sdks/go WORKDIR /src/components/payments COPY go.* . - COPY --dir pkg cmd internal . + COPY --dir cmd pkg internal . COPY main.go . SAVE ARTIFACT /src +compile-configs: + FROM core+builder-image + COPY (+sources/*) /src + WORKDIR /src/components/payments/internal/connectors/plugins/public + FOR c IN $(ls -d */ | sed 's#/##') + RUN echo "{\"$c\":" >> raw_configs.json + RUN cat /src/components/payments/internal/connectors/plugins/public/$c/config.json >> raw_configs.json + RUN echo "}" >> raw_configs.json + END + RUN jq --slurp 'add' raw_configs.json > configs.json + SAVE ARTIFACT /src/components/payments/internal/connectors/plugins/public/configs.json /configs.json + +compile-plugins: + FROM core+builder-image + COPY (+sources/*) /src + COPY (+compile-configs/configs.json) /src/components/payments/internal/connectors/plugins/configs.json + WORKDIR /src/components/payments/internal/connectors/plugins/public + FOR c IN $(ls -d */ | sed 's#/##') + WORKDIR /src/components/payments/internal/connectors/plugins/public/$c/cmd + DO --pass-args core+GO_COMPILE --VERSION=$VERSION + WORKDIR /src + SAVE ARTIFACT /src/components/payments/internal/connectors/plugins/public/$c/cmd/main ./plugins/$c + END + compile: FROM core+builder-image COPY (+sources/*) /src + COPY (+compile-configs/configs.json) /src/components/payments/internal/connectors/plugins/configs.json WORKDIR /src/components/payments ARG VERSION=latest DO --pass-args core+GO_COMPILE --VERSION=$VERSION @@ -28,6 +53,10 @@ build-image: ENTRYPOINT ["/bin/payments"] CMD ["serve"] COPY (+compile/main) /bin/payments + COPY (+compile-plugins/plugins) /plugins + FOR c IN $(ls /plugins/*) + RUN chmod +x $c + END ARG REPOSITORY=ghcr.io ARG tag=latest DO core+SAVE_IMAGE --COMPONENT=payments --REPOSITORY=${REPOSITORY} --TAG=$tag @@ -79,21 +108,21 @@ tidy: WORKDIR /src/components/payments DO --pass-args stack+GO_TIDY -generate-generic-connector-client: - FROM openapitools/openapi-generator-cli:v6.6.0 - WORKDIR /src - COPY cmd/connectors/internal/connectors/generic/client/generic-openapi.yaml . - RUN docker-entrypoint.sh generate \ - -i ./generic-openapi.yaml \ - -g go \ - -o ./generated \ - --git-user-id=formancehq \ - --git-repo-id=payments \ - -p packageVersion=latest \ - -p isGoSubmodule=true \ - -p packageName=genericclient - RUN rm -rf ./generated/test - SAVE ARTIFACT ./generated AS LOCAL ./cmd/connectors/internal/connectors/generic/client/generated +# generate-generic-connector-client: +# FROM openapitools/openapi-generator-cli:v6.6.0 +# WORKDIR /src +# COPY cmd/connectors/internal/connectors/generic/client/generic-openapi.yaml . +# RUN docker-entrypoint.sh generate \ +# -i ./generic-openapi.yaml \ +# -g go \ +# -o ./generated \ +# --git-user-id=formancehq \ +# --git-repo-id=payments \ +# -p packageVersion=latest \ +# -p isGoSubmodule=true \ +# -p packageName=genericclient +# RUN rm -rf ./generated/test +# SAVE ARTIFACT ./generated AS LOCAL ./cmd/connectors/internal/connectors/generic/client/generated release: BUILD --pass-args stack+goreleaser --path=components/payments \ No newline at end of file diff --git a/components/payments/cmd/api/internal/api/accounts.go b/components/payments/cmd/api/internal/api/accounts.go deleted file mode 100644 index b2e9468ab3..0000000000 --- a/components/payments/cmd/api/internal/api/accounts.go +++ /dev/null @@ -1,245 +0,0 @@ -package api - -import ( - "encoding/json" - "net/http" - "time" - - "github.com/formancehq/go-libs/api" - "github.com/formancehq/go-libs/bun/bunpaginate" - "github.com/formancehq/go-libs/pointer" - "github.com/formancehq/payments/cmd/api/internal/api/backend" - "github.com/formancehq/payments/cmd/api/internal/api/service" - "github.com/formancehq/payments/cmd/api/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/google/uuid" - "github.com/gorilla/mux" - "go.opentelemetry.io/otel/attribute" -) - -type accountResponse struct { - ID string `json:"id"` - Reference string `json:"reference"` - CreatedAt time.Time `json:"createdAt"` - ConnectorID string `json:"connectorID"` - Provider string `json:"provider"` - DefaultCurrency string `json:"defaultCurrency"` // Deprecated: should be removed soon - DefaultAsset string `json:"defaultAsset"` - AccountName string `json:"accountName"` - Type string `json:"type"` - Metadata map[string]string `json:"metadata"` - Pools []uuid.UUID `json:"pools"` - Raw interface{} `json:"raw"` -} - -func createAccountHandler(b backend.Backend) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "createAccountHandler") - defer span.End() - - w.Header().Set("Content-Type", "application/json") - - var req service.CreateAccountRequest - err := json.NewDecoder(r.Body).Decode(&req) - if err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrMissingOrInvalidBody, err) - return - } - - span.SetAttributes( - attribute.String("request.reference", req.Reference), - attribute.String("request.type", req.Type), - attribute.String("request.connectorID", req.ConnectorID), - attribute.String("request.createdAt", req.CreatedAt.String()), - attribute.String("request.accountName", req.AccountName), - attribute.String("request.defaultAsset", req.DefaultAsset), - ) - - if err := req.Validate(); err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrValidation, err) - return - } - - account, err := b.GetService().CreateAccount(ctx, &req) - if err != nil { - otel.RecordError(span, err) - handleServiceErrors(w, r, err) - return - } - - data := &accountResponse{ - ID: account.ID.String(), - Reference: account.Reference, - CreatedAt: account.CreatedAt, - ConnectorID: account.ConnectorID.String(), - Provider: account.ConnectorID.Provider.String(), - DefaultCurrency: account.DefaultAsset.String(), - DefaultAsset: account.DefaultAsset.String(), - AccountName: account.AccountName, - Type: account.Type.String(), - Raw: account.RawData, - Pools: make([]uuid.UUID, 0), - } - - if account.Metadata != nil { - metadata := make(map[string]string) - for k, v := range account.Metadata { - metadata[k] = v - } - data.Metadata = metadata - } - - err = json.NewEncoder(w).Encode(api.BaseResponse[accountResponse]{ - Data: data, - }) - if err != nil { - otel.RecordError(span, err) - api.InternalServerError(w, r, err) - return - } - } -} - -func listAccountsHandler(b backend.Backend) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "listAccountsHandler") - defer span.End() - - w.Header().Set("Content-Type", "application/json") - - query, err := bunpaginate.Extract[storage.ListAccountsQuery](r, func() (*storage.ListAccountsQuery, error) { - options, err := getPagination(r, storage.AccountQuery{}) - if err != nil { - return nil, err - } - return pointer.For(storage.NewListAccountsQuery(*options)), nil - }) - if err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrValidation, err) - return - } - - cursor, err := b.GetService().ListAccounts(ctx, *query) - if err != nil { - otel.RecordError(span, err) - handleServiceErrors(w, r, err) - return - } - - ret := cursor.Data - data := make([]*accountResponse, len(ret)) - - for i := range ret { - accountType := ret[i].Type - if accountType == models.AccountTypeExternalFormance { - accountType = models.AccountTypeExternal - } - - data[i] = &accountResponse{ - ID: ret[i].ID.String(), - Reference: ret[i].Reference, - CreatedAt: ret[i].CreatedAt, - ConnectorID: ret[i].ConnectorID.String(), - Provider: ret[i].ConnectorID.Provider.String(), - DefaultCurrency: ret[i].DefaultAsset.String(), - DefaultAsset: ret[i].DefaultAsset.String(), - AccountName: ret[i].AccountName, - Type: accountType.String(), - Raw: ret[i].RawData, - } - - if ret[i].Metadata != nil { - metadata := make(map[string]string) - for k, v := range ret[i].Metadata { - metadata[k] = v - } - data[i].Metadata = metadata - } - - data[i].Pools = make([]uuid.UUID, len(ret[i].PoolAccounts)) - for j := range ret[i].PoolAccounts { - data[i].Pools[j] = ret[i].PoolAccounts[j].PoolID - } - } - - err = json.NewEncoder(w).Encode(api.BaseResponse[*accountResponse]{ - Cursor: &bunpaginate.Cursor[*accountResponse]{ - PageSize: cursor.PageSize, - HasMore: cursor.HasMore, - Previous: cursor.Previous, - Next: cursor.Next, - Data: data, - }, - }) - if err != nil { - otel.RecordError(span, err) - api.InternalServerError(w, r, err) - return - } - } -} - -func readAccountHandler(b backend.Backend) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "readAccountHandler") - defer span.End() - - w.Header().Set("Content-Type", "application/json") - - accountID := mux.Vars(r)["accountID"] - - span.SetAttributes(attribute.String("request.accountID", accountID)) - - account, err := b.GetService().GetAccount(ctx, accountID) - if err != nil { - otel.RecordError(span, err) - handleServiceErrors(w, r, err) - return - } - - accountType := account.Type - if accountType == models.AccountTypeExternalFormance { - accountType = models.AccountTypeExternal - } - - data := &accountResponse{ - ID: account.ID.String(), - Reference: account.Reference, - CreatedAt: account.CreatedAt, - ConnectorID: account.ConnectorID.String(), - Provider: account.ConnectorID.Provider.String(), - DefaultCurrency: account.DefaultAsset.String(), - DefaultAsset: account.DefaultAsset.String(), - AccountName: account.AccountName, - Type: accountType.String(), - Raw: account.RawData, - } - - if account.Metadata != nil { - metadata := make(map[string]string) - for k, v := range account.Metadata { - metadata[k] = v - } - data.Metadata = metadata - } - - data.Pools = make([]uuid.UUID, len(account.PoolAccounts)) - for j := range account.PoolAccounts { - data.Pools[j] = account.PoolAccounts[j].PoolID - } - - err = json.NewEncoder(w).Encode(api.BaseResponse[accountResponse]{ - Data: data, - }) - if err != nil { - otel.RecordError(span, err) - api.InternalServerError(w, r, err) - return - } - - } -} diff --git a/components/payments/cmd/api/internal/api/accounts_test.go b/components/payments/cmd/api/internal/api/accounts_test.go deleted file mode 100644 index ed1ccf4089..0000000000 --- a/components/payments/cmd/api/internal/api/accounts_test.go +++ /dev/null @@ -1,749 +0,0 @@ -package api - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "net/http" - "net/http/httptest" - "net/url" - "testing" - "time" - - sharedapi "github.com/formancehq/go-libs/api" - "github.com/formancehq/go-libs/auth" - "github.com/formancehq/go-libs/bun/bunpaginate" - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/go-libs/query" - "github.com/formancehq/payments/cmd/api/internal/api/service" - "github.com/formancehq/payments/cmd/api/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" -) - -func TestCreateAccounts(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - req *service.CreateAccountRequest - expectedStatusCode int - expectedErrorCode string - serviceError error - } - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - - testCases := []testCase{ - { - name: "nomimal", - req: &service.CreateAccountRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - DefaultAsset: "USD/2", - AccountName: "test", - Type: "INTERNAL", - Metadata: map[string]string{ - "foo": "bar", - }, - }, - }, - { - name: "no default asset, but should still pass", - req: &service.CreateAccountRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - DefaultAsset: "", - AccountName: "test", - Type: "INTERNAL", - Metadata: map[string]string{ - "foo": "bar", - }, - }, - }, - { - name: "missing reference", - req: &service.CreateAccountRequest{ - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - DefaultAsset: "USD/2", - AccountName: "test", - Type: "INTERNAL", - Metadata: map[string]string{ - "foo": "bar", - }, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "missing connectorID", - req: &service.CreateAccountRequest{ - Reference: "test", - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - DefaultAsset: "USD/2", - AccountName: "test", - Type: "INTERNAL", - Metadata: map[string]string{ - "foo": "bar", - }, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "missing createdAt", - req: &service.CreateAccountRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - DefaultAsset: "USD/2", - AccountName: "test", - Type: "INTERNAL", - Metadata: map[string]string{ - "foo": "bar", - }, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "createdAt zero", - req: &service.CreateAccountRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Time{}, - DefaultAsset: "USD/2", - AccountName: "test", - Type: "INTERNAL", - Metadata: map[string]string{ - "foo": "bar", - }, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "missing accountName", - req: &service.CreateAccountRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - DefaultAsset: "USD/2", - Type: "INTERNAL", - Metadata: map[string]string{ - "foo": "bar", - }, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "missing type", - req: &service.CreateAccountRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - DefaultAsset: "USD/2", - AccountName: "test", - Metadata: map[string]string{ - "foo": "bar", - }, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "invalid type", - req: &service.CreateAccountRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - DefaultAsset: "USD/2", - AccountName: "test", - Type: "unknown", - Metadata: map[string]string{ - "foo": "bar", - }, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "err validation from backend", - req: &service.CreateAccountRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - DefaultAsset: "USD/2", - AccountName: "test", - Type: "INTERNAL", - Metadata: map[string]string{ - "foo": "bar", - }, - }, - serviceError: service.ErrValidation, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "ErrNotFound from storage", - req: &service.CreateAccountRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - DefaultAsset: "USD/2", - AccountName: "test", - Type: "INTERNAL", - Metadata: map[string]string{ - "foo": "bar", - }, - }, - serviceError: storage.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - }, - { - name: "ErrDuplicateKeyValue from storage", - req: &service.CreateAccountRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - DefaultAsset: "USD/2", - AccountName: "test", - Type: "INTERNAL", - Metadata: map[string]string{ - "foo": "bar", - }, - }, - serviceError: storage.ErrDuplicateKeyValue, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - }, - { - name: "other storage errors from storage", - req: &service.CreateAccountRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - DefaultAsset: "USD/2", - AccountName: "test", - Type: "INTERNAL", - Metadata: map[string]string{ - "foo": "bar", - }, - }, - serviceError: errors.New("some error"), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusOK - } - - createAccountResponse := &models.Account{ - ID: models.AccountID{ - Reference: testCase.req.Reference, - ConnectorID: connectorID, - }, - ConnectorID: connectorID, - CreatedAt: testCase.req.CreatedAt, - Reference: testCase.req.Reference, - DefaultAsset: models.Asset(testCase.req.DefaultAsset), - AccountName: testCase.req.AccountName, - Type: models.AccountType(testCase.req.Type), - Metadata: map[string]string{ - "foo": "bar", - }, - PoolAccounts: make([]*models.PoolAccounts, 0), - } - - expectedCreateAccountResponse := &accountResponse{ - ID: createAccountResponse.ID.String(), - Reference: createAccountResponse.Reference, - CreatedAt: createAccountResponse.CreatedAt, - ConnectorID: createAccountResponse.ConnectorID.String(), - Provider: createAccountResponse.ConnectorID.Provider.String(), - DefaultCurrency: createAccountResponse.DefaultAsset.String(), - DefaultAsset: createAccountResponse.DefaultAsset.String(), - AccountName: createAccountResponse.AccountName, - Type: createAccountResponse.Type.String(), - Metadata: map[string]string{ - "foo": "bar", - }, - Pools: make([]uuid.UUID, 0), - } - - backend, mockService := newTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockService.EXPECT(). - CreateAccount(gomock.Any(), testCase.req). - Return(createAccountResponse, nil) - } - if testCase.serviceError != nil { - mockService.EXPECT(). - CreateAccount(gomock.Any(), testCase.req). - Return(nil, testCase.serviceError) - } - - router := httpRouter(backend, logging.Testing(), sharedapi.ServiceInfo{ - Debug: testing.Verbose(), - }, auth.NewNoAuth(), false) - - var body []byte - if testCase.req != nil { - var err error - body, err = json.Marshal(testCase.req) - require.NoError(t, err) - } - - req := httptest.NewRequest(http.MethodPost, "/accounts", bytes.NewReader(body)) - rec := httptest.NewRecorder() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - var resp sharedapi.BaseResponse[accountResponse] - sharedapi.Decode(t, rec.Body, &resp) - require.Equal(t, expectedCreateAccountResponse, resp.Data) - } else { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } -} - -func TestListAccounts(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - queryParams url.Values - pageSize int - expectedQuery storage.ListAccountsQuery - expectedStatusCode int - serviceError error - expectedErrorCode string - } - - testCases := []testCase{ - { - name: "nomimal", - expectedQuery: storage.NewListAccountsQuery( - storage.NewPaginatedQueryOptions(storage.AccountQuery{}). - WithPageSize(15), - ), - pageSize: 15, - }, - { - name: "page size too low", - queryParams: url.Values{ - "pageSize": {"0"}, - }, - expectedQuery: storage.NewListAccountsQuery( - storage.NewPaginatedQueryOptions(storage.AccountQuery{}). - WithPageSize(15), - ), - pageSize: 15, - }, - { - name: "page size too high", - queryParams: url.Values{ - "pageSize": {"100000"}, - }, - expectedQuery: storage.NewListAccountsQuery( - storage.NewPaginatedQueryOptions(storage.AccountQuery{}). - WithPageSize(100), - ), - pageSize: 100, - }, - { - name: "with invalid page size", - queryParams: url.Values{ - "pageSize": {"nan"}, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "invalid query builder json", - queryParams: url.Values{ - "query": {"invalid"}, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "valid query builder json", - queryParams: url.Values{ - "query": {"{\"$match\": {\"source_account_id\": \"acc1\"}}"}, - }, - expectedQuery: storage.NewListAccountsQuery( - storage.NewPaginatedQueryOptions(storage.AccountQuery{}). - WithPageSize(15). - WithQueryBuilder(query.Match("source_account_id", "acc1")), - ), - pageSize: 15, - }, - { - name: "valid sorter", - queryParams: url.Values{ - "sort": {"source_account_id:asc"}, - }, - expectedQuery: storage.NewListAccountsQuery( - storage.NewPaginatedQueryOptions(storage.AccountQuery{}). - WithPageSize(15). - WithSorter(storage.Sorter{}.Add("source_account_id", storage.SortOrderAsc)), - ), - pageSize: 15, - }, - { - name: "invalid sorter", - queryParams: url.Values{ - "sort": {"source_account_id:invalid"}, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "err validation from backend", - expectedQuery: storage.NewListAccountsQuery( - storage.NewPaginatedQueryOptions(storage.AccountQuery{}). - WithPageSize(15), - ), - serviceError: service.ErrValidation, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "ErrNotFound from storage", - expectedQuery: storage.NewListAccountsQuery( - storage.NewPaginatedQueryOptions(storage.AccountQuery{}). - WithPageSize(15), - ), - serviceError: storage.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - }, - { - name: "ErrDuplicateKeyValue from storage", - expectedQuery: storage.NewListAccountsQuery( - storage.NewPaginatedQueryOptions(storage.AccountQuery{}). - WithPageSize(15), - ), - serviceError: storage.ErrDuplicateKeyValue, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - }, - { - name: "other storage errors from storage", - expectedQuery: storage.NewListAccountsQuery( - storage.NewPaginatedQueryOptions(storage.AccountQuery{}). - WithPageSize(15), - ), - serviceError: errors.New("some error"), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusOK - } - - accounts := []models.Account{ - { - ID: models.AccountID{Reference: "acc1", ConnectorID: models.ConnectorID{Reference: uuid.New(), Provider: models.ConnectorProviderDummyPay}}, - ConnectorID: models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - }, - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Reference: "acc1", - Type: models.AccountTypeInternal, - Metadata: map[string]string{ - "foo": "bar", - }, - }, - { - ID: models.AccountID{Reference: "acc2", ConnectorID: models.ConnectorID{Reference: uuid.New(), Provider: models.ConnectorProviderDummyPay}}, - ConnectorID: models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - }, - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Reference: "acc2", - Type: models.AccountTypeExternalFormance, - }, - } - - listAccountsResponse := &bunpaginate.Cursor[models.Account]{ - PageSize: testCase.pageSize, - HasMore: false, - Previous: "", - Next: "", - Data: accounts, - } - - expectedAccountsResponse := []*accountResponse{ - { - ID: accounts[0].ID.String(), - Reference: accounts[0].Reference, - CreatedAt: accounts[0].CreatedAt, - ConnectorID: accounts[0].ConnectorID.String(), - Provider: accounts[0].ConnectorID.Provider.String(), - DefaultCurrency: accounts[0].DefaultAsset.String(), - DefaultAsset: accounts[0].DefaultAsset.String(), - AccountName: accounts[0].AccountName, - Type: accounts[0].Type.String(), - Pools: []uuid.UUID{}, - Metadata: accounts[0].Metadata, - }, - { - ID: accounts[1].ID.String(), - Reference: accounts[1].Reference, - CreatedAt: accounts[1].CreatedAt, - ConnectorID: accounts[1].ConnectorID.String(), - Provider: accounts[1].ConnectorID.Provider.String(), - DefaultCurrency: accounts[1].DefaultAsset.String(), - DefaultAsset: accounts[1].DefaultAsset.String(), - AccountName: accounts[1].AccountName, - Pools: []uuid.UUID{}, - // Type is converted to external when it is external formance - Type: string(models.AccountTypeExternal), - }, - } - - backend, mockService := newTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockService.EXPECT(). - ListAccounts(gomock.Any(), testCase.expectedQuery). - Return(listAccountsResponse, nil) - } - if testCase.serviceError != nil { - mockService.EXPECT(). - ListAccounts(gomock.Any(), testCase.expectedQuery). - Return(nil, testCase.serviceError) - } - - router := httpRouter(backend, logging.Testing(), sharedapi.ServiceInfo{ - Debug: testing.Verbose(), - }, auth.NewNoAuth(), false) - - req := httptest.NewRequest(http.MethodGet, "/accounts", nil) - rec := httptest.NewRecorder() - req.URL.RawQuery = testCase.queryParams.Encode() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - var resp sharedapi.BaseResponse[*accountResponse] - sharedapi.Decode(t, rec.Body, &resp) - require.Equal(t, expectedAccountsResponse, resp.Cursor.Data) - require.Equal(t, listAccountsResponse.PageSize, resp.Cursor.PageSize) - require.Equal(t, listAccountsResponse.HasMore, resp.Cursor.HasMore) - require.Equal(t, listAccountsResponse.Next, resp.Cursor.Next) - require.Equal(t, listAccountsResponse.Previous, resp.Cursor.Previous) - } else { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } -} - -func TestGetAccount(t *testing.T) { - t.Parallel() - - accountID1 := models.AccountID{ - Reference: "acc1", - ConnectorID: models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - }, - } - accountID2 := models.AccountID{ - Reference: "acc2", - ConnectorID: models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - }, - } - - type testCase struct { - name string - accountID string - serviceError error - expectedAccountID models.AccountID - expectedStatusCode int - expectedErrorCode string - } - - testCases := []testCase{ - { - name: "nomimal acc1", - accountID: accountID1.String(), - expectedAccountID: accountID1, - }, - { - name: "nomimal acc2", - accountID: accountID2.String(), - expectedAccountID: accountID2, - }, - { - name: "err validation from backend", - accountID: accountID1.String(), - expectedAccountID: accountID1, - serviceError: service.ErrValidation, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "ErrNotFound from storage", - accountID: accountID1.String(), - expectedAccountID: accountID1, - serviceError: storage.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - }, - { - name: "ErrDuplicateKeyValue from storage", - accountID: accountID1.String(), - expectedAccountID: accountID1, - serviceError: storage.ErrDuplicateKeyValue, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - }, - { - name: "other storage errors from storage", - accountID: accountID1.String(), - expectedAccountID: accountID1, - serviceError: errors.New("some error"), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusOK - } - - var getAccountResponse *models.Account - var expectedAccountsResponse *accountResponse - if testCase.expectedAccountID == accountID1 { - getAccountResponse = &models.Account{ - ID: models.AccountID{Reference: "acc1", ConnectorID: models.ConnectorID{Reference: uuid.New(), Provider: models.ConnectorProviderDummyPay}}, - ConnectorID: models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - }, - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Reference: "acc1", - Type: models.AccountTypeInternal, - Metadata: map[string]string{ - "foo": "bar", - }, - } - - expectedAccountsResponse = &accountResponse{ - ID: getAccountResponse.ID.String(), - Reference: getAccountResponse.Reference, - CreatedAt: getAccountResponse.CreatedAt, - ConnectorID: getAccountResponse.ConnectorID.String(), - Provider: getAccountResponse.ConnectorID.Provider.String(), - DefaultCurrency: getAccountResponse.DefaultAsset.String(), - DefaultAsset: getAccountResponse.DefaultAsset.String(), - AccountName: getAccountResponse.AccountName, - Metadata: getAccountResponse.Metadata, - Pools: []uuid.UUID{}, - Type: getAccountResponse.Type.String(), - } - } else { - getAccountResponse = &models.Account{ - ID: models.AccountID{Reference: "acc2", ConnectorID: models.ConnectorID{Reference: uuid.New(), Provider: models.ConnectorProviderDummyPay}}, - ConnectorID: models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - }, - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Reference: "acc2", - Type: models.AccountTypeExternalFormance, - } - expectedAccountsResponse = &accountResponse{ - ID: getAccountResponse.ID.String(), - Reference: getAccountResponse.Reference, - CreatedAt: getAccountResponse.CreatedAt, - ConnectorID: getAccountResponse.ConnectorID.String(), - Provider: getAccountResponse.ConnectorID.Provider.String(), - DefaultCurrency: getAccountResponse.DefaultAsset.String(), - DefaultAsset: getAccountResponse.DefaultAsset.String(), - AccountName: getAccountResponse.AccountName, - Pools: []uuid.UUID{}, - // Type is converted to external when it is external formance - Type: models.AccountTypeExternal.String(), - } - } - - backend, mockService := newTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockService.EXPECT(). - GetAccount(gomock.Any(), testCase.expectedAccountID.String()). - Return(getAccountResponse, nil) - } - if testCase.serviceError != nil { - mockService.EXPECT(). - GetAccount(gomock.Any(), testCase.expectedAccountID.String()). - Return(nil, testCase.serviceError) - } - - router := httpRouter(backend, logging.Testing(), sharedapi.ServiceInfo{ - Debug: testing.Verbose(), - }, auth.NewNoAuth(), false) - - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/accounts/%s", testCase.accountID), nil) - rec := httptest.NewRecorder() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - var resp sharedapi.BaseResponse[accountResponse] - sharedapi.Decode(t, rec.Body, &resp) - require.Equal(t, expectedAccountsResponse, resp.Data) - } else { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } -} diff --git a/components/payments/cmd/api/internal/api/api_utils_test.go b/components/payments/cmd/api/internal/api/api_utils_test.go deleted file mode 100644 index 149478e4f1..0000000000 --- a/components/payments/cmd/api/internal/api/api_utils_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package api - -import ( - "testing" - - "github.com/formancehq/payments/cmd/api/internal/api/backend" - "github.com/golang/mock/gomock" -) - -func newTestingBackend(t *testing.T) (*backend.MockBackend, *backend.MockService) { - ctrl := gomock.NewController(t) - mockService := backend.NewMockService(ctrl) - backend := backend.NewMockBackend(ctrl) - backend. - EXPECT(). - GetService(). - MinTimes(0). - Return(mockService) - t.Cleanup(func() { - ctrl.Finish() - }) - return backend, mockService -} diff --git a/components/payments/cmd/api/internal/api/backend/backend.go b/components/payments/cmd/api/internal/api/backend/backend.go deleted file mode 100644 index 72429a2518..0000000000 --- a/components/payments/cmd/api/internal/api/backend/backend.go +++ /dev/null @@ -1,54 +0,0 @@ -package backend - -import ( - "context" - - "github.com/formancehq/go-libs/bun/bunpaginate" - - "github.com/formancehq/payments/cmd/api/internal/api/service" - "github.com/formancehq/payments/cmd/api/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" -) - -//go:generate mockgen -source backend.go -destination backend_generated.go -package backend . Service -type Service interface { - Ping() error - CreateAccount(ctx context.Context, req *service.CreateAccountRequest) (*models.Account, error) - ListAccounts(ctx context.Context, q storage.ListAccountsQuery) (*bunpaginate.Cursor[models.Account], error) - GetAccount(ctx context.Context, id string) (*models.Account, error) - ListBalances(ctx context.Context, q storage.ListBalancesQuery) (*bunpaginate.Cursor[models.Balance], error) - ListBankAccounts(ctx context.Context, a storage.ListBankAccountQuery) (*bunpaginate.Cursor[models.BankAccount], error) - GetBankAccount(ctx context.Context, id uuid.UUID, expand bool) (*models.BankAccount, error) - ListTransferInitiations(ctx context.Context, q storage.ListTransferInitiationsQuery) (*bunpaginate.Cursor[models.TransferInitiation], error) - ReadTransferInitiation(ctx context.Context, id models.TransferInitiationID) (*models.TransferInitiation, error) - CreatePayment(ctx context.Context, req *service.CreatePaymentRequest) (*models.Payment, error) - ListPayments(ctx context.Context, q storage.ListPaymentsQuery) (*bunpaginate.Cursor[models.Payment], error) - GetPayment(ctx context.Context, id string) (*models.Payment, error) - UpdatePaymentMetadata(ctx context.Context, paymentID models.PaymentID, metadata map[string]string) error - CreatePool(ctx context.Context, req *service.CreatePoolRequest) (*models.Pool, error) - AddAccountToPool(ctx context.Context, poolID string, req *service.AddAccountToPoolRequest) error - RemoveAccountFromPool(ctx context.Context, poolID string, accountID string) error - ListPools(ctx context.Context, q storage.ListPoolsQuery) (*bunpaginate.Cursor[models.Pool], error) - GetPool(ctx context.Context, poolID string) (*models.Pool, error) - GetPoolBalance(ctx context.Context, poolID string, atTime string) (*service.GetPoolBalanceResponse, error) - DeletePool(ctx context.Context, poolID string) error -} - -type Backend interface { - GetService() Service -} - -type DefaultBackend struct { - service Service -} - -func (d DefaultBackend) GetService() Service { - return d.service -} - -func NewDefaultBackend(service Service) Backend { - return &DefaultBackend{ - service: service, - } -} diff --git a/components/payments/cmd/api/internal/api/backend/backend_generated.go b/components/payments/cmd/api/internal/api/backend/backend_generated.go deleted file mode 100644 index cc78a24b3e..0000000000 --- a/components/payments/cmd/api/internal/api/backend/backend_generated.go +++ /dev/null @@ -1,372 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: backend.go - -// Package backend is a generated GoMock package. -package backend - -import ( - context "context" - reflect "reflect" - - service "github.com/formancehq/payments/cmd/api/internal/api/service" - storage "github.com/formancehq/payments/cmd/api/internal/storage" - models "github.com/formancehq/payments/internal/models" - bunpaginate "github.com/formancehq/go-libs/bun/bunpaginate" - gomock "github.com/golang/mock/gomock" - uuid "github.com/google/uuid" -) - -// MockService is a mock of Service interface. -type MockService struct { - ctrl *gomock.Controller - recorder *MockServiceMockRecorder -} - -// MockServiceMockRecorder is the mock recorder for MockService. -type MockServiceMockRecorder struct { - mock *MockService -} - -// NewMockService creates a new mock instance. -func NewMockService(ctrl *gomock.Controller) *MockService { - mock := &MockService{ctrl: ctrl} - mock.recorder = &MockServiceMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockService) EXPECT() *MockServiceMockRecorder { - return m.recorder -} - -// AddAccountToPool mocks base method. -func (m *MockService) AddAccountToPool(ctx context.Context, poolID string, req *service.AddAccountToPoolRequest) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddAccountToPool", ctx, poolID, req) - ret0, _ := ret[0].(error) - return ret0 -} - -// AddAccountToPool indicates an expected call of AddAccountToPool. -func (mr *MockServiceMockRecorder) AddAccountToPool(ctx, poolID, req interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddAccountToPool", reflect.TypeOf((*MockService)(nil).AddAccountToPool), ctx, poolID, req) -} - -// CreateAccount mocks base method. -func (m *MockService) CreateAccount(ctx context.Context, req *service.CreateAccountRequest) (*models.Account, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateAccount", ctx, req) - ret0, _ := ret[0].(*models.Account) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreateAccount indicates an expected call of CreateAccount. -func (mr *MockServiceMockRecorder) CreateAccount(ctx, req interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAccount", reflect.TypeOf((*MockService)(nil).CreateAccount), ctx, req) -} - -// CreatePayment mocks base method. -func (m *MockService) CreatePayment(ctx context.Context, req *service.CreatePaymentRequest) (*models.Payment, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreatePayment", ctx, req) - ret0, _ := ret[0].(*models.Payment) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreatePayment indicates an expected call of CreatePayment. -func (mr *MockServiceMockRecorder) CreatePayment(ctx, req interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePayment", reflect.TypeOf((*MockService)(nil).CreatePayment), ctx, req) -} - -// CreatePool mocks base method. -func (m *MockService) CreatePool(ctx context.Context, req *service.CreatePoolRequest) (*models.Pool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreatePool", ctx, req) - ret0, _ := ret[0].(*models.Pool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreatePool indicates an expected call of CreatePool. -func (mr *MockServiceMockRecorder) CreatePool(ctx, req interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePool", reflect.TypeOf((*MockService)(nil).CreatePool), ctx, req) -} - -// DeletePool mocks base method. -func (m *MockService) DeletePool(ctx context.Context, poolID string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeletePool", ctx, poolID) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeletePool indicates an expected call of DeletePool. -func (mr *MockServiceMockRecorder) DeletePool(ctx, poolID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePool", reflect.TypeOf((*MockService)(nil).DeletePool), ctx, poolID) -} - -// GetAccount mocks base method. -func (m *MockService) GetAccount(ctx context.Context, id string) (*models.Account, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAccount", ctx, id) - ret0, _ := ret[0].(*models.Account) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetAccount indicates an expected call of GetAccount. -func (mr *MockServiceMockRecorder) GetAccount(ctx, id interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccount", reflect.TypeOf((*MockService)(nil).GetAccount), ctx, id) -} - -// GetBankAccount mocks base method. -func (m *MockService) GetBankAccount(ctx context.Context, id uuid.UUID, expand bool) (*models.BankAccount, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetBankAccount", ctx, id, expand) - ret0, _ := ret[0].(*models.BankAccount) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetBankAccount indicates an expected call of GetBankAccount. -func (mr *MockServiceMockRecorder) GetBankAccount(ctx, id, expand interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBankAccount", reflect.TypeOf((*MockService)(nil).GetBankAccount), ctx, id, expand) -} - -// GetPayment mocks base method. -func (m *MockService) GetPayment(ctx context.Context, id string) (*models.Payment, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPayment", ctx, id) - ret0, _ := ret[0].(*models.Payment) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetPayment indicates an expected call of GetPayment. -func (mr *MockServiceMockRecorder) GetPayment(ctx, id interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPayment", reflect.TypeOf((*MockService)(nil).GetPayment), ctx, id) -} - -// GetPool mocks base method. -func (m *MockService) GetPool(ctx context.Context, poolID string) (*models.Pool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPool", ctx, poolID) - ret0, _ := ret[0].(*models.Pool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetPool indicates an expected call of GetPool. -func (mr *MockServiceMockRecorder) GetPool(ctx, poolID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPool", reflect.TypeOf((*MockService)(nil).GetPool), ctx, poolID) -} - -// GetPoolBalance mocks base method. -func (m *MockService) GetPoolBalance(ctx context.Context, poolID, atTime string) (*service.GetPoolBalanceResponse, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPoolBalance", ctx, poolID, atTime) - ret0, _ := ret[0].(*service.GetPoolBalanceResponse) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetPoolBalance indicates an expected call of GetPoolBalance. -func (mr *MockServiceMockRecorder) GetPoolBalance(ctx, poolID, atTime interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPoolBalance", reflect.TypeOf((*MockService)(nil).GetPoolBalance), ctx, poolID, atTime) -} - -// ListAccounts mocks base method. -func (m *MockService) ListAccounts(ctx context.Context, q storage.ListAccountsQuery) (*bunpaginate.Cursor[models.Account], error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAccounts", ctx, q) - ret0, _ := ret[0].(*bunpaginate.Cursor[models.Account]) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListAccounts indicates an expected call of ListAccounts. -func (mr *MockServiceMockRecorder) ListAccounts(ctx, q interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAccounts", reflect.TypeOf((*MockService)(nil).ListAccounts), ctx, q) -} - -// ListBalances mocks base method. -func (m *MockService) ListBalances(ctx context.Context, q storage.ListBalancesQuery) (*bunpaginate.Cursor[models.Balance], error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListBalances", ctx, q) - ret0, _ := ret[0].(*bunpaginate.Cursor[models.Balance]) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListBalances indicates an expected call of ListBalances. -func (mr *MockServiceMockRecorder) ListBalances(ctx, q interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBalances", reflect.TypeOf((*MockService)(nil).ListBalances), ctx, q) -} - -// ListBankAccounts mocks base method. -func (m *MockService) ListBankAccounts(ctx context.Context, a storage.ListBankAccountQuery) (*bunpaginate.Cursor[models.BankAccount], error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListBankAccounts", ctx, a) - ret0, _ := ret[0].(*bunpaginate.Cursor[models.BankAccount]) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListBankAccounts indicates an expected call of ListBankAccounts. -func (mr *MockServiceMockRecorder) ListBankAccounts(ctx, a interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBankAccounts", reflect.TypeOf((*MockService)(nil).ListBankAccounts), ctx, a) -} - -// ListPayments mocks base method. -func (m *MockService) ListPayments(ctx context.Context, q storage.ListPaymentsQuery) (*bunpaginate.Cursor[models.Payment], error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListPayments", ctx, q) - ret0, _ := ret[0].(*bunpaginate.Cursor[models.Payment]) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListPayments indicates an expected call of ListPayments. -func (mr *MockServiceMockRecorder) ListPayments(ctx, q interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPayments", reflect.TypeOf((*MockService)(nil).ListPayments), ctx, q) -} - -// ListPools mocks base method. -func (m *MockService) ListPools(ctx context.Context, q storage.ListPoolsQuery) (*bunpaginate.Cursor[models.Pool], error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListPools", ctx, q) - ret0, _ := ret[0].(*bunpaginate.Cursor[models.Pool]) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListPools indicates an expected call of ListPools. -func (mr *MockServiceMockRecorder) ListPools(ctx, q interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPools", reflect.TypeOf((*MockService)(nil).ListPools), ctx, q) -} - -// ListTransferInitiations mocks base method. -func (m *MockService) ListTransferInitiations(ctx context.Context, q storage.ListTransferInitiationsQuery) (*bunpaginate.Cursor[models.TransferInitiation], error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListTransferInitiations", ctx, q) - ret0, _ := ret[0].(*bunpaginate.Cursor[models.TransferInitiation]) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListTransferInitiations indicates an expected call of ListTransferInitiations. -func (mr *MockServiceMockRecorder) ListTransferInitiations(ctx, q interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTransferInitiations", reflect.TypeOf((*MockService)(nil).ListTransferInitiations), ctx, q) -} - -// Ping mocks base method. -func (m *MockService) Ping() error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Ping") - ret0, _ := ret[0].(error) - return ret0 -} - -// Ping indicates an expected call of Ping. -func (mr *MockServiceMockRecorder) Ping() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*MockService)(nil).Ping)) -} - -// ReadTransferInitiation mocks base method. -func (m *MockService) ReadTransferInitiation(ctx context.Context, id models.TransferInitiationID) (*models.TransferInitiation, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ReadTransferInitiation", ctx, id) - ret0, _ := ret[0].(*models.TransferInitiation) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ReadTransferInitiation indicates an expected call of ReadTransferInitiation. -func (mr *MockServiceMockRecorder) ReadTransferInitiation(ctx, id interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadTransferInitiation", reflect.TypeOf((*MockService)(nil).ReadTransferInitiation), ctx, id) -} - -// RemoveAccountFromPool mocks base method. -func (m *MockService) RemoveAccountFromPool(ctx context.Context, poolID, accountID string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RemoveAccountFromPool", ctx, poolID, accountID) - ret0, _ := ret[0].(error) - return ret0 -} - -// RemoveAccountFromPool indicates an expected call of RemoveAccountFromPool. -func (mr *MockServiceMockRecorder) RemoveAccountFromPool(ctx, poolID, accountID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveAccountFromPool", reflect.TypeOf((*MockService)(nil).RemoveAccountFromPool), ctx, poolID, accountID) -} - -// UpdatePaymentMetadata mocks base method. -func (m *MockService) UpdatePaymentMetadata(ctx context.Context, paymentID models.PaymentID, metadata map[string]string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdatePaymentMetadata", ctx, paymentID, metadata) - ret0, _ := ret[0].(error) - return ret0 -} - -// UpdatePaymentMetadata indicates an expected call of UpdatePaymentMetadata. -func (mr *MockServiceMockRecorder) UpdatePaymentMetadata(ctx, paymentID, metadata interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePaymentMetadata", reflect.TypeOf((*MockService)(nil).UpdatePaymentMetadata), ctx, paymentID, metadata) -} - -// MockBackend is a mock of Backend interface. -type MockBackend struct { - ctrl *gomock.Controller - recorder *MockBackendMockRecorder -} - -// MockBackendMockRecorder is the mock recorder for MockBackend. -type MockBackendMockRecorder struct { - mock *MockBackend -} - -// NewMockBackend creates a new mock instance. -func NewMockBackend(ctrl *gomock.Controller) *MockBackend { - mock := &MockBackend{ctrl: ctrl} - mock.recorder = &MockBackendMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockBackend) EXPECT() *MockBackendMockRecorder { - return m.recorder -} - -// GetService mocks base method. -func (m *MockBackend) GetService() Service { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetService") - ret0, _ := ret[0].(Service) - return ret0 -} - -// GetService indicates an expected call of GetService. -func (mr *MockBackendMockRecorder) GetService() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetService", reflect.TypeOf((*MockBackend)(nil).GetService)) -} diff --git a/components/payments/cmd/api/internal/api/balances_test.go b/components/payments/cmd/api/internal/api/balances_test.go deleted file mode 100644 index ec351dfdf9..0000000000 --- a/components/payments/cmd/api/internal/api/balances_test.go +++ /dev/null @@ -1,377 +0,0 @@ -package api - -import ( - "errors" - "fmt" - "math/big" - "net/http" - "net/http/httptest" - "net/url" - "strconv" - "testing" - "time" - - "github.com/formancehq/go-libs/bun/bunpaginate" - - sharedapi "github.com/formancehq/go-libs/api" - "github.com/formancehq/go-libs/auth" - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/go-libs/query" - "github.com/formancehq/payments/cmd/api/internal/api/service" - "github.com/formancehq/payments/cmd/api/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" -) - -func TestGetBalances(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - accountID string - queryParams url.Values - pageSize int - expectedQuery storage.ListBalancesQuery - expectedStatusCode int - expectedErrorCode string - serviceError error - } - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - accountID := models.AccountID{ - Reference: "acc1", - ConnectorID: connectorID, - } - accountIDString := accountID.String() - testCases := []testCase{ - { - name: "nomimal", - queryParams: url.Values{ - "to": []string{time.Date(2023, 11, 23, 8, 0, 0, 0, time.UTC).Format(time.RFC3339Nano)}, - }, - expectedQuery: storage.NewListBalancesQuery( - storage.NewPaginatedQueryOptions( - storage.NewBalanceQuery(). - WithAccountID(&accountID). - WithTo(time.Date(2023, 11, 23, 8, 0, 0, 0, time.UTC)), - ).WithPageSize(15), - ), - pageSize: 15, - accountID: accountIDString, - }, - { - name: "with invalid accountID", - accountID: "invalid", - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "with valid limit", - queryParams: url.Values{ - "limit": {"10"}, - "to": []string{time.Date(2023, 11, 23, 8, 0, 0, 0, time.UTC).Format(time.RFC3339Nano)}, - }, - expectedQuery: storage.NewListBalancesQuery( - storage.NewPaginatedQueryOptions( - storage.NewBalanceQuery(). - WithAccountID(&accountID). - WithTo(time.Date(2023, 11, 23, 8, 0, 0, 0, time.UTC)), - ).WithPageSize(10), - ), - pageSize: 15, - accountID: accountIDString, - }, - { - name: "with invalid limit", - queryParams: url.Values{ - "limit": {"nan"}, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - accountID: accountIDString, - }, - { - name: "with from and to", - queryParams: url.Values{ - "from": []string{time.Date(2023, 11, 20, 6, 0, 0, 0, time.UTC).Format(time.RFC3339Nano)}, - "to": []string{time.Date(2023, 11, 23, 8, 0, 0, 0, time.UTC).Format(time.RFC3339Nano)}, - }, - expectedQuery: storage.NewListBalancesQuery( - storage.NewPaginatedQueryOptions( - storage.NewBalanceQuery(). - WithAccountID(&accountID). - WithFrom(time.Date(2023, 11, 20, 6, 0, 0, 0, time.UTC)). - WithTo(time.Date(2023, 11, 23, 8, 0, 0, 0, time.UTC)), - ).WithPageSize(15), - ), - accountID: accountIDString, - }, - { - name: "with invalid from", - queryParams: url.Values{ - "from": []string{"invalid"}, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - accountID: accountIDString, - }, - { - name: "with invalid to", - queryParams: url.Values{ - "to": []string{"invalid"}, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - accountID: accountIDString, - }, - { - name: "page size too low, should use the default value", - queryParams: url.Values{ - "pageSize": {"0"}, - "to": []string{time.Date(2023, 11, 23, 8, 0, 0, 0, time.UTC).Format(time.RFC3339Nano)}, - }, - expectedQuery: storage.NewListBalancesQuery( - storage.NewPaginatedQueryOptions( - storage.NewBalanceQuery(). - WithAccountID(&accountID). - WithTo(time.Date(2023, 11, 23, 8, 0, 0, 0, time.UTC)), - ).WithPageSize(15), - ), - pageSize: 15, - accountID: accountIDString, - }, - { - name: "page size too high", - queryParams: url.Values{ - "pageSize": {"100000"}, - "to": []string{time.Date(2023, 11, 23, 8, 0, 0, 0, time.UTC).Format(time.RFC3339Nano)}, - }, - expectedQuery: storage.NewListBalancesQuery( - storage.NewPaginatedQueryOptions( - storage.NewBalanceQuery(). - WithAccountID(&accountID). - WithTo(time.Date(2023, 11, 23, 8, 0, 0, 0, time.UTC)), - ).WithPageSize(100), - ), - pageSize: 100, - accountID: accountIDString, - }, - { - name: "with invalid page size", - queryParams: url.Values{ - "pageSize": {"nan"}, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - accountID: accountIDString, - }, - { - name: "invalid query builder json", - queryParams: url.Values{ - "query": {"invalid"}, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - accountID: accountIDString, - }, - { - name: "valid query builder json", - queryParams: url.Values{ - "query": {"{\"$match\": {\"account_id\": \"acc1\"}}"}, - "to": []string{time.Date(2023, 11, 23, 8, 0, 0, 0, time.UTC).Format(time.RFC3339Nano)}, - }, - expectedQuery: storage.NewListBalancesQuery( - storage.NewPaginatedQueryOptions( - storage.NewBalanceQuery(). - WithAccountID(&accountID). - WithTo(time.Date(2023, 11, 23, 8, 0, 0, 0, time.UTC)), - ).WithPageSize(15). - WithQueryBuilder(query.Match("account_id", "acc1")), - ), - pageSize: 15, - accountID: accountIDString, - }, - { - name: "valid sorter", - queryParams: url.Values{ - "sort": {"account_id:asc"}, - "to": []string{time.Date(2023, 11, 23, 8, 0, 0, 0, time.UTC).Format(time.RFC3339Nano)}, - }, - expectedQuery: storage.NewListBalancesQuery( - storage.NewPaginatedQueryOptions( - storage.NewBalanceQuery(). - WithAccountID(&accountID). - WithTo(time.Date(2023, 11, 23, 8, 0, 0, 0, time.UTC)), - ).WithPageSize(15). - WithSorter(storage.Sorter{}.Add("account_id", storage.SortOrderAsc)), - ), - pageSize: 15, - accountID: accountIDString, - }, - { - name: "invalid sorter", - queryParams: url.Values{ - "sort": {"account_id:invalid"}, - "to": []string{time.Date(2023, 11, 23, 8, 0, 0, 0, time.UTC).Format(time.RFC3339Nano)}, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - accountID: accountIDString, - }, - { - name: "err validation from backend", - queryParams: url.Values{ - "to": []string{time.Date(2023, 11, 23, 8, 0, 0, 0, time.UTC).Format(time.RFC3339Nano)}, - }, - expectedQuery: storage.NewListBalancesQuery( - storage.NewPaginatedQueryOptions( - storage.NewBalanceQuery(). - WithAccountID(&accountID). - WithTo(time.Date(2023, 11, 23, 8, 0, 0, 0, time.UTC)), - ).WithPageSize(15), - ), - serviceError: service.ErrValidation, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - accountID: accountIDString, - }, - { - name: "ErrNotFound from storage", - queryParams: url.Values{ - "to": []string{time.Date(2023, 11, 23, 8, 0, 0, 0, time.UTC).Format(time.RFC3339Nano)}, - }, - expectedQuery: storage.NewListBalancesQuery( - storage.NewPaginatedQueryOptions( - storage.NewBalanceQuery(). - WithAccountID(&accountID). - WithTo(time.Date(2023, 11, 23, 8, 0, 0, 0, time.UTC)), - ).WithPageSize(15), - ), - serviceError: storage.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - accountID: accountIDString, - }, - { - name: "ErrDuplicateKeyValue from storage", - queryParams: url.Values{ - "to": []string{time.Date(2023, 11, 23, 8, 0, 0, 0, time.UTC).Format(time.RFC3339Nano)}, - }, - expectedQuery: storage.NewListBalancesQuery( - storage.NewPaginatedQueryOptions( - storage.NewBalanceQuery(). - WithAccountID(&accountID). - WithTo(time.Date(2023, 11, 23, 8, 0, 0, 0, time.UTC)), - ).WithPageSize(15), - ), - serviceError: storage.ErrDuplicateKeyValue, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - accountID: accountIDString, - }, - { - name: "other storage errors from storage", - queryParams: url.Values{ - "to": []string{time.Date(2023, 11, 23, 8, 0, 0, 0, time.UTC).Format(time.RFC3339Nano)}, - }, - expectedQuery: storage.NewListBalancesQuery( - storage.NewPaginatedQueryOptions( - storage.NewBalanceQuery(). - WithAccountID(&accountID). - WithTo(time.Date(2023, 11, 23, 8, 0, 0, 0, time.UTC)), - ).WithPageSize(15), - ), - serviceError: errors.New("some error"), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - accountID: accountIDString, - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusOK - } - - balances := []models.Balance{ - { - AccountID: accountID, - Asset: "EUR/2", - Balance: big.NewInt(100), - CreatedAt: time.Date(2023, 11, 23, 8, 0, 0, 0, time.UTC), - LastUpdatedAt: time.Date(2023, 11, 23, 9, 0, 0, 0, time.UTC), - ConnectorID: connectorID, - }, - } - - listBalancesResponse := &bunpaginate.Cursor[models.Balance]{ - PageSize: testCase.pageSize, - HasMore: false, - Previous: "", - Next: "", - Data: balances, - } - - if limit, ok := testCase.queryParams["limit"]; ok { - testCase.pageSize, _ = strconv.Atoi(limit[0]) - } - - expectedBalancessResponse := []*balancesResponse{ - { - AccountID: balances[0].AccountID.String(), - CreatedAt: balances[0].CreatedAt, - LastUpdatedAt: balances[0].LastUpdatedAt, - Currency: balances[0].Asset.String(), - Asset: balances[0].Asset.String(), - Balance: balances[0].Balance, - }, - } - - backend, mockService := newTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockService.EXPECT(). - ListBalances(gomock.Any(), testCase.expectedQuery). - Return(listBalancesResponse, nil) - } - if testCase.serviceError != nil { - mockService.EXPECT(). - ListBalances(gomock.Any(), testCase.expectedQuery). - Return(nil, testCase.serviceError) - } - - router := httpRouter(backend, logging.Testing(), sharedapi.ServiceInfo{ - Debug: testing.Verbose(), - }, auth.NewNoAuth(), false) - - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/accounts/%s/balances", testCase.accountID), nil) - rec := httptest.NewRecorder() - req.URL.RawQuery = testCase.queryParams.Encode() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - var resp sharedapi.BaseResponse[*balancesResponse] - sharedapi.Decode(t, rec.Body, &resp) - require.Equal(t, expectedBalancessResponse, resp.Cursor.Data) - require.Equal(t, listBalancesResponse.PageSize, resp.Cursor.PageSize) - require.Equal(t, listBalancesResponse.HasMore, resp.Cursor.HasMore) - require.Equal(t, listBalancesResponse.Next, resp.Cursor.Next) - require.Equal(t, listBalancesResponse.Previous, resp.Cursor.Previous) - } else { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } -} diff --git a/components/payments/cmd/api/internal/api/bank_accounts.go b/components/payments/cmd/api/internal/api/bank_accounts.go deleted file mode 100644 index 526e59db4d..0000000000 --- a/components/payments/cmd/api/internal/api/bank_accounts.go +++ /dev/null @@ -1,186 +0,0 @@ -package api - -import ( - "encoding/json" - "net/http" - "time" - - "github.com/formancehq/go-libs/api" - "github.com/formancehq/go-libs/bun/bunpaginate" - "github.com/formancehq/go-libs/pointer" - "github.com/formancehq/payments/cmd/api/internal/api/backend" - "github.com/formancehq/payments/cmd/api/internal/storage" - "github.com/formancehq/payments/internal/otel" - "github.com/google/uuid" - "github.com/gorilla/mux" - "go.opentelemetry.io/otel/attribute" -) - -type bankAccountRelatedAccountsResponse struct { - ID string `json:"id"` - CreatedAt time.Time `json:"createdAt"` - AccountID string `json:"accountID"` - ConnectorID string `json:"connectorID"` - Provider string `json:"provider"` -} - -type bankAccountResponse struct { - ID string `json:"id"` - Name string `json:"name"` - CreatedAt time.Time `json:"createdAt"` - Country string `json:"country"` - Iban string `json:"iban,omitempty"` - AccountNumber string `json:"accountNumber,omitempty"` - SwiftBicCode string `json:"swiftBicCode,omitempty"` - Metadata map[string]string `json:"metadata,omitempty"` - RelatedAccounts []*bankAccountRelatedAccountsResponse `json:"relatedAccounts,omitempty"` - - // Deprecated fields, but clients still use them - // They correspond to the first bank account adjustment now. - ConnectorID string `json:"connectorID"` - Provider string `json:"provider,omitempty"` - AccountID string `json:"accountID,omitempty"` -} - -func listBankAccountsHandler(b backend.Backend) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "listBankAccountsHandler") - defer span.End() - - w.Header().Set("Content-Type", "application/json") - - query, err := bunpaginate.Extract[storage.ListBankAccountQuery](r, func() (*storage.ListBankAccountQuery, error) { - options, err := getPagination(r, storage.BankAccountQuery{}) - if err != nil { - return nil, err - } - return pointer.For(storage.NewListBankAccountQuery(*options)), nil - }) - if err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrValidation, err) - return - } - - cursor, err := b.GetService().ListBankAccounts(ctx, *query) - if err != nil { - otel.RecordError(span, err) - handleServiceErrors(w, r, err) - return - } - - ret := cursor.Data - data := make([]*bankAccountResponse, len(ret)) - - for i := range ret { - data[i] = &bankAccountResponse{ - ID: ret[i].ID.String(), - Name: ret[i].Name, - CreatedAt: ret[i].CreatedAt, - Country: ret[i].Country, - Metadata: ret[i].Metadata, - } - - // Deprecated fields, but clients still use them - if len(ret[i].RelatedAccounts) > 0 { - data[i].ConnectorID = ret[i].RelatedAccounts[0].ConnectorID.String() - data[i].AccountID = ret[i].RelatedAccounts[0].AccountID.String() - data[i].Provider = ret[i].RelatedAccounts[0].ConnectorID.Provider.String() - } - - for _, adjustment := range ret[i].RelatedAccounts { - data[i].RelatedAccounts = append(data[i].RelatedAccounts, &bankAccountRelatedAccountsResponse{ - ID: adjustment.ID.String(), - CreatedAt: adjustment.CreatedAt, - AccountID: adjustment.AccountID.String(), - ConnectorID: adjustment.ConnectorID.String(), - Provider: adjustment.ConnectorID.Provider.String(), - }) - } - } - - err = json.NewEncoder(w).Encode(api.BaseResponse[*bankAccountResponse]{ - Cursor: &bunpaginate.Cursor[*bankAccountResponse]{ - PageSize: cursor.PageSize, - HasMore: cursor.HasMore, - Previous: cursor.Previous, - Next: cursor.Next, - Data: data, - }, - }) - if err != nil { - otel.RecordError(span, err) - api.InternalServerError(w, r, err) - return - } - } -} - -func readBankAccountHandler(b backend.Backend) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "readBankAccountHandler") - defer span.End() - - w.Header().Set("Content-Type", "application/json") - - bankAccountID, err := uuid.Parse(mux.Vars(r)["bankAccountID"]) - if err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrInvalidID, err) - return - } - - span.SetAttributes(attribute.String("request.bankAccountID", bankAccountID.String())) - - account, err := b.GetService().GetBankAccount(ctx, bankAccountID, true) - if err != nil { - otel.RecordError(span, err) - handleServiceErrors(w, r, err) - return - } - - if err := account.Offuscate(); err != nil { - otel.RecordError(span, err) - api.InternalServerError(w, r, err) - return - } - - data := &bankAccountResponse{ - ID: account.ID.String(), - Name: account.Name, - CreatedAt: account.CreatedAt, - Country: account.Country, - Iban: account.IBAN, - AccountNumber: account.AccountNumber, - SwiftBicCode: account.SwiftBicCode, - Metadata: account.Metadata, - } - - // Deprecated fields, but clients still use them - if len(account.RelatedAccounts) > 0 { - data.ConnectorID = account.RelatedAccounts[0].ConnectorID.String() - data.AccountID = account.RelatedAccounts[0].AccountID.String() - data.Provider = account.RelatedAccounts[0].ConnectorID.Provider.String() - } - - for _, adjustment := range account.RelatedAccounts { - data.RelatedAccounts = append(data.RelatedAccounts, &bankAccountRelatedAccountsResponse{ - ID: adjustment.ID.String(), - CreatedAt: adjustment.CreatedAt, - AccountID: adjustment.AccountID.String(), - ConnectorID: adjustment.ConnectorID.String(), - Provider: adjustment.ConnectorID.Provider.String(), - }) - } - - err = json.NewEncoder(w).Encode(api.BaseResponse[bankAccountResponse]{ - Data: data, - }) - if err != nil { - otel.RecordError(span, err) - api.InternalServerError(w, r, err) - return - } - - } -} diff --git a/components/payments/cmd/api/internal/api/bank_accounts_test.go b/components/payments/cmd/api/internal/api/bank_accounts_test.go deleted file mode 100644 index e1387555db..0000000000 --- a/components/payments/cmd/api/internal/api/bank_accounts_test.go +++ /dev/null @@ -1,451 +0,0 @@ -package api - -import ( - "errors" - "fmt" - "net/http" - "net/http/httptest" - "net/url" - "testing" - "time" - - "github.com/formancehq/go-libs/bun/bunpaginate" - - sharedapi "github.com/formancehq/go-libs/api" - "github.com/formancehq/go-libs/auth" - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/go-libs/query" - "github.com/formancehq/payments/cmd/api/internal/api/service" - "github.com/formancehq/payments/cmd/api/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" -) - -func TestListBankAccounts(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - queryParams url.Values - pageSize int - expectedQuery storage.ListBankAccountQuery - expectedStatusCode int - expectedErrorCode string - serviceError error - } - - testCases := []testCase{ - { - name: "nomimal", - expectedQuery: storage.NewListBankAccountQuery( - storage.NewPaginatedQueryOptions(storage.BankAccountQuery{}). - WithPageSize(15), - ), - pageSize: 15, - }, - { - name: "page size too low", - queryParams: url.Values{ - "pageSize": {"0"}, - }, - expectedQuery: storage.NewListBankAccountQuery( - storage.NewPaginatedQueryOptions(storage.BankAccountQuery{}). - WithPageSize(15), - ), - pageSize: 15, - }, - { - name: "page size too high", - queryParams: url.Values{ - "pageSize": {"100000"}, - }, - expectedQuery: storage.NewListBankAccountQuery( - storage.NewPaginatedQueryOptions(storage.BankAccountQuery{}). - WithPageSize(100), - ), - pageSize: 100, - }, - { - name: "with invalid page size", - queryParams: url.Values{ - "pageSize": {"nan"}, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "invalid query builder json", - queryParams: url.Values{ - "query": {"invalid"}, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "valid query builder json", - queryParams: url.Values{ - "query": {"{\"$match\": {\"source_account_id\": \"acc1\"}}"}, - }, - expectedQuery: storage.NewListBankAccountQuery( - storage.NewPaginatedQueryOptions(storage.BankAccountQuery{}). - WithPageSize(15). - WithQueryBuilder(query.Match("source_account_id", "acc1")), - ), - pageSize: 15, - }, - { - name: "valid sorter", - queryParams: url.Values{ - "sort": {"source_account_id:asc"}, - }, - expectedQuery: storage.NewListBankAccountQuery( - storage.NewPaginatedQueryOptions(storage.BankAccountQuery{}). - WithPageSize(15). - WithSorter(storage.Sorter{}.Add("source_account_id", storage.SortOrderAsc)), - ), - pageSize: 15, - }, - { - name: "invalid sorter", - queryParams: url.Values{ - "sort": {"source_account_id:invalid"}, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "err validation from backend", - expectedQuery: storage.NewListBankAccountQuery( - storage.NewPaginatedQueryOptions(storage.BankAccountQuery{}). - WithPageSize(15), - ), - serviceError: service.ErrValidation, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "ErrNotFound from storage", - expectedQuery: storage.NewListBankAccountQuery( - storage.NewPaginatedQueryOptions(storage.BankAccountQuery{}). - WithPageSize(15), - ), - serviceError: storage.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - }, - { - name: "ErrDuplicateKeyValue from storage", - expectedQuery: storage.NewListBankAccountQuery( - storage.NewPaginatedQueryOptions(storage.BankAccountQuery{}). - WithPageSize(15), - ), - serviceError: storage.ErrDuplicateKeyValue, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - }, - { - name: "other storage errors from storage", - expectedQuery: storage.NewListBankAccountQuery( - storage.NewPaginatedQueryOptions(storage.BankAccountQuery{}). - WithPageSize(15), - ), - serviceError: errors.New("some error"), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - } - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusOK - } - - b1ID := uuid.New() - b2ID := uuid.New() - - bankAccounts := []models.BankAccount{ - { - ID: b1ID, - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Name: "ba1", - AccountNumber: "0112345678", - IBAN: "FR7630006000011234567890189", - SwiftBicCode: "HBUKGB4B", - Country: "FR", - RelatedAccounts: []*models.BankAccountRelatedAccount{ - { - ID: uuid.New(), - BankAccountID: b1ID, - ConnectorID: connectorID, - AccountID: models.AccountID{ - Reference: "acc1", - ConnectorID: connectorID, - }, - }, - }, - }, - { - ID: b2ID, - CreatedAt: time.Date(2023, 11, 23, 8, 0, 0, 0, time.UTC), - Name: "ba2", - AccountNumber: "0112345679", - IBAN: "FR7630006000011234567890188", - SwiftBicCode: "ABCDGB4B", - Country: "DE", - RelatedAccounts: []*models.BankAccountRelatedAccount{ - { - ID: uuid.New(), - BankAccountID: b2ID, - ConnectorID: connectorID, - AccountID: models.AccountID{ - Reference: "acc2", - ConnectorID: connectorID, - }, - }, - }, - }, - } - listBankAccountsResponse := &bunpaginate.Cursor[models.BankAccount]{ - PageSize: testCase.pageSize, - HasMore: false, - Previous: "", - Next: "", - Data: bankAccounts, - } - - expectedBankAccountsResponse := []*bankAccountResponse{ - { - ID: bankAccounts[0].ID.String(), - Name: bankAccounts[0].Name, - CreatedAt: bankAccounts[0].CreatedAt, - Country: bankAccounts[0].Country, - ConnectorID: bankAccounts[0].RelatedAccounts[0].ConnectorID.String(), - AccountID: bankAccounts[0].RelatedAccounts[0].AccountID.String(), - Provider: bankAccounts[0].RelatedAccounts[0].ConnectorID.Provider.String(), - RelatedAccounts: []*bankAccountRelatedAccountsResponse{ - { - ID: bankAccounts[0].RelatedAccounts[0].ID.String(), - AccountID: bankAccounts[0].RelatedAccounts[0].AccountID.String(), - ConnectorID: bankAccounts[0].RelatedAccounts[0].ConnectorID.String(), - Provider: bankAccounts[0].RelatedAccounts[0].ConnectorID.Provider.String(), - }, - }, - }, - { - ID: bankAccounts[1].ID.String(), - Name: bankAccounts[1].Name, - CreatedAt: bankAccounts[1].CreatedAt, - Country: bankAccounts[1].Country, - ConnectorID: bankAccounts[1].RelatedAccounts[0].ConnectorID.String(), - AccountID: bankAccounts[1].RelatedAccounts[0].AccountID.String(), - Provider: bankAccounts[1].RelatedAccounts[0].ConnectorID.Provider.String(), - RelatedAccounts: []*bankAccountRelatedAccountsResponse{ - { - ID: bankAccounts[1].RelatedAccounts[0].ID.String(), - AccountID: bankAccounts[1].RelatedAccounts[0].AccountID.String(), - ConnectorID: bankAccounts[1].RelatedAccounts[0].ConnectorID.String(), - Provider: bankAccounts[1].RelatedAccounts[0].ConnectorID.Provider.String(), - }, - }, - }, - } - - backend, mockService := newTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockService.EXPECT(). - ListBankAccounts(gomock.Any(), testCase.expectedQuery). - Return(listBankAccountsResponse, nil) - } - if testCase.serviceError != nil { - mockService.EXPECT(). - ListBankAccounts(gomock.Any(), testCase.expectedQuery). - Return(nil, testCase.serviceError) - } - - router := httpRouter(backend, logging.Testing(), sharedapi.ServiceInfo{ - Debug: testing.Verbose(), - }, auth.NewNoAuth(), false) - - req := httptest.NewRequest(http.MethodGet, "/bank-accounts", nil) - rec := httptest.NewRecorder() - req.URL.RawQuery = testCase.queryParams.Encode() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - var resp sharedapi.BaseResponse[*bankAccountResponse] - sharedapi.Decode(t, rec.Body, &resp) - require.Equal(t, expectedBankAccountsResponse, resp.Cursor.Data) - require.Equal(t, listBankAccountsResponse.PageSize, resp.Cursor.PageSize) - require.Equal(t, listBankAccountsResponse.HasMore, resp.Cursor.HasMore) - require.Equal(t, listBankAccountsResponse.Next, resp.Cursor.Next) - require.Equal(t, listBankAccountsResponse.Previous, resp.Cursor.Previous) - } else { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } -} - -func TestGetBankAccount(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - bankAccountUUID string - expectedBankAccountUUID uuid.UUID - expectedStatusCode int - expectedErrorCode string - serviceError error - } - - uuid1 := uuid.New() - testCases := []testCase{ - { - name: "nomimal", - bankAccountUUID: uuid1.String(), - expectedBankAccountUUID: uuid1, - }, - { - name: "invalid uuid", - bankAccountUUID: "invalid", - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrInvalidID, - }, - { - name: "err validation from backend", - bankAccountUUID: uuid1.String(), - expectedBankAccountUUID: uuid1, - serviceError: service.ErrValidation, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "ErrNotFound from storage", - bankAccountUUID: uuid1.String(), - expectedBankAccountUUID: uuid1, - serviceError: storage.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - }, - { - name: "ErrDuplicateKeyValue from storage", - bankAccountUUID: uuid1.String(), - expectedBankAccountUUID: uuid1, - serviceError: storage.ErrDuplicateKeyValue, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - }, - { - name: "other storage errors from storage", - bankAccountUUID: uuid1.String(), - expectedBankAccountUUID: uuid1, - serviceError: errors.New("some error"), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusOK - } - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - getBankAccountResponse := &models.BankAccount{ - ID: uuid1, - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Name: "ba1", - AccountNumber: "13719713158835300", - IBAN: "FR7630006000011234567890188", - SwiftBicCode: "ABCDGB4B", - Country: "FR", - RelatedAccounts: []*models.BankAccountRelatedAccount{ - { - ID: uuid.New(), - BankAccountID: uuid1, - ConnectorID: connectorID, - AccountID: models.AccountID{ - Reference: "acc1", - ConnectorID: connectorID, - }, - }, - }, - } - - expectedBankAccountResponse := &bankAccountResponse{ - ID: getBankAccountResponse.ID.String(), - Name: getBankAccountResponse.Name, - CreatedAt: getBankAccountResponse.CreatedAt, - Country: getBankAccountResponse.Country, - ConnectorID: getBankAccountResponse.RelatedAccounts[0].ConnectorID.String(), - Provider: getBankAccountResponse.RelatedAccounts[0].ConnectorID.Provider.String(), - AccountID: getBankAccountResponse.RelatedAccounts[0].AccountID.String(), - Iban: "FR76*******************0188", - AccountNumber: "13************300", - SwiftBicCode: "ABCDGB4B", - RelatedAccounts: []*bankAccountRelatedAccountsResponse{ - { - ID: getBankAccountResponse.RelatedAccounts[0].ID.String(), - AccountID: getBankAccountResponse.RelatedAccounts[0].AccountID.String(), - ConnectorID: getBankAccountResponse.RelatedAccounts[0].ConnectorID.String(), - Provider: getBankAccountResponse.RelatedAccounts[0].ConnectorID.Provider.String(), - }, - }, - } - - backend, mockService := newTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockService.EXPECT(). - GetBankAccount(gomock.Any(), testCase.expectedBankAccountUUID, true). - Return(getBankAccountResponse, nil) - } - if testCase.serviceError != nil { - mockService.EXPECT(). - GetBankAccount(gomock.Any(), testCase.expectedBankAccountUUID, true). - Return(nil, testCase.serviceError) - } - - router := httpRouter(backend, logging.Testing(), sharedapi.ServiceInfo{ - Debug: testing.Verbose(), - }, auth.NewNoAuth(), false) - - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/bank-accounts/%s", testCase.bankAccountUUID), nil) - rec := httptest.NewRecorder() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - var resp sharedapi.BaseResponse[bankAccountResponse] - sharedapi.Decode(t, rec.Body, &resp) - require.Equal(t, expectedBankAccountResponse, resp.Data) - } else { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } -} diff --git a/components/payments/cmd/api/internal/api/health.go b/components/payments/cmd/api/internal/api/health.go deleted file mode 100644 index 3ba6427d8a..0000000000 --- a/components/payments/cmd/api/internal/api/health.go +++ /dev/null @@ -1,26 +0,0 @@ -package api - -import ( - "net/http" - - "github.com/formancehq/go-libs/api" - "github.com/formancehq/payments/cmd/api/internal/api/backend" -) - -func healthHandler(b backend.Backend) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if err := b.GetService().Ping(); err != nil { - api.InternalServerError(w, r, err) - - return - } - - w.WriteHeader(http.StatusOK) - } -} - -func liveHandler() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - } -} diff --git a/components/payments/cmd/api/internal/api/metadata.go b/components/payments/cmd/api/internal/api/metadata.go deleted file mode 100644 index 9067a65b2e..0000000000 --- a/components/payments/cmd/api/internal/api/metadata.go +++ /dev/null @@ -1,60 +0,0 @@ -package api - -import ( - "encoding/json" - "net/http" - - "github.com/formancehq/go-libs/api" - "github.com/formancehq/payments/cmd/api/internal/api/backend" - "github.com/formancehq/payments/cmd/api/internal/api/service" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" - - "github.com/gorilla/mux" -) - -func updateMetadataHandler(b backend.Backend) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "updateMetadataHandler") - defer span.End() - - paymentID, err := models.PaymentIDFromString(mux.Vars(r)["paymentID"]) - if err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrInvalidID, err) - return - } - - span.SetAttributes(attribute.String("request.paymentID", paymentID.String())) - - var metadata service.UpdateMetadataRequest - if r.ContentLength == 0 { - var err = errors.New("body is required") - otel.RecordError(span, err) - api.BadRequest(w, ErrMissingOrInvalidBody, err) - return - } - - err = json.NewDecoder(r.Body).Decode(&metadata) - if err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrMissingOrInvalidBody, err) - return - } - - for k, v := range metadata { - span.SetAttributes(attribute.String("request.metadata."+k, v)) - } - - err = b.GetService().UpdatePaymentMetadata(ctx, *paymentID, metadata) - if err != nil { - otel.RecordError(span, err) - handleServiceErrors(w, r, err) - return - } - - w.WriteHeader(http.StatusNoContent) - } -} diff --git a/components/payments/cmd/api/internal/api/metadata_test.go b/components/payments/cmd/api/internal/api/metadata_test.go deleted file mode 100644 index df8dfbd9a7..0000000000 --- a/components/payments/cmd/api/internal/api/metadata_test.go +++ /dev/null @@ -1,149 +0,0 @@ -package api - -import ( - "errors" - "fmt" - "net/http" - "net/http/httptest" - "strings" - "testing" - - sharedapi "github.com/formancehq/go-libs/api" - "github.com/formancehq/go-libs/auth" - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/api/internal/api/service" - "github.com/formancehq/payments/cmd/api/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" -) - -func TestMetadata(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - paymentID string - body string - expectedPaymentID models.PaymentID - expectedStatusCode int - expectedErrorCode string - serviceError error - } - - paymentID := models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: "p1", - Type: models.PaymentTypePayIn, - }, - ConnectorID: models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - }, - } - - testCases := []testCase{ - { - name: "nominal", - paymentID: paymentID.String(), - body: "{\"foo\":\"bar\"}", - expectedPaymentID: paymentID, - }, - { - name: "missing body", - paymentID: paymentID.String(), - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrMissingOrInvalidBody, - }, - { - name: "invalid body", - paymentID: paymentID.String(), - body: "invalid", - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrMissingOrInvalidBody, - }, - { - name: "invalid paymentID", - paymentID: "invalid", - body: "{\"foo\":\"bar\"}", - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrInvalidID, - }, - { - name: "nominal", - paymentID: paymentID.String(), - body: "{\"foo\":\"bar\"}", - expectedPaymentID: paymentID, - serviceError: service.ErrValidation, - expectedErrorCode: ErrValidation, - expectedStatusCode: http.StatusBadRequest, - }, - { - name: "nominal", - paymentID: paymentID.String(), - body: "{\"foo\":\"bar\"}", - expectedPaymentID: paymentID, - serviceError: storage.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - }, - { - name: "nominal", - paymentID: paymentID.String(), - body: "{\"foo\":\"bar\"}", - expectedPaymentID: paymentID, - serviceError: storage.ErrDuplicateKeyValue, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - }, - { - name: "nominal", - paymentID: paymentID.String(), - body: "{\"foo\":\"bar\"}", - expectedPaymentID: paymentID, - serviceError: errors.New("some error"), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusNoContent - } - - backend, mockService := newTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockService.EXPECT(). - UpdatePaymentMetadata(gomock.Any(), testCase.expectedPaymentID, map[string]string{"foo": "bar"}). - Return(nil) - } - if testCase.serviceError != nil { - mockService.EXPECT(). - UpdatePaymentMetadata(gomock.Any(), testCase.expectedPaymentID, map[string]string{"foo": "bar"}). - Return(testCase.serviceError) - } - - router := httpRouter(backend, logging.Testing(), sharedapi.ServiceInfo{ - Debug: testing.Verbose(), - }, auth.NewNoAuth(), false) - - req := httptest.NewRequest(http.MethodPatch, fmt.Sprintf("/payments/%s/metadata", testCase.paymentID), strings.NewReader(testCase.body)) - rec := httptest.NewRecorder() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode >= 300 || testCase.expectedStatusCode < 200 { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } -} diff --git a/components/payments/cmd/api/internal/api/module.go b/components/payments/cmd/api/internal/api/module.go deleted file mode 100644 index 3cd1e54bc8..0000000000 --- a/components/payments/cmd/api/internal/api/module.go +++ /dev/null @@ -1,98 +0,0 @@ -package api - -import ( - "context" - "errors" - "net/http" - "runtime/debug" - - "github.com/formancehq/go-libs/auth" - "github.com/formancehq/go-libs/logging" - - "github.com/formancehq/go-libs/api" - "github.com/formancehq/go-libs/httpserver" - "github.com/formancehq/go-libs/otlp" - "github.com/formancehq/payments/cmd/api/internal/api/backend" - "github.com/formancehq/payments/cmd/api/internal/api/service" - "github.com/formancehq/payments/cmd/api/internal/storage" - "github.com/formancehq/payments/internal/messages" - "github.com/gorilla/mux" - "github.com/rs/cors" - "github.com/sirupsen/logrus" - "go.uber.org/fx" -) - -const ( - otelTracesFlag = "otel-traces" - serviceName = "Payments" - - ErrUniqueReference = "CONFLICT" - ErrNotFound = "NOT_FOUND" - ErrInvalidID = "INVALID_ID" - ErrMissingOrInvalidBody = "MISSING_OR_INVALID_BODY" - ErrValidation = "VALIDATION" -) - -func HTTPModule(serviceInfo api.ServiceInfo, bind string, stackURL string, otelTraces bool) fx.Option { - return fx.Options( - fx.Invoke(func(m *mux.Router, lc fx.Lifecycle) { - lc.Append(httpserver.NewHook(m, httpserver.WithAddress(bind))) - }), - fx.Provide(func(store *storage.Storage) service.Store { - return store - }), - fx.Provide(func() *messages.Messages { - return messages.NewMessages(stackURL) - }), - fx.Provide(fx.Annotate(service.New, fx.As(new(backend.Service)))), - fx.Provide(backend.NewDefaultBackend), - fx.Supply(serviceInfo), - fx.Provide(func(b backend.Backend, - logger logging.Logger, - serviceInfo api.ServiceInfo, - a auth.Authenticator) *mux.Router { - return httpRouter(b, logger, serviceInfo, a, otelTraces) - }), - ) -} - -func httpRecoveryFunc(otelTraces bool) func(context.Context, interface{}) { - return func(ctx context.Context, e interface{}) { - if otelTraces { - otlp.RecordAsError(ctx, e) - } else { - logrus.Errorln(e) - debug.PrintStack() - } - } -} - -func httpCorsHandler() func(http.Handler) http.Handler { - return cors.New(cors.Options{ - AllowedOrigins: []string{"*"}, - AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut}, - AllowCredentials: true, - }).Handler -} - -func httpServeFunc(handler http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - handler.ServeHTTP(w, r) - }) -} - -func handleServiceErrors(w http.ResponseWriter, r *http.Request, err error) { - switch { - case errors.Is(err, storage.ErrDuplicateKeyValue): - api.BadRequest(w, ErrUniqueReference, err) - case errors.Is(err, storage.ErrNotFound): - api.NotFound(w, err) - case errors.Is(err, storage.ErrValidation): - api.BadRequest(w, ErrValidation, err) - case errors.Is(err, service.ErrValidation): - api.BadRequest(w, ErrValidation, err) - default: - api.InternalServerError(w, r, err) - } -} diff --git a/components/payments/cmd/api/internal/api/payments.go b/components/payments/cmd/api/internal/api/payments.go deleted file mode 100644 index e353edbbc8..0000000000 --- a/components/payments/cmd/api/internal/api/payments.go +++ /dev/null @@ -1,291 +0,0 @@ -package api - -import ( - "encoding/json" - "math/big" - "net/http" - "time" - - "github.com/formancehq/go-libs/api" - "github.com/formancehq/go-libs/bun/bunpaginate" - "github.com/formancehq/go-libs/pointer" - "github.com/formancehq/payments/cmd/api/internal/api/backend" - "github.com/formancehq/payments/cmd/api/internal/api/service" - "github.com/formancehq/payments/cmd/api/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/gorilla/mux" - "go.opentelemetry.io/otel/attribute" -) - -type paymentResponse struct { - ID string `json:"id"` - Reference string `json:"reference"` - SourceAccountID string `json:"sourceAccountID"` - DestinationAccountID string `json:"destinationAccountID"` - Type string `json:"type"` - Provider models.ConnectorProvider `json:"provider"` - ConnectorID string `json:"connectorID"` - Status models.PaymentStatus `json:"status"` - Amount *big.Int `json:"amount"` - InitialAmount *big.Int `json:"initialAmount"` - Scheme models.PaymentScheme `json:"scheme"` - Asset string `json:"asset"` - CreatedAt time.Time `json:"createdAt"` - Raw interface{} `json:"raw"` - Adjustments []paymentAdjustment `json:"adjustments"` - Metadata map[string]string `json:"metadata"` -} - -type paymentAdjustment struct { - Reference string `json:"reference" bson:"reference"` - CreatedAt time.Time `json:"createdAt" bson:"createdAt"` - Status models.PaymentStatus `json:"status" bson:"status"` - Amount *big.Int `json:"amount" bson:"amount"` - Raw interface{} `json:"raw" bson:"raw"` -} - -func createPaymentHandler(b backend.Backend) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "createPaymentHandler") - defer span.End() - - w.Header().Set("Content-Type", "application/json") - - var req service.CreatePaymentRequest - err := json.NewDecoder(r.Body).Decode(&req) - if err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrMissingOrInvalidBody, err) - return - } - - span.SetAttributes( - attribute.String("request.reference", req.Reference), - attribute.String("request.sourceAccountID", req.SourceAccountID), - attribute.String("request.destinationAccountID", req.DestinationAccountID), - attribute.String("request.type", req.Type), - attribute.String("request.connectorID", req.ConnectorID), - attribute.String("request.scheme", req.Scheme), - attribute.String("request.status", req.Status), - attribute.String("request.asset", req.Asset), - attribute.String("request.amount", req.Amount.String()), - attribute.String("request.createdAt", req.CreatedAt.String()), - ) - - if err := req.Validate(); err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrValidation, err) - return - } - - payment, err := b.GetService().CreatePayment(ctx, &req) - if err != nil { - otel.RecordError(span, err) - handleServiceErrors(w, r, err) - return - } - - data := paymentResponse{ - ID: payment.ID.String(), - Reference: payment.Reference, - Type: payment.Type.String(), - ConnectorID: payment.ConnectorID.String(), - Provider: payment.ConnectorID.Provider, - Status: payment.Status, - Amount: payment.Amount, - InitialAmount: payment.InitialAmount, - Scheme: payment.Scheme, - Asset: payment.Asset.String(), - CreatedAt: payment.CreatedAt, - Raw: payment.RawData, - Adjustments: make([]paymentAdjustment, len(payment.Adjustments)), - } - - if payment.SourceAccountID != nil { - data.SourceAccountID = payment.SourceAccountID.String() - } - - if payment.DestinationAccountID != nil { - data.DestinationAccountID = payment.DestinationAccountID.String() - } - - err = json.NewEncoder(w).Encode(api.BaseResponse[paymentResponse]{ - Data: &data, - }) - if err != nil { - otel.RecordError(span, err) - api.InternalServerError(w, r, err) - return - } - } -} - -func listPaymentsHandler(b backend.Backend) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "listPaymentsHandler") - defer span.End() - - w.Header().Set("Content-Type", "application/json") - - query, err := bunpaginate.Extract[storage.ListPaymentsQuery](r, func() (*storage.ListPaymentsQuery, error) { - options, err := getPagination(r, storage.PaymentQuery{}) - if err != nil { - return nil, err - } - return pointer.For(storage.NewListPaymentsQuery(*options)), nil - }) - if err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrValidation, err) - return - } - - cursor, err := b.GetService().ListPayments(ctx, *query) - if err != nil { - otel.RecordError(span, err) - handleServiceErrors(w, r, err) - return - } - - ret := cursor.Data - data := make([]*paymentResponse, len(ret)) - - for i := range ret { - data[i] = &paymentResponse{ - ID: ret[i].ID.String(), - Reference: ret[i].Reference, - Type: ret[i].Type.String(), - ConnectorID: ret[i].ConnectorID.String(), - Provider: ret[i].Connector.Provider, - Status: ret[i].Status, - Amount: ret[i].Amount, - InitialAmount: ret[i].InitialAmount, - Scheme: ret[i].Scheme, - Asset: ret[i].Asset.String(), - CreatedAt: ret[i].CreatedAt, - Raw: ret[i].RawData, - Adjustments: make([]paymentAdjustment, len(ret[i].Adjustments)), - } - - if ret[i].Connector != nil { - data[i].Provider = ret[i].Connector.Provider - } - - if ret[i].SourceAccountID != nil { - data[i].SourceAccountID = ret[i].SourceAccountID.String() - } - - if ret[i].DestinationAccountID != nil { - data[i].DestinationAccountID = ret[i].DestinationAccountID.String() - } - - for adjustmentIdx := range ret[i].Adjustments { - data[i].Adjustments[adjustmentIdx] = paymentAdjustment{ - Reference: ret[i].Adjustments[adjustmentIdx].Reference, - Status: ret[i].Adjustments[adjustmentIdx].Status, - Amount: ret[i].Adjustments[adjustmentIdx].Amount, - CreatedAt: ret[i].Adjustments[adjustmentIdx].CreatedAt, - Raw: ret[i].Adjustments[adjustmentIdx].RawData, - } - } - - if ret[i].Metadata != nil { - data[i].Metadata = make(map[string]string) - - for metadataIDx := range ret[i].Metadata { - data[i].Metadata[ret[i].Metadata[metadataIDx].Key] = ret[i].Metadata[metadataIDx].Value - } - } - } - - err = json.NewEncoder(w).Encode(api.BaseResponse[*paymentResponse]{ - Cursor: &bunpaginate.Cursor[*paymentResponse]{ - PageSize: cursor.PageSize, - HasMore: cursor.HasMore, - Previous: cursor.Previous, - Next: cursor.Next, - Data: data, - }, - }) - if err != nil { - otel.RecordError(span, err) - api.InternalServerError(w, r, err) - return - } - } -} - -func readPaymentHandler(b backend.Backend) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "readPaymentHandler") - defer span.End() - - w.Header().Set("Content-Type", "application/json") - - paymentID := mux.Vars(r)["paymentID"] - - span.SetAttributes(attribute.String("request.paymentID", paymentID)) - - payment, err := b.GetService().GetPayment(ctx, paymentID) - if err != nil { - otel.RecordError(span, err) - handleServiceErrors(w, r, err) - return - } - - data := paymentResponse{ - ID: payment.ID.String(), - Reference: payment.Reference, - Type: payment.Type.String(), - ConnectorID: payment.ConnectorID.String(), - Status: payment.Status, - Amount: payment.Amount, - InitialAmount: payment.InitialAmount, - Scheme: payment.Scheme, - Asset: payment.Asset.String(), - CreatedAt: payment.CreatedAt, - Raw: payment.RawData, - Adjustments: make([]paymentAdjustment, len(payment.Adjustments)), - } - - if payment.SourceAccountID != nil { - data.SourceAccountID = payment.SourceAccountID.String() - } - - if payment.DestinationAccountID != nil { - data.DestinationAccountID = payment.DestinationAccountID.String() - } - - if payment.Connector != nil { - data.Provider = payment.Connector.Provider - } - - for i := range payment.Adjustments { - data.Adjustments[i] = paymentAdjustment{ - Reference: payment.Adjustments[i].Reference, - Status: payment.Adjustments[i].Status, - Amount: payment.Adjustments[i].Amount, - CreatedAt: payment.Adjustments[i].CreatedAt, - Raw: payment.Adjustments[i].RawData, - } - } - - if payment.Metadata != nil { - data.Metadata = make(map[string]string) - - for metadataIDx := range payment.Metadata { - data.Metadata[payment.Metadata[metadataIDx].Key] = payment.Metadata[metadataIDx].Value - } - } - - err = json.NewEncoder(w).Encode(api.BaseResponse[paymentResponse]{ - Data: &data, - }) - if err != nil { - otel.RecordError(span, err) - api.InternalServerError(w, r, err) - return - } - } -} diff --git a/components/payments/cmd/api/internal/api/payments_test.go b/components/payments/cmd/api/internal/api/payments_test.go deleted file mode 100644 index fc4c513b0c..0000000000 --- a/components/payments/cmd/api/internal/api/payments_test.go +++ /dev/null @@ -1,971 +0,0 @@ -package api - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "math/big" - "net/http" - "net/http/httptest" - "net/url" - "testing" - "time" - - "github.com/formancehq/go-libs/bun/bunpaginate" - - sharedapi "github.com/formancehq/go-libs/api" - "github.com/formancehq/go-libs/auth" - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/go-libs/query" - "github.com/formancehq/payments/cmd/api/internal/api/service" - "github.com/formancehq/payments/cmd/api/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" -) - -func TestCreatePayments(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - req *service.CreatePaymentRequest - expectedStatusCode int - expectedErrorCode string - serviceError error - } - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - - sourceAccountID := models.AccountID{ - Reference: "acc1", - ConnectorID: connectorID, - } - - destinationAccountID := models.AccountID{ - Reference: "acc2", - ConnectorID: connectorID, - } - - testCases := []testCase{ - { - name: "nomimal", - req: &service.CreatePaymentRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Amount: big.NewInt(100), - Type: string(models.PaymentTypeTransfer), - Status: string(models.PaymentStatusSucceeded), - Scheme: string(models.PaymentSchemeOther), - Asset: "EUR/2", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - }, - }, - { - name: "no source account id, but should still pass", - req: &service.CreatePaymentRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Amount: big.NewInt(100), - Type: string(models.PaymentTypeTransfer), - Status: string(models.PaymentStatusSucceeded), - Scheme: string(models.PaymentSchemeOther), - Asset: "EUR/2", - DestinationAccountID: destinationAccountID.String(), - }, - }, - { - name: "no destination account id, but should still pass", - req: &service.CreatePaymentRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Amount: big.NewInt(100), - Type: string(models.PaymentTypeTransfer), - Status: string(models.PaymentStatusSucceeded), - Scheme: string(models.PaymentSchemeOther), - Asset: "EUR/2", - SourceAccountID: sourceAccountID.String(), - }, - }, - { - name: "missing reference", - req: &service.CreatePaymentRequest{ - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Amount: big.NewInt(100), - Type: string(models.PaymentTypeTransfer), - Status: string(models.PaymentStatusSucceeded), - Scheme: string(models.PaymentSchemeOther), - Asset: "EUR/2", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "missing createdAt", - req: &service.CreatePaymentRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - Amount: big.NewInt(100), - Type: string(models.PaymentTypeTransfer), - Status: string(models.PaymentStatusSucceeded), - Scheme: string(models.PaymentSchemeOther), - Asset: "EUR/2", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "created at to zero", - req: &service.CreatePaymentRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Time{}, - Amount: big.NewInt(100), - Type: string(models.PaymentTypeTransfer), - Status: string(models.PaymentStatusSucceeded), - Scheme: string(models.PaymentSchemeOther), - Asset: "EUR/2", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "missing connectorID", - req: &service.CreatePaymentRequest{ - Reference: "test", - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Amount: big.NewInt(100), - Type: string(models.PaymentTypeTransfer), - Status: string(models.PaymentStatusSucceeded), - Scheme: string(models.PaymentSchemeOther), - Asset: "EUR/2", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "missing amount", - req: &service.CreatePaymentRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Type: string(models.PaymentTypeTransfer), - Status: string(models.PaymentStatusSucceeded), - Scheme: string(models.PaymentSchemeOther), - Asset: "EUR/2", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "missing type", - req: &service.CreatePaymentRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Amount: big.NewInt(100), - Status: string(models.PaymentStatusSucceeded), - Scheme: string(models.PaymentSchemeOther), - Asset: "EUR/2", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "invalid type", - req: &service.CreatePaymentRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Amount: big.NewInt(100), - Type: "invalid", - Status: string(models.PaymentStatusSucceeded), - Scheme: string(models.PaymentSchemeOther), - Asset: "EUR/2", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "missing status", - req: &service.CreatePaymentRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Amount: big.NewInt(100), - Type: string(models.PaymentTypeTransfer), - Scheme: string(models.PaymentSchemeOther), - Asset: "EUR/2", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "invalid status", - req: &service.CreatePaymentRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Amount: big.NewInt(100), - Type: string(models.PaymentTypeTransfer), - Status: "invalid", - Scheme: string(models.PaymentSchemeOther), - Asset: "EUR/2", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "missing scheme", - req: &service.CreatePaymentRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Amount: big.NewInt(100), - Type: string(models.PaymentTypeTransfer), - Status: string(models.PaymentStatusSucceeded), - Asset: "EUR/2", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "invalid scheme", - req: &service.CreatePaymentRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Amount: big.NewInt(100), - Type: string(models.PaymentTypeTransfer), - Status: string(models.PaymentStatusSucceeded), - Scheme: "invalid", - Asset: "EUR/2", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "missing asset", - req: &service.CreatePaymentRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Amount: big.NewInt(100), - Type: string(models.PaymentTypeTransfer), - Status: string(models.PaymentStatusSucceeded), - Scheme: string(models.PaymentSchemeOther), - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "invalid asset", - req: &service.CreatePaymentRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Amount: big.NewInt(100), - Type: string(models.PaymentTypeTransfer), - Status: string(models.PaymentStatusSucceeded), - Scheme: string(models.PaymentSchemeOther), - Asset: "invalid", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "err validation from backend", - req: &service.CreatePaymentRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Amount: big.NewInt(100), - Type: string(models.PaymentTypeTransfer), - Status: string(models.PaymentStatusSucceeded), - Scheme: string(models.PaymentSchemeOther), - Asset: "EUR/2", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - }, - serviceError: service.ErrValidation, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "ErrNotFound from storage", - req: &service.CreatePaymentRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Amount: big.NewInt(100), - Type: string(models.PaymentTypeTransfer), - Status: string(models.PaymentStatusSucceeded), - Scheme: string(models.PaymentSchemeOther), - Asset: "EUR/2", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - }, - serviceError: storage.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - }, - { - name: "ErrDuplicateKeyValue from storage", - req: &service.CreatePaymentRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Amount: big.NewInt(100), - Type: string(models.PaymentTypeTransfer), - Status: string(models.PaymentStatusSucceeded), - Scheme: string(models.PaymentSchemeOther), - Asset: "EUR/2", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - }, - serviceError: storage.ErrDuplicateKeyValue, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - }, - { - name: "other storage errors from storage", - req: &service.CreatePaymentRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Amount: big.NewInt(100), - Type: string(models.PaymentTypeTransfer), - Status: string(models.PaymentStatusSucceeded), - Scheme: string(models.PaymentSchemeOther), - Asset: "EUR/2", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - }, - serviceError: errors.New("some error"), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusOK - } - - createPaymentResponse := &models.Payment{ - ID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: testCase.req.Reference, - Type: models.PaymentTypeTransfer, - }, - ConnectorID: connectorID, - }, - ConnectorID: connectorID, - CreatedAt: testCase.req.CreatedAt, - Reference: testCase.req.Reference, - Amount: testCase.req.Amount, - InitialAmount: testCase.req.Amount, - Type: models.PaymentTypeTransfer, - Status: models.PaymentStatusSucceeded, - Scheme: models.PaymentSchemeOther, - Asset: models.Asset("EUR/2"), - SourceAccountID: &sourceAccountID, - DestinationAccountID: &destinationAccountID, - } - - expectedCreatePaymentResponse := &paymentResponse{ - ID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: "test", - Type: models.PaymentTypeTransfer, - }, - ConnectorID: connectorID, - }.String(), - Reference: "test", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - Type: string(createPaymentResponse.Type), - Provider: createPaymentResponse.ConnectorID.Provider, - ConnectorID: createPaymentResponse.ConnectorID.String(), - Status: createPaymentResponse.Status, - InitialAmount: createPaymentResponse.Amount, - Amount: createPaymentResponse.Amount, - Scheme: createPaymentResponse.Scheme, - Asset: createPaymentResponse.Asset.String(), - CreatedAt: createPaymentResponse.CreatedAt, - Adjustments: make([]paymentAdjustment, len(createPaymentResponse.Adjustments)), - } - - backend, mockService := newTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockService.EXPECT(). - CreatePayment(gomock.Any(), testCase.req). - Return(createPaymentResponse, nil) - } - if testCase.serviceError != nil { - mockService.EXPECT(). - CreatePayment(gomock.Any(), testCase.req). - Return(nil, testCase.serviceError) - } - - router := httpRouter(backend, logging.Testing(), sharedapi.ServiceInfo{ - Debug: testing.Verbose(), - }, auth.NewNoAuth(), false) - - var body []byte - if testCase.req != nil { - var err error - body, err = json.Marshal(testCase.req) - require.NoError(t, err) - } - - req := httptest.NewRequest(http.MethodPost, "/payments", bytes.NewReader(body)) - rec := httptest.NewRecorder() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - var resp sharedapi.BaseResponse[paymentResponse] - sharedapi.Decode(t, rec.Body, &resp) - require.Equal(t, expectedCreatePaymentResponse, resp.Data) - } else { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } -} - -func TestPayments(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - queryParams url.Values - pageSize int - expectedQuery storage.ListPaymentsQuery - expectedStatusCode int - expectedErrorCode string - serviceError error - } - - testCases := []testCase{ - { - name: "nomimal", - expectedQuery: storage.NewListPaymentsQuery( - storage.NewPaginatedQueryOptions(storage.PaymentQuery{}). - WithPageSize(15), - ), - pageSize: 15, - }, - { - name: "page size too low", - queryParams: url.Values{ - "pageSize": {"0"}, - }, - expectedQuery: storage.NewListPaymentsQuery( - storage.NewPaginatedQueryOptions(storage.PaymentQuery{}). - WithPageSize(15), - ), - pageSize: 15, - }, - { - name: "page size too high", - queryParams: url.Values{ - "pageSize": {"100000"}, - }, - expectedQuery: storage.NewListPaymentsQuery( - storage.NewPaginatedQueryOptions(storage.PaymentQuery{}). - WithPageSize(100), - ), - pageSize: 100, - }, - { - name: "with invalid page size", - queryParams: url.Values{ - "pageSize": {"nan"}, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "invalid query builder json", - queryParams: url.Values{ - "query": {"invalid"}, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "valid query builder json", - queryParams: url.Values{ - "query": {"{\"$match\": {\"source_account_id\": \"acc1\"}}"}, - }, - expectedQuery: storage.NewListPaymentsQuery( - storage.NewPaginatedQueryOptions(storage.PaymentQuery{}). - WithPageSize(15). - WithQueryBuilder(query.Match("source_account_id", "acc1")), - ), - pageSize: 15, - }, - { - name: "valid sorter", - queryParams: url.Values{ - "sort": {"source_account_id:asc"}, - }, - expectedQuery: storage.NewListPaymentsQuery( - storage.NewPaginatedQueryOptions(storage.PaymentQuery{}). - WithPageSize(15). - WithSorter(storage.Sorter{}.Add("source_account_id", storage.SortOrderAsc)), - ), - pageSize: 15, - }, - { - name: "invalid sorter", - queryParams: url.Values{ - "sort": {"source_account_id:invalid"}, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "err validation from backend", - expectedQuery: storage.NewListPaymentsQuery( - storage.NewPaginatedQueryOptions(storage.PaymentQuery{}). - WithPageSize(15), - ), - serviceError: service.ErrValidation, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "ErrNotFound from storage", - expectedQuery: storage.NewListPaymentsQuery( - storage.NewPaginatedQueryOptions(storage.PaymentQuery{}). - WithPageSize(15), - ), - serviceError: storage.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - }, - { - name: "ErrDuplicateKeyValue from storage", - expectedQuery: storage.NewListPaymentsQuery( - storage.NewPaginatedQueryOptions(storage.PaymentQuery{}). - WithPageSize(15), - ), - serviceError: storage.ErrDuplicateKeyValue, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - }, - { - name: "other storage errors from storage", - expectedQuery: storage.NewListPaymentsQuery( - storage.NewPaginatedQueryOptions(storage.PaymentQuery{}). - WithPageSize(15), - ), - serviceError: errors.New("some error"), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusOK - } - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - - payments := []models.Payment{ - { - ID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: "p1", - Type: models.PaymentTypePayIn, - }, - ConnectorID: connectorID, - }, - ConnectorID: connectorID, - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Reference: "p1", - Amount: big.NewInt(100), - InitialAmount: big.NewInt(1000), - Type: models.PaymentTypePayIn, - Status: models.PaymentStatusPending, - Scheme: models.PaymentSchemeCardMasterCard, - Asset: models.Asset("USD/2"), - SourceAccountID: &models.AccountID{ - Reference: "acc1", - ConnectorID: connectorID, - }, - Connector: &models.Connector{ - Provider: models.ConnectorProviderDummyPay, - }, - }, - { - ID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: "p2", - Type: models.PaymentTypePayOut, - }, - ConnectorID: connectorID, - }, - ConnectorID: connectorID, - CreatedAt: time.Date(2023, 11, 22, 9, 0, 0, 0, time.UTC), - Reference: "p2", - Amount: big.NewInt(1000), - InitialAmount: big.NewInt(10000), - Type: models.PaymentTypePayOut, - Status: models.PaymentStatusSucceeded, - Scheme: models.PaymentSchemeCardVisa, - Asset: models.Asset("EUR/2"), - DestinationAccountID: &models.AccountID{ - Reference: "acc1", - ConnectorID: connectorID, - }, - Connector: &models.Connector{ - Provider: models.ConnectorProviderDummyPay, - }, - }, - } - listPaymentsResponse := &bunpaginate.Cursor[models.Payment]{ - PageSize: testCase.pageSize, - HasMore: false, - Previous: "", - Next: "", - Data: payments, - } - - expectedPaymentsResponse := []*paymentResponse{ - { - ID: payments[0].ID.String(), - Reference: payments[0].Reference, - SourceAccountID: payments[0].SourceAccountID.String(), - DestinationAccountID: payments[0].DestinationAccountID.String(), - Type: payments[0].Type.String(), - Provider: payments[0].Connector.Provider, - ConnectorID: payments[0].ConnectorID.String(), - Status: payments[0].Status, - InitialAmount: payments[0].InitialAmount, - Amount: payments[0].Amount, - Scheme: payments[0].Scheme, - Asset: payments[0].Asset.String(), - CreatedAt: payments[0].CreatedAt, - Adjustments: make([]paymentAdjustment, len(payments[0].Adjustments)), - }, - { - ID: payments[1].ID.String(), - Reference: payments[1].Reference, - SourceAccountID: payments[1].SourceAccountID.String(), - DestinationAccountID: payments[1].DestinationAccountID.String(), - Type: payments[1].Type.String(), - Provider: payments[1].Connector.Provider, - ConnectorID: payments[1].ConnectorID.String(), - Status: payments[1].Status, - InitialAmount: payments[1].InitialAmount, - Amount: payments[1].Amount, - Scheme: payments[1].Scheme, - Asset: payments[1].Asset.String(), - CreatedAt: payments[1].CreatedAt, - Adjustments: make([]paymentAdjustment, len(payments[0].Adjustments)), - }, - } - - backend, mockService := newTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockService.EXPECT(). - ListPayments(gomock.Any(), testCase.expectedQuery). - Return(listPaymentsResponse, nil) - } - if testCase.serviceError != nil { - mockService.EXPECT(). - ListPayments(gomock.Any(), testCase.expectedQuery). - Return(nil, testCase.serviceError) - } - - router := httpRouter(backend, logging.Testing(), sharedapi.ServiceInfo{ - Debug: testing.Verbose(), - }, auth.NewNoAuth(), false) - - req := httptest.NewRequest(http.MethodGet, "/payments", nil) - rec := httptest.NewRecorder() - req.URL.RawQuery = testCase.queryParams.Encode() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - var resp sharedapi.BaseResponse[*paymentResponse] - sharedapi.Decode(t, rec.Body, &resp) - require.Equal(t, expectedPaymentsResponse, resp.Cursor.Data) - require.Equal(t, listPaymentsResponse.PageSize, resp.Cursor.PageSize) - require.Equal(t, listPaymentsResponse.HasMore, resp.Cursor.HasMore) - require.Equal(t, listPaymentsResponse.Next, resp.Cursor.Next) - require.Equal(t, listPaymentsResponse.Previous, resp.Cursor.Previous) - } else { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } -} - -func TestGetPayment(t *testing.T) { - t.Parallel() - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - paymentID1 := models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: "p1", - Type: models.PaymentTypePayIn, - }, - ConnectorID: connectorID, - } - paymentID2 := models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: "p2", - Type: models.PaymentTypePayOut, - }, - ConnectorID: connectorID, - } - - type testCase struct { - name string - paymentID string - expectedPaymentID models.PaymentID - expectedStatusCode int - expectedErrorCode string - serviceError error - } - - testCases := []testCase{ - { - name: "nomimal p1", - paymentID: paymentID1.String(), - expectedPaymentID: paymentID1, - }, - { - name: "nomimal p2", - paymentID: paymentID2.String(), - expectedPaymentID: paymentID2, - }, - { - name: "err validation from backend", - paymentID: paymentID1.String(), - expectedPaymentID: paymentID1, - serviceError: service.ErrValidation, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "ErrNotFound from storage", - paymentID: paymentID1.String(), - expectedPaymentID: paymentID1, - serviceError: storage.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - }, - { - name: "ErrDuplicateKeyValue from storage", - paymentID: paymentID1.String(), - expectedPaymentID: paymentID1, - serviceError: storage.ErrDuplicateKeyValue, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - }, - { - name: "other storage errors from storage", - paymentID: paymentID1.String(), - expectedPaymentID: paymentID1, - serviceError: errors.New("some error"), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusOK - } - - var getPaymentResponse *models.Payment - var expectedPaymentResponse *paymentResponse - if testCase.expectedPaymentID == paymentID1 { - getPaymentResponse = &models.Payment{ - ID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: "p1", - Type: models.PaymentTypePayIn, - }, - ConnectorID: connectorID, - }, - ConnectorID: connectorID, - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Reference: "p1", - Amount: big.NewInt(100), - InitialAmount: big.NewInt(1000), - Type: models.PaymentTypePayIn, - Status: models.PaymentStatusPending, - Scheme: models.PaymentSchemeCardMasterCard, - Asset: models.Asset("USD/2"), - SourceAccountID: &models.AccountID{ - Reference: "acc1", - ConnectorID: connectorID, - }, - Connector: &models.Connector{ - Provider: models.ConnectorProviderDummyPay, - }, - } - - expectedPaymentResponse = &paymentResponse{ - ID: getPaymentResponse.ID.String(), - Reference: getPaymentResponse.Reference, - SourceAccountID: getPaymentResponse.SourceAccountID.String(), - DestinationAccountID: getPaymentResponse.DestinationAccountID.String(), - Type: getPaymentResponse.Type.String(), - Provider: getPaymentResponse.Connector.Provider, - ConnectorID: getPaymentResponse.ConnectorID.String(), - Status: getPaymentResponse.Status, - InitialAmount: getPaymentResponse.InitialAmount, - Amount: getPaymentResponse.Amount, - Scheme: getPaymentResponse.Scheme, - Asset: getPaymentResponse.Asset.String(), - CreatedAt: getPaymentResponse.CreatedAt, - Adjustments: make([]paymentAdjustment, len(getPaymentResponse.Adjustments)), - } - } else { - getPaymentResponse = &models.Payment{ - ID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: "p2", - Type: models.PaymentTypePayOut, - }, - ConnectorID: connectorID, - }, - ConnectorID: connectorID, - CreatedAt: time.Date(2023, 11, 22, 9, 0, 0, 0, time.UTC), - Reference: "p2", - Amount: big.NewInt(1000), - InitialAmount: big.NewInt(10000), - Type: models.PaymentTypePayOut, - Status: models.PaymentStatusSucceeded, - Scheme: models.PaymentSchemeCardVisa, - Asset: models.Asset("EUR/2"), - DestinationAccountID: &models.AccountID{ - Reference: "acc1", - ConnectorID: connectorID, - }, - Connector: &models.Connector{ - Provider: models.ConnectorProviderDummyPay, - }, - } - expectedPaymentResponse = &paymentResponse{ - ID: getPaymentResponse.ID.String(), - Reference: getPaymentResponse.Reference, - SourceAccountID: getPaymentResponse.SourceAccountID.String(), - DestinationAccountID: getPaymentResponse.DestinationAccountID.String(), - Type: getPaymentResponse.Type.String(), - Provider: getPaymentResponse.Connector.Provider, - ConnectorID: getPaymentResponse.ConnectorID.String(), - Status: getPaymentResponse.Status, - InitialAmount: getPaymentResponse.InitialAmount, - Amount: getPaymentResponse.Amount, - Scheme: getPaymentResponse.Scheme, - Asset: getPaymentResponse.Asset.String(), - CreatedAt: getPaymentResponse.CreatedAt, - Adjustments: make([]paymentAdjustment, len(getPaymentResponse.Adjustments)), - } - } - - backend, mockService := newTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockService.EXPECT(). - GetPayment(gomock.Any(), testCase.expectedPaymentID.String()). - Return(getPaymentResponse, nil) - } - if testCase.serviceError != nil { - mockService.EXPECT(). - GetPayment(gomock.Any(), testCase.expectedPaymentID.String()). - Return(nil, testCase.serviceError) - } - - router := httpRouter(backend, logging.Testing(), sharedapi.ServiceInfo{ - Debug: testing.Verbose(), - }, auth.NewNoAuth(), false) - - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/payments/%s", testCase.paymentID), nil) - rec := httptest.NewRecorder() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - var resp sharedapi.BaseResponse[paymentResponse] - sharedapi.Decode(t, rec.Body, &resp) - require.Equal(t, expectedPaymentResponse, resp.Data) - } else { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } -} diff --git a/components/payments/cmd/api/internal/api/pools.go b/components/payments/cmd/api/internal/api/pools.go deleted file mode 100644 index 52858deb3e..0000000000 --- a/components/payments/cmd/api/internal/api/pools.go +++ /dev/null @@ -1,353 +0,0 @@ -package api - -import ( - "encoding/json" - "math/big" - "net/http" - "strings" - - "github.com/formancehq/go-libs/api" - "github.com/formancehq/go-libs/bun/bunpaginate" - "github.com/formancehq/go-libs/pointer" - "github.com/formancehq/payments/cmd/api/internal/api/backend" - "github.com/formancehq/payments/cmd/api/internal/api/service" - "github.com/formancehq/payments/cmd/api/internal/storage" - "github.com/formancehq/payments/internal/otel" - "github.com/gorilla/mux" - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" -) - -type poolResponse struct { - ID string `json:"id"` - Name string `json:"name"` - Accounts []string `json:"accounts"` -} - -func createPoolHandler(b backend.Backend) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "createPoolHandler") - defer span.End() - - w.Header().Set("Content-Type", "application/json") - - var createPoolRequest service.CreatePoolRequest - err := json.NewDecoder(r.Body).Decode(&createPoolRequest) - if err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrMissingOrInvalidBody, err) - return - } - - span.SetAttributes( - attribute.String("request.name", createPoolRequest.Name), - attribute.String("request.accounts", strings.Join(createPoolRequest.AccountIDs, ",")), - ) - - if err := createPoolRequest.Validate(); err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrValidation, err) - return - } - - pool, err := b.GetService().CreatePool(ctx, &createPoolRequest) - if err != nil { - otel.RecordError(span, err) - handleServiceErrors(w, r, err) - return - } - - accounts := make([]string, len(pool.PoolAccounts)) - for i := range pool.PoolAccounts { - accounts[i] = pool.PoolAccounts[i].AccountID.String() - } - - data := &poolResponse{ - ID: pool.ID.String(), - Name: pool.Name, - Accounts: accounts, - } - - err = json.NewEncoder(w).Encode(api.BaseResponse[poolResponse]{ - Data: data, - }) - if err != nil { - otel.RecordError(span, err) - api.InternalServerError(w, r, err) - return - } - } -} - -func addAccountToPoolHandler(b backend.Backend) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "addAccountToPoolHandler") - defer span.End() - - poolID, ok := mux.Vars(r)["poolID"] - if !ok { - var err = errors.New("missing poolID") - otel.RecordError(span, err) - api.BadRequest(w, ErrInvalidID, err) - return - } - - span.SetAttributes(attribute.String("request.poolID", poolID)) - - var addAccountToPoolRequest service.AddAccountToPoolRequest - err := json.NewDecoder(r.Body).Decode(&addAccountToPoolRequest) - if err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrMissingOrInvalidBody, err) - return - } - - span.SetAttributes( - attribute.String("request.accountID", addAccountToPoolRequest.AccountID), - ) - - if err := addAccountToPoolRequest.Validate(); err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrValidation, err) - return - } - - err = b.GetService().AddAccountToPool(ctx, poolID, &addAccountToPoolRequest) - if err != nil { - otel.RecordError(span, err) - handleServiceErrors(w, r, err) - return - } - - w.WriteHeader(http.StatusNoContent) - } -} - -func removeAccountFromPoolHandler(b backend.Backend) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "removeAccountFromPoolHandler") - defer span.End() - - poolID, ok := mux.Vars(r)["poolID"] - if !ok { - var err = errors.New("missing poolID") - otel.RecordError(span, err) - api.BadRequest(w, ErrInvalidID, err) - return - } - - span.SetAttributes(attribute.String("request.poolID", poolID)) - - accountID, ok := mux.Vars(r)["accountID"] - if !ok { - var err = errors.New("missing accountID") - otel.RecordError(span, err) - api.BadRequest(w, ErrInvalidID, err) - return - } - - span.SetAttributes(attribute.String("request.accountID", accountID)) - - err := b.GetService().RemoveAccountFromPool(ctx, poolID, accountID) - if err != nil { - otel.RecordError(span, err) - handleServiceErrors(w, r, err) - return - } - - w.WriteHeader(http.StatusNoContent) - } -} - -func listPoolHandler(b backend.Backend) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "listPoolHandler") - defer span.End() - - w.Header().Set("Content-Type", "application/json") - - query, err := bunpaginate.Extract[storage.ListPoolsQuery](r, func() (*storage.ListPoolsQuery, error) { - options, err := getPagination(r, storage.PoolQuery{}) - if err != nil { - return nil, err - } - return pointer.For(storage.NewListPoolsQuery(*options)), nil - }) - if err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrValidation, err) - return - } - - cursor, err := b.GetService().ListPools(ctx, *query) - if err != nil { - otel.RecordError(span, err) - handleServiceErrors(w, r, err) - return - } - - ret := cursor.Data - data := make([]*poolResponse, len(ret)) - - for i := range ret { - accounts := make([]string, len(ret[i].PoolAccounts)) - for j := range ret[i].PoolAccounts { - accounts[j] = ret[i].PoolAccounts[j].AccountID.String() - } - - data[i] = &poolResponse{ - ID: ret[i].ID.String(), - Name: ret[i].Name, - Accounts: accounts, - } - } - - err = json.NewEncoder(w).Encode(api.BaseResponse[*poolResponse]{ - Cursor: &bunpaginate.Cursor[*poolResponse]{ - PageSize: cursor.PageSize, - HasMore: cursor.HasMore, - Previous: cursor.Previous, - Next: cursor.Next, - Data: data, - }, - }) - if err != nil { - otel.RecordError(span, err) - api.InternalServerError(w, r, err) - return - } - } -} - -func getPoolHandler(b backend.Backend) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "getPoolHandler") - defer span.End() - - poolID, ok := mux.Vars(r)["poolID"] - if !ok { - err := errors.New("missing poolID") - otel.RecordError(span, err) - api.BadRequest(w, ErrInvalidID, err) - return - } - - span.SetAttributes(attribute.String("request.poolID", poolID)) - - pool, err := b.GetService().GetPool(ctx, poolID) - if err != nil { - otel.RecordError(span, err) - handleServiceErrors(w, r, err) - return - } - - accounts := make([]string, len(pool.PoolAccounts)) - for i := range pool.PoolAccounts { - accounts[i] = pool.PoolAccounts[i].AccountID.String() - } - - data := &poolResponse{ - ID: pool.ID.String(), - Name: pool.Name, - Accounts: accounts, - } - - err = json.NewEncoder(w).Encode(api.BaseResponse[poolResponse]{ - Data: data, - }) - if err != nil { - otel.RecordError(span, err) - api.InternalServerError(w, r, err) - return - } - } -} - -type poolBalancesResponse struct { - Balances []*poolBalanceResponse `json:"balances"` -} - -type poolBalanceResponse struct { - Amount *big.Int `json:"amount"` - Asset string `json:"asset"` -} - -func getPoolBalances(b backend.Backend) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "getPoolBalances") - defer span.End() - - poolID, ok := mux.Vars(r)["poolID"] - if !ok { - var err = errors.New("missing poolID") - otel.RecordError(span, err) - api.BadRequest(w, ErrInvalidID, err) - return - } - - span.SetAttributes(attribute.String("request.poolID", poolID)) - - atTime := r.URL.Query().Get("at") - if atTime == "" { - var err = errors.New("missing atTime") - otel.RecordError(span, err) - api.BadRequest(w, ErrValidation, err) - return - } - - span.SetAttributes(attribute.String("request.atTime", atTime)) - - balance, err := b.GetService().GetPoolBalance(ctx, poolID, atTime) - if err != nil { - otel.RecordError(span, err) - handleServiceErrors(w, r, err) - return - } - - data := &poolBalancesResponse{ - Balances: make([]*poolBalanceResponse, len(balance.Balances)), - } - - for i := range balance.Balances { - data.Balances[i] = &poolBalanceResponse{ - Amount: balance.Balances[i].Amount, - Asset: balance.Balances[i].Asset, - } - } - - err = json.NewEncoder(w).Encode(api.BaseResponse[poolBalancesResponse]{ - Data: data, - }) - if err != nil { - otel.RecordError(span, err) - api.InternalServerError(w, r, err) - return - } - } -} - -func deletePoolHandler(b backend.Backend) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "deletePoolHandler") - defer span.End() - - poolID, ok := mux.Vars(r)["poolID"] - if !ok { - var err = errors.New("missing poolID") - otel.RecordError(span, err) - api.BadRequest(w, ErrInvalidID, err) - return - } - - span.SetAttributes(attribute.String("request.poolID", poolID)) - - err := b.GetService().DeletePool(ctx, poolID) - if err != nil { - otel.RecordError(span, err) - handleServiceErrors(w, r, err) - return - } - - w.WriteHeader(http.StatusNoContent) - } -} diff --git a/components/payments/cmd/api/internal/api/pools_test.go b/components/payments/cmd/api/internal/api/pools_test.go deleted file mode 100644 index db1db5d20f..0000000000 --- a/components/payments/cmd/api/internal/api/pools_test.go +++ /dev/null @@ -1,1043 +0,0 @@ -package api - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "math/big" - "net/http" - "net/http/httptest" - "net/url" - "testing" - "time" - - "github.com/formancehq/go-libs/bun/bunpaginate" - - sharedapi "github.com/formancehq/go-libs/api" - "github.com/formancehq/go-libs/auth" - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/go-libs/query" - "github.com/formancehq/payments/cmd/api/internal/api/service" - "github.com/formancehq/payments/cmd/api/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" -) - -func TestCreatePool(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - req *service.CreatePoolRequest - expectedStatusCode int - serviceError error - expectedErrorCode string - } - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - - accountID := models.AccountID{ - Reference: "acc1", - ConnectorID: connectorID, - } - - uuid1 := uuid.New() - - testCases := []testCase{ - { - name: "nominal", - req: &service.CreatePoolRequest{ - Name: "test", - AccountIDs: []string{accountID.String()}, - }, - }, - { - name: "no accounts", - req: &service.CreatePoolRequest{ - Name: "test", - AccountIDs: []string{}, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "missing name", - req: &service.CreatePoolRequest{ - Name: "", - AccountIDs: []string{accountID.String()}, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "err validation from backend", - req: &service.CreatePoolRequest{ - Name: "test", - AccountIDs: []string{accountID.String()}, - }, - serviceError: service.ErrValidation, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "ErrNotFound from storage", - req: &service.CreatePoolRequest{ - Name: "test", - AccountIDs: []string{accountID.String()}, - }, - serviceError: storage.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - }, - { - name: "ErrDuplicateKeyValue from storage", - req: &service.CreatePoolRequest{ - Name: "test", - AccountIDs: []string{accountID.String()}, - }, - serviceError: storage.ErrDuplicateKeyValue, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - }, - { - name: "other storage errors from storage", - req: &service.CreatePoolRequest{ - Name: "test", - AccountIDs: []string{accountID.String()}, - }, - serviceError: errors.New("some error"), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusOK - } - - createPoolResponse := &models.Pool{ - ID: uuid1, - Name: testCase.req.Name, - PoolAccounts: []*models.PoolAccounts{ - { - PoolID: uuid1, - AccountID: accountID, - }, - }, - } - - accounts := make([]string, len(createPoolResponse.PoolAccounts)) - for i := range createPoolResponse.PoolAccounts { - accounts[i] = createPoolResponse.PoolAccounts[i].AccountID.String() - } - expectedCreatePoolResponse := &poolResponse{ - ID: createPoolResponse.ID.String(), - Name: createPoolResponse.Name, - Accounts: accounts, - } - - backend, mockService := newTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockService.EXPECT(). - CreatePool(gomock.Any(), testCase.req). - Return(createPoolResponse, nil) - } - if testCase.serviceError != nil { - mockService.EXPECT(). - CreatePool(gomock.Any(), testCase.req). - Return(nil, testCase.serviceError) - } - - router := httpRouter(backend, logging.Testing(), sharedapi.ServiceInfo{ - Debug: testing.Verbose(), - }, auth.NewNoAuth(), false) - - var body []byte - if testCase.req != nil { - var err error - body, err = json.Marshal(testCase.req) - require.NoError(t, err) - } - - req := httptest.NewRequest(http.MethodPost, "/pools", bytes.NewReader(body)) - rec := httptest.NewRecorder() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - var resp sharedapi.BaseResponse[poolResponse] - sharedapi.Decode(t, rec.Body, &resp) - require.Equal(t, expectedCreatePoolResponse, resp.Data) - } else { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } -} - -func TestAddAccountToPool(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - req *service.AddAccountToPoolRequest - poolID string - expectedStatusCode int - serviceError error - expectedErrorCode string - } - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - - accountID := models.AccountID{ - Reference: "acc1", - ConnectorID: connectorID, - } - - uuid1 := uuid.New() - - testCases := []testCase{ - { - name: "nominal", - req: &service.AddAccountToPoolRequest{ - AccountID: accountID.String(), - }, - poolID: uuid1.String(), - }, - { - name: "missing accountID", - req: &service.AddAccountToPoolRequest{ - AccountID: "", - }, - poolID: uuid1.String(), - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "missing body", - poolID: uuid1.String(), - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrMissingOrInvalidBody, - }, - { - name: "err validation from backend", - poolID: uuid1.String(), - req: &service.AddAccountToPoolRequest{ - AccountID: accountID.String(), - }, - serviceError: service.ErrValidation, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "ErrNotFound from storage", - poolID: uuid1.String(), - req: &service.AddAccountToPoolRequest{ - AccountID: accountID.String(), - }, - serviceError: storage.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - }, - { - name: "ErrDuplicateKeyValue from storage", - poolID: uuid1.String(), - req: &service.AddAccountToPoolRequest{ - AccountID: accountID.String(), - }, - serviceError: storage.ErrDuplicateKeyValue, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - }, - { - name: "other storage errors from storage", - poolID: uuid1.String(), - req: &service.AddAccountToPoolRequest{ - AccountID: accountID.String(), - }, - serviceError: errors.New("some error"), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusNoContent - } - - backend, mockService := newTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockService.EXPECT(). - AddAccountToPool(gomock.Any(), testCase.poolID, testCase.req). - Return(nil) - } - if testCase.serviceError != nil { - mockService.EXPECT(). - AddAccountToPool(gomock.Any(), testCase.poolID, testCase.req). - Return(testCase.serviceError) - } - - router := httpRouter(backend, logging.Testing(), sharedapi.ServiceInfo{ - Debug: testing.Verbose(), - }, auth.NewNoAuth(), false) - - var body []byte - if testCase.req != nil { - var err error - body, err = json.Marshal(testCase.req) - require.NoError(t, err) - } - - req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/pools/%s/accounts", testCase.poolID), bytes.NewReader(body)) - rec := httptest.NewRecorder() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode >= 300 || testCase.expectedStatusCode < 200 { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } - -} - -func TestRemoveAccountFromPool(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - poolID string - accountID string - serviceError error - expectedStatusCode int - expectedErrorCode string - } - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - accountID := models.AccountID{ - Reference: "acc1", - ConnectorID: connectorID, - } - uuid1 := uuid.New() - - testCases := []testCase{ - { - name: "nominal", - poolID: uuid1.String(), - accountID: accountID.String(), - }, - { - name: "err validation from backend", - poolID: uuid1.String(), - accountID: accountID.String(), - serviceError: service.ErrValidation, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "ErrNotFound from storage", - poolID: uuid1.String(), - accountID: accountID.String(), - serviceError: storage.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - }, - { - name: "ErrDuplicateKeyValue from storage", - poolID: uuid1.String(), - accountID: accountID.String(), - serviceError: storage.ErrDuplicateKeyValue, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - }, - { - name: "other storage errors from storage", - poolID: uuid1.String(), - accountID: accountID.String(), - serviceError: errors.New("some error"), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusNoContent - } - - backend, mockService := newTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockService.EXPECT(). - RemoveAccountFromPool(gomock.Any(), testCase.poolID, testCase.accountID). - Return(nil) - } - if testCase.serviceError != nil { - mockService.EXPECT(). - RemoveAccountFromPool(gomock.Any(), testCase.poolID, testCase.accountID). - Return(testCase.serviceError) - } - - router := httpRouter(backend, logging.Testing(), sharedapi.ServiceInfo{ - Debug: testing.Verbose(), - }, auth.NewNoAuth(), false) - - req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/pools/%s/accounts/%s", testCase.poolID, testCase.accountID), nil) - rec := httptest.NewRecorder() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode >= 300 || testCase.expectedStatusCode < 200 { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } -} - -func TestListPools(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - queryParams url.Values - pageSize int - expectedQuery storage.ListPoolsQuery - expectedStatusCode int - serviceError error - expectedErrorCode string - } - - testCases := []testCase{ - { - name: "nomimal", - expectedQuery: storage.NewListPoolsQuery( - storage.NewPaginatedQueryOptions(storage.PoolQuery{}). - WithPageSize(15), - ), - pageSize: 15, - }, - { - name: "page size too low", - queryParams: url.Values{ - "pageSize": {"0"}, - }, - expectedQuery: storage.NewListPoolsQuery( - storage.NewPaginatedQueryOptions(storage.PoolQuery{}). - WithPageSize(15), - ), - pageSize: 15, - }, - { - name: "page size too high", - queryParams: url.Values{ - "pageSize": {"100000"}, - }, - expectedQuery: storage.NewListPoolsQuery( - storage.NewPaginatedQueryOptions(storage.PoolQuery{}). - WithPageSize(100), - ), - pageSize: 100, - }, - { - name: "with invalid page size", - queryParams: url.Values{ - "pageSize": {"nan"}, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "invalid query builder json", - queryParams: url.Values{ - "query": {"invalid"}, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "valid query builder json", - queryParams: url.Values{ - "query": {"{\"$match\": {\"source_account_id\": \"acc1\"}}"}, - }, - expectedQuery: storage.NewListPoolsQuery( - storage.NewPaginatedQueryOptions(storage.PoolQuery{}). - WithPageSize(15). - WithQueryBuilder(query.Match("source_account_id", "acc1")), - ), - pageSize: 15, - }, - { - name: "valid sorter", - queryParams: url.Values{ - "sort": {"source_account_id:asc"}, - }, - expectedQuery: storage.NewListPoolsQuery( - storage.NewPaginatedQueryOptions(storage.PoolQuery{}). - WithPageSize(15). - WithSorter(storage.Sorter{}.Add("source_account_id", storage.SortOrderAsc)), - ), - pageSize: 15, - }, - { - name: "invalid sorter", - queryParams: url.Values{ - "sort": {"source_account_id:invalid"}, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - - { - name: "err validation from backend", - expectedQuery: storage.NewListPoolsQuery( - storage.NewPaginatedQueryOptions(storage.PoolQuery{}). - WithPageSize(15), - ), - serviceError: service.ErrValidation, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "ErrNotFound from storage", - expectedQuery: storage.NewListPoolsQuery( - storage.NewPaginatedQueryOptions(storage.PoolQuery{}). - WithPageSize(15), - ), - serviceError: storage.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - }, - { - name: "ErrDuplicateKeyValue from storage", - expectedQuery: storage.NewListPoolsQuery( - storage.NewPaginatedQueryOptions(storage.PoolQuery{}). - WithPageSize(15), - ), - serviceError: storage.ErrDuplicateKeyValue, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - }, - { - name: "other storage errors from storage", - expectedQuery: storage.NewListPoolsQuery( - storage.NewPaginatedQueryOptions(storage.PoolQuery{}). - WithPageSize(15), - ), - serviceError: errors.New("some error"), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - } - - poolID1 := uuid.New() - poolID2 := uuid.New() - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - - accountID1 := models.AccountID{ - Reference: "acc1", - ConnectorID: connectorID, - } - - accountID2 := models.AccountID{ - Reference: "acc2", - ConnectorID: connectorID, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusOK - } - - pools := []models.Pool{ - { - ID: poolID1, - Name: "test1", - PoolAccounts: []*models.PoolAccounts{ - { - PoolID: poolID1, - AccountID: accountID1, - }, - }, - }, - { - ID: poolID2, - Name: "test2", - PoolAccounts: []*models.PoolAccounts{ - { - PoolID: poolID2, - AccountID: accountID1, - }, - { - PoolID: poolID2, - AccountID: accountID2, - }, - }, - }, - } - listPoolsResponse := &bunpaginate.Cursor[models.Pool]{ - PageSize: testCase.pageSize, - HasMore: false, - Previous: "", - Next: "", - Data: pools, - } - - accounts1 := make([]string, len(pools[0].PoolAccounts)) - for i := range pools[0].PoolAccounts { - accounts1[i] = pools[0].PoolAccounts[i].AccountID.String() - } - - accounts2 := make([]string, len(pools[1].PoolAccounts)) - for i := range pools[1].PoolAccounts { - accounts2[i] = pools[1].PoolAccounts[i].AccountID.String() - } - expectedListPoolsResponse := []*poolResponse{ - { - ID: pools[0].ID.String(), - Name: pools[0].Name, - Accounts: accounts1, - }, - { - ID: pools[1].ID.String(), - Name: pools[1].Name, - Accounts: accounts2, - }, - } - - backend, mockService := newTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockService.EXPECT(). - ListPools(gomock.Any(), testCase.expectedQuery). - Return(listPoolsResponse, nil) - } - if testCase.serviceError != nil { - mockService.EXPECT(). - ListPools(gomock.Any(), testCase.expectedQuery). - Return(nil, testCase.serviceError) - } - - router := httpRouter(backend, logging.Testing(), sharedapi.ServiceInfo{ - Debug: testing.Verbose(), - }, auth.NewNoAuth(), false) - - req := httptest.NewRequest(http.MethodGet, "/pools", nil) - rec := httptest.NewRecorder() - req.URL.RawQuery = testCase.queryParams.Encode() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - var resp sharedapi.BaseResponse[*poolResponse] - sharedapi.Decode(t, rec.Body, &resp) - require.Equal(t, expectedListPoolsResponse, resp.Cursor.Data) - require.Equal(t, listPoolsResponse.PageSize, resp.Cursor.PageSize) - require.Equal(t, listPoolsResponse.HasMore, resp.Cursor.HasMore) - require.Equal(t, listPoolsResponse.Next, resp.Cursor.Next) - require.Equal(t, listPoolsResponse.Previous, resp.Cursor.Previous) - } else { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } -} - -func TestGetPool(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - poolID string - serviceError error - expectedPoolID uuid.UUID - expectedStatusCode int - expectedErrorCode string - } - - uuid1 := uuid.New() - testCases := []testCase{ - { - name: "nomimal", - poolID: uuid1.String(), - expectedPoolID: uuid1, - }, - { - name: "err validation from backend", - poolID: uuid1.String(), - expectedPoolID: uuid1, - serviceError: service.ErrValidation, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "ErrNotFound from storage", - poolID: uuid1.String(), - expectedPoolID: uuid1, - serviceError: storage.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - }, - { - name: "ErrDuplicateKeyValue from storage", - poolID: uuid1.String(), - expectedPoolID: uuid1, - serviceError: storage.ErrDuplicateKeyValue, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - }, - { - name: "other storage errors from storage", - poolID: uuid1.String(), - expectedPoolID: uuid1, - serviceError: errors.New("some error"), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - } - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - - accountID1 := models.AccountID{ - Reference: "acc1", - ConnectorID: connectorID, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusOK - } - - getPoolResponse := &models.Pool{ - ID: uuid1, - Name: "test1", - PoolAccounts: []*models.PoolAccounts{ - { - PoolID: uuid1, - AccountID: accountID1, - }, - }, - } - - accounts := make([]string, len(getPoolResponse.PoolAccounts)) - for i := range getPoolResponse.PoolAccounts { - accounts[i] = getPoolResponse.PoolAccounts[i].AccountID.String() - } - expectedPoolResponse := &poolResponse{ - ID: uuid1.String(), - Name: getPoolResponse.Name, - Accounts: accounts, - } - - backend, mockService := newTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockService.EXPECT(). - GetPool(gomock.Any(), testCase.poolID). - Return(getPoolResponse, nil) - } - if testCase.serviceError != nil { - mockService.EXPECT(). - GetPool(gomock.Any(), testCase.poolID). - Return(nil, testCase.serviceError) - } - - router := httpRouter(backend, logging.Testing(), sharedapi.ServiceInfo{ - Debug: testing.Verbose(), - }, auth.NewNoAuth(), false) - - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/pools/%s", testCase.poolID), nil) - rec := httptest.NewRecorder() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - var resp sharedapi.BaseResponse[poolResponse] - sharedapi.Decode(t, rec.Body, &resp) - require.Equal(t, expectedPoolResponse, resp.Data) - } else { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } -} - -func TestGetPoolBalance(t *testing.T) { - t.Parallel() - - uuid1 := uuid.New() - type testCase struct { - name string - queryParams url.Values - poolID string - serviceError error - expectedStatusCode int - expectedErrorCode string - } - - atTime := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).UTC() - testCases := []testCase{ - { - name: "nominal", - poolID: uuid1.String(), - queryParams: url.Values{ - "at": {atTime.Format(time.RFC3339)}, - }, - }, - { - name: "missing at", - poolID: uuid1.String(), - - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "err validation from backend", - poolID: uuid1.String(), - queryParams: url.Values{ - "at": {atTime.Format(time.RFC3339)}, - }, - serviceError: service.ErrValidation, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "ErrNotFound from storage", - poolID: uuid1.String(), - queryParams: url.Values{ - "at": {atTime.Format(time.RFC3339)}, - }, - serviceError: storage.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - }, - { - name: "ErrDuplicateKeyValue from storage", - poolID: uuid1.String(), - queryParams: url.Values{ - "at": {atTime.Format(time.RFC3339)}, - }, - serviceError: storage.ErrDuplicateKeyValue, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - }, - { - name: "other storage errors from storage", - poolID: uuid1.String(), - queryParams: url.Values{ - "at": {atTime.Format(time.RFC3339)}, - }, - serviceError: errors.New("some error"), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusOK - } - - getPoolBalanceResponse := &service.GetPoolBalanceResponse{ - Balances: []*service.Balance{ - { - Amount: big.NewInt(100), - Asset: "EUR/2", - }, - { - Amount: big.NewInt(12000), - Asset: "USD/2", - }, - }, - } - - expectedPoolBalancesResponse := &poolBalancesResponse{ - Balances: []*poolBalanceResponse{ - { - Amount: getPoolBalanceResponse.Balances[0].Amount, - Asset: getPoolBalanceResponse.Balances[0].Asset, - }, - { - Amount: getPoolBalanceResponse.Balances[1].Amount, - Asset: getPoolBalanceResponse.Balances[1].Asset, - }, - }, - } - - backend, mockService := newTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockService.EXPECT(). - GetPoolBalance(gomock.Any(), testCase.poolID, atTime.Format(time.RFC3339)). - Return(getPoolBalanceResponse, nil) - } - if testCase.serviceError != nil { - mockService.EXPECT(). - GetPoolBalance(gomock.Any(), testCase.poolID, atTime.Format(time.RFC3339)). - Return(nil, testCase.serviceError) - } - - router := httpRouter(backend, logging.Testing(), sharedapi.ServiceInfo{ - Debug: testing.Verbose(), - }, auth.NewNoAuth(), false) - - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/pools/%s/balances", testCase.poolID), nil) - rec := httptest.NewRecorder() - req.URL.RawQuery = testCase.queryParams.Encode() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - var resp sharedapi.BaseResponse[poolBalancesResponse] - sharedapi.Decode(t, rec.Body, &resp) - require.Equal(t, expectedPoolBalancesResponse, resp.Data) - } else { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } - -} - -func TestDeletePool(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - poolID string - serviceError error - expectedStatusCode int - expectedErrorCode string - } - - uuid1 := uuid.New() - testCases := []testCase{ - { - name: "nominal", - poolID: uuid1.String(), - }, - { - name: "err validation from backend", - poolID: uuid1.String(), - serviceError: service.ErrValidation, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "ErrNotFound from storage", - poolID: uuid1.String(), - serviceError: storage.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - }, - { - name: "ErrDuplicateKeyValue from storage", - poolID: uuid1.String(), - serviceError: storage.ErrDuplicateKeyValue, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - }, - { - name: "other storage errors from storage", - poolID: uuid1.String(), - serviceError: errors.New("some error"), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusNoContent - } - - backend, mockService := newTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockService.EXPECT(). - DeletePool(gomock.Any(), testCase.poolID). - Return(nil) - } - if testCase.serviceError != nil { - mockService.EXPECT(). - DeletePool(gomock.Any(), testCase.poolID). - Return(testCase.serviceError) - } - - router := httpRouter(backend, logging.Testing(), sharedapi.ServiceInfo{ - Debug: testing.Verbose(), - }, auth.NewNoAuth(), false) - - req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/pools/%s", testCase.poolID), nil) - rec := httptest.NewRecorder() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode >= 300 || testCase.expectedStatusCode < 200 { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } -} diff --git a/components/payments/cmd/api/internal/api/recovery.go b/components/payments/cmd/api/internal/api/recovery.go deleted file mode 100644 index c856fd0f0d..0000000000 --- a/components/payments/cmd/api/internal/api/recovery.go +++ /dev/null @@ -1,23 +0,0 @@ -package api - -import ( - "context" - "net/http" - - "github.com/formancehq/go-libs/api" - "github.com/pkg/errors" -) - -func recoveryHandler(reporter func(ctx context.Context, e interface{})) func(h http.Handler) http.Handler { - return func(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer func() { - if e := recover(); e != nil { - api.InternalServerError(w, r, errors.New("Internal Server Error")) - reporter(r.Context(), e) - } - }() - h.ServeHTTP(w, r) - }) - } -} diff --git a/components/payments/cmd/api/internal/api/router.go b/components/payments/cmd/api/internal/api/router.go deleted file mode 100644 index b55c237b15..0000000000 --- a/components/payments/cmd/api/internal/api/router.go +++ /dev/null @@ -1,74 +0,0 @@ -package api - -import ( - "net/http" - - "github.com/formancehq/go-libs/api" - "github.com/formancehq/go-libs/auth" - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/api/internal/api/backend" - "github.com/gorilla/mux" - "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux" -) - -func httpRouter( - b backend.Backend, - logger logging.Logger, - serviceInfo api.ServiceInfo, - a auth.Authenticator, - otelTraces bool, -) *mux.Router { - rootMux := mux.NewRouter() - - // We have to keep this recovery handler here to ensure that the health - // endpoint is not panicking - rootMux.Use(recoveryHandler(httpRecoveryFunc(otelTraces))) - rootMux.Use(httpCorsHandler()) - rootMux.Use(httpServeFunc) - rootMux.Use(func(handler http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - handler.ServeHTTP(w, r.WithContext(logging.ContextWithLogger(r.Context(), logger))) - }) - }) - - rootMux.Path("/_health").Handler(healthHandler(b)) - - subRouter := rootMux.NewRoute().Subrouter() - if otelTraces { - subRouter.Use(otelmux.Middleware(serviceName)) - // Add a second recovery handler to ensure that the otel middleware - // is catching the error in the trace - rootMux.Use(recoveryHandler(httpRecoveryFunc(otelTraces))) - } - subRouter.Path("/_live").Handler(liveHandler()) - subRouter.Path("/_info").Handler(api.InfoHandler(serviceInfo)) - - authGroup := subRouter.Name("authenticated").Subrouter() - authGroup.Use(auth.Middleware(a)) - - authGroup.Path("/payments").Methods(http.MethodPost).Handler(createPaymentHandler(b)) - authGroup.Path("/payments").Methods(http.MethodGet).Handler(listPaymentsHandler(b)) - authGroup.Path("/payments/{paymentID}").Methods(http.MethodGet).Handler(readPaymentHandler(b)) - authGroup.Path("/payments/{paymentID}/metadata").Methods(http.MethodPatch).Handler(updateMetadataHandler(b)) - - authGroup.Path("/accounts").Methods(http.MethodPost).Handler(createAccountHandler(b)) - authGroup.Path("/accounts").Methods(http.MethodGet).Handler(listAccountsHandler(b)) - authGroup.Path("/accounts/{accountID}").Methods(http.MethodGet).Handler(readAccountHandler(b)) - authGroup.Path("/accounts/{accountID}/balances").Methods(http.MethodGet).Handler(listBalancesForAccount(b)) - - authGroup.Path("/bank-accounts").Methods(http.MethodGet).Handler(listBankAccountsHandler(b)) - authGroup.Path("/bank-accounts/{bankAccountID}").Methods(http.MethodGet).Handler(readBankAccountHandler(b)) - - authGroup.Path("/transfer-initiations").Methods(http.MethodGet).Handler(listTransferInitiationsHandler(b)) - authGroup.Path("/transfer-initiations/{transferID}").Methods(http.MethodGet).Handler(readTransferInitiationHandler(b)) - - authGroup.Path("/pools").Methods(http.MethodPost).Handler(createPoolHandler(b)) - authGroup.Path("/pools").Methods(http.MethodGet).Handler(listPoolHandler(b)) - authGroup.Path("/pools/{poolID}").Methods(http.MethodGet).Handler(getPoolHandler(b)) - authGroup.Path("/pools/{poolID}").Methods(http.MethodDelete).Handler(deletePoolHandler(b)) - authGroup.Path("/pools/{poolID}/accounts").Methods(http.MethodPost).Handler(addAccountToPoolHandler(b)) - authGroup.Path("/pools/{poolID}/accounts/{accountID}").Methods(http.MethodDelete).Handler(removeAccountFromPoolHandler(b)) - authGroup.Path("/pools/{poolID}/balances").Methods(http.MethodGet).Handler(getPoolBalances(b)) - - return rootMux -} diff --git a/components/payments/cmd/api/internal/api/service/accounts.go b/components/payments/cmd/api/internal/api/service/accounts.go deleted file mode 100644 index ee061a9eeb..0000000000 --- a/components/payments/cmd/api/internal/api/service/accounts.go +++ /dev/null @@ -1,126 +0,0 @@ -package service - -import ( - "context" - "encoding/json" - "time" - - "github.com/formancehq/go-libs/bun/bunpaginate" - "github.com/formancehq/go-libs/publish" - - "github.com/formancehq/payments/cmd/api/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/pkg/events" - "github.com/pkg/errors" -) - -type CreateAccountRequest struct { - Reference string `json:"reference"` - ConnectorID string `json:"connectorID"` - CreatedAt time.Time `json:"createdAt"` - DefaultAsset string `json:"defaultAsset"` - AccountName string `json:"accountName"` - Type string `json:"type"` - Metadata map[string]string `json:"metadata"` -} - -func (r *CreateAccountRequest) Validate() error { - if r.Reference == "" { - return errors.New("reference is required") - } - - if r.ConnectorID == "" { - return errors.New("connectorID is required") - } - - if r.CreatedAt.IsZero() || r.CreatedAt.After(time.Now()) { - return errors.New("createdAt is empty or in the future") - } - - if r.AccountName == "" { - return errors.New("accountName is required") - } - - if r.Type == "" { - return errors.New("type is required") - } - - _, err := models.AccountTypeFromString(r.Type) - if err != nil { - return err - } - - return nil -} - -func (s *Service) CreateAccount(ctx context.Context, req *CreateAccountRequest) (*models.Account, error) { - connectorID, err := models.ConnectorIDFromString(req.ConnectorID) - if err != nil { - return nil, errors.Wrap(ErrValidation, err.Error()) - } - - isInstalled, err := s.store.IsConnectorInstalledByConnectorID(ctx, connectorID) - if err != nil { - return nil, newStorageError(err, "checking if connector is installed") - } - - if !isInstalled { - return nil, errors.Wrap(ErrValidation, "connector is not installed") - } - - accountType, err := models.AccountTypeFromString(req.Type) - if err != nil { - return nil, errors.Wrap(ErrValidation, err.Error()) - } - - raw, err := json.Marshal(req) - if err != nil { - return nil, errors.Wrap(ErrValidation, err.Error()) - } - - account := &models.Account{ - ID: models.AccountID{ - Reference: req.Reference, - ConnectorID: connectorID, - }, - ConnectorID: connectorID, - CreatedAt: req.CreatedAt, - Reference: req.Reference, - DefaultAsset: models.Asset(req.DefaultAsset), - AccountName: req.AccountName, - Type: accountType, - Metadata: req.Metadata, - RawData: raw, - } - - err = s.store.UpsertAccounts(ctx, []*models.Account{account}) - if err != nil { - return nil, newStorageError(err, "creating account") - } - - err = s.publisher.Publish(events.TopicPayments, - publish.NewMessage(ctx, s.messages.NewEventSavedAccounts(connectorID.Provider, account))) - if err != nil { - return nil, errors.Wrap(err, "publishing message") - } - - return account, nil -} - -func (s *Service) ListAccounts(ctx context.Context, q storage.ListAccountsQuery) (*bunpaginate.Cursor[models.Account], error) { - cursor, err := s.store.ListAccounts(ctx, q) - return cursor, newStorageError(err, "listing accounts") -} - -func (s *Service) GetAccount( - ctx context.Context, - accountID string, -) (*models.Account, error) { - _, err := models.AccountIDFromString(accountID) - if err != nil { - return nil, errors.Wrap(ErrValidation, err.Error()) - } - - account, err := s.store.GetAccount(ctx, accountID) - return account, newStorageError(err, "getting account") -} diff --git a/components/payments/cmd/api/internal/api/service/accounts_test.go b/components/payments/cmd/api/internal/api/service/accounts_test.go deleted file mode 100644 index 8ee2de2e66..0000000000 --- a/components/payments/cmd/api/internal/api/service/accounts_test.go +++ /dev/null @@ -1,157 +0,0 @@ -package service - -import ( - "context" - "errors" - "testing" - "time" - - "github.com/formancehq/payments/internal/messages" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/stretchr/testify/require" -) - -func TestCreateAccout(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - request *CreateAccountRequest - isConnectorInstalled bool - expectedError error - } - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - - testCases := []testCase{ - { - name: "nominal", - request: &CreateAccountRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - DefaultAsset: "USD/2", - AccountName: "test", - Type: "INTERNAL", - Metadata: map[string]string{ - "foo": "bar", - }, - }, - isConnectorInstalled: true, - }, - { - name: "nominal without default asset", - request: &CreateAccountRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - AccountName: "test", - Type: "INTERNAL", - Metadata: map[string]string{ - "foo": "bar", - }, - }, - isConnectorInstalled: true, - }, - { - name: "connector not installed", - request: &CreateAccountRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - DefaultAsset: "USD/2", - AccountName: "test", - Type: "INTERNAL", - Metadata: map[string]string{ - "foo": "bar", - }, - }, - isConnectorInstalled: false, - expectedError: ErrValidation, - }, - { - name: "invalid connectorID", - request: &CreateAccountRequest{ - Reference: "test", - ConnectorID: "invalid", - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - DefaultAsset: "USD/2", - AccountName: "test", - Type: "INTERNAL", - Metadata: map[string]string{ - "foo": "bar", - }, - }, - expectedError: ErrValidation, - isConnectorInstalled: true, - }, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - store := &MockStore{} - service := New(store.WithIsConnectorInstalled(tc.isConnectorInstalled), &MockPublisher{}, messages.NewMessages("")) - p, err := service.CreateAccount(context.Background(), tc.request) - if tc.expectedError != nil { - require.True(t, errors.Is(err, tc.expectedError)) - } else { - require.NoError(t, err) - require.NotNil(t, p) - } - }) - } -} - -func TestGetAccount(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - accountID string - expectedError error - } - - accountID := models.AccountID{ - Reference: "a1", - ConnectorID: models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - }, - } - - testCases := []testCase{ - { - name: "nominal", - accountID: accountID.String(), - expectedError: nil, - }, - { - name: "invalid accountID", - accountID: "invalid", - expectedError: ErrValidation, - }, - } - - service := New(&MockStore{}, &MockPublisher{}, messages.NewMessages("")) - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - _, err := service.GetAccount(context.Background(), tc.accountID) - if tc.expectedError != nil { - require.True(t, errors.Is(err, tc.expectedError)) - } else { - require.NoError(t, err) - } - }) - } -} diff --git a/components/payments/cmd/api/internal/api/service/balance.go b/components/payments/cmd/api/internal/api/service/balance.go deleted file mode 100644 index 759703469a..0000000000 --- a/components/payments/cmd/api/internal/api/service/balance.go +++ /dev/null @@ -1,15 +0,0 @@ -package service - -import ( - "context" - - "github.com/formancehq/go-libs/bun/bunpaginate" - - "github.com/formancehq/payments/cmd/api/internal/storage" - "github.com/formancehq/payments/internal/models" -) - -func (s *Service) ListBalances(ctx context.Context, q storage.ListBalancesQuery) (*bunpaginate.Cursor[models.Balance], error) { - cursor, err := s.store.ListBalances(ctx, q) - return cursor, newStorageError(err, "listing balances") -} diff --git a/components/payments/cmd/api/internal/api/service/bank_accounts.go b/components/payments/cmd/api/internal/api/service/bank_accounts.go deleted file mode 100644 index c23612dc91..0000000000 --- a/components/payments/cmd/api/internal/api/service/bank_accounts.go +++ /dev/null @@ -1,21 +0,0 @@ -package service - -import ( - "context" - - "github.com/formancehq/go-libs/bun/bunpaginate" - - "github.com/formancehq/payments/cmd/api/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" -) - -func (s *Service) ListBankAccounts(ctx context.Context, q storage.ListBankAccountQuery) (*bunpaginate.Cursor[models.BankAccount], error) { - cursor, err := s.store.ListBankAccounts(ctx, q) - return cursor, newStorageError(err, "listing bank accounts") -} - -func (s *Service) GetBankAccount(ctx context.Context, id uuid.UUID, expand bool) (*models.BankAccount, error) { - account, err := s.store.GetBankAccount(ctx, id, expand) - return account, newStorageError(err, "getting bank account") -} diff --git a/components/payments/cmd/api/internal/api/service/errors.go b/components/payments/cmd/api/internal/api/service/errors.go deleted file mode 100644 index 7c1fe16104..0000000000 --- a/components/payments/cmd/api/internal/api/service/errors.go +++ /dev/null @@ -1,38 +0,0 @@ -package service - -import ( - "errors" - "fmt" -) - -var ( - ErrValidation = errors.New("validation error") -) - -type storageError struct { - err error - msg string -} - -func (e *storageError) Error() string { - return fmt.Sprintf("%s: %s", e.msg, e.err) -} - -func (e *storageError) Is(err error) bool { - _, ok := err.(*storageError) - return ok -} - -func (e *storageError) Unwrap() error { - return e.err -} - -func newStorageError(err error, msg string) error { - if err == nil { - return nil - } - return &storageError{ - err: err, - msg: msg, - } -} diff --git a/components/payments/cmd/api/internal/api/service/payments.go b/components/payments/cmd/api/internal/api/service/payments.go deleted file mode 100644 index 64d9b19931..0000000000 --- a/components/payments/cmd/api/internal/api/service/payments.go +++ /dev/null @@ -1,176 +0,0 @@ -package service - -import ( - "context" - "encoding/json" - "math/big" - "time" - - "github.com/formancehq/go-libs/bun/bunpaginate" - - "github.com/formancehq/go-libs/publish" - "github.com/formancehq/payments/cmd/api/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/pkg/events" - "github.com/pkg/errors" -) - -type CreatePaymentRequest struct { - Reference string `json:"reference"` - ConnectorID string `json:"connectorID"` - CreatedAt time.Time `json:"createdAt"` - Amount *big.Int `json:"amount"` - Type string `json:"type"` - Status string `json:"status"` - Scheme string `json:"scheme"` - Asset string `json:"asset"` - SourceAccountID string `json:"sourceAccountID"` - DestinationAccountID string `json:"destinationAccountID"` -} - -func (r *CreatePaymentRequest) Validate() error { - if r.Reference == "" { - return errors.New("reference is required") - } - - if r.ConnectorID == "" { - return errors.New("connectorID is required") - } - - if r.CreatedAt.IsZero() || r.CreatedAt.After(time.Now()) { - return errors.New("createdAt is empty or in the future") - } - - if r.Amount == nil { - return errors.New("amount is required") - } - - if r.Type == "" { - return errors.New("type is required") - } - - if _, err := models.PaymentTypeFromString(r.Type); err != nil { - return errors.Wrap(err, "invalid type") - } - - if r.Status == "" { - return errors.New("status is required") - } - - if _, err := models.PaymentStatusFromString(r.Status); err != nil { - return errors.Wrap(err, "invalid status") - } - - if r.Scheme == "" { - return errors.New("scheme is required") - } - - if _, err := models.PaymentSchemeFromString(r.Scheme); err != nil { - return errors.Wrap(err, "invalid scheme") - } - - if r.Asset == "" { - return errors.New("asset is required") - } - - _, _, err := models.GetCurrencyAndPrecisionFromAsset(models.Asset(r.Asset)) - if err != nil { - return errors.Wrap(err, "invalid asset") - } - - return nil -} - -func (s *Service) CreatePayment(ctx context.Context, req *CreatePaymentRequest) (*models.Payment, error) { - connectorID, err := models.ConnectorIDFromString(req.ConnectorID) - if err != nil { - return nil, errors.Wrap(ErrValidation, err.Error()) - } - - isInstalled, err := s.store.IsConnectorInstalledByConnectorID(ctx, connectorID) - if err != nil { - return nil, newStorageError(err, "checking if connector is installed") - } - - if !isInstalled { - return nil, errors.Wrap(ErrValidation, "connector is not installed") - } - - var sourceAccountID *models.AccountID - if req.SourceAccountID != "" { - sourceAccountID, err = models.AccountIDFromString(req.SourceAccountID) - if err != nil { - return nil, errors.Wrap(ErrValidation, err.Error()) - } - } - - var destinationAccountID *models.AccountID - if req.DestinationAccountID != "" { - destinationAccountID, err = models.AccountIDFromString(req.DestinationAccountID) - if err != nil { - return nil, errors.Wrap(ErrValidation, err.Error()) - } - } - - raw, err := json.Marshal(req) - if err != nil { - return nil, errors.Wrap(ErrValidation, err.Error()) - } - - payment := &models.Payment{ - ID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: req.Reference, - Type: models.PaymentType(req.Type), - }, - ConnectorID: connectorID, - }, - ConnectorID: connectorID, - CreatedAt: req.CreatedAt, - Reference: req.Reference, - Amount: req.Amount, - InitialAmount: req.Amount, - Type: models.PaymentType(req.Type), - Status: models.PaymentStatus(req.Status), - Scheme: models.PaymentScheme(req.Scheme), - Asset: models.Asset(req.Asset), - SourceAccountID: sourceAccountID, - DestinationAccountID: destinationAccountID, - RawData: raw, - } - - err = s.store.UpsertPayments(ctx, []*models.Payment{payment}) - if err != nil { - return nil, newStorageError(err, "creating payment") - } - - err = s.publisher.Publish(events.TopicPayments, - publish.NewMessage(ctx, s.messages.NewEventSavedPayments(connectorID.Provider, payment))) - if err != nil { - return nil, errors.Wrap(err, "publishing message") - } - - return payment, nil -} - -func (s *Service) ListPayments(ctx context.Context, q storage.ListPaymentsQuery) (*bunpaginate.Cursor[models.Payment], error) { - cursor, err := s.store.ListPayments(ctx, q) - return cursor, newStorageError(err, "listing payments") -} - -func (s *Service) GetPayment(ctx context.Context, id string) (*models.Payment, error) { - _, err := models.PaymentIDFromString(id) - if err != nil { - return nil, errors.Wrap(ErrValidation, err.Error()) - } - - payment, err := s.store.GetPayment(ctx, id) - return payment, newStorageError(err, "getting payment") -} - -type UpdateMetadataRequest map[string]string - -func (s *Service) UpdatePaymentMetadata(ctx context.Context, paymentID models.PaymentID, metadata map[string]string) error { - err := s.store.UpdatePaymentMetadata(ctx, paymentID, metadata) - return newStorageError(err, "updating payment metadata") -} diff --git a/components/payments/cmd/api/internal/api/service/payments_test.go b/components/payments/cmd/api/internal/api/service/payments_test.go deleted file mode 100644 index df51f87d13..0000000000 --- a/components/payments/cmd/api/internal/api/service/payments_test.go +++ /dev/null @@ -1,208 +0,0 @@ -package service - -import ( - "context" - "errors" - "math/big" - "testing" - "time" - - "github.com/formancehq/payments/internal/messages" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/stretchr/testify/require" -) - -func TestCreatePayment(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - request *CreatePaymentRequest - isConnectorInstalled bool - expectedError error - } - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - - sourceAccountID := models.AccountID{ - Reference: "acc1", - ConnectorID: connectorID, - } - - destinationAccountID := models.AccountID{ - Reference: "acc2", - ConnectorID: connectorID, - } - - testCases := []testCase{ - { - name: "nominal", - request: &CreatePaymentRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Amount: big.NewInt(100), - Type: string(models.PaymentTypeTransfer), - Status: string(models.PaymentStatusSucceeded), - Scheme: string(models.PaymentSchemeOther), - Asset: "EUR/2", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - }, - isConnectorInstalled: true, - }, - { - name: "connector not installed", - request: &CreatePaymentRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Amount: big.NewInt(100), - Type: string(models.PaymentTypeTransfer), - Status: string(models.PaymentStatusSucceeded), - Scheme: string(models.PaymentSchemeOther), - Asset: "EUR/2", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - }, - isConnectorInstalled: false, - expectedError: ErrValidation, - }, - { - name: "nominal without source or destination account ids", - request: &CreatePaymentRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Amount: big.NewInt(100), - Type: string(models.PaymentTypeTransfer), - Status: string(models.PaymentStatusSucceeded), - Scheme: string(models.PaymentSchemeOther), - Asset: "EUR/2", - }, - isConnectorInstalled: true, - }, - { - name: "invalid connectorID", - request: &CreatePaymentRequest{ - Reference: "test", - ConnectorID: "invalid", - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Amount: big.NewInt(100), - Type: string(models.PaymentTypeTransfer), - Status: string(models.PaymentStatusSucceeded), - Scheme: string(models.PaymentSchemeOther), - Asset: "EUR/2", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - }, - expectedError: ErrValidation, - isConnectorInstalled: true, - }, - { - name: "invalid source account id", - request: &CreatePaymentRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Amount: big.NewInt(100), - Type: string(models.PaymentTypeTransfer), - Status: string(models.PaymentStatusSucceeded), - Scheme: string(models.PaymentSchemeOther), - Asset: "EUR/2", - SourceAccountID: "invalid", - DestinationAccountID: destinationAccountID.String(), - }, - expectedError: ErrValidation, - isConnectorInstalled: true, - }, - { - name: "invalid destination account id", - request: &CreatePaymentRequest{ - Reference: "test", - ConnectorID: connectorID.String(), - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Amount: big.NewInt(100), - Type: string(models.PaymentTypeTransfer), - Status: string(models.PaymentStatusSucceeded), - Scheme: string(models.PaymentSchemeOther), - Asset: "EUR/2", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: "invalid", - }, - expectedError: ErrValidation, - isConnectorInstalled: true, - }, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - store := &MockStore{} - service := New(store.WithIsConnectorInstalled(tc.isConnectorInstalled), &MockPublisher{}, messages.NewMessages("")) - p, err := service.CreatePayment(context.Background(), tc.request) - if tc.expectedError != nil { - require.True(t, errors.Is(err, tc.expectedError)) - } else { - require.NoError(t, err) - require.NotNil(t, p) - } - }) - } -} - -func TestGetPayment(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - paymentID string - expectedError error - } - - paymentID := models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: "p1", - Type: models.PaymentTypePayIn, - }, - ConnectorID: models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - }, - } - - testCases := []testCase{ - { - name: "nominal", - paymentID: paymentID.String(), - expectedError: nil, - }, - { - name: "invalid paymentID", - paymentID: "invalid", - expectedError: ErrValidation, - }, - } - - service := New(&MockStore{}, &MockPublisher{}, messages.NewMessages("")) - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - _, err := service.GetPayment(context.Background(), tc.paymentID) - if tc.expectedError != nil { - require.True(t, errors.Is(err, tc.expectedError)) - } else { - require.NoError(t, err) - } - }) - } -} diff --git a/components/payments/cmd/api/internal/api/service/ping.go b/components/payments/cmd/api/internal/api/service/ping.go deleted file mode 100644 index e33dd7ba06..0000000000 --- a/components/payments/cmd/api/internal/api/service/ping.go +++ /dev/null @@ -1,5 +0,0 @@ -package service - -func (s *Service) Ping() error { - return s.store.Ping() -} diff --git a/components/payments/cmd/api/internal/api/service/pools.go b/components/payments/cmd/api/internal/api/service/pools.go deleted file mode 100644 index a0e82a7af4..0000000000 --- a/components/payments/cmd/api/internal/api/service/pools.go +++ /dev/null @@ -1,259 +0,0 @@ -package service - -import ( - "context" - "math/big" - "time" - - "github.com/formancehq/go-libs/bun/bunpaginate" - - "github.com/formancehq/go-libs/publish" - "github.com/formancehq/payments/cmd/api/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/pkg/events" - "github.com/google/uuid" - "github.com/pkg/errors" -) - -type CreatePoolRequest struct { - Name string `json:"name"` - AccountIDs []string `json:"accountIDs"` -} - -func (c *CreatePoolRequest) Validate() error { - if c.Name == "" { - return errors.New("name is required") - } - - if len(c.AccountIDs) == 0 { - return errors.New("accountIDs is required") - } - - return nil -} - -func (s *Service) CreatePool( - ctx context.Context, - req *CreatePoolRequest, -) (*models.Pool, error) { - pool := &models.Pool{ - Name: req.Name, - CreatedAt: time.Now().UTC(), - } - - err := s.store.CreatePool(ctx, pool) - if err != nil { - return nil, newStorageError(err, "creating pool") - } - - poolAccounts := make([]*models.PoolAccounts, len(req.AccountIDs)) - for i, accountID := range req.AccountIDs { - aID, err := models.AccountIDFromString(accountID) - if err != nil { - return nil, errors.Wrap(ErrValidation, err.Error()) - } - - poolAccounts[i] = &models.PoolAccounts{ - PoolID: pool.ID, - AccountID: *aID, - } - } - - err = s.store.AddAccountsToPool(ctx, poolAccounts) - if err != nil { - return nil, newStorageError(err, "adding accounts to pool") - } - pool.PoolAccounts = poolAccounts - - err = s.publisher.Publish(events.TopicPayments, - publish.NewMessage(ctx, s.messages.NewEventSavedPool(pool))) - if err != nil { - return nil, errors.Wrap(err, "publishing message") - } - - return pool, nil -} - -type AddAccountToPoolRequest struct { - AccountID string `json:"accountID"` -} - -func (c *AddAccountToPoolRequest) Validate() error { - if c.AccountID == "" { - return errors.New("accountID is required") - } - - return nil -} - -func (s *Service) AddAccountToPool( - ctx context.Context, - poolID string, - req *AddAccountToPoolRequest, -) error { - id, err := uuid.Parse(poolID) - if err != nil { - return errors.Wrap(ErrValidation, err.Error()) - } - - aID, err := models.AccountIDFromString(req.AccountID) - if err != nil { - return errors.Wrap(ErrValidation, err.Error()) - } - - if err := s.store.AddAccountToPool(ctx, &models.PoolAccounts{ - PoolID: id, - AccountID: *aID, - }); err != nil { - return newStorageError(err, "adding account to pool") - } - - pool, err := s.store.GetPool(ctx, id) - if err != nil { - return newStorageError(err, "getting pool") - } - - err = s.publisher.Publish(events.TopicPayments, - publish.NewMessage(ctx, s.messages.NewEventSavedPool(pool))) - if err != nil { - return errors.Wrap(err, "publishing message") - } - - return nil -} - -func (s *Service) RemoveAccountFromPool( - ctx context.Context, - poolID string, - accountID string, -) error { - id, err := uuid.Parse(poolID) - if err != nil { - return errors.Wrap(ErrValidation, err.Error()) - } - - aID, err := models.AccountIDFromString(accountID) - if err != nil { - return errors.Wrap(ErrValidation, err.Error()) - } - - if err := s.store.RemoveAccountFromPool(ctx, &models.PoolAccounts{ - PoolID: id, - AccountID: *aID, - }); err != nil { - return newStorageError(err, "removing account from pool") - } - - pool, err := s.store.GetPool(ctx, id) - if err != nil { - return newStorageError(err, "getting pool") - } - - err = s.publisher.Publish(events.TopicPayments, - publish.NewMessage(ctx, s.messages.NewEventSavedPool(pool))) - if err != nil { - return errors.Wrap(err, "publishing message") - } - - return nil -} - -func (s *Service) ListPools(ctx context.Context, q storage.ListPoolsQuery) (*bunpaginate.Cursor[models.Pool], error) { - cursor, err := s.store.ListPools(ctx, q) - return cursor, newStorageError(err, "listing pools") -} - -func (s *Service) GetPool( - ctx context.Context, - poolID string, -) (*models.Pool, error) { - id, err := uuid.Parse(poolID) - if err != nil { - return nil, errors.Wrap(ErrValidation, err.Error()) - } - - pool, err := s.store.GetPool(ctx, id) - return pool, newStorageError(err, "getting pool") -} - -type GetPoolBalanceResponse struct { - Balances []*Balance -} - -type Balance struct { - Amount *big.Int - Asset string -} - -func (s *Service) GetPoolBalance( - ctx context.Context, - poolID string, - atTime string, -) (*GetPoolBalanceResponse, error) { - id, err := uuid.Parse(poolID) - if err != nil { - return nil, errors.Wrap(ErrValidation, err.Error()) - } - - at, err := time.Parse(time.RFC3339, atTime) - if err != nil { - return nil, errors.Wrap(ErrValidation, err.Error()) - } - - pool, err := s.store.GetPool(ctx, id) - if err != nil { - return nil, newStorageError(err, "getting pool") - } - - res := make(map[string]*big.Int) - for _, poolAccount := range pool.PoolAccounts { - balances, err := s.store.GetBalancesAt(ctx, poolAccount.AccountID, at) - if err != nil { - return nil, newStorageError(err, "getting balances") - } - - for _, balance := range balances { - amount, ok := res[balance.Asset.String()] - if !ok { - amount = big.NewInt(0) - } - - amount.Add(amount, balance.Balance) - res[balance.Asset.String()] = amount - } - } - - balances := make([]*Balance, 0, len(res)) - for asset, amount := range res { - balances = append(balances, &Balance{ - Asset: asset, - Amount: amount, - }) - } - - return &GetPoolBalanceResponse{ - Balances: balances, - }, nil -} - -func (s *Service) DeletePool( - ctx context.Context, - poolID string, -) error { - id, err := uuid.Parse(poolID) - if err != nil { - return errors.Wrap(ErrValidation, err.Error()) - } - - if err := s.store.DeletePool(ctx, id); err != nil { - return newStorageError(err, "deleting pool") - } - - err = s.publisher.Publish(events.TopicPayments, - publish.NewMessage(ctx, s.messages.NewEventDeletePool(id))) - if err != nil { - return errors.Wrap(err, "publishing message") - } - - return nil -} diff --git a/components/payments/cmd/api/internal/api/service/pools_test.go b/components/payments/cmd/api/internal/api/service/pools_test.go deleted file mode 100644 index e7b3a22874..0000000000 --- a/components/payments/cmd/api/internal/api/service/pools_test.go +++ /dev/null @@ -1,349 +0,0 @@ -package service - -import ( - "context" - "encoding/json" - "errors" - "math/big" - "testing" - "time" - - "github.com/ThreeDotsLabs/watermill/message" - "github.com/formancehq/payments/internal/messages" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/stretchr/testify/require" -) - -func TestCreatePool(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - request *CreatePoolRequest - expectedError error - } - - accountID := models.AccountID{ - Reference: "acc1", - ConnectorID: models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - }, - } - - testCases := []testCase{ - { - name: "nominal", - request: &CreatePoolRequest{ - Name: "pool1", - AccountIDs: []string{accountID.String()}, - }, - expectedError: nil, - }, - { - name: "invalid accountID", - request: &CreatePoolRequest{ - Name: "pool1", - AccountIDs: []string{"invalid"}, - }, - expectedError: ErrValidation, - }, - } - - m := &MockPublisher{} - messageChan := make(chan *message.Message, 1) - service := New(&MockStore{}, m.WithMessagesChan(messageChan), messages.NewMessages("")) - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - pool, err := service.CreatePool(context.Background(), tc.request) - if tc.expectedError != nil { - require.True(t, errors.Is(err, tc.expectedError)) - } else { - require.NoError(t, err) - require.NotNil(t, pool) - - require.Eventually(t, func() bool { - select { - case msg := <-messageChan: - type poolPayload struct { - Payload struct { - ID string `json:"id"` - Name string `json:"name"` - AccountIDS []string `json:"accountIDs"` - } `json:"payload"` - } - - var p poolPayload - require.NoError(t, json.Unmarshal(msg.Payload, &p)) - require.Equal(t, pool.ID.String(), p.Payload.ID) - require.Equal(t, tc.request.Name, p.Payload.Name) - require.Equal(t, tc.request.AccountIDs, p.Payload.AccountIDS) - return true - } - }, 10*time.Second, 100*time.Millisecond) - } - }) - } -} - -func TestGetPool(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - poolID string - expectedError error - } - - uuid1 := uuid.New() - - testCases := []testCase{ - { - name: "nominal", - poolID: uuid1.String(), - expectedError: nil, - }, - { - name: "invalid poolID", - poolID: "invalid", - expectedError: ErrValidation, - }, - } - - service := New(&MockStore{}, &MockPublisher{}, messages.NewMessages("")) - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - _, err := service.GetPool(context.Background(), tc.poolID) - if tc.expectedError != nil { - require.True(t, errors.Is(err, tc.expectedError)) - } else { - require.NoError(t, err) - } - }) - } -} - -func TestAddAccountToPool(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - poolID string - accountID string - expectedError error - } - - uuid1 := uuid.New() - accountID := models.AccountID{ - Reference: "acc1", - ConnectorID: models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - }, - } - - testCases := []testCase{ - { - name: "nominal", - poolID: uuid1.String(), - accountID: accountID.String(), - expectedError: nil, - }, - { - name: "invalid poolID", - poolID: "invalid", - accountID: accountID.String(), - expectedError: ErrValidation, - }, - { - name: "invalid accountID", - poolID: uuid1.String(), - accountID: "invalid", - expectedError: ErrValidation, - }, - } - - service := New(&MockStore{}, &MockPublisher{}, messages.NewMessages("")) - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - err := service.AddAccountToPool(context.Background(), tc.poolID, &AddAccountToPoolRequest{ - AccountID: tc.accountID, - }) - if tc.expectedError != nil { - require.True(t, errors.Is(err, tc.expectedError)) - } else { - require.NoError(t, err) - } - }) - } - -} - -func TestRemoveAccountFromPool(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - poolID string - accountID string - expectedError error - } - - uuid1 := uuid.New() - accountID := models.AccountID{ - Reference: "acc1", - ConnectorID: models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - }, - } - - testCases := []testCase{ - { - name: "nominal", - poolID: uuid1.String(), - accountID: accountID.String(), - expectedError: nil, - }, - { - name: "invalid poolID", - poolID: "invalid", - accountID: accountID.String(), - expectedError: ErrValidation, - }, - { - name: "invalid accountID", - poolID: uuid1.String(), - accountID: "invalid", - expectedError: ErrValidation, - }, - } - - service := New(&MockStore{}, &MockPublisher{}, messages.NewMessages("")) - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - err := service.RemoveAccountFromPool(context.Background(), tc.poolID, tc.accountID) - if tc.expectedError != nil { - require.True(t, errors.Is(err, tc.expectedError)) - } else { - require.NoError(t, err) - } - }) - } -} - -func TestDeletePool(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - poolID string - expectedError error - } - - uuid1 := uuid.New() - - testCases := []testCase{ - { - name: "nominal", - poolID: uuid1.String(), - expectedError: nil, - }, - { - name: "invalid poolID", - poolID: "invalid", - expectedError: ErrValidation, - }, - } - - service := New(&MockStore{}, &MockPublisher{}, messages.NewMessages("")) - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - err := service.DeletePool(context.Background(), tc.poolID) - if tc.expectedError != nil { - require.True(t, errors.Is(err, tc.expectedError)) - } else { - require.NoError(t, err) - } - }) - } - -} - -func TestGetPoolBalance(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - poolID string - atTime string - expectedError error - } - - uuid1 := uuid.New() - - testCases := []testCase{ - { - name: "nominal", - poolID: uuid1.String(), - atTime: "2021-01-01T00:00:00Z", - }, - { - name: "invalid poolID", - poolID: "invalid", - atTime: "2021-01-01T00:00:00Z", - expectedError: ErrValidation, - }, - { - name: "invalid atTime", - poolID: uuid1.String(), - atTime: "invalid", - expectedError: ErrValidation, - }, - } - - service := New(&MockStore{}, &MockPublisher{}, messages.NewMessages("")) - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - expectedResponseMap := map[string]*big.Int{ - "EUR/2": big.NewInt(200), - "USD/2": big.NewInt(300), - } - - balances, err := service.GetPoolBalance(context.Background(), tc.poolID, tc.atTime) - if tc.expectedError != nil { - require.True(t, errors.Is(err, tc.expectedError)) - } else { - require.NoError(t, err) - - require.Equal(t, 2, len(balances.Balances)) - for _, balance := range balances.Balances { - expectedAmount, ok := expectedResponseMap[balance.Asset] - require.True(t, ok) - require.Equal(t, expectedAmount, balance.Amount) - } - } - }) - } -} diff --git a/components/payments/cmd/api/internal/api/service/service.go b/components/payments/cmd/api/internal/api/service/service.go deleted file mode 100644 index 387e594f0a..0000000000 --- a/components/payments/cmd/api/internal/api/service/service.go +++ /dev/null @@ -1,53 +0,0 @@ -package service - -import ( - "context" - "time" - - "github.com/formancehq/go-libs/bun/bunpaginate" - - "github.com/ThreeDotsLabs/watermill/message" - "github.com/formancehq/payments/cmd/api/internal/storage" - "github.com/formancehq/payments/internal/messages" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" -) - -type Store interface { - Ping() error - IsConnectorInstalledByConnectorID(ctx context.Context, connectorID models.ConnectorID) (bool, error) - UpsertAccounts(ctx context.Context, accounts []*models.Account) error - ListAccounts(ctx context.Context, q storage.ListAccountsQuery) (*bunpaginate.Cursor[models.Account], error) - GetAccount(ctx context.Context, id string) (*models.Account, error) - ListBalances(ctx context.Context, q storage.ListBalancesQuery) (*bunpaginate.Cursor[models.Balance], error) - GetBalancesAt(ctx context.Context, accountID models.AccountID, at time.Time) ([]*models.Balance, error) - ListBankAccounts(ctx context.Context, q storage.ListBankAccountQuery) (*bunpaginate.Cursor[models.BankAccount], error) - GetBankAccount(ctx context.Context, id uuid.UUID, expand bool) (*models.BankAccount, error) - UpsertPayments(ctx context.Context, payments []*models.Payment) error - ListPayments(ctx context.Context, q storage.ListPaymentsQuery) (*bunpaginate.Cursor[models.Payment], error) - GetPayment(ctx context.Context, id string) (*models.Payment, error) - UpdatePaymentMetadata(ctx context.Context, paymentID models.PaymentID, metadata map[string]string) error - ListTransferInitiations(ctx context.Context, q storage.ListTransferInitiationsQuery) (*bunpaginate.Cursor[models.TransferInitiation], error) - GetTransferInitiation(ctx context.Context, id models.TransferInitiationID) (*models.TransferInitiation, error) - CreatePool(ctx context.Context, pool *models.Pool) error - AddAccountToPool(ctx context.Context, poolAccount *models.PoolAccounts) error - AddAccountsToPool(ctx context.Context, poolAccounts []*models.PoolAccounts) error - RemoveAccountFromPool(ctx context.Context, poolAccount *models.PoolAccounts) error - ListPools(ctx context.Context, q storage.ListPoolsQuery) (*bunpaginate.Cursor[models.Pool], error) - GetPool(ctx context.Context, poolID uuid.UUID) (*models.Pool, error) - DeletePool(ctx context.Context, poolID uuid.UUID) error -} - -type Service struct { - store Store - publisher message.Publisher - messages *messages.Messages -} - -func New(store Store, publisher message.Publisher, messages *messages.Messages) *Service { - return &Service{ - store: store, - publisher: publisher, - messages: messages, - } -} diff --git a/components/payments/cmd/api/internal/api/service/service_test.go b/components/payments/cmd/api/internal/api/service/service_test.go deleted file mode 100644 index bf31a11393..0000000000 --- a/components/payments/cmd/api/internal/api/service/service_test.go +++ /dev/null @@ -1,180 +0,0 @@ -package service - -import ( - "context" - "math/big" - "time" - - "github.com/formancehq/go-libs/bun/bunpaginate" - - "github.com/ThreeDotsLabs/watermill/message" - "github.com/formancehq/payments/cmd/api/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" -) - -type MockStore struct { - isConnectorInstalled bool -} - -func (m *MockStore) WithIsConnectorInstalled(isConnectorInstalled bool) *MockStore { - m.isConnectorInstalled = isConnectorInstalled - return m -} - -func (m *MockStore) Ping() error { - return nil -} - -func (m *MockStore) IsConnectorInstalledByConnectorID(ctx context.Context, connectorID models.ConnectorID) (bool, error) { - return m.isConnectorInstalled, nil -} - -func (m *MockStore) ListBalances(ctx context.Context, q storage.ListBalancesQuery) (*bunpaginate.Cursor[models.Balance], error) { - return nil, nil -} - -func (m *MockStore) GetBalancesAt(ctx context.Context, accountID models.AccountID, atTime time.Time) ([]*models.Balance, error) { - return []*models.Balance{ - { - AccountID: accountID, - Asset: "EUR/2", - Balance: big.NewInt(100), - }, - { - AccountID: accountID, - Asset: "USD/2", - Balance: big.NewInt(150), - }, - }, nil -} - -func (m *MockStore) UpsertAccounts(ctx context.Context, accounts []*models.Account) error { - return nil -} - -func (m *MockStore) ListAccounts(ctx context.Context, q storage.ListAccountsQuery) (*bunpaginate.Cursor[models.Account], error) { - return nil, nil -} - -func (m *MockStore) GetAccount(ctx context.Context, id string) (*models.Account, error) { - return nil, nil -} - -func (m *MockStore) ListBankAccounts(ctx context.Context, q storage.ListBankAccountQuery) (*bunpaginate.Cursor[models.BankAccount], error) { - return nil, nil -} - -func (m *MockStore) GetBankAccount(ctx context.Context, id uuid.UUID, expand bool) (*models.BankAccount, error) { - return nil, nil -} - -func (m *MockStore) UpsertPayments(ctx context.Context, payments []*models.Payment) error { - return nil -} - -func (m *MockStore) ListPayments(ctx context.Context, q storage.ListPaymentsQuery) (*bunpaginate.Cursor[models.Payment], error) { - return nil, nil -} - -func (m *MockStore) GetPayment(ctx context.Context, id string) (*models.Payment, error) { - return nil, nil -} - -func (m *MockStore) UpdatePaymentMetadata(ctx context.Context, paymentID models.PaymentID, metadata map[string]string) error { - return nil -} - -func (m *MockStore) ListTransferInitiations(ctx context.Context, q storage.ListTransferInitiationsQuery) (*bunpaginate.Cursor[models.TransferInitiation], error) { - return nil, nil -} - -func (m *MockStore) GetTransferInitiation(ctx context.Context, id models.TransferInitiationID) (*models.TransferInitiation, error) { - return nil, nil -} - -func (m *MockStore) CreatePool(ctx context.Context, pool *models.Pool) error { - return nil -} - -func (m *MockStore) AddAccountsToPool(ctx context.Context, poolAccounts []*models.PoolAccounts) error { - return nil -} - -func (m *MockStore) AddAccountToPool(ctx context.Context, poolAccount *models.PoolAccounts) error { - return nil -} - -func (m *MockStore) RemoveAccountFromPool(ctx context.Context, poolAccount *models.PoolAccounts) error { - return nil -} - -func (m *MockStore) ListPools(ctx context.Context, q storage.ListPoolsQuery) (*bunpaginate.Cursor[models.Pool], error) { - return nil, nil -} - -func (m *MockStore) GetPool(ctx context.Context, poolID uuid.UUID) (*models.Pool, error) { - return &models.Pool{ - ID: poolID, - Name: "test", - PoolAccounts: []*models.PoolAccounts{ - { - PoolID: poolID, - AccountID: models.AccountID{ - Reference: "acc1", - ConnectorID: models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - }, - }, - }, - { - PoolID: poolID, - AccountID: models.AccountID{ - Reference: "acc2", - ConnectorID: models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - }, - }, - }, - }, - }, nil -} - -func (m *MockStore) DeletePool(ctx context.Context, poolID uuid.UUID) error { - return nil -} - -type MockPublisher struct { - errorToSend error - messagesChan chan *message.Message -} - -func (m *MockPublisher) WithError(err error) *MockPublisher { - m.errorToSend = err - return m -} - -func (m *MockPublisher) WithMessagesChan(messagesChan chan *message.Message) *MockPublisher { - m.messagesChan = messagesChan - return m -} - -func (m *MockPublisher) Publish(topic string, messages ...*message.Message) error { - if m.errorToSend != nil { - return m.errorToSend - } - - if m.messagesChan != nil { - for _, msg := range messages { - m.messagesChan <- msg - } - } - - return nil -} - -func (m *MockPublisher) Close() error { - return nil -} diff --git a/components/payments/cmd/api/internal/api/service/transfer_initiations.go b/components/payments/cmd/api/internal/api/service/transfer_initiations.go deleted file mode 100644 index 4b8ea64191..0000000000 --- a/components/payments/cmd/api/internal/api/service/transfer_initiations.go +++ /dev/null @@ -1,20 +0,0 @@ -package service - -import ( - "context" - - "github.com/formancehq/go-libs/bun/bunpaginate" - - "github.com/formancehq/payments/cmd/api/internal/storage" - "github.com/formancehq/payments/internal/models" -) - -func (s *Service) ListTransferInitiations(ctx context.Context, q storage.ListTransferInitiationsQuery) (*bunpaginate.Cursor[models.TransferInitiation], error) { - cursor, err := s.store.ListTransferInitiations(ctx, q) - return cursor, newStorageError(err, "listing transfer initiations") -} - -func (s *Service) ReadTransferInitiation(ctx context.Context, id models.TransferInitiationID) (*models.TransferInitiation, error) { - transferInitiation, err := s.store.GetTransferInitiation(ctx, id) - return transferInitiation, newStorageError(err, "reading transfer initiation") -} diff --git a/components/payments/cmd/api/internal/api/transfer_initiation.go b/components/payments/cmd/api/internal/api/transfer_initiation.go deleted file mode 100644 index ea1a0670be..0000000000 --- a/components/payments/cmd/api/internal/api/transfer_initiation.go +++ /dev/null @@ -1,208 +0,0 @@ -package api - -import ( - "encoding/json" - "math/big" - "net/http" - "time" - - "github.com/formancehq/go-libs/api" - "github.com/formancehq/go-libs/bun/bunpaginate" - "github.com/formancehq/go-libs/pointer" - "github.com/formancehq/payments/cmd/api/internal/api/backend" - "github.com/formancehq/payments/cmd/api/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/gorilla/mux" - "go.opentelemetry.io/otel/attribute" -) - -type transferInitiationResponse struct { - ID string `json:"id"` - Reference string `json:"reference"` - CreatedAt time.Time `json:"createdAt"` - ScheduledAt time.Time `json:"scheduledAt"` - Description string `json:"description"` - SourceAccountID string `json:"sourceAccountID"` - DestinationAccountID string `json:"destinationAccountID"` - ConnectorID string `json:"connectorID"` - Provider string `json:"provider"` - Type string `json:"type"` - Amount *big.Int `json:"amount"` - InitialAmount *big.Int `json:"initialAmount"` - Asset string `json:"asset"` - Status string `json:"status"` - Error string `json:"error"` - Metadata map[string]string `json:"metadata"` -} - -type transferInitiationPaymentsResponse struct { - PaymentID string `json:"paymentID"` - CreatedAt time.Time `json:"createdAt"` - Status string `json:"status"` - Error string `json:"error"` -} - -type transferInitiationAdjustmentsResponse struct { - AdjustmentID string `json:"adjustmentID"` - CreatedAt time.Time `json:"createdAt"` - Status string `json:"status"` - Error string `json:"error"` - Metadata map[string]string `json:"metadata"` -} - -type readTransferInitiationResponse struct { - transferInitiationResponse - RelatedPayments []*transferInitiationPaymentsResponse `json:"relatedPayments"` - RelatedAdjustments []*transferInitiationAdjustmentsResponse `json:"relatedAdjustments"` -} - -func readTransferInitiationHandler(b backend.Backend) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "readTransferInitiationHandler") - defer span.End() - - w.Header().Set("Content-Type", "application/json") - - transferID, err := models.TransferInitiationIDFromString(mux.Vars(r)["transferID"]) - if err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrInvalidID, err) - return - } - - span.SetAttributes(attribute.String("request.transferID", transferID.String())) - - ret, err := b.GetService().ReadTransferInitiation(ctx, transferID) - if err != nil { - otel.RecordError(span, err) - handleServiceErrors(w, r, err) - return - } - - data := &readTransferInitiationResponse{ - transferInitiationResponse: transferInitiationResponse{ - ID: ret.ID.String(), - Reference: ret.ID.Reference, - CreatedAt: ret.CreatedAt, - ScheduledAt: ret.ScheduledAt, - Description: ret.Description, - SourceAccountID: ret.SourceAccountID.String(), - DestinationAccountID: ret.DestinationAccountID.String(), - ConnectorID: ret.ConnectorID.String(), - Provider: ret.Provider.String(), - Type: ret.Type.String(), - Amount: ret.Amount, - InitialAmount: ret.InitialAmount, - Asset: ret.Asset.String(), - Metadata: ret.Metadata, - }, - } - - if len(ret.RelatedAdjustments) > 0 { - // Take the status and error from the last adjustment - data.Status = ret.RelatedAdjustments[0].Status.String() - data.Error = ret.RelatedAdjustments[0].Error - } - - for _, adjustments := range ret.RelatedAdjustments { - data.RelatedAdjustments = append(data.RelatedAdjustments, &transferInitiationAdjustmentsResponse{ - AdjustmentID: adjustments.ID.String(), - CreatedAt: adjustments.CreatedAt, - Status: adjustments.Status.String(), - Error: adjustments.Error, - Metadata: adjustments.Metadata, - }) - } - - for _, payments := range ret.RelatedPayments { - data.RelatedPayments = append(data.RelatedPayments, &transferInitiationPaymentsResponse{ - PaymentID: payments.PaymentID.String(), - CreatedAt: payments.CreatedAt, - Status: payments.Status.String(), - Error: payments.Error, - }) - } - - err = json.NewEncoder(w).Encode(api.BaseResponse[readTransferInitiationResponse]{ - Data: data, - }) - if err != nil { - otel.RecordError(span, err) - api.InternalServerError(w, r, err) - return - } - } -} - -func listTransferInitiationsHandler(b backend.Backend) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "listTransferInitiationsHandler") - defer span.End() - - w.Header().Set("Content-Type", "application/json") - - query, err := bunpaginate.Extract[storage.ListTransferInitiationsQuery](r, func() (*storage.ListTransferInitiationsQuery, error) { - options, err := getPagination(r, storage.TransferInitiationQuery{}) - if err != nil { - return nil, err - } - return pointer.For(storage.NewListTransferInitiationsQuery(*options)), nil - }) - if err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrValidation, err) - return - } - - cursor, err := b.GetService().ListTransferInitiations(ctx, *query) - if err != nil { - otel.RecordError(span, err) - handleServiceErrors(w, r, err) - return - } - - ret := cursor.Data - data := make([]*transferInitiationResponse, len(ret)) - for i := range ret { - ret[i].SortRelatedAdjustments() - data[i] = &transferInitiationResponse{ - ID: ret[i].ID.String(), - Reference: ret[i].ID.Reference, - CreatedAt: ret[i].CreatedAt, - ScheduledAt: ret[i].ScheduledAt, - Description: ret[i].Description, - SourceAccountID: ret[i].SourceAccountID.String(), - DestinationAccountID: ret[i].DestinationAccountID.String(), - Provider: ret[i].Provider.String(), - ConnectorID: ret[i].ConnectorID.String(), - Type: ret[i].Type.String(), - Amount: ret[i].Amount, - InitialAmount: ret[i].InitialAmount, - Asset: ret[i].Asset.String(), - Metadata: ret[i].Metadata, - } - - if len(ret[i].RelatedAdjustments) > 0 { - // Take the status and error from the last adjustment - data[i].Status = ret[i].RelatedAdjustments[0].Status.String() - data[i].Error = ret[i].RelatedAdjustments[0].Error - } - } - - err = json.NewEncoder(w).Encode(api.BaseResponse[*transferInitiationResponse]{ - Cursor: &bunpaginate.Cursor[*transferInitiationResponse]{ - PageSize: cursor.PageSize, - HasMore: cursor.HasMore, - Previous: cursor.Previous, - Next: cursor.Next, - Data: data, - }, - }) - if err != nil { - otel.RecordError(span, err) - api.InternalServerError(w, r, err) - return - } - } -} diff --git a/components/payments/cmd/api/internal/api/transfer_initiation_test.go b/components/payments/cmd/api/internal/api/transfer_initiation_test.go deleted file mode 100644 index 93fdf5bf34..0000000000 --- a/components/payments/cmd/api/internal/api/transfer_initiation_test.go +++ /dev/null @@ -1,677 +0,0 @@ -package api - -import ( - "errors" - "fmt" - "math/big" - "net/http" - "net/http/httptest" - "net/url" - "testing" - "time" - - "github.com/formancehq/go-libs/bun/bunpaginate" - - sharedapi "github.com/formancehq/go-libs/api" - "github.com/formancehq/go-libs/auth" - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/go-libs/query" - "github.com/formancehq/payments/cmd/api/internal/api/service" - "github.com/formancehq/payments/cmd/api/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" -) - -func TestListTransferInitiations(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - queryParams url.Values - pageSize int - expectedQuery storage.ListTransferInitiationsQuery - expectedStatusCode int - expectedErrorCode string - serviceError error - } - - testCases := []testCase{ - { - name: "nomimal", - expectedQuery: storage.NewListTransferInitiationsQuery( - storage.NewPaginatedQueryOptions(storage.TransferInitiationQuery{}). - WithPageSize(15), - ), - pageSize: 15, - }, - { - name: "page size too low", - queryParams: url.Values{ - "pageSize": {"0"}, - }, - expectedQuery: storage.NewListTransferInitiationsQuery( - storage.NewPaginatedQueryOptions(storage.TransferInitiationQuery{}). - WithPageSize(15), - ), - pageSize: 15, - }, - { - name: "page size too high", - queryParams: url.Values{ - "pageSize": {"100000"}, - }, - expectedQuery: storage.NewListTransferInitiationsQuery( - storage.NewPaginatedQueryOptions(storage.TransferInitiationQuery{}). - WithPageSize(100), - ), - pageSize: 100, - }, - { - name: "with invalid page size", - queryParams: url.Values{ - "pageSize": {"nan"}, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "invalid query builder json", - queryParams: url.Values{ - "query": {"invalid"}, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "valid query builder json", - queryParams: url.Values{ - "query": {"{\"$match\": {\"source_account_id\": \"acc1\"}}"}, - }, - expectedQuery: storage.NewListTransferInitiationsQuery( - storage.NewPaginatedQueryOptions(storage.TransferInitiationQuery{}). - WithPageSize(15). - WithQueryBuilder(query.Match("source_account_id", "acc1")), - ), - pageSize: 15, - }, - { - name: "valid sorter", - queryParams: url.Values{ - "sort": {"source_account_id:asc"}, - }, - expectedQuery: storage.NewListTransferInitiationsQuery( - storage.NewPaginatedQueryOptions(storage.TransferInitiationQuery{}). - WithPageSize(15). - WithSorter(storage.Sorter{}.Add("source_account_id", storage.SortOrderAsc)), - ), - pageSize: 15, - }, - { - name: "invalid sorter", - queryParams: url.Values{ - "sort": {"source_account_id:invalid"}, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "err validation from backend", - expectedQuery: storage.NewListTransferInitiationsQuery( - storage.NewPaginatedQueryOptions(storage.TransferInitiationQuery{}). - WithPageSize(15), - ), - serviceError: service.ErrValidation, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "ErrNotFound from storage", - expectedQuery: storage.NewListTransferInitiationsQuery( - storage.NewPaginatedQueryOptions(storage.TransferInitiationQuery{}). - WithPageSize(15), - ), - serviceError: storage.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - }, - { - name: "ErrDuplicateKeyValue from storage", - expectedQuery: storage.NewListTransferInitiationsQuery( - storage.NewPaginatedQueryOptions(storage.TransferInitiationQuery{}). - WithPageSize(15), - ), - serviceError: storage.ErrDuplicateKeyValue, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - }, - { - name: "other storage errors from storage", - expectedQuery: storage.NewListTransferInitiationsQuery( - storage.NewPaginatedQueryOptions(storage.TransferInitiationQuery{}). - WithPageSize(15), - ), - serviceError: errors.New("some error"), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusOK - } - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - - tfs := []models.TransferInitiation{ - { - ID: models.TransferInitiationID{ - Reference: "t1", - ConnectorID: connectorID, - }, - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - ScheduledAt: time.Date(2023, 11, 22, 8, 30, 0, 0, time.UTC), - Description: "test1", - Type: models.TransferInitiationTypePayout, - SourceAccountID: &models.AccountID{ - Reference: "acc1", - ConnectorID: connectorID, - }, - DestinationAccountID: models.AccountID{ - Reference: "acc2", - ConnectorID: connectorID, - }, - Provider: models.ConnectorProviderDummyPay, - ConnectorID: connectorID, - Amount: big.NewInt(100), - Asset: models.Asset("EUR/2"), - RelatedAdjustments: []*models.TransferInitiationAdjustment{ - { - ID: uuid.New(), - TransferInitiationID: models.TransferInitiationID{ - Reference: "t1", - ConnectorID: connectorID, - }, - CreatedAt: time.Date(2023, 11, 22, 8, 45, 0, 0, time.UTC), - Status: models.TransferInitiationStatusProcessed, - }, - { - ID: uuid.New(), - TransferInitiationID: models.TransferInitiationID{ - Reference: "t1", - ConnectorID: connectorID, - }, - CreatedAt: time.Date(2023, 11, 22, 8, 40, 0, 0, time.UTC), - Status: models.TransferInitiationStatusWaitingForValidation, - }, - }, - RelatedPayments: []*models.TransferInitiationPayment{ - { - TransferInitiationID: models.TransferInitiationID{ - Reference: "t1", - ConnectorID: connectorID, - }, - PaymentID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: "p1", - Type: models.PaymentTypePayIn, - }, - ConnectorID: connectorID, - }, - CreatedAt: time.Date(2023, 11, 22, 8, 30, 0, 0, time.UTC), - Status: models.TransferInitiationStatusProcessed, - Error: "", - }, - }, - Metadata: map[string]string{ - "foo": "bar", - }, - }, - { - ID: models.TransferInitiationID{ - Reference: "t2", - ConnectorID: connectorID, - }, - CreatedAt: time.Date(2023, 11, 22, 9, 0, 0, 0, time.UTC), - ScheduledAt: time.Date(2023, 11, 22, 9, 30, 0, 0, time.UTC), - Description: "test2", - Type: models.TransferInitiationTypeTransfer, - SourceAccountID: &models.AccountID{ - Reference: "acc3", - ConnectorID: connectorID, - }, - DestinationAccountID: models.AccountID{ - Reference: "acc4", - ConnectorID: connectorID, - }, - Provider: models.ConnectorProviderDummyPay, - ConnectorID: connectorID, - Amount: big.NewInt(2000), - Asset: models.Asset("USD/2"), - RelatedAdjustments: []*models.TransferInitiationAdjustment{ - { - ID: uuid.New(), - TransferInitiationID: models.TransferInitiationID{ - Reference: "t2", - ConnectorID: connectorID, - }, - CreatedAt: time.Date(2023, 11, 22, 9, 45, 0, 0, time.UTC), - Status: models.TransferInitiationStatusFailed, - Error: "error", - }, - { - ID: uuid.New(), - TransferInitiationID: models.TransferInitiationID{ - Reference: "t2", - ConnectorID: connectorID, - }, - CreatedAt: time.Date(2023, 11, 22, 9, 40, 0, 0, time.UTC), - Status: models.TransferInitiationStatusWaitingForValidation, - }, - }, - }, - } - listTFsResponse := &bunpaginate.Cursor[models.TransferInitiation]{ - PageSize: testCase.pageSize, - HasMore: false, - Previous: "", - Next: "", - Data: tfs, - } - - expectedTFsResponse := []*transferInitiationResponse{ - { - ID: tfs[0].ID.String(), - Reference: tfs[0].ID.Reference, - CreatedAt: tfs[0].CreatedAt, - ScheduledAt: tfs[0].ScheduledAt, - Description: tfs[0].Description, - SourceAccountID: tfs[0].SourceAccountID.String(), - DestinationAccountID: tfs[0].DestinationAccountID.String(), - Provider: tfs[0].Provider.String(), - Type: tfs[0].Type.String(), - Amount: tfs[0].Amount, - Asset: tfs[0].Asset.String(), - Status: models.TransferInitiationStatusProcessed.String(), - ConnectorID: tfs[0].ConnectorID.String(), - Error: "", - Metadata: tfs[0].Metadata, - }, - { - ID: tfs[1].ID.String(), - Reference: tfs[1].ID.Reference, - CreatedAt: tfs[1].CreatedAt, - ScheduledAt: tfs[1].ScheduledAt, - Description: tfs[1].Description, - SourceAccountID: tfs[1].SourceAccountID.String(), - DestinationAccountID: tfs[1].DestinationAccountID.String(), - Provider: tfs[1].Provider.String(), - Type: tfs[1].Type.String(), - Amount: tfs[1].Amount, - Asset: tfs[1].Asset.String(), - ConnectorID: tfs[1].ConnectorID.String(), - Status: models.TransferInitiationStatusFailed.String(), - Error: "error", - Metadata: tfs[1].Metadata, - }, - } - - backend, mockService := newTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockService.EXPECT(). - ListTransferInitiations(gomock.Any(), testCase.expectedQuery). - Return(listTFsResponse, nil) - } - if testCase.serviceError != nil { - mockService.EXPECT(). - ListTransferInitiations(gomock.Any(), testCase.expectedQuery). - Return(nil, testCase.serviceError) - } - - router := httpRouter(backend, logging.Testing(), sharedapi.ServiceInfo{ - Debug: testing.Verbose(), - }, auth.NewNoAuth(), false) - - req := httptest.NewRequest(http.MethodGet, "/transfer-initiations", nil) - rec := httptest.NewRecorder() - req.URL.RawQuery = testCase.queryParams.Encode() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - var resp sharedapi.BaseResponse[*transferInitiationResponse] - sharedapi.Decode(t, rec.Body, &resp) - require.Equal(t, expectedTFsResponse, resp.Cursor.Data) - require.Equal(t, listTFsResponse.PageSize, resp.Cursor.PageSize) - require.Equal(t, listTFsResponse.HasMore, resp.Cursor.HasMore) - require.Equal(t, listTFsResponse.Next, resp.Cursor.Next) - require.Equal(t, listTFsResponse.Previous, resp.Cursor.Previous) - } else { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } -} - -func TestGetTransferInitiation(t *testing.T) { - t.Parallel() - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - tfID1 := models.TransferInitiationID{ - Reference: "t1", - ConnectorID: connectorID, - } - tfID2 := models.TransferInitiationID{ - Reference: "t2", - ConnectorID: connectorID, - } - - type testCase struct { - name string - tfID string - expectedTFID models.TransferInitiationID - expectedStatusCode int - expectedErrorCode string - serviceError error - } - - testCases := []testCase{ - { - name: "nomimal acc1", - tfID: tfID1.String(), - expectedTFID: tfID1, - }, - { - name: "nomimal acc2", - tfID: tfID2.String(), - expectedTFID: tfID2, - }, - { - name: "invalid tf ID", - tfID: "invalid", - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrInvalidID, - }, - { - name: "err validation from backend", - tfID: tfID1.String(), - expectedTFID: tfID1, - serviceError: service.ErrValidation, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "ErrNotFound from storage", - tfID: tfID1.String(), - expectedTFID: tfID1, - serviceError: storage.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - }, - { - name: "ErrDuplicateKeyValue from storage", - tfID: tfID1.String(), - expectedTFID: tfID1, - serviceError: storage.ErrDuplicateKeyValue, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - }, - { - name: "other storage errors from storage", - tfID: tfID1.String(), - expectedTFID: tfID1, - serviceError: errors.New("some error"), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusOK - } - - var getTransferInitiationResponse *models.TransferInitiation - var expectedTransferInitiationResponse *readTransferInitiationResponse - if testCase.expectedTFID == tfID1 { - getTransferInitiationResponse = &models.TransferInitiation{ - ID: models.TransferInitiationID{ - Reference: "t1", - ConnectorID: connectorID, - }, - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - ScheduledAt: time.Date(2023, 11, 22, 8, 30, 0, 0, time.UTC), - Description: "test1", - Type: models.TransferInitiationTypePayout, - SourceAccountID: &models.AccountID{ - Reference: "acc1", - ConnectorID: connectorID, - }, - DestinationAccountID: models.AccountID{ - Reference: "acc2", - ConnectorID: connectorID, - }, - Provider: models.ConnectorProviderDummyPay, - ConnectorID: connectorID, - Amount: big.NewInt(100), - Asset: models.Asset("EUR/2"), - RelatedAdjustments: []*models.TransferInitiationAdjustment{ - { - ID: uuid.New(), - TransferInitiationID: models.TransferInitiationID{ - Reference: "t1", - ConnectorID: connectorID, - }, - CreatedAt: time.Date(2023, 11, 22, 8, 45, 0, 0, time.UTC), - Status: models.TransferInitiationStatusProcessed, - }, - { - ID: uuid.New(), - TransferInitiationID: models.TransferInitiationID{ - Reference: "t1", - ConnectorID: connectorID, - }, - CreatedAt: time.Date(2023, 11, 22, 8, 40, 0, 0, time.UTC), - Status: models.TransferInitiationStatusWaitingForValidation, - }, - }, - RelatedPayments: []*models.TransferInitiationPayment{ - { - TransferInitiationID: models.TransferInitiationID{ - Reference: "t1", - ConnectorID: connectorID, - }, - PaymentID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: "p1", - Type: models.PaymentTypePayIn, - }, - ConnectorID: connectorID, - }, - CreatedAt: time.Date(2023, 11, 22, 8, 30, 0, 0, time.UTC), - Status: models.TransferInitiationStatusProcessed, - Error: "", - }, - }, - Metadata: map[string]string{ - "foo": "bar", - }, - } - - expectedTransferInitiationResponse = &readTransferInitiationResponse{ - transferInitiationResponse: transferInitiationResponse{ - ID: getTransferInitiationResponse.ID.String(), - Reference: getTransferInitiationResponse.ID.Reference, - CreatedAt: getTransferInitiationResponse.CreatedAt, - ScheduledAt: getTransferInitiationResponse.ScheduledAt, - Description: getTransferInitiationResponse.Description, - SourceAccountID: getTransferInitiationResponse.SourceAccountID.String(), - DestinationAccountID: getTransferInitiationResponse.DestinationAccountID.String(), - Provider: getTransferInitiationResponse.Provider.String(), - Type: getTransferInitiationResponse.Type.String(), - Amount: getTransferInitiationResponse.Amount, - ConnectorID: getTransferInitiationResponse.ConnectorID.String(), - Asset: getTransferInitiationResponse.Asset.String(), - Status: models.TransferInitiationStatusProcessed.String(), - Error: "", - Metadata: getTransferInitiationResponse.Metadata, - }, - RelatedPayments: []*transferInitiationPaymentsResponse{ - { - PaymentID: getTransferInitiationResponse.RelatedPayments[0].PaymentID.String(), - CreatedAt: getTransferInitiationResponse.RelatedPayments[0].CreatedAt, - Status: getTransferInitiationResponse.RelatedPayments[0].Status.String(), - Error: getTransferInitiationResponse.RelatedPayments[0].Error, - }, - }, - RelatedAdjustments: []*transferInitiationAdjustmentsResponse{ - { - AdjustmentID: getTransferInitiationResponse.RelatedAdjustments[0].ID.String(), - CreatedAt: getTransferInitiationResponse.RelatedAdjustments[0].CreatedAt, - Status: getTransferInitiationResponse.RelatedAdjustments[0].Status.String(), - Error: getTransferInitiationResponse.RelatedAdjustments[0].Error, - Metadata: getTransferInitiationResponse.RelatedAdjustments[0].Metadata, - }, - { - AdjustmentID: getTransferInitiationResponse.RelatedAdjustments[1].ID.String(), - CreatedAt: getTransferInitiationResponse.RelatedAdjustments[1].CreatedAt, - Status: getTransferInitiationResponse.RelatedAdjustments[1].Status.String(), - Error: getTransferInitiationResponse.RelatedAdjustments[1].Error, - Metadata: getTransferInitiationResponse.RelatedAdjustments[1].Metadata, - }, - }, - } - } else { - getTransferInitiationResponse = &models.TransferInitiation{ - ID: models.TransferInitiationID{ - Reference: "t2", - ConnectorID: connectorID, - }, - CreatedAt: time.Date(2023, 11, 22, 9, 0, 0, 0, time.UTC), - ScheduledAt: time.Date(2023, 11, 22, 9, 30, 0, 0, time.UTC), - Description: "test2", - Type: models.TransferInitiationTypeTransfer, - SourceAccountID: &models.AccountID{ - Reference: "acc3", - ConnectorID: connectorID, - }, - DestinationAccountID: models.AccountID{ - Reference: "acc4", - ConnectorID: connectorID, - }, - Provider: models.ConnectorProviderDummyPay, - ConnectorID: connectorID, - Amount: big.NewInt(2000), - Asset: models.Asset("USD/2"), - RelatedAdjustments: []*models.TransferInitiationAdjustment{ - { - ID: uuid.New(), - TransferInitiationID: models.TransferInitiationID{ - Reference: "t2", - ConnectorID: connectorID, - }, - CreatedAt: time.Date(2023, 11, 22, 9, 45, 0, 0, time.UTC), - Status: models.TransferInitiationStatusFailed, - Error: "error", - }, - { - ID: uuid.New(), - TransferInitiationID: models.TransferInitiationID{ - Reference: "t2", - ConnectorID: connectorID, - }, - CreatedAt: time.Date(2023, 11, 22, 9, 40, 0, 0, time.UTC), - Status: models.TransferInitiationStatusWaitingForValidation, - }, - }, - } - expectedTransferInitiationResponse = &readTransferInitiationResponse{ - transferInitiationResponse: transferInitiationResponse{ - ID: getTransferInitiationResponse.ID.String(), - Reference: getTransferInitiationResponse.ID.Reference, - CreatedAt: getTransferInitiationResponse.CreatedAt, - ScheduledAt: getTransferInitiationResponse.ScheduledAt, - Description: getTransferInitiationResponse.Description, - SourceAccountID: getTransferInitiationResponse.SourceAccountID.String(), - DestinationAccountID: getTransferInitiationResponse.DestinationAccountID.String(), - Provider: getTransferInitiationResponse.Provider.String(), - Type: getTransferInitiationResponse.Type.String(), - Amount: getTransferInitiationResponse.Amount, - ConnectorID: getTransferInitiationResponse.ConnectorID.String(), - Asset: getTransferInitiationResponse.Asset.String(), - Status: models.TransferInitiationStatusFailed.String(), - Error: "error", - Metadata: getTransferInitiationResponse.Metadata, - }, - RelatedAdjustments: []*transferInitiationAdjustmentsResponse{ - { - AdjustmentID: getTransferInitiationResponse.RelatedAdjustments[0].ID.String(), - CreatedAt: getTransferInitiationResponse.RelatedAdjustments[0].CreatedAt, - Status: getTransferInitiationResponse.RelatedAdjustments[0].Status.String(), - Error: getTransferInitiationResponse.RelatedAdjustments[0].Error, - Metadata: getTransferInitiationResponse.RelatedAdjustments[0].Metadata, - }, - { - AdjustmentID: getTransferInitiationResponse.RelatedAdjustments[1].ID.String(), - CreatedAt: getTransferInitiationResponse.RelatedAdjustments[1].CreatedAt, - Status: getTransferInitiationResponse.RelatedAdjustments[1].Status.String(), - Error: getTransferInitiationResponse.RelatedAdjustments[1].Error, - Metadata: getTransferInitiationResponse.RelatedAdjustments[1].Metadata, - }, - }, - } - } - - backend, mockService := newTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockService.EXPECT(). - ReadTransferInitiation(gomock.Any(), testCase.expectedTFID). - Return(getTransferInitiationResponse, nil) - } - if testCase.serviceError != nil { - mockService.EXPECT(). - ReadTransferInitiation(gomock.Any(), testCase.expectedTFID). - Return(nil, testCase.serviceError) - } - - router := httpRouter(backend, logging.Testing(), sharedapi.ServiceInfo{ - Debug: testing.Verbose(), - }, auth.NewNoAuth(), false) - - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/transfer-initiations/%s", testCase.tfID), nil) - rec := httptest.NewRecorder() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - var resp sharedapi.BaseResponse[readTransferInitiationResponse] - sharedapi.Decode(t, rec.Body, &resp) - require.Equal(t, expectedTransferInitiationResponse, resp.Data) - } else { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } -} diff --git a/components/payments/cmd/api/internal/api/utils.go b/components/payments/cmd/api/internal/api/utils.go deleted file mode 100644 index 86d06917c0..0000000000 --- a/components/payments/cmd/api/internal/api/utils.go +++ /dev/null @@ -1,76 +0,0 @@ -package api - -import ( - "io" - "net/http" - "strings" - - "github.com/formancehq/go-libs/bun/bunpaginate" - "github.com/formancehq/go-libs/pointer" - "github.com/formancehq/go-libs/query" - "github.com/formancehq/payments/cmd/api/internal/storage" - "github.com/pkg/errors" -) - -func getQueryBuilder(r *http.Request) (query.Builder, error) { - data, err := io.ReadAll(r.Body) - if err != nil { - return nil, err - } - - if len(data) > 0 { - return query.ParseJSON(string(data)) - } else { - // In order to be backward compatible - return query.ParseJSON(r.URL.Query().Get("query")) - } -} - -func getSorter(r *http.Request) (storage.Sorter, error) { - var sorter storage.Sorter - - if sortParams := r.URL.Query()["sort"]; sortParams != nil { - for _, s := range sortParams { - parts := strings.SplitN(s, ":", 2) - - var order storage.SortOrder - - if len(parts) > 1 { - //nolint:goconst // allow duplicate string - switch parts[1] { - case "asc", "ASC": - order = storage.SortOrderAsc - case "dsc", "desc", "DSC", "DESC": - order = storage.SortOrderDesc - default: - return sorter, errors.New("sort order not well specified, got " + parts[1]) - } - } - - column := parts[0] - - sorter = sorter.Add(column, order) - } - } - - return sorter, nil -} - -func getPagination[T any](r *http.Request, options T) (*storage.PaginatedQueryOptions[T], error) { - qb, err := getQueryBuilder(r) - if err != nil { - return nil, err - } - - sorter, err := getSorter(r) - if err != nil { - return nil, err - } - - pageSize, err := bunpaginate.GetPageSize(r) - if err != nil { - return nil, err - } - - return pointer.For(storage.NewPaginatedQueryOptions(options).WithQueryBuilder(qb).WithSorter(sorter).WithPageSize(pageSize)), nil -} diff --git a/components/payments/cmd/api/internal/storage/accounts.go b/components/payments/cmd/api/internal/storage/accounts.go deleted file mode 100644 index 8a99efbc03..0000000000 --- a/components/payments/cmd/api/internal/storage/accounts.go +++ /dev/null @@ -1,114 +0,0 @@ -package storage - -import ( - "context" - "fmt" - - "github.com/formancehq/go-libs/bun/bunpaginate" - "github.com/formancehq/go-libs/query" - "github.com/formancehq/payments/internal/models" - "github.com/pkg/errors" - "github.com/uptrace/bun" -) - -type AccountQuery struct{} - -type ListAccountsQuery bunpaginate.OffsetPaginatedQuery[PaginatedQueryOptions[AccountQuery]] - -func NewListAccountsQuery(opts PaginatedQueryOptions[AccountQuery]) ListAccountsQuery { - return ListAccountsQuery{ - PageSize: opts.PageSize, - Order: bunpaginate.OrderAsc, - Options: opts, - } -} - -func (s *Storage) accountQueryContext(qb query.Builder) (string, []any, error) { - return qb.Build(query.ContextFn(func(key, operator string, value any) (string, []any, error) { - switch { - case key == "reference": - return fmt.Sprintf("%s %s ?", key, query.DefaultComparisonOperatorsMapping[operator]), []any{value}, nil - case metadataRegex.Match([]byte(key)): - if operator != "$match" { - return "", nil, errors.Wrap(ErrValidation, "'metadata' column can only be used with $match") - } - match := metadataRegex.FindAllStringSubmatch(key, 3) - - key := "metadata" - return key + " @> ?", []any{map[string]any{ - match[0][1]: value, - }}, nil - default: - return "", nil, errors.Wrap(ErrValidation, fmt.Sprintf("unknown key '%s' when building query", key)) - } - })) -} - -func (s *Storage) ListAccounts(ctx context.Context, q ListAccountsQuery) (*bunpaginate.Cursor[models.Account], error) { - var ( - where string - args []any - err error - ) - if q.Options.QueryBuilder != nil { - where, args, err = s.accountQueryContext(q.Options.QueryBuilder) - if err != nil { - return nil, err - } - } - - return PaginateWithOffset[PaginatedQueryOptions[AccountQuery], models.Account](s, ctx, - (*bunpaginate.OffsetPaginatedQuery[PaginatedQueryOptions[AccountQuery]])(&q), - func(query *bun.SelectQuery) *bun.SelectQuery { - query = query.Relation("PoolAccounts") - - if where != "" { - query = query.Where(where, args...) - } - - if q.Options.Sorter != nil { - query = q.Options.Sorter.Apply(query) - } else { - query = query.Order("created_at DESC") - } - - return query - }, - ) -} - -func (s *Storage) GetAccount(ctx context.Context, id string) (*models.Account, error) { - var account models.Account - - err := s.db.NewSelect(). - Model(&account). - Relation("PoolAccounts"). - Where("account.id = ?", id). - Scan(ctx) - if err != nil { - return nil, e("failed to get account", err) - } - - return &account, nil -} - -func (s *Storage) UpsertAccounts(ctx context.Context, accounts []*models.Account) error { - if len(accounts) == 0 { - return nil - } - - _, err := s.db.NewInsert(). - Model(&accounts). - On("CONFLICT (id) DO UPDATE"). - Set("connector_id = EXCLUDED.connector_id"). - Set("raw_data = EXCLUDED.raw_data"). - Set("default_currency = EXCLUDED.default_currency"). - Set("account_name = EXCLUDED.account_name"). - Set("metadata = EXCLUDED.metadata"). - Exec(ctx) - if err != nil { - return e("failed to create accounts", err) - } - - return nil -} diff --git a/components/payments/cmd/api/internal/storage/accounts_test.go b/components/payments/cmd/api/internal/storage/accounts_test.go deleted file mode 100644 index 3b8bb25bfb..0000000000 --- a/components/payments/cmd/api/internal/storage/accounts_test.go +++ /dev/null @@ -1,266 +0,0 @@ -package storage - -import ( - "context" - "testing" - "time" - - "github.com/formancehq/go-libs/bun/bunpaginate" - "github.com/formancehq/go-libs/query" - "github.com/formancehq/payments/internal/models" - "github.com/stretchr/testify/require" -) - -func insertAccounts(t *testing.T, store *Storage, connectorID models.ConnectorID) []models.AccountID { - id1 := models.AccountID{ - Reference: "test_account", - ConnectorID: connectorID, - } - acc1 := models.Account{ - ID: id1, - ConnectorID: connectorID, - CreatedAt: time.Date(2023, 11, 14, 8, 0, 0, 0, time.UTC), - Reference: "test_account", - AccountName: "test", - Type: models.AccountTypeInternal, - Metadata: map[string]string{"foo": "bar"}, - } - - _, err := store.DB().NewInsert(). - Model(&acc1). - Exec(context.Background()) - require.NoError(t, err) - - id2 := models.AccountID{ - Reference: "test_account2", - ConnectorID: connectorID, - } - acc2 := models.Account{ - ID: id2, - ConnectorID: connectorID, - CreatedAt: time.Date(2023, 11, 14, 9, 0, 0, 0, time.UTC), - Reference: "test_account2", - AccountName: "test2", - Type: models.AccountTypeInternal, - Metadata: map[string]string{ - "foo": "bar", - "foo2": "bar2", - }, - } - - _, err = store.DB().NewInsert(). - Model(&acc2). - Exec(context.Background()) - require.NoError(t, err) - - return []models.AccountID{id1, id2} -} - -func TestListAccounts(t *testing.T) { - t.Parallel() - - store := newStore(t) - - connectorID := installConnector(t, store) - insertAccounts(t, store, connectorID) - - acc1 := models.Account{ - ID: models.AccountID{ - Reference: "test_account", - ConnectorID: connectorID, - }, - ConnectorID: connectorID, - CreatedAt: time.Date(2023, 11, 14, 8, 0, 0, 0, time.UTC), - Reference: "test_account", - AccountName: "test", - Type: models.AccountTypeInternal, - Metadata: map[string]string{ - "foo": "bar", - }, - } - - acc2 := models.Account{ - ID: models.AccountID{ - Reference: "test_account2", - ConnectorID: connectorID, - }, - ConnectorID: connectorID, - CreatedAt: time.Date(2023, 11, 14, 9, 0, 0, 0, time.UTC), - Reference: "test_account2", - AccountName: "test2", - Type: models.AccountTypeInternal, - Metadata: map[string]string{ - "foo": "bar", - "foo2": "bar2", - }, - } - - t.Run("list all accounts with page size 1", func(t *testing.T) { - t.Parallel() - - cursor, err := store.ListAccounts( - context.Background(), - NewListAccountsQuery(NewPaginatedQueryOptions(AccountQuery{}).WithPageSize(1)), - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 1) - require.True(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - require.Equal(t, acc2, cursor.Data[0]) - - var query ListAccountsQuery - err = bunpaginate.UnmarshalCursor(cursor.Next, &query) - require.NoError(t, err) - cursor, err = store.ListAccounts( - context.Background(), - query, - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 1) - require.False(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - require.Equal(t, acc1, cursor.Data[0]) - - err = bunpaginate.UnmarshalCursor(cursor.Previous, &query) - require.NoError(t, err) - cursor, err = store.ListAccounts( - context.Background(), - query, - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 1) - require.True(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - require.Equal(t, acc2, cursor.Data[0]) - }) - - t.Run("list all accounts with page size 2", func(t *testing.T) { - t.Parallel() - - cursor, err := store.ListAccounts( - context.Background(), - NewListAccountsQuery(NewPaginatedQueryOptions(AccountQuery{}).WithPageSize(2)), - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 2) - require.False(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - cursor.Data[1].CreatedAt = cursor.Data[1].CreatedAt.UTC() - require.Equal(t, acc2, cursor.Data[0]) - require.Equal(t, acc1, cursor.Data[1]) - }) - - t.Run("list all accounts with page size > number of accounts", func(t *testing.T) { - t.Parallel() - - cursor, err := store.ListAccounts( - context.Background(), - NewListAccountsQuery(NewPaginatedQueryOptions(AccountQuery{}).WithPageSize(10)), - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 2) - require.False(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - cursor.Data[1].CreatedAt = cursor.Data[1].CreatedAt.UTC() - require.Equal(t, acc2, cursor.Data[0]) - require.Equal(t, acc1, cursor.Data[1]) - }) - - t.Run("list all accounts with reference", func(t *testing.T) { - t.Parallel() - - cursor, err := store.ListAccounts( - context.Background(), - NewListAccountsQuery(NewPaginatedQueryOptions(AccountQuery{}). - WithPageSize(10). - WithQueryBuilder(query.Match("reference", "test_account")), - ), - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 1) - require.False(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - require.Equal(t, acc1, cursor.Data[0]) - }) - - t.Run("list all accounts with unknown reference", func(t *testing.T) { - t.Parallel() - - cursor, err := store.ListAccounts( - context.Background(), - NewListAccountsQuery(NewPaginatedQueryOptions(AccountQuery{}). - WithPageSize(10). - WithQueryBuilder(query.Match("reference", "unknown")), - ), - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 0) - require.False(t, cursor.HasMore) - }) - - t.Run("list all accounts with metadata (1)", func(t *testing.T) { - t.Parallel() - - cursor, err := store.ListAccounts( - context.Background(), - NewListAccountsQuery(NewPaginatedQueryOptions(AccountQuery{}). - WithPageSize(10). - WithQueryBuilder(query.Match("metadata[foo]", "bar")), - ), - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 2) - require.False(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - cursor.Data[1].CreatedAt = cursor.Data[1].CreatedAt.UTC() - require.Equal(t, acc2, cursor.Data[0]) - require.Equal(t, acc1, cursor.Data[1]) - }) - - t.Run("list all accounts with metadata (2)", func(t *testing.T) { - t.Parallel() - - cursor, err := store.ListAccounts( - context.Background(), - NewListAccountsQuery(NewPaginatedQueryOptions(AccountQuery{}). - WithPageSize(10). - WithQueryBuilder(query.Match("metadata[foo2]", "bar2")), - ), - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 1) - require.False(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - require.Equal(t, acc2, cursor.Data[0]) - }) - - t.Run("list all accounts with unknown metadata key", func(t *testing.T) { - t.Parallel() - - cursor, err := store.ListAccounts( - context.Background(), - NewListAccountsQuery(NewPaginatedQueryOptions(AccountQuery{}). - WithPageSize(10). - WithQueryBuilder(query.Match("metadata[unknown]", "bar")), - ), - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 0) - require.False(t, cursor.HasMore) - }) - - t.Run("list all accounts with unknown metadata value", func(t *testing.T) { - t.Parallel() - - cursor, err := store.ListAccounts( - context.Background(), - NewListAccountsQuery(NewPaginatedQueryOptions(AccountQuery{}). - WithPageSize(10). - WithQueryBuilder(query.Match("metadata[foo]", "unknown")), - ), - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 0) - require.False(t, cursor.HasMore) - }) -} diff --git a/components/payments/cmd/api/internal/storage/balances.go b/components/payments/cmd/api/internal/storage/balances.go deleted file mode 100644 index 501851efde..0000000000 --- a/components/payments/cmd/api/internal/storage/balances.go +++ /dev/null @@ -1,150 +0,0 @@ -package storage - -import ( - "context" - "fmt" - "time" - - "github.com/formancehq/go-libs/bun/bunpaginate" - "github.com/formancehq/payments/internal/models" - "github.com/pkg/errors" - "github.com/uptrace/bun" -) - -type BalanceQuery struct { - AccountID *models.AccountID - Currency string - From time.Time - To time.Time -} - -func NewBalanceQuery() BalanceQuery { - return BalanceQuery{} -} - -func (b BalanceQuery) WithAccountID(accountID *models.AccountID) BalanceQuery { - b.AccountID = accountID - - return b -} - -func (b BalanceQuery) WithCurrency(currency string) BalanceQuery { - b.Currency = currency - - return b -} - -func (b BalanceQuery) WithFrom(from time.Time) BalanceQuery { - b.From = from - - return b -} - -func (b BalanceQuery) WithTo(to time.Time) BalanceQuery { - b.To = to - - return b -} - -func applyBalanceQuery(query *bun.SelectQuery, balanceQuery BalanceQuery) *bun.SelectQuery { - if balanceQuery.AccountID != nil { - query = query.Where("balance.account_id = ?", balanceQuery.AccountID) - } - - if balanceQuery.Currency != "" { - query = query.Where("balance.currency = ?", balanceQuery.Currency) - } - - if !balanceQuery.From.IsZero() { - query = query.Where("balance.last_updated_at >= ?", balanceQuery.From) - } - - if !balanceQuery.To.IsZero() { - query = query.Where("(balance.created_at <= ?)", balanceQuery.To) - } - - return query -} - -type ListBalancesQuery bunpaginate.OffsetPaginatedQuery[PaginatedQueryOptions[BalanceQuery]] - -func NewListBalancesQuery(opts PaginatedQueryOptions[BalanceQuery]) ListBalancesQuery { - return ListBalancesQuery{ - Order: bunpaginate.OrderAsc, - PageSize: opts.PageSize, - Options: opts, - } -} - -func (s *Storage) ListBalances(ctx context.Context, q ListBalancesQuery) (*bunpaginate.Cursor[models.Balance], error) { - return PaginateWithOffset[PaginatedQueryOptions[BalanceQuery], models.Balance](s, ctx, - (*bunpaginate.OffsetPaginatedQuery[PaginatedQueryOptions[BalanceQuery]])(&q), - func(query *bun.SelectQuery) *bun.SelectQuery { - query = applyBalanceQuery(query, q.Options.Options) - - if q.Options.Sorter != nil { - query = q.Options.Sorter.Apply(query) - } else { - query = query.Order("created_at DESC") - } - - return query - }, - ) -} - -func (s *Storage) ListBalanceCurrencies(ctx context.Context, accountID models.AccountID) ([]string, error) { - var currencies []string - - err := s.db.NewSelect(). - ColumnExpr("DISTINCT currency"). - Model(&models.Balance{}). - Where("account_id = ?", accountID). - Scan(ctx, ¤cies) - if err != nil { - return nil, e("failed to list balance currencies", err) - } - - return currencies, nil -} - -func (s *Storage) GetBalanceAtByCurrency(ctx context.Context, accountID models.AccountID, currency string, at time.Time) (*models.Balance, error) { - var balance models.Balance - - err := s.db.NewSelect(). - Model(&balance). - Where("account_id = ?", accountID). - Where("currency = ?", currency). - Where("created_at <= ?", at). - Where("last_updated_at >= ?", at). - Order("last_updated_at DESC"). - Limit(1). - Scan(ctx) - if err != nil { - return nil, e("failed to get balance", err) - } - - return &balance, nil -} - -func (s *Storage) GetBalancesAt(ctx context.Context, accountID models.AccountID, at time.Time) ([]*models.Balance, error) { - currencies, err := s.ListBalanceCurrencies(ctx, accountID) - if err != nil { - return nil, fmt.Errorf("failed to list balance currencies: %w", err) - } - - var balances []*models.Balance - for _, currency := range currencies { - balance, err := s.GetBalanceAtByCurrency(ctx, accountID, currency, at) - if err != nil { - if errors.Is(err, ErrNotFound) { - continue - } - return nil, fmt.Errorf("failed to get balance: %w", err) - } - - balances = append(balances, balance) - } - - return balances, nil -} diff --git a/components/payments/cmd/api/internal/storage/balances_test.go b/components/payments/cmd/api/internal/storage/balances_test.go deleted file mode 100644 index 6930901af8..0000000000 --- a/components/payments/cmd/api/internal/storage/balances_test.go +++ /dev/null @@ -1,365 +0,0 @@ -package storage - -import ( - "context" - "math/big" - "testing" - "time" - - "github.com/formancehq/go-libs/bun/bunpaginate" - "github.com/formancehq/payments/internal/models" - "github.com/stretchr/testify/require" -) - -func insertBalances(t *testing.T, store *Storage, accountID models.AccountID) []models.Balance { - b1 := models.Balance{ - AccountID: accountID, - Asset: "EUR/2", - Balance: big.NewInt(100), - CreatedAt: time.Date(2023, 11, 14, 10, 0, 0, 0, time.UTC), - LastUpdatedAt: time.Date(2023, 11, 14, 11, 0, 0, 0, time.UTC), - } - - b2 := models.Balance{ - AccountID: accountID, - Asset: "EUR/2", - Balance: big.NewInt(200), - CreatedAt: time.Date(2023, 11, 14, 11, 0, 0, 0, time.UTC), - LastUpdatedAt: time.Date(2023, 11, 14, 11, 30, 0, 0, time.UTC), - } - - b3 := models.Balance{ - AccountID: accountID, - Asset: "EUR/2", - Balance: big.NewInt(150), - CreatedAt: time.Date(2023, 11, 14, 11, 30, 0, 0, time.UTC), - LastUpdatedAt: time.Date(2023, 11, 14, 11, 45, 0, 0, time.UTC), - } - - b4 := models.Balance{ - AccountID: accountID, - Asset: "USD/2", - Balance: big.NewInt(1000), - CreatedAt: time.Date(2023, 11, 14, 10, 30, 0, 0, time.UTC), - LastUpdatedAt: time.Date(2023, 11, 14, 12, 0, 0, 0, time.UTC), - } - - balances := []models.Balance{b1, b2, b3, b4} - _, err := store.DB().NewInsert(). - Model(&balances). - Exec(context.Background()) - require.NoError(t, err) - - return balances -} - -func TestListBalances(t *testing.T) { - t.Parallel() - - store := newStore(t) - - connectorID := installConnector(t, store) - accounts := insertAccounts(t, store, connectorID) - balancesPerAccountAndAssets := make(map[string]map[string][]models.Balance) - for _, account := range accounts { - if balancesPerAccountAndAssets[account.String()] == nil { - balancesPerAccountAndAssets[account.String()] = make(map[string][]models.Balance) - } - - balances := insertBalances(t, store, account) - for _, balance := range balances { - balancesPerAccountAndAssets[account.String()][balance.Asset.String()] = append(balancesPerAccountAndAssets[account.String()][balance.Asset.String()], balance) - } - } - - t.Run("list all balances with page size 1", func(t *testing.T) { - t.Parallel() - - cursor, err := store.ListBalances( - context.Background(), - NewListBalancesQuery(NewPaginatedQueryOptions(NewBalanceQuery().WithAccountID(&accounts[0])).WithPageSize(1)), - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 1) - require.True(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - cursor.Data[0].LastUpdatedAt = cursor.Data[0].LastUpdatedAt.UTC() - require.Equal(t, balancesPerAccountAndAssets[accounts[0].String()]["EUR/2"][2], cursor.Data[0]) - - var query ListBalancesQuery - err = bunpaginate.UnmarshalCursor(cursor.Next, &query) - require.NoError(t, err) - cursor, err = store.ListBalances( - context.Background(), - query, - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 1) - require.True(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - cursor.Data[0].LastUpdatedAt = cursor.Data[0].LastUpdatedAt.UTC() - require.Equal(t, balancesPerAccountAndAssets[accounts[0].String()]["EUR/2"][1], cursor.Data[0]) - - err = bunpaginate.UnmarshalCursor(cursor.Next, &query) - require.NoError(t, err) - cursor, err = store.ListBalances( - context.Background(), - query, - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 1) - require.True(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - cursor.Data[0].LastUpdatedAt = cursor.Data[0].LastUpdatedAt.UTC() - require.Equal(t, balancesPerAccountAndAssets[accounts[0].String()]["USD/2"][0], cursor.Data[0]) - - err = bunpaginate.UnmarshalCursor(cursor.Next, &query) - require.NoError(t, err) - cursor, err = store.ListBalances( - context.Background(), - query, - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 1) - require.False(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - cursor.Data[0].LastUpdatedAt = cursor.Data[0].LastUpdatedAt.UTC() - require.Equal(t, balancesPerAccountAndAssets[accounts[0].String()]["EUR/2"][0], cursor.Data[0]) - - err = bunpaginate.UnmarshalCursor(cursor.Previous, &query) - require.NoError(t, err) - cursor, err = store.ListBalances( - context.Background(), - query, - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 1) - require.True(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - cursor.Data[0].LastUpdatedAt = cursor.Data[0].LastUpdatedAt.UTC() - require.Equal(t, balancesPerAccountAndAssets[accounts[0].String()]["USD/2"][0], cursor.Data[0]) - }) - - t.Run("list all balances with page size 2", func(t *testing.T) { - t.Parallel() - - cursor, err := store.ListBalances( - context.Background(), - NewListBalancesQuery(NewPaginatedQueryOptions(NewBalanceQuery().WithAccountID(&accounts[0])).WithPageSize(2)), - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 2) - require.True(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - cursor.Data[1].CreatedAt = cursor.Data[1].CreatedAt.UTC() - cursor.Data[0].LastUpdatedAt = cursor.Data[0].LastUpdatedAt.UTC() - cursor.Data[1].LastUpdatedAt = cursor.Data[1].LastUpdatedAt.UTC() - require.Equal(t, balancesPerAccountAndAssets[accounts[0].String()]["EUR/2"][2], cursor.Data[0]) - require.Equal(t, balancesPerAccountAndAssets[accounts[0].String()]["EUR/2"][1], cursor.Data[1]) - - var query ListBalancesQuery - err = bunpaginate.UnmarshalCursor(cursor.Next, &query) - require.NoError(t, err) - cursor, err = store.ListBalances( - context.Background(), - query, - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 2) - require.False(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - cursor.Data[1].CreatedAt = cursor.Data[1].CreatedAt.UTC() - cursor.Data[0].LastUpdatedAt = cursor.Data[0].LastUpdatedAt.UTC() - cursor.Data[1].LastUpdatedAt = cursor.Data[1].LastUpdatedAt.UTC() - require.Equal(t, balancesPerAccountAndAssets[accounts[0].String()]["USD/2"][0], cursor.Data[0]) - require.Equal(t, balancesPerAccountAndAssets[accounts[0].String()]["EUR/2"][0], cursor.Data[1]) - - err = bunpaginate.UnmarshalCursor(cursor.Previous, &query) - require.NoError(t, err) - cursor, err = store.ListBalances( - context.Background(), - query, - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 2) - require.True(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - cursor.Data[1].CreatedAt = cursor.Data[1].CreatedAt.UTC() - cursor.Data[0].LastUpdatedAt = cursor.Data[0].LastUpdatedAt.UTC() - cursor.Data[1].LastUpdatedAt = cursor.Data[1].LastUpdatedAt.UTC() - require.Equal(t, balancesPerAccountAndAssets[accounts[0].String()]["EUR/2"][2], cursor.Data[0]) - require.Equal(t, balancesPerAccountAndAssets[accounts[0].String()]["EUR/2"][1], cursor.Data[1]) - }) - - t.Run("list balances for asset", func(t *testing.T) { - t.Parallel() - - cursor, err := store.ListBalances( - context.Background(), - NewListBalancesQuery(NewPaginatedQueryOptions(NewBalanceQuery().WithAccountID(&accounts[0]).WithCurrency("USD/2")).WithPageSize(15)), - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 1) - require.False(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - cursor.Data[0].LastUpdatedAt = cursor.Data[0].LastUpdatedAt.UTC() - require.Equal(t, balancesPerAccountAndAssets[accounts[0].String()]["USD/2"][0], cursor.Data[0]) - }) - - t.Run("list balances for asset and limit", func(t *testing.T) { - t.Parallel() - - cursor, err := store.ListBalances( - context.Background(), - NewListBalancesQuery( - NewPaginatedQueryOptions( - NewBalanceQuery(). - WithAccountID(&accounts[0]). - WithCurrency("EUR/2"), - ). - WithPageSize(1), - ), - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 1) - require.True(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - cursor.Data[0].LastUpdatedAt = cursor.Data[0].LastUpdatedAt.UTC() - require.Equal(t, balancesPerAccountAndAssets[accounts[0].String()]["EUR/2"][2], cursor.Data[0]) - }) - - t.Run("list balances for asset and time range", func(t *testing.T) { - t.Parallel() - - cursor, err := store.ListBalances( - context.Background(), - NewListBalancesQuery( - NewPaginatedQueryOptions( - NewBalanceQuery(). - WithAccountID(&accounts[0]). - WithFrom(time.Date(2023, 11, 14, 10, 15, 0, 0, time.UTC)). - WithTo(time.Date(2023, 11, 14, 11, 15, 0, 0, time.UTC)), - ). - WithPageSize(15), - ), - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 3) - require.False(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - cursor.Data[0].LastUpdatedAt = cursor.Data[0].LastUpdatedAt.UTC() - cursor.Data[1].CreatedAt = cursor.Data[1].CreatedAt.UTC() - cursor.Data[1].LastUpdatedAt = cursor.Data[1].LastUpdatedAt.UTC() - cursor.Data[2].CreatedAt = cursor.Data[2].CreatedAt.UTC() - cursor.Data[2].LastUpdatedAt = cursor.Data[2].LastUpdatedAt.UTC() - require.Equal(t, balancesPerAccountAndAssets[accounts[0].String()]["EUR/2"][1], cursor.Data[0]) - require.Equal(t, balancesPerAccountAndAssets[accounts[0].String()]["USD/2"][0], cursor.Data[1]) - require.Equal(t, balancesPerAccountAndAssets[accounts[0].String()]["EUR/2"][0], cursor.Data[2]) - }) - - t.Run("get balances at a precise time", func(t *testing.T) { - t.Parallel() - - cursor, err := store.ListBalances( - context.Background(), - NewListBalancesQuery( - NewPaginatedQueryOptions( - NewBalanceQuery(). - WithAccountID(&accounts[0]). - WithCurrency("EUR/2"). - WithTo(time.Date(2023, 11, 14, 11, 15, 0, 0, time.UTC)), - ).WithPageSize(1), - ), - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 1) - require.True(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - cursor.Data[0].LastUpdatedAt = cursor.Data[0].LastUpdatedAt.UTC() - require.Equal(t, balancesPerAccountAndAssets[accounts[0].String()]["EUR/2"][1], cursor.Data[0]) - - cursor, err = store.ListBalances( - context.Background(), - NewListBalancesQuery( - NewPaginatedQueryOptions( - NewBalanceQuery(). - WithAccountID(&accounts[0]). - WithCurrency("EUR/2"). - WithTo(time.Date(2023, 11, 14, 9, 0, 0, 0, time.UTC)), - ).WithPageSize(1), - ), - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 0) - require.False(t, cursor.HasMore) - }) -} - -func TestGetBalanceAt(t *testing.T) { - t.Parallel() - - store := newStore(t) - - connectorID := installConnector(t, store) - accounts := insertAccounts(t, store, connectorID) - balancesPerAccountAndAssets := make(map[string]map[string][]models.Balance) - for _, account := range accounts { - if balancesPerAccountAndAssets[account.String()] == nil { - balancesPerAccountAndAssets[account.String()] = make(map[string][]models.Balance) - } - - balances := insertBalances(t, store, account) - for _, balance := range balances { - balancesPerAccountAndAssets[account.String()][balance.Asset.String()] = append(balancesPerAccountAndAssets[account.String()][balance.Asset.String()], balance) - } - } - - // Should have only one EUR/2 balance of 100 - t.Run("get balance at 10:15", func(t *testing.T) { - balances, err := store.GetBalancesAt(context.Background(), accounts[0], time.Date(2023, 11, 14, 10, 15, 0, 0, time.UTC)) - require.NoError(t, err) - require.Len(t, balances, 1) - balances[0].CreatedAt = balances[0].CreatedAt.UTC() - balances[0].LastUpdatedAt = balances[0].LastUpdatedAt.UTC() - require.Equal(t, &balancesPerAccountAndAssets[accounts[0].String()]["EUR/2"][0], balances[0]) - require.Equal(t, big.NewInt(100), balances[0].Balance) - }) - - t.Run("get balance at 11:15", func(t *testing.T) { - balances, err := store.GetBalancesAt(context.Background(), accounts[0], time.Date(2023, 11, 14, 11, 15, 0, 0, time.UTC)) - require.NoError(t, err) - require.Len(t, balances, 2) - balances[0].CreatedAt = balances[0].CreatedAt.UTC() - balances[0].LastUpdatedAt = balances[0].LastUpdatedAt.UTC() - require.Equal(t, &balancesPerAccountAndAssets[accounts[0].String()]["EUR/2"][1], balances[0]) - require.Equal(t, big.NewInt(200), balances[0].Balance) - balances[1].CreatedAt = balances[1].CreatedAt.UTC() - balances[1].LastUpdatedAt = balances[1].LastUpdatedAt.UTC() - require.Equal(t, &balancesPerAccountAndAssets[accounts[0].String()]["USD/2"][0], balances[1]) - require.Equal(t, big.NewInt(1000), balances[1].Balance) - }) - - t.Run("get balance at 11:45", func(t *testing.T) { - balances, err := store.GetBalancesAt(context.Background(), accounts[0], time.Date(2023, 11, 14, 11, 45, 0, 0, time.UTC)) - require.NoError(t, err) - require.Len(t, balances, 2) - balances[0].CreatedAt = balances[0].CreatedAt.UTC() - balances[0].LastUpdatedAt = balances[0].LastUpdatedAt.UTC() - require.Equal(t, &balancesPerAccountAndAssets[accounts[0].String()]["EUR/2"][2], balances[0]) - require.Equal(t, big.NewInt(150), balances[0].Balance) - balances[1].CreatedAt = balances[1].CreatedAt.UTC() - balances[1].LastUpdatedAt = balances[1].LastUpdatedAt.UTC() - require.Equal(t, &balancesPerAccountAndAssets[accounts[0].String()]["USD/2"][0], balances[1]) - require.Equal(t, big.NewInt(1000), balances[1].Balance) - }) - - t.Run("get balance at 12:00", func(t *testing.T) { - balances, err := store.GetBalancesAt(context.Background(), accounts[0], time.Date(2023, 11, 14, 12, 0, 0, 0, time.UTC)) - require.NoError(t, err) - require.Len(t, balances, 1) - balances[0].CreatedAt = balances[0].CreatedAt.UTC() - balances[0].LastUpdatedAt = balances[0].LastUpdatedAt.UTC() - require.Equal(t, &balancesPerAccountAndAssets[accounts[0].String()]["USD/2"][0], balances[0]) - require.Equal(t, big.NewInt(1000), balances[0].Balance) - }) -} diff --git a/components/payments/cmd/api/internal/storage/bank_accounts.go b/components/payments/cmd/api/internal/storage/bank_accounts.go deleted file mode 100644 index a523ee86fb..0000000000 --- a/components/payments/cmd/api/internal/storage/bank_accounts.go +++ /dev/null @@ -1,63 +0,0 @@ -package storage - -import ( - "context" - - "github.com/formancehq/go-libs/bun/bunpaginate" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/uptrace/bun" -) - -type BankAccountQuery struct{} - -type ListBankAccountQuery bunpaginate.OffsetPaginatedQuery[PaginatedQueryOptions[BankAccountQuery]] - -func NewListBankAccountQuery(opts PaginatedQueryOptions[BankAccountQuery]) ListBankAccountQuery { - return ListBankAccountQuery{ - PageSize: opts.PageSize, - Order: bunpaginate.OrderAsc, - Options: opts, - } -} - -func (s *Storage) ListBankAccounts(ctx context.Context, q ListBankAccountQuery) (*bunpaginate.Cursor[models.BankAccount], error) { - return PaginateWithOffset[PaginatedQueryOptions[BankAccountQuery], models.BankAccount](s, ctx, - (*bunpaginate.OffsetPaginatedQuery[PaginatedQueryOptions[BankAccountQuery]])(&q), - func(query *bun.SelectQuery) *bun.SelectQuery { - query = query. - Relation("RelatedAccounts") - - if q.Options.Sorter != nil { - query = q.Options.Sorter.Apply(query) - } else { - query = query.Order("created_at DESC") - } - - return query - }, - ) -} - -func (s *Storage) GetBankAccount(ctx context.Context, id uuid.UUID, expand bool) (*models.BankAccount, error) { - var account models.BankAccount - query := s.db.NewSelect(). - Model(&account). - Column("id", "created_at", "name", "created_at", "country", "metadata"). - Relation("RelatedAccounts") - - if expand { - query = query.ColumnExpr("pgp_sym_decrypt(account_number, ?, ?) AS decrypted_account_number", s.configEncryptionKey, encryptionOptions). - ColumnExpr("pgp_sym_decrypt(iban, ?, ?) AS decrypted_iban", s.configEncryptionKey, encryptionOptions). - ColumnExpr("pgp_sym_decrypt(swift_bic_code, ?, ?) AS decrypted_swift_bic_code", s.configEncryptionKey, encryptionOptions) - } - - err := query. - Where("id = ?", id). - Scan(ctx) - if err != nil { - return nil, e("get bank account", err) - } - - return &account, nil -} diff --git a/components/payments/cmd/api/internal/storage/bank_accounts_test.go b/components/payments/cmd/api/internal/storage/bank_accounts_test.go deleted file mode 100644 index c2e1b2ac77..0000000000 --- a/components/payments/cmd/api/internal/storage/bank_accounts_test.go +++ /dev/null @@ -1,132 +0,0 @@ -package storage - -import ( - "context" - "testing" - "time" - - "github.com/formancehq/go-libs/bun/bunpaginate" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/stretchr/testify/require" -) - -func insertBankAccounts(t *testing.T, store *Storage, connectorID models.ConnectorID) []models.BankAccount { - acc1 := models.BankAccount{ - ID: uuid.New(), - CreatedAt: time.Date(2023, 11, 14, 8, 0, 0, 0, time.UTC), - Name: "test_1", - IBAN: "FR7630006000011234567890189", - Country: "FR", - Metadata: map[string]string{ - "foo": "bar", - }, - } - _, err := store.DB().NewInsert(). - Model(&acc1). - Exec(context.Background()) - require.NoError(t, err) - - acc2 := models.BankAccount{ - ID: uuid.New(), - CreatedAt: time.Date(2023, 11, 14, 9, 0, 0, 0, time.UTC), - Name: "test_2", - IBAN: "FR7630006000011234567891234", - Country: "GB", - Metadata: map[string]string{ - "foo2": "bar2", - }, - } - _, err = store.DB().NewInsert(). - Model(&acc2). - Exec(context.Background()) - require.NoError(t, err) - - return []models.BankAccount{acc1, acc2} -} - -func TestListBankAccounts(t *testing.T) { - t.Parallel() - - store := newStore(t) - - connectorID := installConnector(t, store) - bankAccounts := insertBankAccounts(t, store, connectorID) - - for i := range bankAccounts { - bankAccounts[i].CreatedAt = bankAccounts[i].CreatedAt.UTC() - // The listing of bank accounts does not sent the IBAN info - bankAccounts[i].IBAN = "" - } - - t.Run("list all bank accounts with page size 1", func(t *testing.T) { - t.Parallel() - - cursor, err := store.ListBankAccounts( - context.Background(), - NewListBankAccountQuery(NewPaginatedQueryOptions(BankAccountQuery{}).WithPageSize(1)), - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 1) - require.True(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - require.Equal(t, bankAccounts[1], cursor.Data[0]) - - var query ListBankAccountQuery - err = bunpaginate.UnmarshalCursor(cursor.Next, &query) - require.NoError(t, err) - cursor, err = store.ListBankAccounts( - context.Background(), - query, - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 1) - require.False(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - require.Equal(t, bankAccounts[0], cursor.Data[0]) - - err = bunpaginate.UnmarshalCursor(cursor.Previous, &query) - require.NoError(t, err) - cursor, err = store.ListBankAccounts( - context.Background(), - query, - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 1) - require.True(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - require.Equal(t, bankAccounts[1], cursor.Data[0]) - }) - - t.Run("list all bank accounts with page size 2", func(t *testing.T) { - t.Parallel() - - cursor, err := store.ListBankAccounts( - context.Background(), - NewListBankAccountQuery(NewPaginatedQueryOptions(BankAccountQuery{}).WithPageSize(2)), - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 2) - require.False(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - cursor.Data[1].CreatedAt = cursor.Data[1].CreatedAt.UTC() - require.Equal(t, bankAccounts[1], cursor.Data[0]) - require.Equal(t, bankAccounts[0], cursor.Data[1]) - }) - - t.Run("list all bank accounts with page size > number of bank accounts", func(t *testing.T) { - t.Parallel() - - cursor, err := store.ListBankAccounts( - context.Background(), - NewListBankAccountQuery(NewPaginatedQueryOptions(BankAccountQuery{}).WithPageSize(10)), - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 2) - require.False(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - cursor.Data[1].CreatedAt = cursor.Data[1].CreatedAt.UTC() - require.Equal(t, bankAccounts[1], cursor.Data[0]) - require.Equal(t, bankAccounts[0], cursor.Data[1]) - }) -} diff --git a/components/payments/cmd/api/internal/storage/connectors.go b/components/payments/cmd/api/internal/storage/connectors.go deleted file mode 100644 index bc38ea4a4a..0000000000 --- a/components/payments/cmd/api/internal/storage/connectors.go +++ /dev/null @@ -1,19 +0,0 @@ -package storage - -import ( - "context" - - "github.com/formancehq/payments/internal/models" -) - -func (s *Storage) IsConnectorInstalledByConnectorID(ctx context.Context, connectorID models.ConnectorID) (bool, error) { - exists, err := s.db.NewSelect(). - Model(&models.Connector{}). - Where("id = ?", connectorID). - Exists(ctx) - if err != nil { - return false, e("find connector", err) - } - - return exists, nil -} diff --git a/components/payments/cmd/api/internal/storage/connectors_test.go b/components/payments/cmd/api/internal/storage/connectors_test.go deleted file mode 100644 index f2e9a7ef1e..0000000000 --- a/components/payments/cmd/api/internal/storage/connectors_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package storage - -import ( - "context" - "encoding/json" - "testing" - "time" - - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/stretchr/testify/require" -) - -const testEncryptionOptions = "compress-algo=1, cipher-algo=aes256" -const encryptionKey = "test" - -// Helpers to add test data -func installConnector(t *testing.T, store *Storage) models.ConnectorID { - db := store.DB() - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - connector := &models.Connector{ - ID: connectorID, - Name: "test_connector", - CreatedAt: time.Date(2023, 11, 13, 0, 0, 0, 0, time.UTC), - Provider: models.ConnectorProviderDummyPay, - } - - _, err := db.NewInsert().Model(connector).Exec(context.Background()) - require.NoError(t, err) - - _, err = db.NewUpdate(). - Model(&models.Connector{}). - Set("config = pgp_sym_encrypt(?::TEXT, ?, ?)", json.RawMessage(`{}`), encryptionKey, testEncryptionOptions). - Where("id = ?", connectorID). // Connector name is unique - Exec(context.Background()) - require.NoError(t, err) - - return connectorID -} diff --git a/components/payments/cmd/api/internal/storage/main_test.go b/components/payments/cmd/api/internal/storage/main_test.go deleted file mode 100644 index 29828d318c..0000000000 --- a/components/payments/cmd/api/internal/storage/main_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package storage - -import ( - "context" - "crypto/rand" - "testing" - - "github.com/formancehq/go-libs/testing/docker" - "github.com/formancehq/go-libs/testing/utils" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/go-libs/testing/platform/pgtesting" - migrationstorage "github.com/formancehq/payments/internal/storage" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/stdlib" - "github.com/stretchr/testify/require" - "github.com/uptrace/bun" - "github.com/uptrace/bun/dialect/pgdialect" -) - -var ( - srv *pgtesting.PostgresServer -) - -func TestMain(m *testing.M) { - utils.WithTestMain(func(t *utils.TestingTForMain) int { - srv = pgtesting.CreatePostgresServer(t, docker.NewPool(t, logging.Testing())) - - return m.Run() - }) -} - -func newStore(t *testing.T) *Storage { - t.Helper() - - pgServer := srv.NewDatabase(t) - - config, err := pgx.ParseConfig(pgServer.ConnString()) - require.NoError(t, err) - - key := make([]byte, 64) - _, err = rand.Read(key) - require.NoError(t, err) - - db := bun.NewDB(stdlib.OpenDB(*config), pgdialect.New()) - t.Cleanup(func() { - _ = db.Close() - }) - - err = migrationstorage.Migrate(context.Background(), db) - require.NoError(t, err) - - store := NewStorage( - db, - string(key), - ) - - return store -} diff --git a/components/payments/cmd/api/internal/storage/metadata.go b/components/payments/cmd/api/internal/storage/metadata.go deleted file mode 100644 index 7625655816..0000000000 --- a/components/payments/cmd/api/internal/storage/metadata.go +++ /dev/null @@ -1,39 +0,0 @@ -package storage - -import ( - "context" - "time" - - "github.com/formancehq/payments/internal/models" -) - -func (s *Storage) UpdatePaymentMetadata(ctx context.Context, paymentID models.PaymentID, metadata map[string]string) error { - var metadataToInsert []models.PaymentMetadata // nolint:prealloc // it's against a map - - for key, value := range metadata { - metadataToInsert = append(metadataToInsert, models.PaymentMetadata{ - PaymentID: paymentID, - Key: key, - Value: value, - Changelog: []models.MetadataChangelog{ - { - CreatedAt: time.Now(), - Value: value, - }, - }, - }) - } - - _, err := s.db.NewInsert(). - Model(&metadataToInsert). - On("CONFLICT (payment_id, key) DO UPDATE"). - Set("value = EXCLUDED.value"). - Set("changelog = payment_metadata.changelog || EXCLUDED.changelog"). - Where("payment_metadata.value != EXCLUDED.value"). - Exec(ctx) - if err != nil { - return e("failed to update payment metadata", err) - } - - return nil -} diff --git a/components/payments/cmd/api/internal/storage/module.go b/components/payments/cmd/api/internal/storage/module.go deleted file mode 100644 index e000fdff85..0000000000 --- a/components/payments/cmd/api/internal/storage/module.go +++ /dev/null @@ -1,30 +0,0 @@ -package storage - -import ( - "context" - - "github.com/formancehq/go-libs/bun/bunconnect" - "github.com/formancehq/go-libs/logging" - "github.com/uptrace/bun" - "go.uber.org/fx" -) - -func Module(connectionOptions bunconnect.ConnectionOptions, configEncryptionKey string, debug bool) fx.Option { - return fx.Options( - bunconnect.Module(connectionOptions, debug), - fx.Provide(func(db *bun.DB) *Storage { - return NewStorage(db, configEncryptionKey) - }), - fx.Invoke(func(lc fx.Lifecycle, repo *Storage) { - lc.Append(fx.Hook{ - OnStart: func(ctx context.Context) error { - logging.FromContext(ctx).Debug("Ping database...") - - // TODO: Check migrations state and panic if migrations are not applied - - return nil - }, - }) - }), - ) -} diff --git a/components/payments/cmd/api/internal/storage/paginate.go b/components/payments/cmd/api/internal/storage/paginate.go deleted file mode 100644 index d8853fc5e9..0000000000 --- a/components/payments/cmd/api/internal/storage/paginate.go +++ /dev/null @@ -1,77 +0,0 @@ -package storage - -import ( - "context" - "encoding/json" - - "github.com/formancehq/go-libs/bun/bunpaginate" - "github.com/formancehq/go-libs/query" - "github.com/uptrace/bun" -) - -type PaginatedQueryOptions[T any] struct { - QueryBuilder query.Builder `json:"qb"` - Sorter Sorter - PageSize uint64 `json:"pageSize"` - Options T `json:"options"` -} - -func (v *PaginatedQueryOptions[T]) UnmarshalJSON(data []byte) error { - type aux struct { - QueryBuilder json.RawMessage `json:"qb"` - Sorter Sorter `json:"Sorter"` - PageSize uint64 `json:"pageSize"` - Options T `json:"options"` - } - x := &aux{} - if err := json.Unmarshal(data, x); err != nil { - return err - } - - *v = PaginatedQueryOptions[T]{ - PageSize: x.PageSize, - Options: x.Options, - Sorter: x.Sorter, - } - - var err error - if x.QueryBuilder != nil { - v.QueryBuilder, err = query.ParseJSON(string(x.QueryBuilder)) - if err != nil { - return err - } - } - - return nil -} - -func (opts PaginatedQueryOptions[T]) WithQueryBuilder(qb query.Builder) PaginatedQueryOptions[T] { - opts.QueryBuilder = qb - - return opts -} - -func (opts PaginatedQueryOptions[T]) WithSorter(sorter Sorter) PaginatedQueryOptions[T] { - opts.Sorter = sorter - - return opts -} - -func (opts PaginatedQueryOptions[T]) WithPageSize(pageSize uint64) PaginatedQueryOptions[T] { - opts.PageSize = pageSize - - return opts -} - -func NewPaginatedQueryOptions[T any](options T) PaginatedQueryOptions[T] { - return PaginatedQueryOptions[T]{ - Options: options, - PageSize: bunpaginate.QueryDefaultPageSize, - } -} - -func PaginateWithOffset[FILTERS any, RETURN any](s *Storage, ctx context.Context, - q *bunpaginate.OffsetPaginatedQuery[FILTERS], builders ...func(query *bun.SelectQuery) *bun.SelectQuery) (*bunpaginate.Cursor[RETURN], error) { - query := s.db.NewSelect() - return bunpaginate.UsingOffset[FILTERS, RETURN](ctx, query, *q, builders...) -} diff --git a/components/payments/cmd/api/internal/storage/payments.go b/components/payments/cmd/api/internal/storage/payments.go deleted file mode 100644 index 4a3b0df99c..0000000000 --- a/components/payments/cmd/api/internal/storage/payments.go +++ /dev/null @@ -1,213 +0,0 @@ -package storage - -import ( - "context" - "fmt" - "time" - - "github.com/formancehq/go-libs/bun/bunpaginate" - "github.com/formancehq/go-libs/query" - "github.com/formancehq/payments/internal/models" - "github.com/pkg/errors" - "github.com/uptrace/bun" -) - -type PaymentQuery struct{} - -type ListPaymentsQuery bunpaginate.OffsetPaginatedQuery[PaginatedQueryOptions[PaymentQuery]] - -func NewListPaymentsQuery(opts PaginatedQueryOptions[PaymentQuery]) ListPaymentsQuery { - return ListPaymentsQuery{ - PageSize: opts.PageSize, - Order: bunpaginate.OrderAsc, - Options: opts, - } -} - -func (s *Storage) paymentsQueryContext(qb query.Builder) (map[string]string, string, []any, error) { - metadata := make(map[string]string) - - where, args, err := qb.Build(query.ContextFn(func(key, operator string, value any) (string, []any, error) { - switch { - case key == "reference": - return fmt.Sprintf("%s %s ?", key, query.DefaultComparisonOperatorsMapping[operator]), []any{value}, nil - - case key == "type", - key == "status", - key == "asset": - if operator != "$match" { - return "", nil, errors.Wrap(ErrValidation, "'type' column can only be used with $match") - } - return fmt.Sprintf("%s = ?", key), []any{value}, nil - - case key == "connectorID": - if operator != "$match" { - return "", nil, errors.Wrap(ErrValidation, "'type' column can only be used with $match") - } - return "connector_id = ?", []any{value}, nil - - case key == "amount": - return fmt.Sprintf("%s %s ?", key, query.DefaultComparisonOperatorsMapping[operator]), []any{value}, nil - - case key == "initialAmount": - return fmt.Sprintf("initial_amount %s ?", query.DefaultComparisonOperatorsMapping[operator]), []any{value}, nil - - case metadataRegex.Match([]byte(key)): - if operator != "$match" { - return "", nil, errors.Wrap(ErrValidation, "'metadata' column can only be used with $match") - } - match := metadataRegex.FindAllStringSubmatch(key, 3) - - valueString, ok := value.(string) - if !ok { - return "", nil, errors.Wrap(ErrValidation, fmt.Sprintf("metadata value must be a string, got %T", value)) - } - - metadata[match[0][1]] = valueString - - // Do nothing here, as we don't want to add this to the query - return "", nil, nil - - default: - return "", nil, errors.Wrap(ErrValidation, fmt.Sprintf("unknown key '%s' when building query", key)) - } - })) - - return metadata, where, args, err -} - -func (s *Storage) ListPayments(ctx context.Context, q ListPaymentsQuery) (*bunpaginate.Cursor[models.Payment], error) { - var ( - metadata map[string]string - where string - args []any - err error - ) - if q.Options.QueryBuilder != nil { - metadata, where, args, err = s.paymentsQueryContext(q.Options.QueryBuilder) - if err != nil { - return nil, err - } - } - - return PaginateWithOffset[PaginatedQueryOptions[PaymentQuery], models.Payment](s, ctx, - (*bunpaginate.OffsetPaginatedQuery[PaginatedQueryOptions[PaymentQuery]])(&q), - func(query *bun.SelectQuery) *bun.SelectQuery { - query = query. - Relation("Metadata"). - Relation("Connector"). - Relation("Adjustments") - - if where != "" { - query = query.Where(where, args...) - } - - if len(metadata) > 0 { - metadataQuery := s.db.NewSelect().Model((*models.PaymentMetadata)(nil)) - for key, value := range metadata { - metadataQuery = metadataQuery.Where("payment_metadata.key = ? AND payment_metadata.value = ?", key, value) - } - query = query.With("_metadata", metadataQuery) - query = query.Where("payment.id IN (SELECT payment_id FROM _metadata)") - } - - if q.Options.Sorter != nil { - query = q.Options.Sorter.Apply(query) - } else { - query = query.Order("created_at DESC") - } - - return query - }, - ) -} - -func (s *Storage) GetPayment(ctx context.Context, id string) (*models.Payment, error) { - var payment models.Payment - - err := s.db.NewSelect(). - Model(&payment). - Relation("Connector"). - Relation("Metadata"). - Relation("Adjustments"). - Where("payment.id = ?", id). - Scan(ctx) - if err != nil { - return nil, e(fmt.Sprintf("failed to get payment %s", id), err) - } - - return &payment, nil -} - -func (s *Storage) UpsertPayments(ctx context.Context, payments []*models.Payment) error { - if len(payments) == 0 { - return nil - } - - _, err := s.db.NewInsert(). - Model(&payments). - On("CONFLICT (reference) DO UPDATE"). - Set("amount = EXCLUDED.amount"). - Set("type = EXCLUDED.type"). - Set("status = EXCLUDED.status"). - Set("raw_data = EXCLUDED.raw_data"). - Set("scheme = EXCLUDED.scheme"). - Set("asset = EXCLUDED.asset"). - Set("source_account_id = EXCLUDED.source_account_id"). - Set("destination_account_id = EXCLUDED.destination_account_id"). - Exec(ctx) - if err != nil { - return e("failed to create payments", err) - } - - var adjustments []*models.PaymentAdjustment - var metadata []*models.PaymentMetadata - - for i := range payments { - for _, adjustment := range payments[i].Adjustments { - if adjustment.Reference == "" { - continue - } - - adjustment.PaymentID = payments[i].ID - - adjustments = append(adjustments, adjustment) - } - - for _, data := range payments[i].Metadata { - data.PaymentID = payments[i].ID - data.Changelog = append(data.Changelog, - models.MetadataChangelog{ - CreatedAt: time.Now(), - Value: data.Value, - }) - - metadata = append(metadata, data) - } - } - - if len(adjustments) > 0 { - _, err = s.db.NewInsert(). - Model(&adjustments). - On("CONFLICT (reference) DO NOTHING"). - Exec(ctx) - if err != nil { - return e("failed to create adjustments", err) - } - } - - if len(metadata) > 0 { - _, err = s.db.NewInsert(). - Model(&metadata). - On("CONFLICT (payment_id, key) DO UPDATE"). - Set("value = EXCLUDED.value"). - Set("changelog = metadata.changelog || EXCLUDED.changelog"). - Where("metadata.value != EXCLUDED.value"). - Exec(ctx) - if err != nil { - return e("failed to create metadata", err) - } - } - - return nil -} diff --git a/components/payments/cmd/api/internal/storage/payments_test.go b/components/payments/cmd/api/internal/storage/payments_test.go deleted file mode 100644 index 997094c024..0000000000 --- a/components/payments/cmd/api/internal/storage/payments_test.go +++ /dev/null @@ -1,166 +0,0 @@ -package storage - -import ( - "context" - "math/big" - "testing" - "time" - - "github.com/formancehq/go-libs/bun/bunpaginate" - "github.com/formancehq/payments/internal/models" - "github.com/stretchr/testify/require" -) - -func insertPayments(t *testing.T, store *Storage, connectorID models.ConnectorID) []models.Payment { - p1 := models.Payment{ - ID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: "test_1", - Type: models.PaymentTypePayIn, - }, - ConnectorID: connectorID, - }, - ConnectorID: connectorID, - CreatedAt: time.Date(2023, 11, 14, 8, 0, 0, 0, time.UTC), - Reference: "test_1", - Amount: big.NewInt(100), - InitialAmount: big.NewInt(100), - Type: models.PaymentTypePayIn, - Status: models.PaymentStatusPending, - Scheme: models.PaymentSchemeA2A, - Asset: models.Asset("USD/2"), - SourceAccountID: &models.AccountID{ - Reference: "test_account", - ConnectorID: connectorID, - }, - DestinationAccountID: &models.AccountID{ - Reference: "test_account2", - ConnectorID: connectorID, - }, - } - _, err := store.DB().NewInsert(). - Model(&p1). - Exec(context.Background()) - require.NoError(t, err) - - p2 := models.Payment{ - ID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: "test_2", - Type: models.PaymentTypePayOut, - }, - ConnectorID: connectorID, - }, - ConnectorID: connectorID, - CreatedAt: time.Date(2023, 11, 14, 9, 0, 0, 0, time.UTC), - Reference: "test_2", - Amount: big.NewInt(200), - InitialAmount: big.NewInt(100), - Type: models.PaymentTypePayOut, - Status: models.PaymentStatusPending, - Scheme: models.PaymentSchemeA2A, - Asset: models.Asset("EUR/2"), - SourceAccountID: &models.AccountID{ - Reference: "test_account", - ConnectorID: connectorID, - }, - DestinationAccountID: &models.AccountID{ - Reference: "test_account2", - ConnectorID: connectorID, - }, - } - _, err = store.DB().NewInsert(). - Model(&p2). - Exec(context.Background()) - require.NoError(t, err) - - return []models.Payment{p1, p2} -} - -func TestListPayments(t *testing.T) { - t.Parallel() - - store := newStore(t) - - connectorID := installConnector(t, store) - insertAccounts(t, store, connectorID) - payments := insertPayments(t, store, connectorID) - - t.Run("list all payments with page size 1", func(t *testing.T) { - t.Parallel() - - cursor, err := store.ListPayments( - context.Background(), - NewListPaymentsQuery(NewPaginatedQueryOptions(PaymentQuery{}).WithPageSize(1)), - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 1) - require.True(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - cursor.Data[0].Connector = nil - require.Equal(t, payments[1], cursor.Data[0]) - - var query ListPaymentsQuery - err = bunpaginate.UnmarshalCursor(cursor.Next, &query) - require.NoError(t, err) - cursor, err = store.ListPayments( - context.Background(), - query, - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 1) - require.False(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - cursor.Data[0].Connector = nil - require.Equal(t, payments[0], cursor.Data[0]) - - err = bunpaginate.UnmarshalCursor(cursor.Previous, &query) - require.NoError(t, err) - cursor, err = store.ListPayments( - context.Background(), - query, - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 1) - require.True(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - cursor.Data[0].Connector = nil - require.Equal(t, payments[1], cursor.Data[0]) - }) - - t.Run("list all payments with page size 2", func(t *testing.T) { - t.Parallel() - - cursor, err := store.ListPayments( - context.Background(), - NewListPaymentsQuery(NewPaginatedQueryOptions(PaymentQuery{}).WithPageSize(2)), - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 2) - require.False(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - cursor.Data[0].Connector = nil - cursor.Data[1].CreatedAt = cursor.Data[1].CreatedAt.UTC() - cursor.Data[1].Connector = nil - require.Equal(t, payments[1], cursor.Data[0]) - require.Equal(t, payments[0], cursor.Data[1]) - }) - - t.Run("list all payments with page size > number of payments", func(t *testing.T) { - t.Parallel() - - cursor, err := store.ListPayments( - context.Background(), - NewListPaymentsQuery(NewPaginatedQueryOptions(PaymentQuery{}).WithPageSize(10)), - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 2) - require.False(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - cursor.Data[0].Connector = nil - cursor.Data[1].CreatedAt = cursor.Data[1].CreatedAt.UTC() - cursor.Data[1].Connector = nil - require.Equal(t, payments[1], cursor.Data[0]) - require.Equal(t, payments[0], cursor.Data[1]) - }) -} diff --git a/components/payments/cmd/api/internal/storage/ping.go b/components/payments/cmd/api/internal/storage/ping.go deleted file mode 100644 index 2832abb0b1..0000000000 --- a/components/payments/cmd/api/internal/storage/ping.go +++ /dev/null @@ -1,5 +0,0 @@ -package storage - -func (s *Storage) Ping() error { - return s.db.Ping() -} diff --git a/components/payments/cmd/api/internal/storage/pools.go b/components/payments/cmd/api/internal/storage/pools.go deleted file mode 100644 index d9827f9c2c..0000000000 --- a/components/payments/cmd/api/internal/storage/pools.go +++ /dev/null @@ -1,117 +0,0 @@ -package storage - -import ( - "context" - - "github.com/formancehq/go-libs/bun/bunpaginate" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/uptrace/bun" -) - -func (s *Storage) CreatePool(ctx context.Context, pool *models.Pool) error { - var id uuid.UUID - err := s.db.NewInsert(). - Model(pool). - Returning("id"). - Scan(ctx, &id) - if err != nil { - return e("failed to create pool", err) - } - pool.ID = id - - return nil -} - -func (s *Storage) AddAccountsToPool(ctx context.Context, poolAccounts []*models.PoolAccounts) error { - _, err := s.db.NewInsert(). - Model(&poolAccounts). - Exec(ctx) - if err != nil { - return e("failed to add accounts to pool", err) - } - - return nil -} - -func (s *Storage) AddAccountToPool(ctx context.Context, poolAccount *models.PoolAccounts) error { - _, err := s.db.NewInsert(). - Model(poolAccount). - Exec(ctx) - if err != nil { - return e("failed to add account to pool", err) - } - - return nil -} - -func (s *Storage) RemoveAccountFromPool(ctx context.Context, poolAccount *models.PoolAccounts) error { - _, err := s.db.NewDelete(). - Model(poolAccount). - Where("pool_id = ?", poolAccount.PoolID). - Where("account_id = ?", poolAccount.AccountID). - Exec(ctx) - if err != nil { - return e("failed to remove account from pool", err) - } - - return nil -} - -type PoolQuery struct{} - -type ListPoolsQuery bunpaginate.OffsetPaginatedQuery[PaginatedQueryOptions[PoolQuery]] - -func NewListPoolsQuery(opts PaginatedQueryOptions[PoolQuery]) ListPoolsQuery { - return ListPoolsQuery{ - PageSize: opts.PageSize, - Order: bunpaginate.OrderAsc, - Options: opts, - } -} - -func (s *Storage) ListPools(ctx context.Context, q ListPoolsQuery) (*bunpaginate.Cursor[models.Pool], error) { - cursor, err := PaginateWithOffset[PaginatedQueryOptions[PoolQuery], models.Pool](s, ctx, - (*bunpaginate.OffsetPaginatedQuery[PaginatedQueryOptions[PoolQuery]])(&q), - func(query *bun.SelectQuery) *bun.SelectQuery { - query = query. - Relation("PoolAccounts") - - if q.Options.Sorter != nil { - query = q.Options.Sorter.Apply(query) - } else { - query = query.Order("created_at DESC") - } - - return query - }, - ) - return cursor, err -} - -func (s *Storage) GetPool(ctx context.Context, poolID uuid.UUID) (*models.Pool, error) { - var pool models.Pool - - err := s.db.NewSelect(). - Model(&pool). - Where("id = ?", poolID). - Relation("PoolAccounts"). - Scan(ctx) - if err != nil { - return nil, e("failed to get pool", err) - } - - return &pool, nil -} - -func (s *Storage) DeletePool(ctx context.Context, poolID uuid.UUID) error { - _, err := s.db.NewDelete(). - Model(&models.Pool{}). - Where("id = ?", poolID). - Exec(ctx) - if err != nil { - return e("failed to delete pool", err) - } - - return nil -} diff --git a/components/payments/cmd/api/internal/storage/pools_test.go b/components/payments/cmd/api/internal/storage/pools_test.go deleted file mode 100644 index 5c6d78fe8d..0000000000 --- a/components/payments/cmd/api/internal/storage/pools_test.go +++ /dev/null @@ -1,202 +0,0 @@ -package storage - -import ( - "context" - "testing" - "time" - - "github.com/formancehq/go-libs/bun/bunpaginate" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/stretchr/testify/require" -) - -func insertPools(t *testing.T, store *Storage, accountIDs []models.AccountID) []uuid.UUID { - pool1 := models.Pool{ - Name: "test", - CreatedAt: time.Date(2023, 11, 14, 8, 0, 0, 0, time.UTC), - } - var uuid1 uuid.UUID - err := store.DB().NewInsert(). - Model(&pool1). - Returning("id"). - Scan(context.Background(), &uuid1) - require.NoError(t, err) - - poolAccounts1 := models.PoolAccounts{ - PoolID: uuid1, - AccountID: accountIDs[0], - } - _, err = store.DB().NewInsert(). - Model(&poolAccounts1). - Exec(context.Background()) - require.NoError(t, err) - - var uuid2 uuid.UUID - pool2 := models.Pool{ - Name: "test2", - CreatedAt: time.Date(2023, 11, 14, 9, 0, 0, 0, time.UTC), - } - err = store.DB().NewInsert(). - Model(&pool2). - Returning("id"). - Scan(context.Background(), &uuid2) - require.NoError(t, err) - - poolAccounts2 := []*models.PoolAccounts{ - { - PoolID: uuid2, - AccountID: accountIDs[0], - }, - { - PoolID: uuid2, - AccountID: accountIDs[1], - }, - } - _, err = store.DB().NewInsert(). - Model(&poolAccounts2). - Exec(context.Background()) - require.NoError(t, err) - - return []uuid.UUID{uuid1, uuid2} -} - -func TestCreatePools(t *testing.T) { - t.Parallel() - - store := newStore(t) - - connectorID := installConnector(t, store) - accounts := insertAccounts(t, store, connectorID) - - pool := &models.Pool{ - Name: "test", - CreatedAt: time.Date(2023, 11, 14, 8, 0, 0, 0, time.UTC), - PoolAccounts: []*models.PoolAccounts{}, - } - for _, account := range accounts { - pool.PoolAccounts = append(pool.PoolAccounts, &models.PoolAccounts{ - AccountID: account, - }) - } - - err := store.CreatePool(context.Background(), pool) - require.NoError(t, err) - require.NotEqual(t, uuid.Nil, pool.ID) -} - -func TestAddAccountsToPool(t *testing.T) { - t.Parallel() - - store := newStore(t) - - connectorID := installConnector(t, store) - accounts := insertAccounts(t, store, connectorID) - poolIDs := insertPools(t, store, accounts) - - poolAccounts := []*models.PoolAccounts{ - { - PoolID: poolIDs[0], - AccountID: accounts[1], - }, - } - - err := store.AddAccountsToPool(context.Background(), poolAccounts) - require.NoError(t, err) - - pool, err := store.GetPool(context.Background(), poolIDs[0]) - require.NoError(t, err) - require.Equal(t, 2, len(pool.PoolAccounts)) - require.Equal(t, accounts[0], pool.PoolAccounts[0].AccountID) - require.Equal(t, accounts[1], pool.PoolAccounts[1].AccountID) -} - -func TestRemoveAccoutsToPool(t *testing.T) { - t.Parallel() - - store := newStore(t) - - connectorID := installConnector(t, store) - accounts := insertAccounts(t, store, connectorID) - poolIDs := insertPools(t, store, accounts) - - poolAccounts := []*models.PoolAccounts{ - { - PoolID: poolIDs[0], - AccountID: accounts[0], - }, - } - - err := store.RemoveAccountFromPool(context.Background(), poolAccounts[0]) - require.NoError(t, err) - - pool, err := store.GetPool(context.Background(), poolIDs[0]) - require.NoError(t, err) - require.Equal(t, 0, len(pool.PoolAccounts)) -} - -func TestListPools(t *testing.T) { - t.Parallel() - - store := newStore(t) - - connectorID := installConnector(t, store) - accounts := insertAccounts(t, store, connectorID) - insertedPools := insertPools(t, store, accounts) - - t.Run("list all pools", func(t *testing.T) { - t.Parallel() - - cursor, err := store.ListPools( - context.Background(), - NewListPoolsQuery(NewPaginatedQueryOptions(PoolQuery{}).WithPageSize(15)), - ) - require.NoError(t, err) - require.Equal(t, 2, len(cursor.Data)) - require.Equal(t, 15, cursor.PageSize) - require.Equal(t, false, cursor.HasMore) - require.Equal(t, "", cursor.Previous) - require.Equal(t, "", cursor.Next) - require.Equal(t, insertedPools[1], cursor.Data[0].ID) - require.Equal(t, 2, len(cursor.Data[0].PoolAccounts)) - require.Equal(t, insertedPools[0], cursor.Data[1].ID) - require.Equal(t, 1, len(cursor.Data[1].PoolAccounts)) - }) - - t.Run("list all pools with page size 1", func(t *testing.T) { - t.Parallel() - - cursor, err := store.ListPools( - context.Background(), - NewListPoolsQuery(NewPaginatedQueryOptions(PoolQuery{}).WithPageSize(1)), - ) - require.NoError(t, err) - require.Equal(t, 1, len(cursor.Data)) - require.Equal(t, 1, cursor.PageSize) - require.Equal(t, true, cursor.HasMore) - require.Equal(t, "", cursor.Previous) - require.Equal(t, insertedPools[1], cursor.Data[0].ID) - require.Equal(t, 2, len(cursor.Data[0].PoolAccounts)) - - var query ListPoolsQuery - err = bunpaginate.UnmarshalCursor(cursor.Next, &query) - require.NoError(t, err) - cursor, err = store.ListPools(context.Background(), query) - require.NoError(t, err) - require.Equal(t, 1, len(cursor.Data)) - require.Equal(t, 1, cursor.PageSize) - require.Equal(t, false, cursor.HasMore) - require.Equal(t, insertedPools[0], cursor.Data[0].ID) - require.Equal(t, 1, len(cursor.Data[0].PoolAccounts)) - - err = bunpaginate.UnmarshalCursor(cursor.Previous, &query) - require.NoError(t, err) - cursor, err = store.ListPools(context.Background(), query) - require.NoError(t, err) - require.Equal(t, 1, len(cursor.Data)) - require.Equal(t, 1, cursor.PageSize) - require.Equal(t, true, cursor.HasMore) - require.Equal(t, insertedPools[1], cursor.Data[0].ID) - require.Equal(t, 2, len(cursor.Data[0].PoolAccounts)) - }) -} diff --git a/components/payments/cmd/api/internal/storage/repository.go b/components/payments/cmd/api/internal/storage/repository.go deleted file mode 100644 index 6b203bf62b..0000000000 --- a/components/payments/cmd/api/internal/storage/repository.go +++ /dev/null @@ -1,20 +0,0 @@ -package storage - -import ( - "github.com/uptrace/bun" -) - -type Storage struct { - db *bun.DB - configEncryptionKey string -} - -const encryptionOptions = "compress-algo=1, cipher-algo=aes256" - -func NewStorage(db *bun.DB, configEncryptionKey string) *Storage { - return &Storage{db: db, configEncryptionKey: configEncryptionKey} -} - -func (s *Storage) DB() *bun.DB { - return s.db -} diff --git a/components/payments/cmd/api/internal/storage/sort.go b/components/payments/cmd/api/internal/storage/sort.go deleted file mode 100644 index 2ec3d5c0a0..0000000000 --- a/components/payments/cmd/api/internal/storage/sort.go +++ /dev/null @@ -1,33 +0,0 @@ -package storage - -import ( - "fmt" - - "github.com/uptrace/bun" -) - -type SortOrder string - -const ( - SortOrderAsc SortOrder = "asc" - SortOrderDesc SortOrder = "desc" -) - -type sortExpression struct { - Column string `json:"column"` - Order SortOrder `json:"order"` -} - -type Sorter []sortExpression - -func (s Sorter) Add(column string, order SortOrder) Sorter { - return append(s, sortExpression{column, order}) -} - -func (s Sorter) Apply(query *bun.SelectQuery) *bun.SelectQuery { - for _, expr := range s { - query = query.Order(fmt.Sprintf("%s %s", expr.Column, expr.Order)) - } - - return query -} diff --git a/components/payments/cmd/api/internal/storage/transfer_initiation.go b/components/payments/cmd/api/internal/storage/transfer_initiation.go deleted file mode 100644 index 313639e8e8..0000000000 --- a/components/payments/cmd/api/internal/storage/transfer_initiation.go +++ /dev/null @@ -1,112 +0,0 @@ -package storage - -import ( - "context" - "fmt" - - "github.com/formancehq/go-libs/bun/bunpaginate" - "github.com/formancehq/go-libs/query" - "github.com/formancehq/payments/internal/models" - "github.com/uptrace/bun" -) - -func (s *Storage) GetTransferInitiation(ctx context.Context, id models.TransferInitiationID) (*models.TransferInitiation, error) { - var transferInitiation models.TransferInitiation - - query := s.db.NewSelect(). - Column("id", "connector_id", "created_at", "scheduled_at", "description", "type", "source_account_id", "destination_account_id", "provider", "initial_amount", "amount", "asset", "metadata"). - Model(&transferInitiation). - Relation("RelatedAdjustments"). - Where("id = ?", id) - - err := query.Scan(ctx) - if err != nil { - return nil, e("failed to get transfer initiation", err) - } - - transferInitiation.SortRelatedAdjustments() - - transferInitiation.RelatedPayments, err = s.ReadTransferInitiationPayments(ctx, id) - if err != nil { - return nil, e("failed to get transfer initiation payments", err) - } - - return &transferInitiation, nil -} - -func (s *Storage) ReadTransferInitiationPayments(ctx context.Context, id models.TransferInitiationID) ([]*models.TransferInitiationPayment, error) { - var payments []*models.TransferInitiationPayment - - query := s.db.NewSelect(). - Column("transfer_initiation_id", "payment_id", "created_at", "status", "error"). - Model(&payments). - Where("transfer_initiation_id = ?", id). - Order("created_at DESC") - - err := query.Scan(ctx) - if err != nil { - return nil, e("failed to get transfer initiation payments", err) - } - - return payments, nil -} - -type TransferInitiationQuery struct{} - -type ListTransferInitiationsQuery bunpaginate.OffsetPaginatedQuery[PaginatedQueryOptions[TransferInitiationQuery]] - -func NewListTransferInitiationsQuery(opts PaginatedQueryOptions[TransferInitiationQuery]) ListTransferInitiationsQuery { - return ListTransferInitiationsQuery{ - PageSize: opts.PageSize, - Order: bunpaginate.OrderAsc, - Options: opts, - } -} - -func (s *Storage) ListTransferInitiations(ctx context.Context, q ListTransferInitiationsQuery) (*bunpaginate.Cursor[models.TransferInitiation], error) { - return PaginateWithOffset[PaginatedQueryOptions[TransferInitiationQuery], models.TransferInitiation](s, ctx, - (*bunpaginate.OffsetPaginatedQuery[PaginatedQueryOptions[TransferInitiationQuery]])(&q), - func(query *bun.SelectQuery) *bun.SelectQuery { - query = query. - Column("id", "connector_id", "created_at", "scheduled_at", "description", "type", "source_account_id", "destination_account_id", "provider", "initial_amount", "amount", "asset", "metadata"). - Relation("RelatedAdjustments") - - if q.Options.QueryBuilder != nil { - where, args, err := s.transferInitiationQueryContext(q.Options.QueryBuilder) - if err != nil { - // TODO: handle error - panic(err) - } - query = query.Where(where, args...) - } - - if q.Options.Sorter != nil { - query = q.Options.Sorter.Apply(query) - } else { - query = query.Order("created_at DESC") - } - - return query - }, - ) -} - -func (s *Storage) transferInitiationQueryContext(qb query.Builder) (string, []any, error) { - return qb.Build(query.ContextFn(func(key, operator string, value any) (string, []any, error) { - switch { - case key == "source_account_id", key == "destination_account_id": - if operator != "$match" { - return "", nil, fmt.Errorf("'%s' columns can only be used with $match", key) - } - - switch accountID := value.(type) { - case string: - return fmt.Sprintf("%s = ?", key), []any{accountID}, nil - default: - return "", nil, fmt.Errorf("unexpected type %T for column '%s'", accountID, key) - } - default: - return "", nil, fmt.Errorf("unknown key '%s' when building query", key) - } - })) -} diff --git a/components/payments/cmd/api/internal/storage/transfer_initiation_test.go b/components/payments/cmd/api/internal/storage/transfer_initiation_test.go deleted file mode 100644 index 0cd9aae7ef..0000000000 --- a/components/payments/cmd/api/internal/storage/transfer_initiation_test.go +++ /dev/null @@ -1,167 +0,0 @@ -package storage - -import ( - "context" - "math/big" - "testing" - "time" - - "github.com/formancehq/go-libs/bun/bunpaginate" - "github.com/formancehq/payments/internal/models" - "github.com/stretchr/testify/require" -) - -func insertTransferInitiation(t *testing.T, store *Storage, connectorID models.ConnectorID) []models.TransferInitiation { - tf1 := models.TransferInitiation{ - ID: models.TransferInitiationID{ - Reference: "tf_1", - ConnectorID: connectorID, - }, - CreatedAt: time.Date(2023, 11, 14, 8, 0, 0, 0, time.UTC), - ScheduledAt: time.Date(2023, 11, 14, 8, 0, 0, 0, time.UTC), - Description: "test_1", - Type: models.TransferInitiationTypeTransfer, - SourceAccountID: &models.AccountID{ - Reference: "test_account", - ConnectorID: connectorID, - }, - DestinationAccountID: models.AccountID{ - Reference: "test_account2", - ConnectorID: connectorID, - }, - Provider: models.ConnectorProviderDummyPay, - ConnectorID: connectorID, - Amount: big.NewInt(100), - InitialAmount: big.NewInt(100), - Asset: "EUR/2", - Metadata: map[string]string{ - "foo": "bar", - }, - } - _, err := store.DB().NewInsert(). - Model(&tf1). - Exec(context.Background()) - require.NoError(t, err) - - tf2 := models.TransferInitiation{ - ID: models.TransferInitiationID{ - Reference: "tf_2", - ConnectorID: connectorID, - }, - CreatedAt: time.Date(2023, 11, 14, 9, 0, 0, 0, time.UTC), - ScheduledAt: time.Date(2023, 11, 14, 9, 0, 0, 0, time.UTC), - Description: "test_2", - Type: models.TransferInitiationTypeTransfer, - SourceAccountID: &models.AccountID{ - Reference: "test_account", - ConnectorID: connectorID, - }, - DestinationAccountID: models.AccountID{ - Reference: "test_account2", - ConnectorID: connectorID, - }, - Provider: models.ConnectorProviderDummyPay, - ConnectorID: connectorID, - Amount: big.NewInt(100), - InitialAmount: big.NewInt(100), - Asset: "USD/2", - Metadata: map[string]string{ - "foo2": "bar2", - }, - } - - _, err = store.DB().NewInsert(). - Model(&tf2). - Exec(context.Background()) - require.NoError(t, err) - - return []models.TransferInitiation{tf1, tf2} -} - -func TestListTransferInitiation(t *testing.T) { - t.Parallel() - - store := newStore(t) - - connectorID := installConnector(t, store) - insertAccounts(t, store, connectorID) - tfs := insertTransferInitiation(t, store, connectorID) - - t.Run("list all transfer initiations with page size 1", func(t *testing.T) { - t.Parallel() - - cursor, err := store.ListTransferInitiations( - context.Background(), - NewListTransferInitiationsQuery(NewPaginatedQueryOptions(TransferInitiationQuery{}).WithPageSize(1)), - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 1) - require.True(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - cursor.Data[0].ScheduledAt = cursor.Data[0].ScheduledAt.UTC() - require.Equal(t, tfs[1], cursor.Data[0]) - - var query ListTransferInitiationsQuery - err = bunpaginate.UnmarshalCursor(cursor.Next, &query) - require.NoError(t, err) - cursor, err = store.ListTransferInitiations( - context.Background(), - query, - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 1) - require.False(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - cursor.Data[0].ScheduledAt = cursor.Data[0].ScheduledAt.UTC() - require.Equal(t, tfs[0], cursor.Data[0]) - - err = bunpaginate.UnmarshalCursor(cursor.Previous, &query) - require.NoError(t, err) - cursor, err = store.ListTransferInitiations( - context.Background(), - query, - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 1) - require.True(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - cursor.Data[0].ScheduledAt = cursor.Data[0].ScheduledAt.UTC() - require.Equal(t, tfs[1], cursor.Data[0]) - }) - - t.Run("list all transfer initiations with page size 2", func(t *testing.T) { - t.Parallel() - - cursor, err := store.ListTransferInitiations( - context.Background(), - NewListTransferInitiationsQuery(NewPaginatedQueryOptions(TransferInitiationQuery{}).WithPageSize(2)), - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 2) - require.False(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - cursor.Data[0].ScheduledAt = cursor.Data[0].ScheduledAt.UTC() - cursor.Data[1].CreatedAt = cursor.Data[1].CreatedAt.UTC() - cursor.Data[1].ScheduledAt = cursor.Data[1].ScheduledAt.UTC() - require.Equal(t, tfs[1], cursor.Data[0]) - require.Equal(t, tfs[0], cursor.Data[1]) - }) - - t.Run("list all transfer initiations with page size > number of transfer initiations", func(t *testing.T) { - t.Parallel() - - cursor, err := store.ListTransferInitiations( - context.Background(), - NewListTransferInitiationsQuery(NewPaginatedQueryOptions(TransferInitiationQuery{}).WithPageSize(10)), - ) - require.NoError(t, err) - require.Len(t, cursor.Data, 2) - require.False(t, cursor.HasMore) - cursor.Data[0].CreatedAt = cursor.Data[0].CreatedAt.UTC() - cursor.Data[0].ScheduledAt = cursor.Data[0].ScheduledAt.UTC() - cursor.Data[1].CreatedAt = cursor.Data[1].CreatedAt.UTC() - cursor.Data[1].ScheduledAt = cursor.Data[1].ScheduledAt.UTC() - require.Equal(t, tfs[1], cursor.Data[0]) - require.Equal(t, tfs[0], cursor.Data[1]) - }) -} diff --git a/components/payments/cmd/api/root.go b/components/payments/cmd/api/root.go deleted file mode 100644 index 145e5cf88c..0000000000 --- a/components/payments/cmd/api/root.go +++ /dev/null @@ -1,47 +0,0 @@ -package api - -import ( - "github.com/formancehq/go-libs/auth" - "github.com/formancehq/go-libs/aws/iam" - "github.com/formancehq/go-libs/bun/bunconnect" - "github.com/formancehq/go-libs/otlp" - "github.com/formancehq/go-libs/otlp/otlpmetrics" - "github.com/formancehq/go-libs/otlp/otlptraces" - "github.com/formancehq/go-libs/publish" - "github.com/formancehq/go-libs/service" - "github.com/spf13/cobra" -) - -func NewAPI( - version string, - addAutoMigrateCommandFunc func(cmd *cobra.Command), -) *cobra.Command { - - root := &cobra.Command{ - Use: "api", - Short: "api", - DisableAutoGenTag: true, - } - - cobra.EnableTraverseRunHooks = true - - server := newServer(version) - addAutoMigrateCommandFunc(server) - root.AddCommand(server) - - server.Flags().BoolP("toggle", "t", false, "Help message for toggle") - server.Flags().String(configEncryptionKeyFlag, "", "Config encryption key") - server.Flags().String(envFlag, "local", "Environment") - server.Flags().String(listenFlag, ":8080", "Listen address") - - service.AddFlags(server.Flags()) - otlp.AddFlags(server.Flags()) - otlptraces.AddFlags(server.Flags()) - otlpmetrics.AddFlags(server.Flags()) - auth.AddFlags(server.Flags()) - publish.AddFlags(serviceName, server.Flags()) - bunconnect.AddFlags(server.Flags()) - iam.AddFlags(server.Flags()) - - return root -} diff --git a/components/payments/cmd/api/serve.go b/components/payments/cmd/api/serve.go deleted file mode 100644 index ae52dc9ed5..0000000000 --- a/components/payments/cmd/api/serve.go +++ /dev/null @@ -1,91 +0,0 @@ -package api - -import ( - "github.com/bombsimon/logrusr/v3" - sharedapi "github.com/formancehq/go-libs/api" - "github.com/formancehq/go-libs/auth" - "github.com/formancehq/go-libs/bun/bunconnect" - "github.com/formancehq/go-libs/otlp/otlpmetrics" - "github.com/formancehq/go-libs/otlp/otlptraces" - "github.com/formancehq/go-libs/publish" - "github.com/formancehq/go-libs/service" - "github.com/formancehq/payments/cmd/api/internal/api" - "github.com/formancehq/payments/cmd/api/internal/storage" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/metric" - "go.opentelemetry.io/otel/metric/noop" - "go.uber.org/fx" -) - -const ( - stackURLFlag = "stack-url" - configEncryptionKeyFlag = "config-encryption-key" - envFlag = "env" - listenFlag = "listen" - - serviceName = "Payments" -) - -func newServer(version string) *cobra.Command { - return &cobra.Command{ - Use: "serve", - Aliases: []string{"server"}, - Short: "Launch server", - SilenceUsage: true, - RunE: runServer(version), - } -} - -func runServer(version string) func(cmd *cobra.Command, args []string) error { - return func(cmd *cobra.Command, args []string) error { - setLogger() - - databaseOptions, err := prepareDatabaseOptions(cmd, service.IsDebug(cmd)) - if err != nil { - return err - } - - options := make([]fx.Option, 0) - - options = append(options, databaseOptions) - options = append(options, - otlptraces.FXModuleFromFlags(cmd), - otlpmetrics.FXModuleFromFlags(cmd), - auth.FXModuleFromFlags(cmd), - fx.Provide(fx.Annotate(noop.NewMeterProvider, fx.As(new(metric.MeterProvider)))), - ) - options = append(options, publish.FXModuleFromFlags(cmd, service.IsDebug(cmd))) - listen, _ := cmd.Flags().GetString(listenFlag) - stackURL, _ := cmd.Flags().GetString(stackURLFlag) - otlpTraces, _ := cmd.Flags().GetBool(otlptraces.OtelTracesFlag) - - options = append(options, api.HTTPModule(sharedapi.ServiceInfo{ - Version: version, - Debug: service.IsDebug(cmd), - }, listen, stackURL, otlpTraces)) - - return service.New(cmd.OutOrStdout(), options...).Run(cmd) - } -} - -func setLogger() { - // Add a dedicated logger for opentelemetry in case of error - otel.SetLogger(logrusr.New(logrus.New().WithField("component", "otlp"))) -} - -func prepareDatabaseOptions(cmd *cobra.Command, debug bool) (fx.Option, error) { - configEncryptionKey, _ := cmd.Flags().GetString(configEncryptionKeyFlag) - if configEncryptionKey == "" { - return nil, errors.New("missing config encryption key") - } - - connectionOptions, err := bunconnect.ConnectionOptionsFromFlags(cmd) - if err != nil { - return nil, err - } - - return storage.Module(*connectionOptions, configEncryptionKey, debug), nil -} diff --git a/components/payments/cmd/connectors/internal/api/api_utils_test.go b/components/payments/cmd/connectors/internal/api/api_utils_test.go deleted file mode 100644 index 266f5c1cf1..0000000000 --- a/components/payments/cmd/connectors/internal/api/api_utils_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package api - -import ( - "testing" - - "github.com/formancehq/payments/cmd/connectors/internal/api/backend" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/dummypay" - gomock "github.com/golang/mock/gomock" -) - -func newServiceTestingBackend(t *testing.T) (*backend.MockServiceBackend, *backend.MockService) { - ctrl := gomock.NewController(t) - mockService := backend.NewMockService(ctrl) - backend := backend.NewMockServiceBackend(ctrl) - backend. - EXPECT(). - GetService(). - MinTimes(0). - Return(mockService) - t.Cleanup(func() { - ctrl.Finish() - }) - return backend, mockService -} - -func newConnectorManagerTestingBackend(t *testing.T) (*backend.MockManagerBackend[dummypay.Config], *backend.MockManager[dummypay.Config]) { - ctrl := gomock.NewController(t) - mockManager := backend.NewMockManager[dummypay.Config](ctrl) - backend := backend.NewMockManagerBackend[dummypay.Config](ctrl) - backend. - EXPECT(). - GetManager(). - MinTimes(0). - Return(mockManager) - t.Cleanup(func() { - ctrl.Finish() - }) - return backend, mockManager -} - -func ptr[T any](v T) *T { - return &v -} diff --git a/components/payments/cmd/connectors/internal/api/backend/backend.go b/components/payments/cmd/connectors/internal/api/backend/backend.go deleted file mode 100644 index 119117d37b..0000000000 --- a/components/payments/cmd/connectors/internal/api/backend/backend.go +++ /dev/null @@ -1,77 +0,0 @@ -package backend - -import ( - "context" - - "github.com/formancehq/go-libs/bun/bunpaginate" - - manager "github.com/formancehq/payments/cmd/connectors/internal/api/connectors_manager" - "github.com/formancehq/payments/cmd/connectors/internal/api/service" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" -) - -//go:generate mockgen -source backend.go -destination backend_generated.go -package backend . Service -type Service interface { - Ping() error - CreateBankAccount(ctx context.Context, req *service.CreateBankAccountRequest) (*models.BankAccount, error) - ForwardBankAccountToConnector(ctx context.Context, id string, req *service.ForwardBankAccountToConnectorRequest) (*models.BankAccount, error) - UpdateBankAccountMetadata(ctx context.Context, id string, req *service.UpdateBankAccountMetadataRequest) error - ListConnectors(ctx context.Context) ([]*models.Connector, error) - CreateTransferInitiation(ctx context.Context, req *service.CreateTransferInitiationRequest) (*models.TransferInitiation, error) - UpdateTransferInitiationStatus(ctx context.Context, transferID string, req *service.UpdateTransferInitiationStatusRequest) error - RetryTransferInitiation(ctx context.Context, id string) error - DeleteTransferInitiation(ctx context.Context, id string) error - ReverseTransferInitiation(ctx context.Context, transferID string, req *service.ReverseTransferInitiationRequest) (*models.TransferReversal, error) -} - -//go:generate mockgen -source backend.go -destination backend_generated.go -package backend . Manager -type Manager[ConnectorConfig models.ConnectorConfigObject] interface { - IsInstalled(ctx context.Context, connectorID models.ConnectorID) (bool, error) - Connectors() map[string]*manager.ConnectorManager - ReadConfig(ctx context.Context, connectorID models.ConnectorID) (ConnectorConfig, error) - UpdateConfig(ctx context.Context, connectorID models.ConnectorID, config ConnectorConfig) error - ListTasksStates(ctx context.Context, connectorID models.ConnectorID, q storage.ListTasksQuery) (*bunpaginate.Cursor[models.Task], error) - CreateWebhookAndContext(ctx context.Context, webhook *models.Webhook) (context.Context, error) - ReadTaskState(ctx context.Context, connectorID models.ConnectorID, taskID uuid.UUID) (*models.Task, error) - Install(ctx context.Context, name string, config ConnectorConfig) (models.ConnectorID, error) - Reset(ctx context.Context, connectorID models.ConnectorID) error - Uninstall(ctx context.Context, connectorID models.ConnectorID) error -} - -type ServiceBackend interface { - GetService() Service -} - -type DefaultServiceBackend struct { - service Service -} - -func (d DefaultServiceBackend) GetService() Service { - return d.service -} - -func NewDefaultBackend(service Service) ServiceBackend { - return &DefaultServiceBackend{ - service: service, - } -} - -type ManagerBackend[ConnectorConfig models.ConnectorConfigObject] interface { - GetManager() Manager[ConnectorConfig] -} - -type DefaultManagerBackend[ConnectorConfig models.ConnectorConfigObject] struct { - manager Manager[ConnectorConfig] -} - -func (m DefaultManagerBackend[ConnectorConfig]) GetManager() Manager[ConnectorConfig] { - return m.manager -} - -func NewDefaultManagerBackend[ConnectorConfig models.ConnectorConfigObject](manager Manager[ConnectorConfig]) ManagerBackend[ConnectorConfig] { - return DefaultManagerBackend[ConnectorConfig]{ - manager: manager, - } -} diff --git a/components/payments/cmd/connectors/internal/api/backend/backend_generated.go b/components/payments/cmd/connectors/internal/api/backend/backend_generated.go deleted file mode 100644 index b4f7b09f33..0000000000 --- a/components/payments/cmd/connectors/internal/api/backend/backend_generated.go +++ /dev/null @@ -1,429 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: backend.go - -// Package backend is a generated GoMock package. -package backend - -import ( - context "context" - api "github.com/formancehq/go-libs/bun/bunpaginate" - reflect "reflect" - - connectors_manager "github.com/formancehq/payments/cmd/connectors/internal/api/connectors_manager" - service "github.com/formancehq/payments/cmd/connectors/internal/api/service" - storage "github.com/formancehq/payments/cmd/connectors/internal/storage" - models "github.com/formancehq/payments/internal/models" - gomock "github.com/golang/mock/gomock" - uuid "github.com/google/uuid" -) - -// MockService is a mock of Service interface. -type MockService struct { - ctrl *gomock.Controller - recorder *MockServiceMockRecorder -} - -// MockServiceMockRecorder is the mock recorder for MockService. -type MockServiceMockRecorder struct { - mock *MockService -} - -// NewMockService creates a new mock instance. -func NewMockService(ctrl *gomock.Controller) *MockService { - mock := &MockService{ctrl: ctrl} - mock.recorder = &MockServiceMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockService) EXPECT() *MockServiceMockRecorder { - return m.recorder -} - -// CreateBankAccount mocks base method. -func (m *MockService) CreateBankAccount(ctx context.Context, req *service.CreateBankAccountRequest) (*models.BankAccount, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateBankAccount", ctx, req) - ret0, _ := ret[0].(*models.BankAccount) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreateBankAccount indicates an expected call of CreateBankAccount. -func (mr *MockServiceMockRecorder) CreateBankAccount(ctx, req interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBankAccount", reflect.TypeOf((*MockService)(nil).CreateBankAccount), ctx, req) -} - -// CreateTransferInitiation mocks base method. -func (m *MockService) CreateTransferInitiation(ctx context.Context, req *service.CreateTransferInitiationRequest) (*models.TransferInitiation, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateTransferInitiation", ctx, req) - ret0, _ := ret[0].(*models.TransferInitiation) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreateTransferInitiation indicates an expected call of CreateTransferInitiation. -func (mr *MockServiceMockRecorder) CreateTransferInitiation(ctx, req interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTransferInitiation", reflect.TypeOf((*MockService)(nil).CreateTransferInitiation), ctx, req) -} - -// DeleteTransferInitiation mocks base method. -func (m *MockService) DeleteTransferInitiation(ctx context.Context, id string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteTransferInitiation", ctx, id) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteTransferInitiation indicates an expected call of DeleteTransferInitiation. -func (mr *MockServiceMockRecorder) DeleteTransferInitiation(ctx, id interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTransferInitiation", reflect.TypeOf((*MockService)(nil).DeleteTransferInitiation), ctx, id) -} - -// ForwardBankAccountToConnector mocks base method. -func (m *MockService) ForwardBankAccountToConnector(ctx context.Context, id string, req *service.ForwardBankAccountToConnectorRequest) (*models.BankAccount, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ForwardBankAccountToConnector", ctx, id, req) - ret0, _ := ret[0].(*models.BankAccount) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ForwardBankAccountToConnector indicates an expected call of ForwardBankAccountToConnector. -func (mr *MockServiceMockRecorder) ForwardBankAccountToConnector(ctx, id, req interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ForwardBankAccountToConnector", reflect.TypeOf((*MockService)(nil).ForwardBankAccountToConnector), ctx, id, req) -} - -// ListConnectors mocks base method. -func (m *MockService) ListConnectors(ctx context.Context) ([]*models.Connector, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListConnectors", ctx) - ret0, _ := ret[0].([]*models.Connector) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListConnectors indicates an expected call of ListConnectors. -func (mr *MockServiceMockRecorder) ListConnectors(ctx interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListConnectors", reflect.TypeOf((*MockService)(nil).ListConnectors), ctx) -} - -// Ping mocks base method. -func (m *MockService) Ping() error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Ping") - ret0, _ := ret[0].(error) - return ret0 -} - -// Ping indicates an expected call of Ping. -func (mr *MockServiceMockRecorder) Ping() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*MockService)(nil).Ping)) -} - -// RetryTransferInitiation mocks base method. -func (m *MockService) RetryTransferInitiation(ctx context.Context, id string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RetryTransferInitiation", ctx, id) - ret0, _ := ret[0].(error) - return ret0 -} - -// RetryTransferInitiation indicates an expected call of RetryTransferInitiation. -func (mr *MockServiceMockRecorder) RetryTransferInitiation(ctx, id interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RetryTransferInitiation", reflect.TypeOf((*MockService)(nil).RetryTransferInitiation), ctx, id) -} - -// ReverseTransferInitiation mocks base method. -func (m *MockService) ReverseTransferInitiation(ctx context.Context, transferID string, req *service.ReverseTransferInitiationRequest) (*models.TransferReversal, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ReverseTransferInitiation", ctx, transferID, req) - ret0, _ := ret[0].(*models.TransferReversal) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ReverseTransferInitiation indicates an expected call of ReverseTransferInitiation. -func (mr *MockServiceMockRecorder) ReverseTransferInitiation(ctx, transferID, req interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReverseTransferInitiation", reflect.TypeOf((*MockService)(nil).ReverseTransferInitiation), ctx, transferID, req) -} - -// UpdateBankAccountMetadata mocks base method. -func (m *MockService) UpdateBankAccountMetadata(ctx context.Context, id string, req *service.UpdateBankAccountMetadataRequest) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateBankAccountMetadata", ctx, id, req) - ret0, _ := ret[0].(error) - return ret0 -} - -// UpdateBankAccountMetadata indicates an expected call of UpdateBankAccountMetadata. -func (mr *MockServiceMockRecorder) UpdateBankAccountMetadata(ctx, id, req interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateBankAccountMetadata", reflect.TypeOf((*MockService)(nil).UpdateBankAccountMetadata), ctx, id, req) -} - -// UpdateTransferInitiationStatus mocks base method. -func (m *MockService) UpdateTransferInitiationStatus(ctx context.Context, transferID string, req *service.UpdateTransferInitiationStatusRequest) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateTransferInitiationStatus", ctx, transferID, req) - ret0, _ := ret[0].(error) - return ret0 -} - -// UpdateTransferInitiationStatus indicates an expected call of UpdateTransferInitiationStatus. -func (mr *MockServiceMockRecorder) UpdateTransferInitiationStatus(ctx, transferID, req interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTransferInitiationStatus", reflect.TypeOf((*MockService)(nil).UpdateTransferInitiationStatus), ctx, transferID, req) -} - -// MockManager is a mock of Manager interface. -type MockManager[ConnectorConfig models.ConnectorConfigObject] struct { - ctrl *gomock.Controller - recorder *MockManagerMockRecorder[ConnectorConfig] -} - -// MockManagerMockRecorder is the mock recorder for MockManager. -type MockManagerMockRecorder[ConnectorConfig models.ConnectorConfigObject] struct { - mock *MockManager[ConnectorConfig] -} - -// NewMockManager creates a new mock instance. -func NewMockManager[ConnectorConfig models.ConnectorConfigObject](ctrl *gomock.Controller) *MockManager[ConnectorConfig] { - mock := &MockManager[ConnectorConfig]{ctrl: ctrl} - mock.recorder = &MockManagerMockRecorder[ConnectorConfig]{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockManager[ConnectorConfig]) EXPECT() *MockManagerMockRecorder[ConnectorConfig] { - return m.recorder -} - -// Connectors mocks base method. -func (m *MockManager[ConnectorConfig]) Connectors() map[string]*connectors_manager.ConnectorManager { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Connectors") - ret0, _ := ret[0].(map[string]*connectors_manager.ConnectorManager) - return ret0 -} - -// Connectors indicates an expected call of Connectors. -func (mr *MockManagerMockRecorder[ConnectorConfig]) Connectors() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connectors", reflect.TypeOf((*MockManager[ConnectorConfig])(nil).Connectors)) -} - -// CreateWebhookAndContext mocks base method. -func (m *MockManager[ConnectorConfig]) CreateWebhookAndContext(ctx context.Context, webhook *models.Webhook) (context.Context, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateWebhookAndContext", ctx, webhook) - ret0, _ := ret[0].(context.Context) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreateWebhookAndContext indicates an expected call of CreateWebhookAndContext. -func (mr *MockManagerMockRecorder[ConnectorConfig]) CreateWebhookAndContext(ctx, webhook interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateWebhookAndContext", reflect.TypeOf((*MockManager[ConnectorConfig])(nil).CreateWebhookAndContext), ctx, webhook) -} - -// Install mocks base method. -func (m *MockManager[ConnectorConfig]) Install(ctx context.Context, name string, config ConnectorConfig) (models.ConnectorID, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Install", ctx, name, config) - ret0, _ := ret[0].(models.ConnectorID) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Install indicates an expected call of Install. -func (mr *MockManagerMockRecorder[ConnectorConfig]) Install(ctx, name, config interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Install", reflect.TypeOf((*MockManager[ConnectorConfig])(nil).Install), ctx, name, config) -} - -// IsInstalled mocks base method. -func (m *MockManager[ConnectorConfig]) IsInstalled(ctx context.Context, connectorID models.ConnectorID) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsInstalled", ctx, connectorID) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// IsInstalled indicates an expected call of IsInstalled. -func (mr *MockManagerMockRecorder[ConnectorConfig]) IsInstalled(ctx, connectorID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsInstalled", reflect.TypeOf((*MockManager[ConnectorConfig])(nil).IsInstalled), ctx, connectorID) -} - -// ListTasksStates mocks base method. -func (m *MockManager[ConnectorConfig]) ListTasksStates(ctx context.Context, connectorID models.ConnectorID, q storage.ListTasksQuery) (*api.Cursor[models.Task], error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListTasksStates", ctx, connectorID, q) - ret0, _ := ret[0].(*api.Cursor[models.Task]) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListTasksStates indicates an expected call of ListTasksStates. -func (mr *MockManagerMockRecorder[ConnectorConfig]) ListTasksStates(ctx, connectorID, q interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTasksStates", reflect.TypeOf((*MockManager[ConnectorConfig])(nil).ListTasksStates), ctx, connectorID, q) -} - -// ReadConfig mocks base method. -func (m *MockManager[ConnectorConfig]) ReadConfig(ctx context.Context, connectorID models.ConnectorID) (ConnectorConfig, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ReadConfig", ctx, connectorID) - ret0, _ := ret[0].(ConnectorConfig) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ReadConfig indicates an expected call of ReadConfig. -func (mr *MockManagerMockRecorder[ConnectorConfig]) ReadConfig(ctx, connectorID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadConfig", reflect.TypeOf((*MockManager[ConnectorConfig])(nil).ReadConfig), ctx, connectorID) -} - -// ReadTaskState mocks base method. -func (m *MockManager[ConnectorConfig]) ReadTaskState(ctx context.Context, connectorID models.ConnectorID, taskID uuid.UUID) (*models.Task, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ReadTaskState", ctx, connectorID, taskID) - ret0, _ := ret[0].(*models.Task) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ReadTaskState indicates an expected call of ReadTaskState. -func (mr *MockManagerMockRecorder[ConnectorConfig]) ReadTaskState(ctx, connectorID, taskID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadTaskState", reflect.TypeOf((*MockManager[ConnectorConfig])(nil).ReadTaskState), ctx, connectorID, taskID) -} - -// Reset mocks base method. -func (m *MockManager[ConnectorConfig]) Reset(ctx context.Context, connectorID models.ConnectorID) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Reset", ctx, connectorID) - ret0, _ := ret[0].(error) - return ret0 -} - -// Reset indicates an expected call of Reset. -func (mr *MockManagerMockRecorder[ConnectorConfig]) Reset(ctx, connectorID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Reset", reflect.TypeOf((*MockManager[ConnectorConfig])(nil).Reset), ctx, connectorID) -} - -// Uninstall mocks base method. -func (m *MockManager[ConnectorConfig]) Uninstall(ctx context.Context, connectorID models.ConnectorID) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Uninstall", ctx, connectorID) - ret0, _ := ret[0].(error) - return ret0 -} - -// Uninstall indicates an expected call of Uninstall. -func (mr *MockManagerMockRecorder[ConnectorConfig]) Uninstall(ctx, connectorID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Uninstall", reflect.TypeOf((*MockManager[ConnectorConfig])(nil).Uninstall), ctx, connectorID) -} - -// UpdateConfig mocks base method. -func (m *MockManager[ConnectorConfig]) UpdateConfig(ctx context.Context, connectorID models.ConnectorID, config ConnectorConfig) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpdateConfig", ctx, connectorID, config) - ret0, _ := ret[0].(error) - return ret0 -} - -// UpdateConfig indicates an expected call of UpdateConfig. -func (mr *MockManagerMockRecorder[ConnectorConfig]) UpdateConfig(ctx, connectorID, config interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateConfig", reflect.TypeOf((*MockManager[ConnectorConfig])(nil).UpdateConfig), ctx, connectorID, config) -} - -// MockServiceBackend is a mock of ServiceBackend interface. -type MockServiceBackend struct { - ctrl *gomock.Controller - recorder *MockServiceBackendMockRecorder -} - -// MockServiceBackendMockRecorder is the mock recorder for MockServiceBackend. -type MockServiceBackendMockRecorder struct { - mock *MockServiceBackend -} - -// NewMockServiceBackend creates a new mock instance. -func NewMockServiceBackend(ctrl *gomock.Controller) *MockServiceBackend { - mock := &MockServiceBackend{ctrl: ctrl} - mock.recorder = &MockServiceBackendMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockServiceBackend) EXPECT() *MockServiceBackendMockRecorder { - return m.recorder -} - -// GetService mocks base method. -func (m *MockServiceBackend) GetService() Service { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetService") - ret0, _ := ret[0].(Service) - return ret0 -} - -// GetService indicates an expected call of GetService. -func (mr *MockServiceBackendMockRecorder) GetService() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetService", reflect.TypeOf((*MockServiceBackend)(nil).GetService)) -} - -// MockManagerBackend is a mock of ManagerBackend interface. -type MockManagerBackend[ConnectorConfig models.ConnectorConfigObject] struct { - ctrl *gomock.Controller - recorder *MockManagerBackendMockRecorder[ConnectorConfig] -} - -// MockManagerBackendMockRecorder is the mock recorder for MockManagerBackend. -type MockManagerBackendMockRecorder[ConnectorConfig models.ConnectorConfigObject] struct { - mock *MockManagerBackend[ConnectorConfig] -} - -// NewMockManagerBackend creates a new mock instance. -func NewMockManagerBackend[ConnectorConfig models.ConnectorConfigObject](ctrl *gomock.Controller) *MockManagerBackend[ConnectorConfig] { - mock := &MockManagerBackend[ConnectorConfig]{ctrl: ctrl} - mock.recorder = &MockManagerBackendMockRecorder[ConnectorConfig]{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockManagerBackend[ConnectorConfig]) EXPECT() *MockManagerBackendMockRecorder[ConnectorConfig] { - return m.recorder -} - -// GetManager mocks base method. -func (m *MockManagerBackend[ConnectorConfig]) GetManager() Manager[ConnectorConfig] { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetManager") - ret0, _ := ret[0].(Manager[ConnectorConfig]) - return ret0 -} - -// GetManager indicates an expected call of GetManager. -func (mr *MockManagerBackendMockRecorder[ConnectorConfig]) GetManager() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetManager", reflect.TypeOf((*MockManagerBackend[ConnectorConfig])(nil).GetManager)) -} diff --git a/components/payments/cmd/connectors/internal/api/bank_account.go b/components/payments/cmd/connectors/internal/api/bank_account.go deleted file mode 100644 index 5091454d12..0000000000 --- a/components/payments/cmd/connectors/internal/api/bank_account.go +++ /dev/null @@ -1,240 +0,0 @@ -package api - -import ( - "encoding/json" - "net/http" - "time" - - "github.com/formancehq/go-libs/api" - "github.com/formancehq/payments/cmd/connectors/internal/api/backend" - "github.com/formancehq/payments/cmd/connectors/internal/api/service" - "github.com/formancehq/payments/internal/otel" - "github.com/gorilla/mux" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -type bankAccountRelatedAccountsResponse struct { - ID string `json:"id"` - CreatedAt time.Time `json:"createdAt"` - AccountID string `json:"accountID"` - ConnectorID string `json:"connectorID"` - Provider string `json:"provider"` -} - -type bankAccountResponse struct { - ID string `json:"id"` - Name string `json:"name"` - CreatedAt time.Time `json:"createdAt"` - Country string `json:"country"` - Iban string `json:"iban,omitempty"` - AccountNumber string `json:"accountNumber,omitempty"` - SwiftBicCode string `json:"swiftBicCode,omitempty"` - Metadata map[string]string `json:"metadata,omitempty"` - RelatedAccounts []*bankAccountRelatedAccountsResponse `json:"relatedAccounts,omitempty"` - - // Deprecated fields, but clients still use them - // They correspond to the first adjustment now. - Provider string `json:"provider,omitempty"` - ConnectorID string `json:"connectorID"` - AccountID string `json:"accountID,omitempty"` -} - -func createBankAccountHandler( - b backend.ServiceBackend, -) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "createBankAccountHandler") - defer span.End() - - w.Header().Set("Content-Type", "application/json") - - var bankAccountRequest service.CreateBankAccountRequest - err := json.NewDecoder(r.Body).Decode(&bankAccountRequest) - if err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrMissingOrInvalidBody, err) - return - } - - setAttributesFromRequest(span, &bankAccountRequest) - - if err := bankAccountRequest.Validate(); err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrValidation, err) - return - } - - bankAccount, err := b.GetService().CreateBankAccount(ctx, &bankAccountRequest) - if err != nil { - otel.RecordError(span, err) - handleServiceErrors(w, r, err) - return - } - - span.SetAttributes(attribute.String("bankAccount.id", bankAccount.ID.String())) - span.SetAttributes(attribute.String("bankAccount.createdAt", bankAccount.ID.String())) - - data := &bankAccountResponse{ - ID: bankAccount.ID.String(), - Name: bankAccount.Name, - CreatedAt: bankAccount.CreatedAt, - Country: bankAccount.Country, - Metadata: bankAccount.Metadata, - } - - for _, relatedAccount := range bankAccount.RelatedAccounts { - data.RelatedAccounts = append(data.RelatedAccounts, &bankAccountRelatedAccountsResponse{ - ID: relatedAccount.ID.String(), - CreatedAt: relatedAccount.CreatedAt, - AccountID: relatedAccount.AccountID.String(), - ConnectorID: relatedAccount.ConnectorID.String(), - Provider: relatedAccount.ConnectorID.Provider.String(), - }) - } - - // Keep compatibility with previous api version - data.ConnectorID = bankAccountRequest.ConnectorID - if len(bankAccount.RelatedAccounts) > 0 { - data.AccountID = bankAccount.RelatedAccounts[0].AccountID.String() - data.Provider = bankAccount.RelatedAccounts[0].ConnectorID.Provider.String() - } - - err = json.NewEncoder(w).Encode(api.BaseResponse[bankAccountResponse]{ - Data: data, - }) - if err != nil { - otel.RecordError(span, err) - api.InternalServerError(w, r, err) - return - } - } -} - -func forwardBankAccountToConnector( - b backend.ServiceBackend, -) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "forwardBankAccountToConnector") - defer span.End() - - payload := &service.ForwardBankAccountToConnectorRequest{} - err := json.NewDecoder(r.Body).Decode(payload) - if err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrMissingOrInvalidBody, err) - return - } - - span.SetAttributes(attribute.String("request.connectorID", payload.ConnectorID)) - - if err := payload.Validate(); err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrValidation, err) - return - } - - bankAccountID, ok := mux.Vars(r)["bankAccountID"] - if !ok { - otel.RecordError(span, err) - api.BadRequest(w, ErrInvalidID, err) - return - } - - span.SetAttributes(attribute.String("bankAccount.id", bankAccountID)) - - bankAccount, err := b.GetService().ForwardBankAccountToConnector(ctx, bankAccountID, payload) - if err != nil { - otel.RecordError(span, err) - handleServiceErrors(w, r, err) - return - } - - data := &bankAccountResponse{ - ID: bankAccount.ID.String(), - Name: bankAccount.Name, - CreatedAt: bankAccount.CreatedAt, - Country: bankAccount.Country, - Metadata: bankAccount.Metadata, - } - - for _, relatedAccount := range bankAccount.RelatedAccounts { - data.RelatedAccounts = append(data.RelatedAccounts, &bankAccountRelatedAccountsResponse{ - ID: relatedAccount.ID.String(), - CreatedAt: relatedAccount.CreatedAt, - AccountID: relatedAccount.AccountID.String(), - ConnectorID: relatedAccount.ConnectorID.String(), - Provider: relatedAccount.ConnectorID.Provider.String(), - }) - } - - // Keep compatibility with previous api version - data.ConnectorID = payload.ConnectorID - if len(bankAccount.RelatedAccounts) > 0 { - data.AccountID = bankAccount.RelatedAccounts[0].AccountID.String() - data.Provider = bankAccount.RelatedAccounts[0].ConnectorID.Provider.String() - } - - err = json.NewEncoder(w).Encode(api.BaseResponse[bankAccountResponse]{ - Data: data, - }) - if err != nil { - otel.RecordError(span, err) - api.InternalServerError(w, r, err) - return - } - } -} - -func updateBankAccountMetadataHandler( - b backend.ServiceBackend, -) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "updateBankAccountMetadataHandler") - defer span.End() - - payload := &service.UpdateBankAccountMetadataRequest{} - err := json.NewDecoder(r.Body).Decode(payload) - if err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrMissingOrInvalidBody, err) - return - } - - for k, v := range payload.Metadata { - span.SetAttributes(attribute.String(k, v)) - } - - if err := payload.Validate(); err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrValidation, err) - return - } - - bankAccountID, ok := mux.Vars(r)["bankAccountID"] - if !ok { - otel.RecordError(span, err) - api.BadRequest(w, ErrInvalidID, err) - return - } - - span.SetAttributes(attribute.String("bankAccount.id", bankAccountID)) - - err = b.GetService().UpdateBankAccountMetadata(ctx, bankAccountID, payload) - if err != nil { - otel.RecordError(span, err) - handleServiceErrors(w, r, err) - return - } - - api.NoContent(w) - } -} - -func setAttributesFromRequest(span trace.Span, request *service.CreateBankAccountRequest) { - span.SetAttributes( - attribute.String("request.name", request.Name), - attribute.String("request.country", request.Country), - attribute.String("request.connectorID", request.ConnectorID), - ) -} diff --git a/components/payments/cmd/connectors/internal/api/bank_account_test.go b/components/payments/cmd/connectors/internal/api/bank_account_test.go deleted file mode 100644 index 68189ac260..0000000000 --- a/components/payments/cmd/connectors/internal/api/bank_account_test.go +++ /dev/null @@ -1,626 +0,0 @@ -package api - -import ( - "bytes" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "testing" - "time" - - sharedapi "github.com/formancehq/go-libs/api" - "github.com/formancehq/go-libs/auth" - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/api/service" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/pkg/errors" - "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" -) - -func TestCreateBankAccounts(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - req *service.CreateBankAccountRequest - expectedStatusCode int - expectedErrorCode string - serviceError error - } - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - - acc1 := models.AccountID{ - Reference: "acc1", - ConnectorID: connectorID, - } - - testCases := []testCase{ - { - name: "nominal", - req: &service.CreateBankAccountRequest{ - AccountNumber: "0112345678", - IBAN: "FR7630006000011234567890189", - SwiftBicCode: "HBUKGB4B", - Country: "FR", - ConnectorID: connectorID.String(), - Name: "test_nominal", - }, - }, - { - name: "nominal without connectorID", - req: &service.CreateBankAccountRequest{ - AccountNumber: "0112345678", - IBAN: "FR7630006000011234567890189", - SwiftBicCode: "HBUKGB4B", - Country: "FR", - Name: "test_nominal", - }, - }, - { - name: "no body", - req: nil, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrMissingOrInvalidBody, - }, - { - name: "missing AccountNumber and Iban", - req: &service.CreateBankAccountRequest{ - SwiftBicCode: "HBUKGB4B", - Country: "FR", - ConnectorID: connectorID.String(), - Name: "test_nominal", - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "missing name", - req: &service.CreateBankAccountRequest{ - AccountNumber: "0112345678", - IBAN: "FR7630006000011234567890189", - SwiftBicCode: "HBUKGB4B", - Country: "FR", - ConnectorID: connectorID.String(), - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "missing country", - req: &service.CreateBankAccountRequest{ - AccountNumber: "0112345678", - IBAN: "FR7630006000011234567890189", - SwiftBicCode: "HBUKGB4B", - ConnectorID: connectorID.String(), - Name: "test_nominal", - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - - { - name: "service error duplicate key", - req: &service.CreateBankAccountRequest{ - AccountNumber: "0112345678", - IBAN: "FR7630006000011234567890189", - SwiftBicCode: "HBUKGB4B", - Country: "FR", - ConnectorID: connectorID.String(), - Name: "test_nominal", - }, - serviceError: storage.ErrDuplicateKeyValue, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - }, - { - name: "service error not found", - req: &service.CreateBankAccountRequest{ - AccountNumber: "0112345678", - IBAN: "FR7630006000011234567890189", - SwiftBicCode: "HBUKGB4B", - Country: "FR", - ConnectorID: connectorID.String(), - Name: "test_nominal", - }, - serviceError: storage.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - }, - { - name: "service error validation", - req: &service.CreateBankAccountRequest{ - AccountNumber: "0112345678", - IBAN: "FR7630006000011234567890189", - SwiftBicCode: "HBUKGB4B", - Country: "FR", - ConnectorID: connectorID.String(), - Name: "test_nominal", - }, - serviceError: service.ErrValidation, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "service error invalid ID", - req: &service.CreateBankAccountRequest{ - AccountNumber: "0112345678", - IBAN: "FR7630006000011234567890189", - SwiftBicCode: "HBUKGB4B", - Country: "FR", - ConnectorID: connectorID.String(), - Name: "test_nominal", - }, - serviceError: service.ErrInvalidID, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrInvalidID, - }, - { - name: "service error publish", - req: &service.CreateBankAccountRequest{ - AccountNumber: "0112345678", - IBAN: "FR7630006000011234567890189", - SwiftBicCode: "HBUKGB4B", - Country: "FR", - ConnectorID: connectorID.String(), - Name: "test_nominal", - }, - serviceError: service.ErrPublish, - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - { - name: "service error other errors", - req: &service.CreateBankAccountRequest{ - AccountNumber: "0112345678", - IBAN: "FR7630006000011234567890189", - SwiftBicCode: "HBUKGB4B", - Country: "FR", - ConnectorID: connectorID.String(), - Name: "test_nominal", - }, - serviceError: errors.New("some error"), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusOK - } - - bankAccountID := uuid.New() - createBankAccountResponse := models.BankAccount{ - ID: bankAccountID, - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Name: "test_nominal", - AccountNumber: "0112345678", - IBAN: "FR7630006000011234567890189", - SwiftBicCode: "HBUKGB4B", - Country: "FR", - } - - if testCase.req != nil && testCase.req.ConnectorID != "" { - createBankAccountResponse.RelatedAccounts = []*models.BankAccountRelatedAccount{ - { - ID: uuid.New(), - BankAccountID: bankAccountID, - ConnectorID: connectorID, - AccountID: acc1, - }, - } - } - - expectedCreateBankAccountResponse := &bankAccountResponse{ - ID: createBankAccountResponse.ID.String(), - Name: createBankAccountResponse.Name, - CreatedAt: createBankAccountResponse.CreatedAt, - Country: createBankAccountResponse.Country, - } - - if testCase.req != nil && testCase.req.ConnectorID != "" { - expectedCreateBankAccountResponse.ConnectorID = createBankAccountResponse.RelatedAccounts[0].ConnectorID.String() - expectedCreateBankAccountResponse.AccountID = createBankAccountResponse.RelatedAccounts[0].AccountID.String() - expectedCreateBankAccountResponse.Provider = createBankAccountResponse.RelatedAccounts[0].ConnectorID.Provider.String() - expectedCreateBankAccountResponse.RelatedAccounts = []*bankAccountRelatedAccountsResponse{ - { - ID: createBankAccountResponse.RelatedAccounts[0].ID.String(), - AccountID: createBankAccountResponse.RelatedAccounts[0].AccountID.String(), - ConnectorID: createBankAccountResponse.RelatedAccounts[0].ConnectorID.String(), - Provider: createBankAccountResponse.RelatedAccounts[0].ConnectorID.Provider.String(), - }, - } - } - - backend, mockService := newServiceTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockService.EXPECT(). - CreateBankAccount(gomock.Any(), testCase.req). - Return(&createBankAccountResponse, nil) - } - if testCase.serviceError != nil { - mockService.EXPECT(). - CreateBankAccount(gomock.Any(), testCase.req). - Return(nil, testCase.serviceError) - } - - router := httpRouter(logging.Testing(), backend, sharedapi.ServiceInfo{ - Debug: testing.Verbose(), - }, auth.NewNoAuth(), nil, false) - - var body []byte - if testCase.req != nil { - var err error - body, err = json.Marshal(testCase.req) - require.NoError(t, err) - } - - req := httptest.NewRequest(http.MethodPost, "/bank-accounts", bytes.NewReader(body)) - rec := httptest.NewRecorder() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - var resp sharedapi.BaseResponse[bankAccountResponse] - sharedapi.Decode(t, rec.Body, &resp) - require.Equal(t, expectedCreateBankAccountResponse, resp.Data) - } else { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } -} - -func TestForwardBankAccountToConnector(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - req *service.ForwardBankAccountToConnectorRequest - expectedStatusCode int - expectedErrorCode string - serviceError error - } - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - - acc1 := models.AccountID{ - Reference: "acc1", - ConnectorID: connectorID, - } - - testCases := []testCase{ - { - name: "nominal", - req: &service.ForwardBankAccountToConnectorRequest{ - ConnectorID: connectorID.String(), - }, - }, - { - name: "nominal without connectorID", - req: &service.ForwardBankAccountToConnectorRequest{}, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "no body", - req: nil, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrMissingOrInvalidBody, - }, - { - name: "service error duplicate key", - req: &service.ForwardBankAccountToConnectorRequest{ - ConnectorID: connectorID.String(), - }, - serviceError: storage.ErrDuplicateKeyValue, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - }, - { - name: "service error not found", - req: &service.ForwardBankAccountToConnectorRequest{ - ConnectorID: connectorID.String(), - }, - serviceError: storage.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - }, - { - name: "service error validation", - req: &service.ForwardBankAccountToConnectorRequest{ - ConnectorID: connectorID.String(), - }, - serviceError: service.ErrValidation, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "service error invalid ID", - req: &service.ForwardBankAccountToConnectorRequest{ - ConnectorID: connectorID.String(), - }, - serviceError: service.ErrInvalidID, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrInvalidID, - }, - { - name: "service error publish", - req: &service.ForwardBankAccountToConnectorRequest{ - ConnectorID: connectorID.String(), - }, - serviceError: service.ErrPublish, - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - { - name: "service error other errors", - req: &service.ForwardBankAccountToConnectorRequest{ - ConnectorID: connectorID.String(), - }, - serviceError: errors.New("some error"), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusOK - } - - bankAccountID := uuid.New() - forwardBankAccountResponse := models.BankAccount{ - ID: bankAccountID, - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Name: "test_nominal", - AccountNumber: "0112345678", - IBAN: "FR7630006000011234567890189", - SwiftBicCode: "HBUKGB4B", - Country: "FR", - } - - if testCase.req != nil && testCase.req.ConnectorID != "" { - forwardBankAccountResponse.RelatedAccounts = []*models.BankAccountRelatedAccount{ - { - ID: uuid.New(), - BankAccountID: bankAccountID, - ConnectorID: connectorID, - AccountID: acc1, - }, - } - } - - expectedForwardBankAccountResponse := &bankAccountResponse{ - ID: forwardBankAccountResponse.ID.String(), - Name: forwardBankAccountResponse.Name, - CreatedAt: forwardBankAccountResponse.CreatedAt, - Country: forwardBankAccountResponse.Country, - } - - if testCase.req != nil && testCase.req.ConnectorID != "" { - expectedForwardBankAccountResponse.ConnectorID = forwardBankAccountResponse.RelatedAccounts[0].ConnectorID.String() - expectedForwardBankAccountResponse.AccountID = forwardBankAccountResponse.RelatedAccounts[0].AccountID.String() - expectedForwardBankAccountResponse.Provider = forwardBankAccountResponse.RelatedAccounts[0].ConnectorID.Provider.String() - expectedForwardBankAccountResponse.RelatedAccounts = []*bankAccountRelatedAccountsResponse{ - { - ID: forwardBankAccountResponse.RelatedAccounts[0].ID.String(), - AccountID: forwardBankAccountResponse.RelatedAccounts[0].AccountID.String(), - ConnectorID: forwardBankAccountResponse.RelatedAccounts[0].ConnectorID.String(), - Provider: forwardBankAccountResponse.RelatedAccounts[0].ConnectorID.Provider.String(), - }, - } - } - - backend, mockService := newServiceTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockService.EXPECT(). - ForwardBankAccountToConnector(gomock.Any(), bankAccountID.String(), testCase.req). - Return(&forwardBankAccountResponse, nil) - } - if testCase.serviceError != nil { - mockService.EXPECT(). - ForwardBankAccountToConnector(gomock.Any(), bankAccountID.String(), testCase.req). - Return(nil, testCase.serviceError) - } - - router := httpRouter(logging.Testing(), backend, sharedapi.ServiceInfo{ - Debug: testing.Verbose(), - }, auth.NewNoAuth(), nil, false) - - var body []byte - if testCase.req != nil { - var err error - body, err = json.Marshal(testCase.req) - require.NoError(t, err) - } - - req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/bank-accounts/%s/forward", bankAccountID), bytes.NewReader(body)) - rec := httptest.NewRecorder() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - var resp sharedapi.BaseResponse[bankAccountResponse] - sharedapi.Decode(t, rec.Body, &resp) - require.Equal(t, expectedForwardBankAccountResponse, resp.Data) - } else { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } -} - -func TestUpdateBankAccountMetadata(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - req *service.UpdateBankAccountMetadataRequest - expectedStatusCode int - expectedErrorCode string - serviceError error - } - - testCases := []testCase{ - { - name: "nominal", - req: &service.UpdateBankAccountMetadataRequest{ - Metadata: map[string]string{ - "foo": "bar", - }, - }, - }, - { - name: "empty metadata", - req: &service.UpdateBankAccountMetadataRequest{}, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "no body", - req: nil, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrMissingOrInvalidBody, - }, - { - name: "service error duplicate key", - req: &service.UpdateBankAccountMetadataRequest{ - Metadata: map[string]string{ - "foo": "bar", - }, - }, - serviceError: storage.ErrDuplicateKeyValue, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - }, - { - name: "service error not found", - req: &service.UpdateBankAccountMetadataRequest{ - Metadata: map[string]string{ - "foo": "bar", - }, - }, - serviceError: storage.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - }, - { - name: "service error validation", - req: &service.UpdateBankAccountMetadataRequest{ - Metadata: map[string]string{ - "foo": "bar", - }, - }, - serviceError: service.ErrValidation, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "service error invalid ID", - req: &service.UpdateBankAccountMetadataRequest{ - Metadata: map[string]string{ - "foo": "bar", - }, - }, - serviceError: service.ErrInvalidID, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrInvalidID, - }, - { - name: "service error publish", - req: &service.UpdateBankAccountMetadataRequest{ - Metadata: map[string]string{ - "foo": "bar", - }, - }, - serviceError: service.ErrPublish, - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - { - name: "service error other errors", - req: &service.UpdateBankAccountMetadataRequest{ - Metadata: map[string]string{ - "foo": "bar", - }, - }, - serviceError: errors.New("some error"), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusNoContent - } - - bankAccountID := uuid.New() - - backend, mockService := newServiceTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockService.EXPECT(). - UpdateBankAccountMetadata(gomock.Any(), bankAccountID.String(), testCase.req). - Return(nil) - } - if testCase.serviceError != nil { - mockService.EXPECT(). - UpdateBankAccountMetadata(gomock.Any(), bankAccountID.String(), testCase.req). - Return(testCase.serviceError) - } - - router := httpRouter(logging.Testing(), backend, sharedapi.ServiceInfo{ - Debug: testing.Verbose(), - }, auth.NewNoAuth(), nil, false) - - var body []byte - if testCase.req != nil { - var err error - body, err = json.Marshal(testCase.req) - require.NoError(t, err) - } - - req := httptest.NewRequest(http.MethodPatch, fmt.Sprintf("/bank-accounts/%s/metadata", bankAccountID), bytes.NewReader(body)) - rec := httptest.NewRecorder() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode >= 300 || testCase.expectedStatusCode < 200 { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } -} diff --git a/components/payments/cmd/connectors/internal/api/connector.go b/components/payments/cmd/connectors/internal/api/connector.go deleted file mode 100644 index e027cb35af..0000000000 --- a/components/payments/cmd/connectors/internal/api/connector.go +++ /dev/null @@ -1,483 +0,0 @@ -package api - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "time" - - "github.com/formancehq/go-libs/api" - "github.com/formancehq/go-libs/bun/bunpaginate" - "github.com/formancehq/go-libs/pointer" - "github.com/formancehq/payments/cmd/connectors/internal/api/backend" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/google/uuid" - "github.com/gorilla/mux" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -type APIVersion int - -const ( - V0 APIVersion = iota - V1 APIVersion = iota -) - -func (a APIVersion) String() string { - switch a { - case V0: - return "v0" - case V1: - return "v1" - default: - return "unknown" - } -} - -func updateConfig[Config models.ConnectorConfigObject]( - b backend.ManagerBackend[Config], - apiVersion APIVersion, -) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "updateConfig") - defer span.End() - - span.SetAttributes(attribute.String("apiVersion", apiVersion.String())) - - connectorID, err := getConnectorID(span, b, r, apiVersion) - if err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrInvalidID, err) - return - } - - if connectorNotInstalled(span, b, connectorID, w, r) { - return - } - - var config Config - if r.ContentLength > 0 { - err := json.NewDecoder(r.Body).Decode(&config) - if err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrMissingOrInvalidBody, err) - return - } - } - - err = b.GetManager().UpdateConfig(ctx, connectorID, config) - if err != nil { - otel.RecordError(span, err) - handleConnectorsManagerErrors(w, r, err) - return - } - - api.NoContent(w) - } -} - -func readConfig[Config models.ConnectorConfigObject]( - b backend.ManagerBackend[Config], - apiVersion APIVersion, -) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "readConfig") - defer span.End() - - span.SetAttributes(attribute.String("apiVersion", apiVersion.String())) - - connectorID, err := getConnectorID(span, b, r, apiVersion) - if err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrInvalidID, err) - return - } - - span.SetAttributes(attribute.String("connectorID", connectorID.String())) - - if connectorNotInstalled(span, b, connectorID, w, r) { - return - } - - config, err := b.GetManager().ReadConfig(ctx, connectorID) - if err != nil { - otel.RecordError(span, err) - handleConnectorsManagerErrors(w, r, err) - return - } - - err = json.NewEncoder(w).Encode(api.BaseResponse[Config]{ - Data: &config, - }) - if err != nil { - otel.RecordError(span, err) - api.InternalServerError(w, r, err) - return - } - } -} - -type listTasksResponseElement struct { - ID string `json:"id"` - ConnectorID string `json:"connectorID"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` - Descriptor json.RawMessage `json:"descriptor"` - Status models.TaskStatus `json:"status"` - State json.RawMessage `json:"state"` - Error string `json:"error"` -} - -func listTasks[Config models.ConnectorConfigObject]( - b backend.ManagerBackend[Config], - apiVersion APIVersion, -) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "listTasks") - defer span.End() - - span.SetAttributes(attribute.String("apiVersion", apiVersion.String())) - - connectorID, err := getConnectorID(span, b, r, apiVersion) - if err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrInvalidID, err) - return - } - - if connectorNotInstalled(span, b, connectorID, w, r) { - return - } - - query, err := bunpaginate.Extract[storage.ListTasksQuery](r, func() (*storage.ListTasksQuery, error) { - pageSize, err := bunpaginate.GetPageSize(r) - if err != nil { - return nil, err - } - - return pointer.For(storage.NewListTasksQuery(storage.NewPaginatedQueryOptions(storage.TaskQuery{}).WithPageSize(pageSize))), nil - }) - if err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrValidation, err) - return - } - - span.SetAttributes(attribute.Int("pageSize", int(query.PageSize))) - span.SetAttributes(attribute.String("cursor", r.URL.Query().Get("cursor"))) - - cursor, err := b.GetManager().ListTasksStates(ctx, connectorID, *query) - if err != nil { - otel.RecordError(span, err) - handleConnectorsManagerErrors(w, r, err) - return - } - - tasks := cursor.Data - data := make([]listTasksResponseElement, len(tasks)) - for i, task := range tasks { - data[i] = listTasksResponseElement{ - ID: task.ID.String(), - ConnectorID: task.ConnectorID.String(), - CreatedAt: task.CreatedAt.Format(time.RFC3339), - UpdatedAt: task.UpdatedAt.Format(time.RFC3339), - Descriptor: task.Descriptor, - Status: task.Status, - State: task.State, - Error: task.Error, - } - } - - err = json.NewEncoder(w).Encode(api.BaseResponse[listTasksResponseElement]{ - Cursor: &bunpaginate.Cursor[listTasksResponseElement]{ - PageSize: cursor.PageSize, - HasMore: cursor.HasMore, - Previous: cursor.Previous, - Next: cursor.Next, - Data: data, - }, - }) - if err != nil { - otel.RecordError(span, err) - api.InternalServerError(w, r, err) - return - } - } -} - -func webhooksMiddleware[Config models.ConnectorConfigObject]( - b backend.ManagerBackend[Config], - apiVersion APIVersion, -) func(handler http.Handler) http.Handler { - return func(handler http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "webhooksMiddleware") - defer span.End() - - connectorID, err := getConnectorID(span, b, r, apiVersion) - if err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrInvalidID, err) - return - } - - if connectorNotInstalled(span, b, connectorID, w, r) { - return - } - - body, err := io.ReadAll(r.Body) - if err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrMissingOrInvalidBody, err) - } - defer r.Body.Close() - - webhook := &models.Webhook{ - ID: uuid.New(), - ConnectorID: connectorID, - RequestBody: body, - } - - span.SetAttributes(attribute.String("webhook.id", webhook.ID.String())) - - ctx, err = b.GetManager().CreateWebhookAndContext(ctx, webhook) - if err != nil { - otel.RecordError(span, err) - handleConnectorsManagerErrors(w, r, err) - return - } - - handler.ServeHTTP(w, r.WithContext(ctx)) - }) - } -} - -func readTask[Config models.ConnectorConfigObject]( - b backend.ManagerBackend[Config], - apiVersion APIVersion, -) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "readTask") - defer span.End() - - span.SetAttributes(attribute.String("apiVersion", apiVersion.String())) - - connectorID, err := getConnectorID(span, b, r, apiVersion) - if err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrInvalidID, err) - return - } - - if connectorNotInstalled(span, b, connectorID, w, r) { - return - } - - taskID, err := uuid.Parse(mux.Vars(r)["taskID"]) - if err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrInvalidID, err) - return - } - - span.SetAttributes(attribute.String("taskID", taskID.String())) - - task, err := b.GetManager().ReadTaskState(ctx, connectorID, taskID) - if err != nil { - otel.RecordError(span, err) - handleConnectorsManagerErrors(w, r, err) - return - } - - data := listTasksResponseElement{ - ID: task.ID.String(), - ConnectorID: task.ConnectorID.String(), - CreatedAt: task.CreatedAt.Format(time.RFC3339), - UpdatedAt: task.UpdatedAt.Format(time.RFC3339), - Descriptor: task.Descriptor, - Status: task.Status, - State: task.State, - Error: task.Error, - } - - err = json.NewEncoder(w).Encode(api.BaseResponse[listTasksResponseElement]{ - Data: &data, - }) - if err != nil { - otel.RecordError(span, err) - api.InternalServerError(w, r, err) - return - } - } -} - -func uninstall[Config models.ConnectorConfigObject]( - b backend.ManagerBackend[Config], - apiVersion APIVersion, -) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "uninstall") - defer span.End() - - span.SetAttributes(attribute.String("apiVersion", apiVersion.String())) - - connectorID, err := getConnectorID(span, b, r, apiVersion) - if err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrInvalidID, err) - return - } - - if connectorNotInstalled(span, b, connectorID, w, r) { - return - } - - err = b.GetManager().Uninstall(ctx, connectorID) - if err != nil { - otel.RecordError(span, err) - handleConnectorsManagerErrors(w, r, err) - return - } - - w.WriteHeader(http.StatusNoContent) - } -} - -type installResponse struct { - ConnectorID string `json:"connectorID"` -} - -func install[Config models.ConnectorConfigObject](b backend.ManagerBackend[Config]) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "install") - defer span.End() - - w.Header().Set("Content-Type", "application/json") - - var config Config - if r.ContentLength > 0 { - err := json.NewDecoder(r.Body).Decode(&config) - if err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrMissingOrInvalidBody, err) - return - } - } - - connectorID, err := b.GetManager().Install(ctx, config.ConnectorName(), config) - if err != nil { - otel.RecordError(span, err) - handleConnectorsManagerErrors(w, r, err) - return - } - - w.WriteHeader(http.StatusCreated) - err = json.NewEncoder(w).Encode(api.BaseResponse[installResponse]{ - Data: &installResponse{ - ConnectorID: connectorID.String(), - }, - }) - if err != nil { - otel.RecordError(span, err) - api.InternalServerError(w, r, err) - return - } - } -} - -func reset[Config models.ConnectorConfigObject]( - b backend.ManagerBackend[Config], - apiVersion APIVersion, -) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "reset") - defer span.End() - - span.SetAttributes(attribute.String("apiVersion", apiVersion.String())) - - connectorID, err := getConnectorID(span, b, r, apiVersion) - if err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrInvalidID, err) - return - } - - if connectorNotInstalled(span, b, connectorID, w, r) { - return - } - - err = b.GetManager().Reset(ctx, connectorID) - if err != nil { - otel.RecordError(span, err) - handleConnectorsManagerErrors(w, r, err) - return - } - - w.WriteHeader(http.StatusNoContent) - } -} - -func connectorNotInstalled[Config models.ConnectorConfigObject]( - span trace.Span, - b backend.ManagerBackend[Config], - connectorID models.ConnectorID, - w http.ResponseWriter, r *http.Request, -) bool { - installed, err := b.GetManager().IsInstalled(r.Context(), connectorID) - if err != nil { - otel.RecordError(span, err) - handleConnectorsManagerErrors(w, r, err) - return true - } - - if !installed { - otel.RecordError(span, fmt.Errorf("connector not installed")) - api.BadRequest(w, ErrValidation, fmt.Errorf("connector not installed")) - return true - } - - return false -} - -func getConnectorID[Config models.ConnectorConfigObject]( - span trace.Span, - b backend.ManagerBackend[Config], - r *http.Request, - apiVersion APIVersion, -) (models.ConnectorID, error) { - switch apiVersion { - case V0: - connectors := b.GetManager().Connectors() - if len(connectors) == 0 { - return models.ConnectorID{}, fmt.Errorf("no connectors installed") - } - - span.SetAttributes(attribute.Int("connectors.count", len(connectors))) - - if len(connectors) > 1 { - return models.ConnectorID{}, fmt.Errorf("more than one connectors installed") - } - - for id := range connectors { - return models.MustConnectorIDFromString(id), nil - } - - case V1: - c := mux.Vars(r)["connectorID"] - - span.SetAttributes(attribute.String("connectorID", c)) - - connectorID, err := models.ConnectorIDFromString(c) - if err != nil { - return models.ConnectorID{}, err - } - - return connectorID, nil - } - - return models.ConnectorID{}, fmt.Errorf("unknown API version") -} diff --git a/components/payments/cmd/connectors/internal/api/connector_test.go b/components/payments/cmd/connectors/internal/api/connector_test.go deleted file mode 100644 index c7ad36207b..0000000000 --- a/components/payments/cmd/connectors/internal/api/connector_test.go +++ /dev/null @@ -1,1356 +0,0 @@ -package api - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "net/http" - "net/http/httptest" - "net/url" - "testing" - "time" - - "github.com/formancehq/go-libs/bun/bunpaginate" - - sharedapi "github.com/formancehq/go-libs/api" - "github.com/formancehq/go-libs/auth" - "github.com/formancehq/go-libs/logging" - manager "github.com/formancehq/payments/cmd/connectors/internal/api/connectors_manager" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/dummypay" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" -) - -func TestReadConfig(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - connectorID string - connectors map[string]*manager.ConnectorManager - installed *bool - apiVersion APIVersion - expectedStatusCode int - expectedErrorCode string - managerError error - } - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - - testCases := []testCase{ - // V0 tests - { - name: "nominal V0", - apiVersion: V0, - connectors: map[string]*manager.ConnectorManager{ - connectorID.String(): nil, - }, - installed: ptr(true), - }, - { - name: "too many connectors for V0", - connectors: map[string]*manager.ConnectorManager{ - connectorID.String(): nil, - "1": nil, - }, - apiVersion: V0, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrInvalidID, - }, - { - name: "no connector for V0", - connectors: map[string]*manager.ConnectorManager{}, - apiVersion: V0, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrInvalidID, - }, - // V1 tests - { - name: "nominal V1", - connectorID: connectorID.String(), - apiVersion: V1, - installed: ptr(true), - }, - // Common test for V0 and V1 - { - name: "connector not installed", - connectorID: connectorID.String(), - apiVersion: V1, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - installed: ptr(false), - }, - { - name: "manager error duplicate key value storage", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: storage.ErrDuplicateKeyValue, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - installed: ptr(true), - }, - { - name: "manager error err not found storage", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: storage.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - installed: ptr(true), - }, - { - name: "manager error already installed", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: manager.ErrAlreadyInstalled, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - installed: ptr(true), - }, - { - name: "manager error not installed", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: manager.ErrNotInstalled, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - installed: ptr(true), - }, - { - name: "manager error connector not found", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: manager.ErrConnectorNotFound, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - installed: ptr(true), - }, - { - name: "manager error err not found", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: manager.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - installed: ptr(true), - }, - { - name: "manager error err validation", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: manager.ErrValidation, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - installed: ptr(true), - }, - { - name: "manager error other errors", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: errors.New("some error"), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - installed: ptr(true), - }, - } - - for _, tc := range testCases { - testCase := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusOK - } - - readConfigResponse := dummypay.Config{ - Name: "test", - Directory: "test", - FilePollingPeriod: connectors.Duration{ - Duration: 2 * time.Minute, - }, - } - - backend, _ := newServiceTestingBackend(t) - managerBackend, mockManager := newConnectorManagerTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockManager.EXPECT(). - ReadConfig(gomock.Any(), connectorID). - Return(readConfigResponse, nil) - } - - if testCase.managerError != nil { - mockManager.EXPECT(). - ReadConfig(gomock.Any(), connectorID). - Return(dummypay.Config{}, testCase.managerError) - } - - if testCase.apiVersion == V0 { - mockManager.EXPECT(). - Connectors(). - Return(testCase.connectors) - } - - if testCase.installed != nil { - mockManager.EXPECT(). - IsInstalled(gomock.Any(), connectorID). - Return(*testCase.installed, nil) - } - - router := httpRouter(logging.Testing(), backend, sharedapi.ServiceInfo{}, auth.NewNoAuth(), []connectorHandler{ - { - Handler: connectorRouter[dummypay.Config](models.ConnectorProviderDummyPay, managerBackend), - Provider: models.ConnectorProviderDummyPay, - initiatePayment: func(ctx context.Context, transfer *models.TransferInitiation) error { - return nil - }, - }, - }, false) - - var endpoint string - switch testCase.apiVersion { - case V0: - endpoint = "/connectors/dummy-pay/config" - case V1: - endpoint = fmt.Sprintf("/connectors/dummy-pay/%s/config", testCase.connectorID) - } - req := httptest.NewRequest(http.MethodGet, endpoint, nil) - rec := httptest.NewRecorder() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - var resp sharedapi.BaseResponse[dummypay.Config] - sharedapi.Decode(t, rec.Body, &resp) - require.Equal(t, &readConfigResponse, resp.Data) - } else { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } -} - -func TestListTasks(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - connectorID string - connectors map[string]*manager.ConnectorManager - installed *bool - apiVersion APIVersion - queryParams url.Values - pageSize int - expectedQuery storage.ListTasksQuery - expectedStatusCode int - expectedErrorCode string - managerError error - } - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - - testCases := []testCase{ - // V0 tests - { - name: "nominal V0", - apiVersion: V0, - connectors: map[string]*manager.ConnectorManager{ - connectorID.String(): nil, - }, - expectedQuery: storage.NewListTasksQuery(storage.NewPaginatedQueryOptions(storage.TaskQuery{}).WithPageSize(15)), - connectorID: connectorID.String(), - installed: ptr(true), - queryParams: map[string][]string{}, - pageSize: 15, - }, - { - name: "too many connectors for V0", - connectors: map[string]*manager.ConnectorManager{ - connectorID.String(): nil, - "1": nil, - }, - apiVersion: V0, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrInvalidID, - }, - { - name: "no connector for V0", - connectors: map[string]*manager.ConnectorManager{}, - apiVersion: V0, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrInvalidID, - }, - // V1 tests - { - name: "nominal V1", - expectedQuery: storage.NewListTasksQuery(storage.NewPaginatedQueryOptions(storage.TaskQuery{}).WithPageSize(15)), - connectorID: connectorID.String(), - installed: ptr(true), - apiVersion: V1, - pageSize: 15, - }, - // Common test for V0 and V1 - { - name: "page size too low", - queryParams: url.Values{ - "pageSize": {"0"}, - }, - expectedQuery: storage.NewListTasksQuery(storage.NewPaginatedQueryOptions(storage.TaskQuery{}).WithPageSize(15)), - pageSize: 15, - connectorID: connectorID.String(), - installed: ptr(true), - apiVersion: V1, - }, - { - name: "page size too high", - queryParams: url.Values{ - "pageSize": {"100000"}, - }, - expectedQuery: storage.NewListTasksQuery(storage.NewPaginatedQueryOptions(storage.TaskQuery{}).WithPageSize(100)), - pageSize: 100, - connectorID: connectorID.String(), - installed: ptr(true), - apiVersion: V1, - }, - { - name: "with invalid page size", - queryParams: url.Values{ - "pageSize": {"nan"}, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - connectorID: connectorID.String(), - installed: ptr(true), - apiVersion: V1, - }, - { - name: "connector not installed", - connectorID: connectorID.String(), - apiVersion: V1, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - installed: ptr(false), - }, - { - name: "manager error duplicate key value storage", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: storage.ErrDuplicateKeyValue, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - installed: ptr(true), - expectedQuery: storage.NewListTasksQuery(storage.NewPaginatedQueryOptions(storage.TaskQuery{}).WithPageSize(15)), - }, - { - name: "manager error err not found storage", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: storage.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - installed: ptr(true), - expectedQuery: storage.NewListTasksQuery(storage.NewPaginatedQueryOptions(storage.TaskQuery{}).WithPageSize(15)), - }, - { - name: "manager error already installed", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: manager.ErrAlreadyInstalled, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - installed: ptr(true), - expectedQuery: storage.NewListTasksQuery(storage.NewPaginatedQueryOptions(storage.TaskQuery{}).WithPageSize(15)), - }, - { - name: "manager error not installed", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: manager.ErrNotInstalled, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - installed: ptr(true), - expectedQuery: storage.NewListTasksQuery(storage.NewPaginatedQueryOptions(storage.TaskQuery{}).WithPageSize(15)), - }, - { - name: "manager error connector not found", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: manager.ErrConnectorNotFound, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - installed: ptr(true), - expectedQuery: storage.NewListTasksQuery(storage.NewPaginatedQueryOptions(storage.TaskQuery{}).WithPageSize(15)), - }, - { - name: "manager error err not found", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: manager.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - installed: ptr(true), - expectedQuery: storage.NewListTasksQuery(storage.NewPaginatedQueryOptions(storage.TaskQuery{}).WithPageSize(15)), - }, - { - name: "manager error err validation", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: manager.ErrValidation, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - installed: ptr(true), - expectedQuery: storage.NewListTasksQuery(storage.NewPaginatedQueryOptions(storage.TaskQuery{}).WithPageSize(15)), - }, - { - name: "manager error other errors", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: errors.New("some error"), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - installed: ptr(true), - expectedQuery: storage.NewListTasksQuery(storage.NewPaginatedQueryOptions(storage.TaskQuery{}).WithPageSize(15)), - }, - } - - for _, tc := range testCases { - testCase := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusOK - } - - tasks := []models.Task{ - { - ID: uuid.New(), - ConnectorID: connectorID, - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - UpdatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Name: "test", - Descriptor: []byte("{}"), - Status: models.TaskStatusActive, - State: json.RawMessage("{}"), - }, - } - listTasksResponse := &bunpaginate.Cursor[models.Task]{ - PageSize: testCase.pageSize, - HasMore: false, - Previous: "", - Next: "", - Data: tasks, - } - - expectedListTasksResponse := []listTasksResponseElement{ - { - ID: tasks[0].ID.String(), - ConnectorID: tasks[0].ConnectorID.String(), - CreatedAt: tasks[0].CreatedAt.Format(time.RFC3339), - UpdatedAt: tasks[0].UpdatedAt.Format(time.RFC3339), - Descriptor: tasks[0].Descriptor, - Status: tasks[0].Status, - State: tasks[0].State, - Error: tasks[0].Error, - }, - } - - backend, _ := newServiceTestingBackend(t) - managerBackend, mockManager := newConnectorManagerTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockManager.EXPECT(). - ListTasksStates(gomock.Any(), connectorID, testCase.expectedQuery). - Return(listTasksResponse, nil) - } - - if testCase.managerError != nil { - mockManager.EXPECT(). - ListTasksStates(gomock.Any(), connectorID, testCase.expectedQuery). - Return(nil, testCase.managerError) - } - - if testCase.apiVersion == V0 { - mockManager.EXPECT(). - Connectors(). - Return(testCase.connectors) - } - - if testCase.installed != nil { - mockManager.EXPECT(). - IsInstalled(gomock.Any(), connectorID). - Return(*testCase.installed, nil) - } - - router := httpRouter(logging.Testing(), backend, sharedapi.ServiceInfo{}, auth.NewNoAuth(), []connectorHandler{ - { - Handler: connectorRouter[dummypay.Config](models.ConnectorProviderDummyPay, managerBackend), - Provider: models.ConnectorProviderDummyPay, - initiatePayment: func(ctx context.Context, transfer *models.TransferInitiation) error { - return nil - }, - }, - }, false) - - var endpoint string - switch testCase.apiVersion { - case V0: - endpoint = "/connectors/dummy-pay/tasks" - case V1: - endpoint = fmt.Sprintf("/connectors/dummy-pay/%s/tasks", testCase.connectorID) - } - req := httptest.NewRequest(http.MethodGet, endpoint, nil) - rec := httptest.NewRecorder() - req.URL.RawQuery = testCase.queryParams.Encode() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - var resp sharedapi.BaseResponse[listTasksResponseElement] - sharedapi.Decode(t, rec.Body, &resp) - require.Equal(t, expectedListTasksResponse, resp.Cursor.Data) - require.Equal(t, listTasksResponse.PageSize, resp.Cursor.PageSize) - require.Equal(t, listTasksResponse.HasMore, resp.Cursor.HasMore) - require.Equal(t, listTasksResponse.Next, resp.Cursor.Next) - require.Equal(t, listTasksResponse.Previous, resp.Cursor.Previous) - } else { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } -} - -func TestReadTask(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - connectorID string - taskID string - connectors map[string]*manager.ConnectorManager - installed *bool - apiVersion APIVersion - expectedStatusCode int - expectedErrorCode string - managerError error - } - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - - taskID := uuid.New() - - testCases := []testCase{ - // V0 tests - { - name: "nominal V0", - apiVersion: V0, - connectors: map[string]*manager.ConnectorManager{ - connectorID.String(): nil, - }, - taskID: taskID.String(), - installed: ptr(true), - }, - { - name: "too many connectors for V0", - connectors: map[string]*manager.ConnectorManager{ - connectorID.String(): nil, - "1": nil, - }, - apiVersion: V0, - taskID: taskID.String(), - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrInvalidID, - }, - { - name: "no connector for V0", - connectors: map[string]*manager.ConnectorManager{}, - apiVersion: V0, - taskID: taskID.String(), - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrInvalidID, - }, - // V1 tests - { - name: "nominal V1", - connectorID: connectorID.String(), - taskID: taskID.String(), - apiVersion: V1, - installed: ptr(true), - }, - // Common test for V0 and V1 - { - name: "connector not installed", - connectorID: connectorID.String(), - taskID: taskID.String(), - apiVersion: V1, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - installed: ptr(false), - }, - { - name: "manager error duplicate key value storage", - connectorID: connectorID.String(), - taskID: taskID.String(), - apiVersion: V1, - managerError: storage.ErrDuplicateKeyValue, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - installed: ptr(true), - }, - { - name: "manager error err not found storage", - connectorID: connectorID.String(), - taskID: taskID.String(), - apiVersion: V1, - managerError: storage.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - installed: ptr(true), - }, - { - name: "manager error already installed", - connectorID: connectorID.String(), - taskID: taskID.String(), - apiVersion: V1, - managerError: manager.ErrAlreadyInstalled, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - installed: ptr(true), - }, - { - name: "manager error not installed", - connectorID: connectorID.String(), - taskID: taskID.String(), - apiVersion: V1, - managerError: manager.ErrNotInstalled, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - installed: ptr(true), - }, - { - name: "manager error connector not found", - connectorID: connectorID.String(), - taskID: taskID.String(), - apiVersion: V1, - managerError: manager.ErrConnectorNotFound, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - installed: ptr(true), - }, - { - name: "manager error err not found", - connectorID: connectorID.String(), - taskID: taskID.String(), - apiVersion: V1, - managerError: manager.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - installed: ptr(true), - }, - { - name: "manager error err validation", - connectorID: connectorID.String(), - taskID: taskID.String(), - apiVersion: V1, - managerError: manager.ErrValidation, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - installed: ptr(true), - }, - { - name: "manager error other errors", - connectorID: connectorID.String(), - taskID: taskID.String(), - apiVersion: V1, - managerError: errors.New("some error"), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - installed: ptr(true), - }, - { - name: "invalid task ID", - apiVersion: V1, - connectorID: connectorID.String(), - taskID: "invalid", - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrInvalidID, - installed: ptr(true), - }, - } - - for _, tc := range testCases { - testCase := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusOK - } - - readTaskResponse := &models.Task{ - ID: uuid.New(), - ConnectorID: connectorID, - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - UpdatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Name: "test", - Descriptor: []byte("{}"), - Status: models.TaskStatusActive, - State: json.RawMessage("{}"), - } - - expectedReadTasksResponse := listTasksResponseElement{ - ID: readTaskResponse.ID.String(), - ConnectorID: readTaskResponse.ConnectorID.String(), - CreatedAt: readTaskResponse.CreatedAt.Format(time.RFC3339), - UpdatedAt: readTaskResponse.UpdatedAt.Format(time.RFC3339), - Descriptor: readTaskResponse.Descriptor, - Status: readTaskResponse.Status, - State: readTaskResponse.State, - Error: readTaskResponse.Error, - } - - backend, _ := newServiceTestingBackend(t) - managerBackend, mockManager := newConnectorManagerTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockManager.EXPECT(). - ReadTaskState(gomock.Any(), connectorID, taskID). - Return(readTaskResponse, nil) - } - - if testCase.managerError != nil { - mockManager.EXPECT(). - ReadTaskState(gomock.Any(), connectorID, taskID). - Return(nil, testCase.managerError) - } - - if testCase.apiVersion == V0 { - mockManager.EXPECT(). - Connectors(). - Return(testCase.connectors) - } - - if testCase.installed != nil { - mockManager.EXPECT(). - IsInstalled(gomock.Any(), connectorID). - Return(*testCase.installed, nil) - } - - router := httpRouter(logging.Testing(), backend, sharedapi.ServiceInfo{}, auth.NewNoAuth(), []connectorHandler{ - { - Handler: connectorRouter[dummypay.Config](models.ConnectorProviderDummyPay, managerBackend), - Provider: models.ConnectorProviderDummyPay, - initiatePayment: func(ctx context.Context, transfer *models.TransferInitiation) error { - return nil - }, - }, - }, false) - - var endpoint string - switch testCase.apiVersion { - case V0: - endpoint = fmt.Sprintf("/connectors/dummy-pay/tasks/%s", testCase.taskID) - case V1: - endpoint = fmt.Sprintf("/connectors/dummy-pay/%s/tasks/%s", testCase.connectorID, testCase.taskID) - } - req := httptest.NewRequest(http.MethodGet, endpoint, nil) - rec := httptest.NewRecorder() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - var resp sharedapi.BaseResponse[listTasksResponseElement] - sharedapi.Decode(t, rec.Body, &resp) - require.Equal(t, &expectedReadTasksResponse, resp.Data) - } else { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } -} - -func TestUninstall(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - connectorID string - connectors map[string]*manager.ConnectorManager - installed *bool - apiVersion APIVersion - expectedStatusCode int - expectedErrorCode string - managerError error - } - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - - testCases := []testCase{ - // V0 tests - { - name: "nominal V0", - apiVersion: V0, - connectors: map[string]*manager.ConnectorManager{ - connectorID.String(): nil, - }, - installed: ptr(true), - }, - { - name: "too many connectors for V0", - connectors: map[string]*manager.ConnectorManager{ - connectorID.String(): nil, - "1": nil, - }, - apiVersion: V0, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrInvalidID, - }, - { - name: "no connector for V0", - connectors: map[string]*manager.ConnectorManager{}, - apiVersion: V0, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrInvalidID, - }, - // V1 tests - { - name: "nominal V1", - connectorID: connectorID.String(), - apiVersion: V1, - installed: ptr(true), - }, - // Common test for V0 and V1 - { - name: "connector not installed", - connectorID: connectorID.String(), - apiVersion: V1, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - installed: ptr(false), - }, - { - name: "manager error duplicate key value storage", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: storage.ErrDuplicateKeyValue, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - installed: ptr(true), - }, - { - name: "manager error err not found storage", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: storage.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - installed: ptr(true), - }, - { - name: "manager error already installed", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: manager.ErrAlreadyInstalled, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - installed: ptr(true), - }, - { - name: "manager error not installed", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: manager.ErrNotInstalled, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - installed: ptr(true), - }, - { - name: "manager error connector not found", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: manager.ErrConnectorNotFound, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - installed: ptr(true), - }, - { - name: "manager error err not found", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: manager.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - installed: ptr(true), - }, - { - name: "manager error err validation", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: manager.ErrValidation, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - installed: ptr(true), - }, - { - name: "manager error other errors", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: errors.New("some error"), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - installed: ptr(true), - }, - } - - for _, tc := range testCases { - testCase := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusNoContent - } - - backend, _ := newServiceTestingBackend(t) - managerBackend, mockManager := newConnectorManagerTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockManager.EXPECT(). - Uninstall(gomock.Any(), connectorID). - Return(nil) - } - - if testCase.managerError != nil { - mockManager.EXPECT(). - Uninstall(gomock.Any(), connectorID). - Return(testCase.managerError) - } - - if testCase.apiVersion == V0 { - mockManager.EXPECT(). - Connectors(). - Return(testCase.connectors) - } - - if testCase.installed != nil { - mockManager.EXPECT(). - IsInstalled(gomock.Any(), connectorID). - Return(*testCase.installed, nil) - } - - router := httpRouter(logging.Testing(), backend, sharedapi.ServiceInfo{}, auth.NewNoAuth(), []connectorHandler{ - { - Handler: connectorRouter[dummypay.Config](models.ConnectorProviderDummyPay, managerBackend), - Provider: models.ConnectorProviderDummyPay, - initiatePayment: func(ctx context.Context, transfer *models.TransferInitiation) error { - return nil - }, - }, - }, false) - - var endpoint string - switch testCase.apiVersion { - case V0: - endpoint = "/connectors/dummy-pay" - case V1: - endpoint = fmt.Sprintf("/connectors/dummy-pay/%s", testCase.connectorID) - } - req := httptest.NewRequest(http.MethodDelete, endpoint, nil) - rec := httptest.NewRecorder() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode >= 300 || testCase.expectedStatusCode < 200 { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } -} - -func TestInstall(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - body []byte - expectedStatusCode int - expectedErrorCode string - managerError error - } - - dummypayConfig := dummypay.Config{ - Name: "test", - Directory: "test", - FilePollingPeriod: connectors.Duration{ - Duration: 2 * time.Minute, - }, - } - - body, err := json.Marshal(dummypayConfig) - require.NoError(t, err) - - testCases := []testCase{ - { - name: "nominal", - body: body, - }, - { - name: "invalid body", - body: []byte("invalid"), - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrMissingOrInvalidBody, - }, - { - name: "manager error duplicate key value storage", - body: body, - managerError: storage.ErrDuplicateKeyValue, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - }, - { - name: "manager error err not found storage", - body: body, - managerError: storage.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - }, - { - name: "manager error already installed", - body: body, - managerError: manager.ErrAlreadyInstalled, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "manager error not installed", - body: body, - managerError: manager.ErrNotInstalled, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "manager error connector not found", - body: body, - managerError: manager.ErrConnectorNotFound, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "manager error err not found", - body: body, - managerError: manager.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - }, - { - name: "manager error err validation", - body: body, - managerError: manager.ErrValidation, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "manager error other errors", - body: body, - managerError: errors.New("some error"), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - } - - for _, tc := range testCases { - testCase := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusCreated - } - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - - expectedResponse := installResponse{ - ConnectorID: connectorID.String(), - } - - backend, _ := newServiceTestingBackend(t) - managerBackend, mockManager := newConnectorManagerTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockManager.EXPECT(). - Install(gomock.Any(), dummypayConfig.Name, dummypayConfig). - Return(connectorID, nil) - } - - if testCase.managerError != nil { - mockManager.EXPECT(). - Install(gomock.Any(), dummypayConfig.Name, dummypayConfig). - Return(models.ConnectorID{}, testCase.managerError) - } - - router := httpRouter(logging.Testing(), backend, sharedapi.ServiceInfo{}, auth.NewNoAuth(), []connectorHandler{ - { - Handler: connectorRouter[dummypay.Config](models.ConnectorProviderDummyPay, managerBackend), - Provider: models.ConnectorProviderDummyPay, - initiatePayment: func(ctx context.Context, transfer *models.TransferInitiation) error { - return nil - }, - }, - }, false) - - req := httptest.NewRequest(http.MethodPost, "/connectors/dummy-pay", bytes.NewReader(testCase.body)) - rec := httptest.NewRecorder() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode >= 300 || testCase.expectedStatusCode < 200 { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } else { - var resp sharedapi.BaseResponse[installResponse] - sharedapi.Decode(t, rec.Body, &resp) - require.Equal(t, &expectedResponse, resp.Data) - } - - }) - } -} - -func TestReset(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - connectorID string - connectors map[string]*manager.ConnectorManager - installed *bool - apiVersion APIVersion - expectedStatusCode int - expectedErrorCode string - managerError error - } - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - - testCases := []testCase{ - // V0 tests - { - name: "nominal V0", - apiVersion: V0, - connectors: map[string]*manager.ConnectorManager{ - connectorID.String(): nil, - }, - installed: ptr(true), - }, - { - name: "too many connectors for V0", - connectors: map[string]*manager.ConnectorManager{ - connectorID.String(): nil, - "1": nil, - }, - apiVersion: V0, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrInvalidID, - }, - { - name: "no connector for V0", - connectors: map[string]*manager.ConnectorManager{}, - apiVersion: V0, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrInvalidID, - }, - // V1 tests - { - name: "nominal V1", - connectorID: connectorID.String(), - apiVersion: V1, - installed: ptr(true), - }, - // Common test for V0 and V1 - { - name: "connector not installed", - connectorID: connectorID.String(), - apiVersion: V1, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - installed: ptr(false), - }, - { - name: "manager error duplicate key value storage", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: storage.ErrDuplicateKeyValue, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - installed: ptr(true), - }, - { - name: "manager error err not found storage", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: storage.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - installed: ptr(true), - }, - { - name: "manager error already installed", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: manager.ErrAlreadyInstalled, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - installed: ptr(true), - }, - { - name: "manager error not installed", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: manager.ErrNotInstalled, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - installed: ptr(true), - }, - { - name: "manager error connector not found", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: manager.ErrConnectorNotFound, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - installed: ptr(true), - }, - { - name: "manager error err not found", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: manager.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - installed: ptr(true), - }, - { - name: "manager error err validation", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: manager.ErrValidation, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - installed: ptr(true), - }, - { - name: "manager error other errors", - connectorID: connectorID.String(), - apiVersion: V1, - managerError: errors.New("some error"), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - installed: ptr(true), - }, - } - - for _, tc := range testCases { - testCase := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusNoContent - } - - backend, _ := newServiceTestingBackend(t) - managerBackend, mockManager := newConnectorManagerTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockManager.EXPECT(). - Reset(gomock.Any(), connectorID). - Return(nil) - } - - if testCase.managerError != nil { - mockManager.EXPECT(). - Reset(gomock.Any(), connectorID). - Return(testCase.managerError) - } - - if testCase.apiVersion == V0 { - mockManager.EXPECT(). - Connectors(). - Return(testCase.connectors) - } - - if testCase.installed != nil { - mockManager.EXPECT(). - IsInstalled(gomock.Any(), connectorID). - Return(*testCase.installed, nil) - } - - router := httpRouter(logging.Testing(), backend, sharedapi.ServiceInfo{}, auth.NewNoAuth(), []connectorHandler{ - { - Handler: connectorRouter[dummypay.Config](models.ConnectorProviderDummyPay, managerBackend), - Provider: models.ConnectorProviderDummyPay, - initiatePayment: func(ctx context.Context, transfer *models.TransferInitiation) error { - return nil - }, - }, - }, false) - - var endpoint string - switch testCase.apiVersion { - case V0: - endpoint = "/connectors/dummy-pay/reset" - case V1: - endpoint = fmt.Sprintf("/connectors/dummy-pay/%s/reset", testCase.connectorID) - } - req := httptest.NewRequest(http.MethodPost, endpoint, nil) - rec := httptest.NewRecorder() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode >= 300 || testCase.expectedStatusCode < 200 { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } -} diff --git a/components/payments/cmd/connectors/internal/api/connectorconfigs.go b/components/payments/cmd/connectors/internal/api/connectorconfigs.go deleted file mode 100644 index 6088b97fc0..0000000000 --- a/components/payments/cmd/connectors/internal/api/connectorconfigs.go +++ /dev/null @@ -1,49 +0,0 @@ -package api - -import ( - "encoding/json" - "net/http" - - "github.com/formancehq/go-libs/api" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/adyen" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/atlar" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/bankingcircle" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/configtemplate" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currencycloud" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/dummypay" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/generic" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/mangopay" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/modulr" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/moneycorp" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/stripe" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/wise" -) - -func connectorConfigsHandler() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - // TODO: It's not ideal to re-identify available connectors - // Refactor it when refactoring the HTTP lib. - - configs := configtemplate.BuildConfigs( - atlar.Config{}, - adyen.Config{}, - bankingcircle.Config{}, - currencycloud.Config{}, - dummypay.Config{}, - modulr.Config{}, - stripe.Config{}, - wise.Config{}, - mangopay.Config{}, - moneycorp.Config{}, - generic.Config{}, - ) - - err := json.NewEncoder(w).Encode(api.BaseResponse[configtemplate.Configs]{ - Data: &configs, - }) - if err != nil { - api.InternalServerError(w, r, err) - return - } - } -} diff --git a/components/payments/cmd/connectors/internal/api/connectormodule.go b/components/payments/cmd/connectors/internal/api/connectormodule.go deleted file mode 100644 index 945226e00f..0000000000 --- a/components/payments/cmd/connectors/internal/api/connectormodule.go +++ /dev/null @@ -1,105 +0,0 @@ -package api - -import ( - "context" - "net/http" - - "github.com/ThreeDotsLabs/watermill/message" - "github.com/formancehq/payments/cmd/connectors/internal/api/backend" - manager "github.com/formancehq/payments/cmd/connectors/internal/api/connectors_manager" - "github.com/formancehq/payments/cmd/connectors/internal/api/service" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/metrics" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/messages" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/google/uuid" - "github.com/pkg/errors" - "go.uber.org/dig" - "go.uber.org/fx" -) - -type connectorHandler struct { - Handler http.Handler - WebhookHandler http.Handler - Provider models.ConnectorProvider - - // TODO(polo): refactor to remove this ugly hack to access the connector manager - initiatePayment service.InitiatePaymentHandler - reversePayment service.ReversePaymentHandler - createExternalBankAccount service.BankAccountHandler -} - -func addConnector[ConnectorConfig models.ConnectorConfigObject](loader manager.Loader[ConnectorConfig], -) fx.Option { - return fx.Options( - fx.Provide(func(store *storage.Storage, - publisher message.Publisher, - metricsRegistry metrics.MetricsRegistry, - messages *messages.Messages, - ) *manager.ConnectorsManager[ConnectorConfig] { - schedulerFactory := manager.TaskSchedulerFactoryFn(func( - connectorID models.ConnectorID, resolver task.Resolver, maxTasks int, - ) *task.DefaultTaskScheduler { - return task.NewDefaultScheduler(connectorID, store, func(ctx context.Context, - descriptor models.TaskDescriptor, - taskID uuid.UUID, - ) (*dig.Container, error) { - container := dig.New() - - if err := container.Provide(func() ingestion.Ingester { - return ingestion.NewDefaultIngester(loader.Name(), connectorID, descriptor, store, publisher, messages) - }); err != nil { - return nil, err - } - - if err := container.Provide(func() storage.Reader { - return store - }); err != nil { - return nil, err - } - - return container, nil - }, resolver, metricsRegistry, maxTasks) - }) - - return manager.NewConnectorManager( - loader.Name(), store, loader, schedulerFactory, publisher, messages) - }), - fx.Provide(func(cm *manager.ConnectorsManager[ConnectorConfig]) backend.ManagerBackend[ConnectorConfig] { - return backend.NewDefaultManagerBackend[ConnectorConfig](cm) - }), - fx.Provide(fx.Annotate(func( - store *storage.Storage, - b backend.ManagerBackend[ConnectorConfig], - cm *manager.ConnectorsManager[ConnectorConfig], - ) connectorHandler { - return connectorHandler{ - Handler: connectorRouter(loader.Name(), b), - WebhookHandler: webhookConnectorRouter(loader.Name(), loader.Router(store), b), - Provider: loader.Name(), - initiatePayment: cm.InitiatePayment, - reversePayment: cm.ReversePayment, - createExternalBankAccount: cm.CreateExternalBankAccount, - } - }, fx.ResultTags(`group:"connectorHandlers"`))), - fx.Invoke(func(lc fx.Lifecycle, cm *manager.ConnectorsManager[ConnectorConfig]) { - lc.Append(fx.Hook{ - OnStart: func(ctx context.Context) error { - ctx, span := otel.Tracer().Start(ctx, "connectorsManager.Restore") - defer span.End() - - err := cm.Restore(ctx) - if err != nil && !errors.Is(err, manager.ErrNotInstalled) { - return err - } - - return nil - }, - OnStop: cm.Close, - }) - }), - ) -} diff --git a/components/payments/cmd/connectors/internal/api/connectors_manager/connector_test.go b/components/payments/cmd/connectors/internal/api/connectors_manager/connector_test.go deleted file mode 100644 index e8aac712ea..0000000000 --- a/components/payments/cmd/connectors/internal/api/connectors_manager/connector_test.go +++ /dev/null @@ -1,134 +0,0 @@ -package connectors_manager - -import ( - "context" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" -) - -type ConnectorBuilder struct { - name string - uninstall func(ctx context.Context) error - resolve func(descriptor models.TaskDescriptor) task.Task - install func(ctx task.ConnectorContext) error - initiatePayment func(ctx task.ConnectorContext, transfer *models.TransferInitiation) error - createExternalBankAccount func(ctx task.ConnectorContext, account *models.BankAccount) error -} - -func (b *ConnectorBuilder) WithUninstall( - uninstallFunction func(ctx context.Context) error, -) *ConnectorBuilder { - b.uninstall = uninstallFunction - - return b -} - -func (b *ConnectorBuilder) WithResolve(resolveFunction func(name models.TaskDescriptor) task.Task) *ConnectorBuilder { - b.resolve = resolveFunction - - return b -} - -func (b *ConnectorBuilder) WithInstall(installFunction func(ctx task.ConnectorContext) error) *ConnectorBuilder { - b.install = installFunction - - return b -} - -func (b *ConnectorBuilder) Build() connectors.Connector { - return &BuiltConnector{ - name: b.name, - uninstall: b.uninstall, - resolve: b.resolve, - install: b.install, - initiatePayment: b.initiatePayment, - createExternalBankAccount: b.createExternalBankAccount, - } -} - -func NewConnectorBuilder() *ConnectorBuilder { - return &ConnectorBuilder{} -} - -type BuiltConnector struct { - name string - uninstall func(ctx context.Context) error - resolve func(name models.TaskDescriptor) task.Task - updateConfig func(ctx task.ConnectorContext, config models.ConnectorConfigObject) error - install func(ctx task.ConnectorContext) error - initiatePayment func(ctx task.ConnectorContext, transfer *models.TransferInitiation) error - reversePayment func(ctx task.ConnectorContext, transferReversal *models.TransferReversal) error - createExternalBankAccount func(ctx task.ConnectorContext, account *models.BankAccount) error -} - -func (b *BuiltConnector) UpdateConfig(ctx task.ConnectorContext, config models.ConnectorConfigObject) error { - if b.updateConfig != nil { - return b.updateConfig(ctx, config) - } - - return nil -} - -func (b *BuiltConnector) SupportedCurrenciesAndDecimals() map[string]int { - return map[string]int{} -} - -func (b *BuiltConnector) InitiatePayment(ctx task.ConnectorContext, transfer *models.TransferInitiation) error { - if b.initiatePayment != nil { - return b.initiatePayment(ctx, transfer) - } - - return nil -} - -func (b *BuiltConnector) ReversePayment(ctx task.ConnectorContext, transferReversal *models.TransferReversal) error { - if b.reversePayment != nil { - return b.reversePayment(ctx, transferReversal) - } - - return nil -} - -func (b *BuiltConnector) CreateExternalBankAccount(ctx task.ConnectorContext, account *models.BankAccount) error { - if b.createExternalBankAccount != nil { - return b.createExternalBankAccount(ctx, account) - } - - return nil -} - -func (b *BuiltConnector) HandleWebhook(ctx task.ConnectorContext, webhook *models.Webhook) error { - return nil -} - -func (b *BuiltConnector) Name() string { - return b.name -} - -func (b *BuiltConnector) Install(ctx task.ConnectorContext) error { - if b.install != nil { - return b.install(ctx) - } - - return nil -} - -func (b *BuiltConnector) Uninstall(ctx context.Context) error { - if b.uninstall != nil { - return b.uninstall(ctx) - } - - return nil -} - -func (b *BuiltConnector) Resolve(name models.TaskDescriptor) task.Task { - if b.resolve != nil { - return b.resolve(name) - } - - return nil -} - -var _ connectors.Connector = &BuiltConnector{} diff --git a/components/payments/cmd/connectors/internal/api/connectors_manager/errors.go b/components/payments/cmd/connectors/internal/api/connectors_manager/errors.go deleted file mode 100644 index 81263ec8ad..0000000000 --- a/components/payments/cmd/connectors/internal/api/connectors_manager/errors.go +++ /dev/null @@ -1,44 +0,0 @@ -package connectors_manager - -import ( - "errors" - "fmt" -) - -var ( - ErrNotFound = errors.New("not found") - ErrAlreadyInstalled = errors.New("already installed") - ErrNotInstalled = errors.New("not installed") - ErrNotEnabled = errors.New("not enabled") - ErrAlreadyRunning = errors.New("already running") - ErrConnectorNotFound = errors.New("connector not found") - ErrValidation = errors.New("validation error") -) - -type storageError struct { - err error - msg string -} - -func (e *storageError) Error() string { - return fmt.Sprintf("%s: %s", e.msg, e.err) -} - -func (e *storageError) Is(err error) bool { - _, ok := err.(*storageError) - return ok -} - -func (e *storageError) Unwrap() error { - return e.err -} - -func newStorageError(err error, msg string) error { - if err == nil { - return nil - } - return &storageError{ - err: err, - msg: msg, - } -} diff --git a/components/payments/cmd/connectors/internal/api/connectors_manager/loader.go b/components/payments/cmd/connectors/internal/api/connectors_manager/loader.go deleted file mode 100644 index 38ffcee45e..0000000000 --- a/components/payments/cmd/connectors/internal/api/connectors_manager/loader.go +++ /dev/null @@ -1,107 +0,0 @@ -package connectors_manager - -import ( - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/gorilla/mux" -) - -type Loader[ConnectorConfig models.ConnectorConfigObject] interface { - Name() models.ConnectorProvider - Load(logger logging.Logger, config ConnectorConfig) connectors.Connector - - // ApplyDefaults is used to fill default values of the provided configuration object - ApplyDefaults(t ConnectorConfig) ConnectorConfig - - // Extra routes to be added to the connectors manager API - Router(store *storage.Storage) *mux.Router - - // AllowTasks define how many task the connector can run - // If too many tasks are scheduled by the connector, - // those will be set to pending state and restarted later when some other tasks will be terminated - AllowTasks() int -} - -type LoaderBuilder[ConnectorConfig models.ConnectorConfigObject] struct { - loadFunction func(logger logging.Logger, config ConnectorConfig) connectors.Connector - applyDefaults func(t ConnectorConfig) ConnectorConfig - name models.ConnectorProvider - allowedTasks int -} - -func (b *LoaderBuilder[ConnectorConfig]) WithLoad(loadFunction func(logger logging.Logger, - config ConnectorConfig) connectors.Connector, -) *LoaderBuilder[ConnectorConfig] { - b.loadFunction = loadFunction - - return b -} - -func (b *LoaderBuilder[ConnectorConfig]) WithApplyDefaults( - applyDefaults func(t ConnectorConfig) ConnectorConfig, -) *LoaderBuilder[ConnectorConfig] { - b.applyDefaults = applyDefaults - - return b -} - -func (b *LoaderBuilder[ConnectorConfig]) WithAllowedTasks(v int) *LoaderBuilder[ConnectorConfig] { - b.allowedTasks = v - - return b -} - -func (b *LoaderBuilder[ConnectorConfig]) Build() *BuiltLoader[ConnectorConfig] { - return &BuiltLoader[ConnectorConfig]{ - loadFunction: b.loadFunction, - applyDefaults: b.applyDefaults, - name: b.name, - allowedTasks: b.allowedTasks, - } -} - -func NewLoaderBuilder[ConnectorConfig models.ConnectorConfigObject](name models.ConnectorProvider, -) *LoaderBuilder[ConnectorConfig] { - return &LoaderBuilder[ConnectorConfig]{ - name: name, - } -} - -type BuiltLoader[ConnectorConfig models.ConnectorConfigObject] struct { - loadFunction func(logger logging.Logger, config ConnectorConfig) connectors.Connector - applyDefaults func(t ConnectorConfig) ConnectorConfig - name models.ConnectorProvider - allowedTasks int -} - -func (b *BuiltLoader[ConnectorConfig]) AllowTasks() int { - return b.allowedTasks -} - -func (b *BuiltLoader[ConnectorConfig]) Name() models.ConnectorProvider { - return b.name -} - -func (b *BuiltLoader[ConnectorConfig]) Load(logger logging.Logger, config ConnectorConfig) connectors.Connector { - if b.loadFunction != nil { - return b.loadFunction(logger, config) - } - - return nil -} - -func (b *BuiltLoader[ConnectorConfig]) ApplyDefaults(t ConnectorConfig) ConnectorConfig { - if b.applyDefaults != nil { - return b.applyDefaults(t) - } - - return t -} - -func (b *BuiltLoader[ConnectorConfig]) Router(store *storage.Storage) *mux.Router { - return nil -} - -var _ Loader[models.EmptyConnectorConfig] = &BuiltLoader[models.EmptyConnectorConfig]{} diff --git a/components/payments/cmd/connectors/internal/api/connectors_manager/manager.go b/components/payments/cmd/connectors/internal/api/connectors_manager/manager.go deleted file mode 100644 index f28e06c623..0000000000 --- a/components/payments/cmd/connectors/internal/api/connectors_manager/manager.go +++ /dev/null @@ -1,565 +0,0 @@ -package connectors_manager - -import ( - "context" - "fmt" - "sync" - - "github.com/formancehq/go-libs/bun/bunpaginate" - - "github.com/ThreeDotsLabs/watermill/message" - "github.com/formancehq/go-libs/contextutil" - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/go-libs/publish" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/messages" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/formancehq/payments/pkg/events" - "github.com/google/uuid" - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -type ConnectorManager struct { - connector connectors.Connector - scheduler *task.DefaultTaskScheduler -} - -type ConnectorsManager[Config models.ConnectorConfigObject] struct { - provider models.ConnectorProvider - loader Loader[Config] - store Store - schedulerFactory TaskSchedulerFactory - publisher message.Publisher - messages *messages.Messages - - connectors map[string]*ConnectorManager - mu sync.RWMutex -} - -func (l *ConnectorsManager[ConnectorConfig]) logger(ctx context.Context) logging.Logger { - return logging.FromContext(ctx).WithFields(map[string]interface{}{ - "component": "connector-manager", - "provider": l.loader.Name(), - }) -} - -func (l *ConnectorsManager[ConnectorConfig]) getManager(connectorID models.ConnectorID) (*ConnectorManager, error) { - l.mu.RLock() - defer l.mu.RUnlock() - - connector, ok := l.connectors[connectorID.String()] - if !ok { - return nil, ErrNotInstalled - } - - return connector, nil -} - -func (l *ConnectorsManager[ConnectorConfig]) Connectors() map[string]*ConnectorManager { - l.mu.RLock() - defer l.mu.RUnlock() - - copy := make(map[string]*ConnectorManager, len(l.connectors)) - for k, v := range l.connectors { - copy[k] = v - } - - return copy -} - -func (l *ConnectorsManager[ConnectorConfig]) ReadConfig( - ctx context.Context, - connectorID models.ConnectorID, -) (ConnectorConfig, error) { - var config ConnectorConfig - connector, err := l.store.GetConnector(ctx, connectorID) - if err != nil { - return config, newStorageError(err, "getting connector") - } - - return l.readConfig(ctx, connector) -} - -func (l *ConnectorsManager[ConnectorConfig]) readConfig( - ctx context.Context, - connector *models.Connector, -) (ConnectorConfig, error) { - var config ConnectorConfig - if connector == nil { - var err error - connector, err = l.store.GetConnector(ctx, connector.ID) - if err != nil { - return config, newStorageError(err, "getting connector") - } - } - - err := connector.ParseConfig(&config) - if err != nil { - return config, err - } - - config = l.loader.ApplyDefaults(config) - - return config, nil -} - -func (l *ConnectorsManager[ConnectorConfig]) UpdateConfig( - ctx context.Context, - connectorID models.ConnectorID, - config ConnectorConfig, -) error { - l.logger(ctx).Infof("Updating config of connector: %s", connectorID) - - connectorManager, err := l.getManager(connectorID) - if err != nil { - l.logger(ctx).Errorf("Connector not installed") - return err - } - - config = l.loader.ApplyDefaults(config) - if err = config.Validate(); err != nil { - return err - } - - cfg, err := config.Marshal() - if err != nil { - return err - } - - if err := l.store.UpdateConfig(ctx, connectorID, cfg); err != nil { - return newStorageError(err, "updating connector config") - } - - // Detach the context since we're launching an async task and we're mostly - // coming from a HTTP request. - detachedCtx, span := detachedCtxWithSpan(ctx, trace.SpanFromContext(ctx), "connectorManager.UpdateConfig", connectorID) - defer span.End() - if err := connectorManager.connector.UpdateConfig(task.NewConnectorContext(logging.ContextWithLogger( - detachedCtx, - logging.FromContext(ctx), - ), connectorManager.scheduler), config); err != nil { - switch { - case errors.Is(err, connectors.ErrInvalidConfig): - return errors.Wrap(ErrValidation, err.Error()) - default: - return err - } - } - - return nil -} - -func (l *ConnectorsManager[ConnectorConfig]) load( - ctx context.Context, - connectorID models.ConnectorID, - connectorConfig ConnectorConfig, -) error { - c := l.loader.Load(l.logger(ctx), connectorConfig) - scheduler := l.schedulerFactory.Make(connectorID, c, l.loader.AllowTasks()) - - l.mu.Lock() - l.connectors[connectorID.String()] = &ConnectorManager{ - connector: c, - scheduler: scheduler, - } - l.mu.Unlock() - - return nil -} - -func (l *ConnectorsManager[ConnectorConfig]) Install( - ctx context.Context, - name string, - config ConnectorConfig, -) (models.ConnectorID, error) { - l.logger(ctx).WithFields(map[string]interface{}{ - "config": config, - }).Infof("Install connector %s", name) - - isInstalled, err := l.store.IsInstalledByConnectorName(ctx, name) - if err != nil { - return models.ConnectorID{}, newStorageError(err, "checking if connector is installed") - } - - if isInstalled { - l.logger(ctx).Errorf("Connector already installed") - return models.ConnectorID{}, ErrAlreadyInstalled - } - - config = l.loader.ApplyDefaults(config) - - if err = config.Validate(); err != nil { - return models.ConnectorID{}, err - } - - cfg, err := config.Marshal() - if err != nil { - return models.ConnectorID{}, err - } - - connector := &models.Connector{ - ID: models.ConnectorID{ - Provider: l.provider, - Reference: uuid.New(), - }, - Name: name, - Provider: l.provider, - } - - err = l.store.Install(ctx, connector, cfg) - if err != nil { - return models.ConnectorID{}, newStorageError(err, "installing connector") - } - - if err := l.load(ctx, connector.ID, config); err != nil { - return models.ConnectorID{}, err - } - - connectorManager, err := l.getManager(connector.ID) - if err != nil { - return models.ConnectorID{}, err - } - - // Detach the context since we're launching an async task and we're mostly - // coming from a HTTP request. - detachedCtx, span := detachedCtxWithSpan(ctx, trace.SpanFromContext(ctx), "connectorManager.Install", connector.ID) - defer span.End() - err = connectorManager.connector.Install(task.NewConnectorContext(logging.ContextWithLogger( - detachedCtx, - logging.FromContext(ctx), - ), connectorManager.scheduler)) - if err != nil { - l.logger(ctx).Errorf("Error starting connector: %s", err) - - return models.ConnectorID{}, err - } - - l.logger(ctx).Infof("Connector installed") - - return connector.ID, nil -} - -func (l *ConnectorsManager[ConnectorConfig]) Uninstall(ctx context.Context, connectorID models.ConnectorID) error { - l.logger(ctx).Infof("Uninstalling connector: %s", connectorID) - - connectorManager, err := l.getManager(connectorID) - if err != nil { - l.logger(ctx).Errorf("Connector not installed") - return err - } - - err = connectorManager.scheduler.Shutdown(ctx) - if err != nil { - return err - } - - err = connectorManager.connector.Uninstall(ctx) - if err != nil { - return err - } - - err = l.store.Uninstall(ctx, connectorID) - if err != nil { - return newStorageError(err, "uninstalling connector") - } - - if l.publisher != nil { - err = l.publisher.Publish(events.TopicPayments, - publish.NewMessage(ctx, l.messages.NewEventResetConnector(connectorID))) - if err != nil { - l.logger(ctx).Errorf("Publishing message: %w", err) - } - } - - l.mu.Lock() - delete(l.connectors, connectorID.String()) - l.mu.Unlock() - - l.logger(ctx).Infof("Connector %s uninstalled", connectorID) - - return nil -} - -func (l *ConnectorsManager[ConnectorConfig]) Restore(ctx context.Context) error { - l.logger(ctx).Info("Restoring state for all connectors") - - connectors, err := l.store.ListConnectors(ctx) - if err != nil { - return newStorageError(err, "listing connectors") - } - - for _, connector := range connectors { - if connector.Provider != l.provider { - continue - } - - if err := l.restore(ctx, connector); err != nil { - l.logger(ctx).Errorf("Unable to restore connector %s: %s", connector.Name, err) - return err - } - } - - return nil -} - -func (l *ConnectorsManager[ConnectorConfig]) restore(ctx context.Context, connector *models.Connector) error { - l.logger(ctx).Infof("Restoring state for connector: %s", connector.Name) - - if manager, _ := l.getManager(connector.ID); manager != nil { - return ErrAlreadyRunning - } - - connectorConfig, err := l.readConfig(ctx, connector) - if err != nil { - return err - } - - if err := l.load(ctx, connector.ID, connectorConfig); err != nil { - return err - } - - manager, err := l.getManager(connector.ID) - if err != nil { - return err - } - - if err := manager.scheduler.Restore(ctx); err != nil { - return err - } - - l.logger(ctx).Infof("State restored for connector: %s", connector.Name) - - return nil -} - -func (l *ConnectorsManager[ConnectorConfig]) FindAll(ctx context.Context) ([]*models.Connector, error) { - connectors, err := l.store.ListConnectors(ctx) - if err != nil { - return nil, newStorageError(err, "listing connectors") - } - - providerConnectors := make([]*models.Connector, 0, len(connectors)) - for _, connector := range connectors { - if connector.Provider == l.provider { - providerConnectors = append(providerConnectors, connector) - } - } - - return providerConnectors, nil -} - -func (l *ConnectorsManager[ConnectorConfig]) IsInstalled(ctx context.Context, connectorID models.ConnectorID) (bool, error) { - isInstalled, err := l.store.IsInstalledByConnectorID(ctx, connectorID) - return isInstalled, newStorageError(err, "checking if connector is installed") -} - -func (l *ConnectorsManager[ConnectorConfig]) ListTasksStates( - ctx context.Context, - connectorID models.ConnectorID, - q storage.ListTasksQuery, -) (*bunpaginate.Cursor[models.Task], error) { - connectorManager, err := l.getManager(connectorID) - if err != nil { - return nil, ErrConnectorNotFound - } - - return connectorManager.scheduler.ListTasks(ctx, q) -} - -func (l *ConnectorsManager[Config]) ReadTaskState(ctx context.Context, connectorID models.ConnectorID, taskID uuid.UUID) (*models.Task, error) { - connectorManager, err := l.getManager(connectorID) - if err != nil { - return nil, ErrConnectorNotFound - } - - return connectorManager.scheduler.ReadTask(ctx, taskID) -} - -func (l *ConnectorsManager[ConnectorConfig]) Reset(ctx context.Context, connectorID models.ConnectorID) error { - connector, err := l.store.GetConnector(ctx, connectorID) - if err != nil { - return newStorageError(err, "getting connector") - } - - config, err := l.readConfig(ctx, connector) - if err != nil { - return err - } - - err = l.Uninstall(ctx, connectorID) - if err != nil { - return err - } - - _, err = l.Install(ctx, connector.Name, config) - if err != nil { - return err - } - - err = l.publisher.Publish(events.TopicPayments, - publish.NewMessage(ctx, l.messages.NewEventResetConnector(connectorID))) - if err != nil { - l.logger(ctx).Errorf("Publishing message: %w", err) - } - - return nil -} - -func (l *ConnectorsManager[ConnectorConfig]) InitiatePayment(ctx context.Context, transfer *models.TransferInitiation) error { - connectorManager, err := l.getManager(transfer.ConnectorID) - if err != nil { - return ErrConnectorNotFound - } - - if err := l.validateAssets(ctx, connectorManager, transfer.ConnectorID, transfer.Asset); err != nil { - return err - } - - detachedCtx, span := detachedCtxWithSpan(ctx, trace.SpanFromContext(ctx), "connectorManager.InitiatePayment", transfer.ConnectorID) - defer span.End() - err = connectorManager.connector.InitiatePayment(task.NewConnectorContext(detachedCtx, connectorManager.scheduler), transfer) - if err != nil { - return fmt.Errorf("initiating transfer: %w", err) - } - - return nil -} - -func (l *ConnectorsManager[ConnectorConfig]) ReversePayment(ctx context.Context, transferReversal *models.TransferReversal) error { - connectorManager, err := l.getManager(transferReversal.ConnectorID) - if err != nil { - return ErrConnectorNotFound - } - - if err := l.validateAssets(ctx, connectorManager, transferReversal.ConnectorID, transferReversal.Asset); err != nil { - return err - } - - err = connectorManager.connector.ReversePayment(task.NewConnectorContext(ctx, connectorManager.scheduler), transferReversal) - if err != nil { - return fmt.Errorf("reversing transfer: %w", err) - } - - return nil -} - -func (l *ConnectorsManager[ConnectorConfig]) CreateExternalBankAccount(ctx context.Context, connectorID models.ConnectorID, bankAccount *models.BankAccount) error { - connectorManager, err := l.getManager(connectorID) - if err != nil { - return ErrConnectorNotFound - } - - detachedCtx, span := detachedCtxWithSpan(ctx, trace.SpanFromContext(ctx), "connectorManager.CreateExternalBankAccount", connectorID) - defer span.End() - err = connectorManager.connector.CreateExternalBankAccount(task.NewConnectorContext(detachedCtx, connectorManager.scheduler), bankAccount) - if err != nil { - switch { - case errors.Is(err, connectors.ErrNotImplemented): - return errors.Wrap(ErrValidation, "bank account creation not implemented for this connector") - default: - return fmt.Errorf("creating bank account: %w", err) - } - } - - return nil -} - -func (l *ConnectorsManager[ConnectorConfig]) CreateWebhookAndContext( - ctx context.Context, - webhook *models.Webhook, -) (context.Context, error) { - connectorManager, err := l.getManager(webhook.ConnectorID) - if err != nil { - return nil, ErrConnectorNotFound - } - - if err := l.store.CreateWebhook(ctx, webhook); err != nil { - return nil, newStorageError(err, "creating webhook") - } - - connectorContext := task.NewConnectorContext(ctx, connectorManager.scheduler) - ctx = task.ContextWithConnectorContext(connectors.ContextWithWebhookID(ctx, webhook.ID), connectorContext) - - return ctx, nil -} - -func (l *ConnectorsManager[ConnectorConfig]) validateAssets( - ctx context.Context, - connectorManager *ConnectorManager, - connectorID models.ConnectorID, - asset models.Asset, -) error { - supportedCurrencies := connectorManager.connector.SupportedCurrenciesAndDecimals() - currency, precision, err := models.GetCurrencyAndPrecisionFromAsset(asset) - if err != nil { - return errors.Wrap(ErrValidation, err.Error()) - } - - supportedPrecision, ok := supportedCurrencies[currency] - if !ok { - return errors.Wrap(ErrValidation, fmt.Sprintf("currency %s not supported", currency)) - } - - if precision != int64(supportedPrecision) { - return errors.Wrap(ErrValidation, fmt.Sprintf("currency %s has precision %d, but %d is required", currency, precision, supportedPrecision)) - } - - return nil -} - -func detachedCtxWithSpan( - ctx context.Context, - parentSpan trace.Span, - spanName string, - connectorID models.ConnectorID, -) (context.Context, trace.Span) { - detachedCtx, _ := contextutil.Detached(ctx) - - ctx, span := otel.Tracer().Start( - detachedCtx, - spanName, - trace.WithLinks(trace.Link{ - SpanContext: parentSpan.SpanContext(), - }), - trace.WithAttributes( - attribute.String("connectorID", connectorID.String()), - ), - ) - - return ctx, span -} - -func (l *ConnectorsManager[ConnectorConfig]) Close(ctx context.Context) error { - for _, connectorManager := range l.connectors { - err := connectorManager.scheduler.Shutdown(ctx) - if err != nil { - return err - } - } - - return nil -} - -func NewConnectorManager[ConnectorConfig models.ConnectorConfigObject]( - provider models.ConnectorProvider, - store Store, - loader Loader[ConnectorConfig], - schedulerFactory TaskSchedulerFactory, - publisher message.Publisher, - messages *messages.Messages, -) *ConnectorsManager[ConnectorConfig] { - return &ConnectorsManager[ConnectorConfig]{ - provider: provider, - connectors: make(map[string]*ConnectorManager), - store: store, - loader: loader, - schedulerFactory: schedulerFactory, - publisher: publisher, - messages: messages, - } -} diff --git a/components/payments/cmd/connectors/internal/api/connectors_manager/manager_test.go b/components/payments/cmd/connectors/internal/api/connectors_manager/manager_test.go deleted file mode 100644 index 6a7f5eaa45..0000000000 --- a/components/payments/cmd/connectors/internal/api/connectors_manager/manager_test.go +++ /dev/null @@ -1,208 +0,0 @@ -package connectors_manager - -import ( - "context" - "testing" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/metrics" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/messages" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/sirupsen/logrus" - "github.com/stretchr/testify/require" - "go.uber.org/dig" -) - -func ChanClosed[T any](ch chan T) bool { - select { - case <-ch: - return true - default: - return false - } -} - -type testContext[ConnectorConfig models.ConnectorConfigObject] struct { - manager *ConnectorsManager[ConnectorConfig] - taskStore task.Repository - connectorStore Store - loader Loader[ConnectorConfig] - provider models.ConnectorProvider -} - -func withManager[ConnectorConfig models.ConnectorConfigObject](builder *ConnectorBuilder, - callback func(ctx *testContext[ConnectorConfig]), -) { - l := logrus.New() - if testing.Verbose() { - l.SetLevel(logrus.DebugLevel) - } - - DefaultContainerFactory := task.ContainerCreateFunc(func(ctx context.Context, descriptor models.TaskDescriptor, taskID uuid.UUID) (*dig.Container, error) { - return dig.New(), nil - }) - - taskStore := task.NewInMemoryStore() - managerStore := NewInMemoryStore() - provider := models.ConnectorProvider(uuid.New().String()) - schedulerFactory := TaskSchedulerFactoryFn(func( - connectorID models.ConnectorID, - resolver task.Resolver, - maxTasks int, - ) *task.DefaultTaskScheduler { - return task.NewDefaultScheduler(connectorID, taskStore, - DefaultContainerFactory, resolver, metrics.NewNoOpMetricsRegistry(), maxTasks) - }) - - loader := NewLoaderBuilder[ConnectorConfig](provider). - WithLoad(func(logger logging.Logger, config ConnectorConfig) connectors.Connector { - return builder.Build() - }). - WithAllowedTasks(1). - Build() - manager := NewConnectorManager[ConnectorConfig](provider, managerStore, loader, schedulerFactory, nil, messages.NewMessages("")) - - callback(&testContext[ConnectorConfig]{ - manager: manager, - taskStore: taskStore, - connectorStore: managerStore, - loader: loader, - provider: provider, - }) -} - -func TestInstallConnector(t *testing.T) { - t.Parallel() - - installed := make(chan struct{}) - builder := NewConnectorBuilder(). - WithInstall(func(ctx task.ConnectorContext) error { - close(installed) - - return nil - }) - withManager(builder, func(tc *testContext[models.EmptyConnectorConfig]) { - _, err := tc.manager.Install(context.TODO(), "test1", models.EmptyConnectorConfig{ - Name: "test1", - }) - require.NoError(t, err) - require.True(t, ChanClosed(installed)) - - _, err = tc.manager.Install(context.TODO(), "test1", models.EmptyConnectorConfig{ - Name: "test1", - }) - require.Equal(t, ErrAlreadyInstalled, err) - - connectors, err := tc.manager.FindAll(context.TODO()) - require.NoError(t, err) - require.Len(t, connectors, 1) - require.Equal(t, "test1", connectors[0].Name) - - isInstalled, err := tc.manager.IsInstalled(context.TODO(), connectors[0].ID) - require.NoError(t, err) - require.True(t, isInstalled) - - err = tc.manager.Uninstall(context.TODO(), connectors[0].ID) - require.NoError(t, err) - - isInstalled, err = tc.manager.IsInstalled(context.TODO(), connectors[0].ID) - require.NoError(t, err) - require.False(t, isInstalled) - }) -} - -func TestUninstallConnector(t *testing.T) { - t.Parallel() - - uninstalled := make(chan struct{}) - taskTerminated := make(chan struct{}) - taskStarted := make(chan struct{}) - builder := NewConnectorBuilder(). - WithResolve(func(name models.TaskDescriptor) task.Task { - return func(ctx context.Context, stopChan task.StopChan) { - close(taskStarted) - defer close(taskTerminated) - select { - case flag := <-stopChan: - flag <- struct{}{} - case <-ctx.Done(): - } - } - }). - WithInstall(func(ctx task.ConnectorContext) error { - return ctx.Scheduler().Schedule(ctx.Context(), []byte(uuid.New().String()), models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_NEVER, - }) - }). - WithUninstall(func(ctx context.Context) error { - close(uninstalled) - - return nil - }) - withManager(builder, func(tc *testContext[models.EmptyConnectorConfig]) { - _, err := tc.manager.Install(context.TODO(), "test1", models.EmptyConnectorConfig{ - Name: "test1", - }) - require.NoError(t, err) - <-taskStarted - - connectors, err := tc.manager.FindAll(context.TODO()) - require.NoError(t, err) - require.Len(t, connectors, 1) - require.Equal(t, "test1", connectors[0].Name) - - require.NoError(t, tc.manager.Uninstall(context.TODO(), connectors[0].ID)) - require.True(t, ChanClosed(uninstalled)) - // TODO: We need to give a chance to the connector to properly stop execution - require.True(t, ChanClosed(taskTerminated)) - - isInstalled, err := tc.manager.IsInstalled(context.TODO(), connectors[0].ID) - require.NoError(t, err) - require.False(t, isInstalled) - }) -} - -func TestRestoreConnector(t *testing.T) { - t.Parallel() - - builder := NewConnectorBuilder() - withManager(builder, func(tc *testContext[models.EmptyConnectorConfig]) { - cfg, err := models.EmptyConnectorConfig{ - Name: "test1", - }.Marshal() - require.NoError(t, err) - - connector := &models.Connector{ - ID: models.ConnectorID{ - Provider: tc.provider, - Reference: uuid.New(), - }, - Name: "test1", - Provider: tc.provider, - } - - err = tc.connectorStore.Install(context.TODO(), connector, cfg) - require.NoError(t, err) - - err = tc.manager.Restore(context.TODO()) - require.NoError(t, err) - require.Len(t, tc.manager.Connectors(), 1) - - require.NoError(t, tc.manager.Uninstall(context.TODO(), connector.ID)) - }) -} - -func TestRestoreNotInstalledConnector(t *testing.T) { - t.Parallel() - - builder := NewConnectorBuilder() - withManager(builder, func(tc *testContext[models.EmptyConnectorConfig]) { - err := tc.manager.Restore(context.TODO()) - require.NoError(t, err) - require.Len(t, tc.manager.Connectors(), 0) - }) -} diff --git a/components/payments/cmd/connectors/internal/api/connectors_manager/store.go b/components/payments/cmd/connectors/internal/api/connectors_manager/store.go deleted file mode 100644 index e1cb8570e9..0000000000 --- a/components/payments/cmd/connectors/internal/api/connectors_manager/store.go +++ /dev/null @@ -1,19 +0,0 @@ -package connectors_manager - -import ( - "context" - "encoding/json" - - "github.com/formancehq/payments/internal/models" -) - -type Store interface { - ListConnectors(ctx context.Context) ([]*models.Connector, error) - IsInstalledByConnectorID(ctx context.Context, connectorID models.ConnectorID) (bool, error) - IsInstalledByConnectorName(ctx context.Context, name string) (bool, error) - Install(ctx context.Context, connector *models.Connector, config json.RawMessage) error - Uninstall(ctx context.Context, connectorID models.ConnectorID) error - UpdateConfig(ctx context.Context, connectorID models.ConnectorID, config json.RawMessage) error - GetConnector(ctx context.Context, connectorID models.ConnectorID) (*models.Connector, error) - CreateWebhook(ctx context.Context, webhook *models.Webhook) error -} diff --git a/components/payments/cmd/connectors/internal/api/connectors_manager/storememory_test.go b/components/payments/cmd/connectors/internal/api/connectors_manager/storememory_test.go deleted file mode 100644 index 6da72f4e41..0000000000 --- a/components/payments/cmd/connectors/internal/api/connectors_manager/storememory_test.go +++ /dev/null @@ -1,137 +0,0 @@ -package connectors_manager - -import ( - "context" - "encoding/json" - "sync" - - "github.com/formancehq/payments/internal/models" -) - -type connector struct { - name string - id models.ConnectorID - config json.RawMessage - provider models.ConnectorProvider -} - -type InMemoryConnectorStore struct { - connectorsByID map[string]*connector - connectorsByName map[string]*connector - mu sync.RWMutex -} - -func (i *InMemoryConnectorStore) Uninstall(ctx context.Context, connectorID models.ConnectorID) error { - i.mu.Lock() - defer i.mu.Unlock() - - connector, ok := i.connectorsByID[connectorID.String()] - if !ok { - return nil - } - - delete(i.connectorsByID, connectorID.String()) - delete(i.connectorsByName, connector.name) - - return nil -} - -func (i *InMemoryConnectorStore) ListConnectors(_ context.Context) ([]*models.Connector, error) { - i.mu.RLock() - defer i.mu.RUnlock() - - connectors := make([]*models.Connector, 0, len(i.connectorsByID)) - for _, c := range i.connectorsByID { - connectors = append(connectors, &models.Connector{ - ID: c.id, - Name: c.name, - Config: c.config, - Provider: c.provider, - }) - } - return connectors, nil -} - -func (i *InMemoryConnectorStore) IsInstalledByConnectorID(ctx context.Context, connectorID models.ConnectorID) (bool, error) { - i.mu.RLock() - defer i.mu.RUnlock() - - _, ok := i.connectorsByID[connectorID.String()] - return ok, nil -} - -func (i *InMemoryConnectorStore) IsInstalledByConnectorName(ctx context.Context, name string) (bool, error) { - i.mu.RLock() - defer i.mu.RUnlock() - - _, ok := i.connectorsByName[name] - return ok, nil -} - -func (i *InMemoryConnectorStore) Install(ctx context.Context, newConnector *models.Connector, config json.RawMessage) error { - i.mu.Lock() - defer i.mu.Unlock() - - c := &connector{ - name: newConnector.Name, - id: newConnector.ID, - config: config, - provider: newConnector.Provider, - } - - i.connectorsByID[newConnector.ID.String()] = c - i.connectorsByName[newConnector.Name] = c - - return nil -} - -func (i *InMemoryConnectorStore) UpdateConfig(ctx context.Context, connectorID models.ConnectorID, config json.RawMessage) error { - i.mu.Lock() - defer i.mu.Unlock() - - i.connectorsByID[connectorID.String()].config = config - return nil -} - -func (i *InMemoryConnectorStore) GetConnector(ctx context.Context, connectorID models.ConnectorID) (*models.Connector, error) { - i.mu.RLock() - defer i.mu.RUnlock() - - c, ok := i.connectorsByID[connectorID.String()] - if !ok { - return nil, ErrNotFound - } - - return &models.Connector{ - ID: c.id, - Name: c.name, - Config: c.config, - Provider: c.provider, - }, nil -} - -func (i *InMemoryConnectorStore) ReadConfig(ctx context.Context, connectorID models.ConnectorID, to interface{}) error { - connector, err := i.GetConnector(ctx, connectorID) - if err != nil { - return err - } - - if err = connector.ParseConfig(to); err != nil { - return err - } - - return nil -} - -func (i *InMemoryConnectorStore) CreateWebhook(ctx context.Context, webhook *models.Webhook) error { - return nil -} - -var _ Store = &InMemoryConnectorStore{} - -func NewInMemoryStore() *InMemoryConnectorStore { - return &InMemoryConnectorStore{ - connectorsByID: make(map[string]*connector), - connectorsByName: make(map[string]*connector), - } -} diff --git a/components/payments/cmd/connectors/internal/api/connectors_manager/taskscheduler.go b/components/payments/cmd/connectors/internal/api/connectors_manager/taskscheduler.go deleted file mode 100644 index 86d54ce98a..0000000000 --- a/components/payments/cmd/connectors/internal/api/connectors_manager/taskscheduler.go +++ /dev/null @@ -1,16 +0,0 @@ -package connectors_manager - -import ( - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" -) - -type TaskSchedulerFactory interface { - Make(connectorID models.ConnectorID, resolver task.Resolver, maxTasks int) *task.DefaultTaskScheduler -} - -type TaskSchedulerFactoryFn func(connectorID models.ConnectorID, resolver task.Resolver, maxProcesses int) *task.DefaultTaskScheduler - -func (fn TaskSchedulerFactoryFn) Make(connectorID models.ConnectorID, resolver task.Resolver, maxTasks int) *task.DefaultTaskScheduler { - return fn(connectorID, resolver, maxTasks) -} diff --git a/components/payments/cmd/connectors/internal/api/health.go b/components/payments/cmd/connectors/internal/api/health.go deleted file mode 100644 index 9ed5c10471..0000000000 --- a/components/payments/cmd/connectors/internal/api/health.go +++ /dev/null @@ -1,25 +0,0 @@ -package api - -import ( - "net/http" - - "github.com/formancehq/payments/cmd/connectors/internal/api/backend" -) - -func healthHandler(b backend.ServiceBackend) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if err := b.GetService().Ping(); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - - return - } - - w.WriteHeader(http.StatusOK) - } -} - -func liveHandler() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - } -} diff --git a/components/payments/cmd/connectors/internal/api/module.go b/components/payments/cmd/connectors/internal/api/module.go deleted file mode 100644 index 7a1d4d1d1a..0000000000 --- a/components/payments/cmd/connectors/internal/api/module.go +++ /dev/null @@ -1,166 +0,0 @@ -package api - -import ( - "context" - "errors" - "net/http" - "runtime/debug" - - "github.com/formancehq/go-libs/auth" - "github.com/formancehq/go-libs/logging" - - "github.com/formancehq/go-libs/api" - "github.com/formancehq/go-libs/httpserver" - "github.com/formancehq/go-libs/otlp" - "github.com/formancehq/payments/cmd/connectors/internal/api/backend" - manager "github.com/formancehq/payments/cmd/connectors/internal/api/connectors_manager" - "github.com/formancehq/payments/cmd/connectors/internal/api/service" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/adyen" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/atlar" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/bankingcircle" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currencycloud" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/dummypay" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/generic" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/mangopay" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/modulr" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/moneycorp" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/stripe" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/wise" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/internal/messages" - "github.com/formancehq/payments/internal/models" - "github.com/gorilla/mux" - "github.com/rs/cors" - "github.com/sirupsen/logrus" - "go.uber.org/fx" -) - -const ( - serviceName = "Payments" - - ErrUniqueReference = "CONFLICT" - ErrNotFound = "NOT_FOUND" - ErrInvalidID = "INVALID_ID" - ErrMissingOrInvalidBody = "MISSING_OR_INVALID_BODY" - ErrValidation = "VALIDATION" -) - -func HTTPModule(serviceInfo api.ServiceInfo, bind, stackURL string, otelTraces bool) fx.Option { - return fx.Options( - fx.Invoke(func(m *mux.Router, lc fx.Lifecycle) { - lc.Append(httpserver.NewHook(m, httpserver.WithAddress(bind))) - }), - fx.Supply(serviceInfo), - fx.Provide(fx.Annotate(connectorsHandlerMap, fx.ParamTags(`group:"connectorHandlers"`))), - fx.Provide(func(store *storage.Storage) service.Store { - return store - }), - fx.Provide(fx.Annotate(service.New, fx.As(new(backend.Service)))), - fx.Provide(backend.NewDefaultBackend), - fx.Provide(fx.Annotate(func( - logger logging.Logger, - b backend.ServiceBackend, - serviceInfo api.ServiceInfo, - a auth.Authenticator, - connectorHandlers []connectorHandler, - ) *mux.Router { - return httpRouter(logger, b, serviceInfo, a, connectorHandlers, otelTraces) - }, fx.ParamTags(``, ``, ``, ``, `group:"connectorHandlers"`))), - fx.Provide(func() *messages.Messages { - return messages.NewMessages(stackURL) - }), - addConnector[dummypay.Config](dummypay.NewLoader()), - addConnector[modulr.Config](modulr.NewLoader()), - addConnector[stripe.Config](stripe.NewLoader()), - addConnector[wise.Config](wise.NewLoader()), - addConnector[currencycloud.Config](currencycloud.NewLoader()), - addConnector[bankingcircle.Config](bankingcircle.NewLoader()), - addConnector[mangopay.Config](mangopay.NewLoader()), - addConnector[moneycorp.Config](moneycorp.NewLoader()), - addConnector[atlar.Config](atlar.NewLoader()), - addConnector[adyen.Config](adyen.NewLoader()), - addConnector[generic.Config](generic.NewLoader()), - ) -} - -func connectorsHandlerMap(connectorHandlers []connectorHandler) map[models.ConnectorProvider]*service.ConnectorHandlers { - m := make(map[models.ConnectorProvider]*service.ConnectorHandlers) - for _, h := range connectorHandlers { - if handlers, ok := m[h.Provider]; ok { - handlers.InitiatePaymentHandler = h.initiatePayment - handlers.ReversePaymentHandler = h.reversePayment - handlers.BankAccountHandler = h.createExternalBankAccount - } else { - m[h.Provider] = &service.ConnectorHandlers{ - InitiatePaymentHandler: h.initiatePayment, - ReversePaymentHandler: h.reversePayment, - BankAccountHandler: h.createExternalBankAccount, - } - } - } - return m -} - -func httpRecoveryFunc(otelTraces bool) func(context.Context, interface{}) { - return func(ctx context.Context, e interface{}) { - if otelTraces { - otlp.RecordAsError(ctx, e) - } else { - logrus.Errorln(e) - debug.PrintStack() - } - } -} - -func httpCorsHandler() func(http.Handler) http.Handler { - return cors.New(cors.Options{ - AllowedOrigins: []string{"*"}, - AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut}, - AllowCredentials: true, - }).Handler -} - -func httpServeFunc(handler http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - handler.ServeHTTP(w, r) - }) -} - -func handleServiceErrors(w http.ResponseWriter, r *http.Request, err error) { - switch { - case errors.Is(err, storage.ErrDuplicateKeyValue): - api.BadRequest(w, ErrUniqueReference, err) - case errors.Is(err, storage.ErrNotFound): - api.NotFound(w, err) - case errors.Is(err, service.ErrValidation): - api.BadRequest(w, ErrValidation, err) - case errors.Is(err, service.ErrInvalidID): - api.BadRequest(w, ErrInvalidID, err) - case errors.Is(err, service.ErrPublish): - api.InternalServerError(w, r, err) - default: - api.InternalServerError(w, r, err) - } -} - -func handleConnectorsManagerErrors(w http.ResponseWriter, r *http.Request, err error) { - switch { - case errors.Is(err, storage.ErrDuplicateKeyValue): - api.BadRequest(w, ErrUniqueReference, err) - case errors.Is(err, storage.ErrNotFound): - api.NotFound(w, err) - case errors.Is(err, manager.ErrAlreadyInstalled): - api.BadRequest(w, ErrValidation, err) - case errors.Is(err, manager.ErrNotInstalled): - api.BadRequest(w, ErrValidation, err) - case errors.Is(err, manager.ErrConnectorNotFound): - api.BadRequest(w, ErrValidation, err) - case errors.Is(err, manager.ErrNotFound): - api.NotFound(w, err) - case errors.Is(err, manager.ErrValidation): - api.BadRequest(w, ErrValidation, err) - default: - api.InternalServerError(w, r, err) - } -} diff --git a/components/payments/cmd/connectors/internal/api/read_connectors.go b/components/payments/cmd/connectors/internal/api/read_connectors.go deleted file mode 100644 index cf788a3000..0000000000 --- a/components/payments/cmd/connectors/internal/api/read_connectors.go +++ /dev/null @@ -1,56 +0,0 @@ -package api - -import ( - "encoding/json" - "net/http" - - "github.com/formancehq/go-libs/api" - "github.com/formancehq/payments/cmd/connectors/internal/api/backend" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "go.opentelemetry.io/otel/attribute" -) - -type readConnectorsResponseElement struct { - Provider models.ConnectorProvider `json:"provider" bson:"provider"` - ConnectorID string `json:"connectorID" bson:"connectorID"` - Name string `json:"name" bson:"name"` - Enabled bool `json:"enabled" bson:"enabled"` -} - -func readConnectorsHandler(b backend.ServiceBackend) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "readConnectorsHandler") - defer span.End() - - res, err := b.GetService().ListConnectors(ctx) - if err != nil { - otel.RecordError(span, err) - handleServiceErrors(w, r, err) - return - } - - span.SetAttributes(attribute.Int("count", len(res))) - - data := make([]readConnectorsResponseElement, len(res)) - - for i := range res { - data[i] = readConnectorsResponseElement{ - Provider: res[i].Provider, - ConnectorID: res[i].ID.String(), - Name: res[i].Name, - Enabled: true, - } - } - - err = json.NewEncoder(w).Encode( - api.BaseResponse[[]readConnectorsResponseElement]{ - Data: &data, - }) - if err != nil { - otel.RecordError(span, err) - api.InternalServerError(w, r, err) - return - } - } -} diff --git a/components/payments/cmd/connectors/internal/api/read_connectors_test.go b/components/payments/cmd/connectors/internal/api/read_connectors_test.go deleted file mode 100644 index 0648c6970d..0000000000 --- a/components/payments/cmd/connectors/internal/api/read_connectors_test.go +++ /dev/null @@ -1,134 +0,0 @@ -package api - -import ( - "net/http" - "net/http/httptest" - "testing" - "time" - - sharedapi "github.com/formancehq/go-libs/api" - "github.com/formancehq/go-libs/auth" - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/api/service" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/pkg/errors" - "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" -) - -func TestReadConnectors(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - expectedStatusCode int - expectedErrorCode string - serviceError error - } - - testCases := []testCase{ - { - name: "nominal", - }, - { - name: "service error duplicate key", - serviceError: storage.ErrDuplicateKeyValue, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - }, - { - name: "service error not found", - serviceError: storage.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - }, - { - name: "service error validation", - serviceError: service.ErrValidation, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "service error invalid ID", - serviceError: service.ErrInvalidID, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrInvalidID, - }, - { - name: "service error publish", - serviceError: service.ErrPublish, - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - { - name: "service error unknown", - serviceError: errors.New("unknown"), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusOK - } - - listConnectorsResponse := []*models.Connector{ - { - ID: models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - }, - Name: "c1", - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Provider: models.ConnectorProviderDummyPay, - }, - } - - expectedListConnectorsResponse := []readConnectorsResponseElement{ - { - Provider: listConnectorsResponse[0].Provider, - ConnectorID: listConnectorsResponse[0].ID.String(), - Name: "c1", - Enabled: true, - }, - } - - backend, mockService := newServiceTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockService.EXPECT(). - ListConnectors(gomock.Any()). - Return(listConnectorsResponse, nil) - } - if testCase.serviceError != nil { - mockService.EXPECT(). - ListConnectors(gomock.Any()). - Return(nil, testCase.serviceError) - } - - router := httpRouter(logging.Testing(), backend, sharedapi.ServiceInfo{}, auth.NewNoAuth(), nil, false) - - req := httptest.NewRequest(http.MethodGet, "/connectors", nil) - rec := httptest.NewRecorder() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - var resp sharedapi.BaseResponse[[]readConnectorsResponseElement] - sharedapi.Decode(t, rec.Body, &resp) - require.Equal(t, &expectedListConnectorsResponse, resp.Data) - } else { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } -} diff --git a/components/payments/cmd/connectors/internal/api/recovery.go b/components/payments/cmd/connectors/internal/api/recovery.go deleted file mode 100644 index fce3087e1c..0000000000 --- a/components/payments/cmd/connectors/internal/api/recovery.go +++ /dev/null @@ -1,20 +0,0 @@ -package api - -import ( - "context" - "net/http" -) - -func recoveryHandler(reporter func(ctx context.Context, e interface{})) func(h http.Handler) http.Handler { - return func(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer func() { - if e := recover(); e != nil { - w.WriteHeader(http.StatusInternalServerError) - reporter(r.Context(), e) - } - }() - h.ServeHTTP(w, r) - }) - } -} diff --git a/components/payments/cmd/connectors/internal/api/router.go b/components/payments/cmd/connectors/internal/api/router.go deleted file mode 100644 index f367ff656c..0000000000 --- a/components/payments/cmd/connectors/internal/api/router.go +++ /dev/null @@ -1,157 +0,0 @@ -package api - -import ( - "net/http" - - "github.com/formancehq/go-libs/api" - "github.com/formancehq/go-libs/auth" - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/api/backend" - "github.com/formancehq/payments/internal/models" - "github.com/gorilla/mux" - "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux" -) - -func httpRouter( - logger logging.Logger, - b backend.ServiceBackend, - serviceInfo api.ServiceInfo, - a auth.Authenticator, - connectorHandlers []connectorHandler, - otelTraces bool, -) *mux.Router { - rootMux := mux.NewRouter() - - // We have to keep this recovery handler here to ensure that the health - // endpoint is not panicking - rootMux.Use(recoveryHandler(httpRecoveryFunc(otelTraces))) - rootMux.Use(httpCorsHandler()) - rootMux.Use(httpServeFunc) - rootMux.Use(func(handler http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - handler.ServeHTTP(w, r.WithContext(logging.ContextWithLogger(r.Context(), logger))) - }) - }) - - rootMux.Path("/_health").Handler(healthHandler(b)) - - subRouter := rootMux.NewRoute().Subrouter() - if otelTraces { - subRouter.Use(otelmux.Middleware(serviceName)) - // Add a second recovery handler to ensure that the otel middleware - // is catching the error in the trace - rootMux.Use(recoveryHandler(httpRecoveryFunc(otelTraces))) - } - subRouter.Path("/_live").Handler(liveHandler()) - subRouter.Path("/_info").Handler(api.InfoHandler(serviceInfo)) - - authGroup := subRouter.Name("authenticated").Subrouter() - authGroup.Use(auth.Middleware(a)) - - authGroup.Path("/bank-accounts").Methods(http.MethodPost).Handler(createBankAccountHandler(b)) - authGroup.Path("/bank-accounts/{bankAccountID}/forward").Methods(http.MethodPost).Handler(forwardBankAccountToConnector(b)) - authGroup.Path("/bank-accounts/{bankAccountID}/metadata").Methods(http.MethodPatch).Handler(updateBankAccountMetadataHandler(b)) - - authGroup.Path("/transfer-initiations").Methods(http.MethodPost).Handler(createTransferInitiationHandler(b)) - authGroup.Path("/transfer-initiations/{transferID}/status").Methods(http.MethodPost).Handler(updateTransferInitiationStatusHandler(b)) - authGroup.Path("/transfer-initiations/{transferID}/retry").Methods(http.MethodPost).Handler(retryTransferInitiationHandler(b)) - authGroup.Path("/transfer-initiations/{transferID}/reverse").Methods(http.MethodPost).Handler(reverseTransferInitiationHandler(b)) - authGroup.Path("/transfer-initiations/{transferID}").Methods(http.MethodDelete).Handler(deleteTransferInitiationHandler(b)) - - authGroup.HandleFunc("/connectors", readConnectorsHandler(b)) - - connectorGroupAuthenticated := authGroup.PathPrefix("/connectors").Subrouter() - connectorGroupAuthenticated.Path("/configs").Handler(connectorConfigsHandler()) - - // Needed for webhooks - connectorGroupUnauthenticated := subRouter.PathPrefix("/connectors").Subrouter() - - for _, h := range connectorHandlers { - connectorGroupAuthenticated.PathPrefix("/" + h.Provider.String()).Handler( - http.StripPrefix("/connectors", h.Handler)) - - connectorGroupAuthenticated.PathPrefix("/" + h.Provider.StringLower()).Handler( - http.StripPrefix("/connectors", h.Handler)) - - if h.WebhookHandler != nil { - connectorGroupUnauthenticated.PathPrefix("/webhooks/" + h.Provider.String()).Handler( - http.StripPrefix("/connectors", h.WebhookHandler)) - - connectorGroupUnauthenticated.PathPrefix("/webhooks/" + h.Provider.StringLower()).Handler( - http.StripPrefix("/connectors", h.WebhookHandler)) - } - } - - return rootMux -} - -func connectorRouter[Config models.ConnectorConfigObject]( - provider models.ConnectorProvider, - b backend.ManagerBackend[Config], -) *mux.Router { - r := mux.NewRouter() - - addRoute(r, provider, "", http.MethodPost, install(b)) - addRoute(r, provider, "/{connectorID}", http.MethodDelete, uninstall(b, V1)) - addRoute(r, provider, "/{connectorID}/config", http.MethodGet, readConfig(b, V1)) - addRoute(r, provider, "/{connectorID}/config", http.MethodPost, updateConfig(b, V1)) - addRoute(r, provider, "/{connectorID}/reset", http.MethodPost, reset(b, V1)) - addRoute(r, provider, "/{connectorID}/tasks", http.MethodGet, listTasks(b, V1)) - addRoute(r, provider, "/{connectorID}/tasks/{taskID}", http.MethodGet, readTask(b, V1)) - - // Deprecated routes - addRoute(r, provider, "", http.MethodDelete, uninstall(b, V0)) - addRoute(r, provider, "/config", http.MethodGet, readConfig(b, V0)) - addRoute(r, provider, "/reset", http.MethodPost, reset(b, V0)) - addRoute(r, provider, "/tasks", http.MethodGet, listTasks(b, V0)) - addRoute(r, provider, "/tasks/{taskID}", http.MethodGet, readTask(b, V0)) - - return r -} - -func webhookConnectorRouter[Config models.ConnectorConfigObject]( - provider models.ConnectorProvider, - connectorRouter *mux.Router, - b backend.ManagerBackend[Config], -) *mux.Router { - if connectorRouter == nil { - return nil - } - - r := mux.NewRouter() - - group := r.PathPrefix("/webhooks/" + provider.String() + "/{connectorID}").Subrouter() - group.Use(webhooksMiddleware(b, V1)) - addWebhookRoute(group, connectorRouter) - - groupLower := r.PathPrefix("/webhooks/" + provider.StringLower() + "/{connectorID}").Subrouter() - groupLower.Use(webhooksMiddleware(b, V1)) - addWebhookRoute(groupLower, connectorRouter) - - return r -} - -func addRoute(r *mux.Router, provider models.ConnectorProvider, path, method string, handler http.Handler) { - r.Path("/" + provider.String() + path).Methods(method).Handler(handler) - r.Path("/" + provider.StringLower() + path).Methods(method).Handler(handler) -} - -func addWebhookRoute(r *mux.Router, subRouter *mux.Router) { - subRouter.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { - pathTemplate, err := route.GetPathTemplate() - if err != nil { - return err - } - - methods, err := route.GetMethods() - if err != nil { - return err - } - - for _, method := range methods { - r.Path(pathTemplate).Methods(method).Handler(route.GetHandler()) - } - - return nil - }) -} diff --git a/components/payments/cmd/connectors/internal/api/service/bank_account.go b/components/payments/cmd/connectors/internal/api/service/bank_account.go deleted file mode 100644 index 634877a35d..0000000000 --- a/components/payments/cmd/connectors/internal/api/service/bank_account.go +++ /dev/null @@ -1,190 +0,0 @@ -package service - -import ( - "context" - "time" - - manager "github.com/formancehq/payments/cmd/connectors/internal/api/connectors_manager" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/pkg/errors" -) - -type CreateBankAccountRequest struct { - AccountNumber string `json:"accountNumber"` - IBAN string `json:"iban"` - SwiftBicCode string `json:"swiftBicCode"` - Country string `json:"country"` - ConnectorID string `json:"connectorID"` - Name string `json:"name"` - Metadata map[string]string `json:"metadata"` -} - -func (c *CreateBankAccountRequest) Validate() error { - if c.AccountNumber == "" && c.IBAN == "" { - return errors.New("either accountNumber or iban must be provided") - } - - if c.Name == "" { - return errors.New("name must be provided") - } - - if c.Country == "" { - return errors.New("country must be provided") - } - - return nil -} - -func (s *Service) CreateBankAccount(ctx context.Context, req *CreateBankAccountRequest) (*models.BankAccount, error) { - var handlers *ConnectorHandlers - var connectorID models.ConnectorID - if req.ConnectorID != "" { - var err error - connectorID, err = models.ConnectorIDFromString(req.ConnectorID) - if err != nil { - return nil, errors.Wrap(ErrValidation, err.Error()) - } - - connector, err := s.store.GetConnector(ctx, connectorID) - if err != nil && errors.Is(err, storage.ErrNotFound) { - return nil, errors.Wrap(ErrValidation, "connector not installed") - } else if err != nil { - return nil, newStorageError(err, "getting connector") - } - - var ok bool - handlers, ok = s.connectorHandlers[connector.Provider] - if !ok || handlers.BankAccountHandler == nil { - return nil, errors.Wrap(ErrValidation, "no bank account handler for connector") - } - } - - bankAccount := &models.BankAccount{ - CreatedAt: time.Now().UTC(), - AccountNumber: req.AccountNumber, - IBAN: req.IBAN, - SwiftBicCode: req.SwiftBicCode, - Country: req.Country, - Name: req.Name, - Metadata: req.Metadata, - } - err := s.store.CreateBankAccount(ctx, bankAccount) - if err != nil { - return nil, newStorageError(err, "creating bank account") - } - - if handlers != nil { - if err := handlers.BankAccountHandler(ctx, connectorID, bankAccount); err != nil { - switch { - case errors.Is(err, manager.ErrValidation): - return nil, errors.Wrap(ErrValidation, err.Error()) - case errors.Is(err, manager.ErrConnectorNotFound): - return nil, errors.Wrap(ErrValidation, err.Error()) - default: - return nil, err - } - } - - relatedAccounts, err := s.store.GetBankAccountRelatedAccounts(ctx, bankAccount.ID) - if err != nil { - return nil, newStorageError(err, "fetching bank account") - } - - bankAccount.RelatedAccounts = relatedAccounts - } - - return bankAccount, nil -} - -type ForwardBankAccountToConnectorRequest struct { - ConnectorID string `json:"connectorID"` -} - -func (f *ForwardBankAccountToConnectorRequest) Validate() error { - if f.ConnectorID == "" { - return errors.New("connectorID must be provided") - } - - return nil -} - -func (s *Service) ForwardBankAccountToConnector(ctx context.Context, id string, req *ForwardBankAccountToConnectorRequest) (*models.BankAccount, error) { - bankAccountID, err := uuid.Parse(id) - if err != nil { - return nil, errors.Wrap(ErrInvalidID, err.Error()) - } - - connectorID, err := models.ConnectorIDFromString(req.ConnectorID) - if err != nil { - return nil, errors.Wrap(ErrValidation, err.Error()) - } - - connector, err := s.store.GetConnector(ctx, connectorID) - if err != nil && errors.Is(err, storage.ErrNotFound) { - return nil, errors.Wrap(ErrValidation, "connector not installed") - } else if err != nil { - return nil, newStorageError(err, "getting connector") - } - - handlers, ok := s.connectorHandlers[connector.Provider] - if !ok || handlers.BankAccountHandler == nil { - return nil, errors.Wrap(ErrValidation, "no bank account handler for connector") - } - - bankAccount, err := s.store.GetBankAccount(ctx, bankAccountID, true) - if err != nil { - return nil, newStorageError(err, "fetching bank account") - } - - for _, relatedAccount := range bankAccount.RelatedAccounts { - if relatedAccount.ConnectorID == connectorID { - return nil, errors.Wrap(ErrValidation, "bank account already forwarded to connector") - } - } - - if err := handlers.BankAccountHandler(ctx, connectorID, bankAccount); err != nil { - switch { - case errors.Is(err, manager.ErrValidation): - return nil, errors.Wrap(ErrValidation, err.Error()) - case errors.Is(err, manager.ErrConnectorNotFound): - return nil, errors.Wrap(ErrValidation, err.Error()) - default: - return nil, err - } - } - - relatedAccounts, err := s.store.GetBankAccountRelatedAccounts(ctx, bankAccount.ID) - if err != nil { - return nil, newStorageError(err, "fetching bank account") - } - bankAccount.RelatedAccounts = relatedAccounts - - return bankAccount, err -} - -type UpdateBankAccountMetadataRequest struct { - Metadata map[string]string `json:"metadata"` -} - -func (u *UpdateBankAccountMetadataRequest) Validate() error { - if len(u.Metadata) == 0 { - return errors.New("metadata must be provided") - } - - return nil -} - -func (s *Service) UpdateBankAccountMetadata(ctx context.Context, id string, req *UpdateBankAccountMetadataRequest) error { - bankAccountID, err := uuid.Parse(id) - if err != nil { - return errors.Wrap(ErrInvalidID, err.Error()) - } - - if err := s.store.UpdateBankAccountMetadata(ctx, bankAccountID, req.Metadata); err != nil { - return newStorageError(err, "updating bank account metadata") - } - - return nil -} diff --git a/components/payments/cmd/connectors/internal/api/service/bank_account_test.go b/components/payments/cmd/connectors/internal/api/service/bank_account_test.go deleted file mode 100644 index 46a0bdbc24..0000000000 --- a/components/payments/cmd/connectors/internal/api/service/bank_account_test.go +++ /dev/null @@ -1,411 +0,0 @@ -package service - -import ( - "context" - "testing" - "time" - - manager "github.com/formancehq/payments/cmd/connectors/internal/api/connectors_manager" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/internal/messages" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/pkg/errors" - "github.com/stretchr/testify/require" -) - -func TestCreateBankAccounts(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - req *CreateBankAccountRequest - expectedError error - noBankAccountCreateHandler bool - errorBankAccountCreateHandler error - } - - connectorNotFound := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderCurrencyCloud, - } - - var ErrOther = errors.New("other error") - testCases := []testCase{ - { - name: "nominal", - req: &CreateBankAccountRequest{ - AccountNumber: "0112345678", - IBAN: "FR7630006000011234567890189", - SwiftBicCode: "HBUKGB4B", - Country: "FR", - ConnectorID: connectorDummyPay.ID.String(), - Name: "test_nominal", - }, - }, - { - name: "nominal with metadata", - req: &CreateBankAccountRequest{ - AccountNumber: "0112345678", - IBAN: "FR7630006000011234567890189", - SwiftBicCode: "HBUKGB4B", - Country: "FR", - ConnectorID: connectorDummyPay.ID.String(), - Name: "test_nominal_metadata", - Metadata: map[string]string{"test": "metadata"}, - }, - }, - { - name: "nominal without connectorID", - req: &CreateBankAccountRequest{ - AccountNumber: "0112345678", - IBAN: "FR7630006000011234567890189", - SwiftBicCode: "HBUKGB4B", - Country: "FR", - Name: "test_nominal", - }, - }, - { - name: "invalid connector id", - req: &CreateBankAccountRequest{ - AccountNumber: "0112345678", - IBAN: "FR7630006000011234567890189", - SwiftBicCode: "HBUKGB4B", - Country: "FR", - ConnectorID: "invalid", - Name: "test_nominal", - }, - expectedError: ErrValidation, - }, - { - name: "connector not found", - req: &CreateBankAccountRequest{ - AccountNumber: "0112345678", - IBAN: "FR7630006000011234567890189", - SwiftBicCode: "HBUKGB4B", - Country: "FR", - ConnectorID: connectorNotFound.String(), - Name: "test_nominal", - }, - expectedError: ErrValidation, - }, - { - name: "no connector handler", - req: &CreateBankAccountRequest{ - AccountNumber: "0112345678", - IBAN: "FR7630006000011234567890189", - SwiftBicCode: "HBUKGB4B", - Country: "FR", - ConnectorID: connectorDummyPay.ID.String(), - Name: "test_nominal", - }, - noBankAccountCreateHandler: true, - expectedError: ErrValidation, - }, - { - name: "connector handler error validation", - req: &CreateBankAccountRequest{ - AccountNumber: "0112345678", - IBAN: "FR7630006000011234567890189", - SwiftBicCode: "HBUKGB4B", - Country: "FR", - ConnectorID: connectorDummyPay.ID.String(), - Name: "test_nominal", - }, - errorBankAccountCreateHandler: manager.ErrValidation, - expectedError: ErrValidation, - }, - { - name: "connector handler error connector not found", - req: &CreateBankAccountRequest{ - AccountNumber: "0112345678", - IBAN: "FR7630006000011234567890189", - SwiftBicCode: "HBUKGB4B", - Country: "FR", - ConnectorID: connectorDummyPay.ID.String(), - Name: "test_nominal", - }, - errorBankAccountCreateHandler: manager.ErrConnectorNotFound, - expectedError: ErrValidation, - }, - { - name: "connector handler other error", - req: &CreateBankAccountRequest{ - AccountNumber: "0112345678", - IBAN: "FR7630006000011234567890189", - SwiftBicCode: "HBUKGB4B", - Country: "FR", - ConnectorID: connectorDummyPay.ID.String(), - Name: "test_nominal", - }, - errorBankAccountCreateHandler: ErrOther, - expectedError: ErrOther, - }, - } - - for _, tc := range testCases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - var handlers map[models.ConnectorProvider]*ConnectorHandlers - if !tc.noBankAccountCreateHandler { - handlers = map[models.ConnectorProvider]*ConnectorHandlers{ - models.ConnectorProviderDummyPay: { - BankAccountHandler: func(ctx context.Context, connectorID models.ConnectorID, bankAccount *models.BankAccount) error { - if tc.errorBankAccountCreateHandler != nil { - return tc.errorBankAccountCreateHandler - } - - return nil - }, - }, - } - } - - service := New(&MockStore{}, &MockPublisher{}, messages.NewMessages(""), handlers) - - _, err := service.CreateBankAccount(context.Background(), tc.req) - if tc.expectedError != nil { - require.True(t, errors.Is(err, tc.expectedError)) - } else { - require.NoError(t, err) - } - }) - } -} - -func TestForwardBankAccountToConnector(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - bankAccountID string - req *ForwardBankAccountToConnectorRequest - withBankAccountRelatedAccounts []*models.BankAccountRelatedAccount - expectedError error - noBankAccountForwardHandler bool - errorBankAccountForwardHandler error - } - - connectorNotFound := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderCurrencyCloud, - } - - var ErrOther = errors.New("other error") - bankAccountID := uuid.New() - testCases := []testCase{ - { - name: "nominal", - bankAccountID: uuid.New().String(), - req: &ForwardBankAccountToConnectorRequest{ - ConnectorID: connectorDummyPay.ID.String(), - }, - }, - { - name: "already forwarded to connector", - bankAccountID: bankAccountID.String(), - req: &ForwardBankAccountToConnectorRequest{ - ConnectorID: connectorDummyPay.ID.String(), - }, - withBankAccountRelatedAccounts: []*models.BankAccountRelatedAccount{ - { - ID: uuid.New(), - CreatedAt: time.Now().UTC(), - BankAccountID: bankAccountID, - ConnectorID: connectorDummyPay.ID, - AccountID: models.AccountID{ - Reference: "test", - ConnectorID: connectorDummyPay.ID, - }, - }, - }, - expectedError: ErrValidation, - }, - { - name: "empty bank account id", - req: &ForwardBankAccountToConnectorRequest{ - ConnectorID: connectorDummyPay.ID.String(), - }, - expectedError: ErrInvalidID, - }, - { - name: "invalid bank account id", - bankAccountID: "invalid", - req: &ForwardBankAccountToConnectorRequest{ - ConnectorID: connectorDummyPay.ID.String(), - }, - expectedError: ErrInvalidID, - }, - { - name: "missing connectorID", - bankAccountID: uuid.New().String(), - req: &ForwardBankAccountToConnectorRequest{}, - expectedError: ErrValidation, - }, - { - name: "invalid connector id", - bankAccountID: uuid.New().String(), - req: &ForwardBankAccountToConnectorRequest{ - ConnectorID: "invalid", - }, - expectedError: ErrValidation, - }, - { - name: "connector not found", - bankAccountID: uuid.New().String(), - req: &ForwardBankAccountToConnectorRequest{ - ConnectorID: connectorNotFound.String(), - }, - expectedError: ErrValidation, - }, - { - name: "no connector handler", - bankAccountID: uuid.New().String(), - req: &ForwardBankAccountToConnectorRequest{ - ConnectorID: connectorDummyPay.ID.String(), - }, - noBankAccountForwardHandler: true, - expectedError: ErrValidation, - }, - { - name: "connector handler error validation", - bankAccountID: uuid.New().String(), - req: &ForwardBankAccountToConnectorRequest{ - ConnectorID: connectorDummyPay.ID.String(), - }, - errorBankAccountForwardHandler: manager.ErrValidation, - expectedError: ErrValidation, - }, - { - name: "connector handler error connector not found", - bankAccountID: uuid.New().String(), - req: &ForwardBankAccountToConnectorRequest{ - ConnectorID: connectorDummyPay.ID.String(), - }, - errorBankAccountForwardHandler: manager.ErrConnectorNotFound, - expectedError: ErrValidation, - }, - { - name: "connector handler other error", - bankAccountID: uuid.New().String(), - req: &ForwardBankAccountToConnectorRequest{ - ConnectorID: connectorDummyPay.ID.String(), - }, - errorBankAccountForwardHandler: ErrOther, - expectedError: ErrOther, - }, - } - - for _, tc := range testCases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - var handlers map[models.ConnectorProvider]*ConnectorHandlers - if !tc.noBankAccountForwardHandler { - handlers = map[models.ConnectorProvider]*ConnectorHandlers{ - models.ConnectorProviderDummyPay: { - BankAccountHandler: func(ctx context.Context, connectorID models.ConnectorID, bankAccount *models.BankAccount) error { - if tc.errorBankAccountForwardHandler != nil { - return tc.errorBankAccountForwardHandler - } - - return nil - }, - }, - } - } - - store := &MockStore{} - service := New(store.WithBankAccountRelatedAccounts(tc.withBankAccountRelatedAccounts), &MockPublisher{}, messages.NewMessages(""), handlers) - - _, err := service.ForwardBankAccountToConnector(context.Background(), tc.bankAccountID, tc.req) - if tc.expectedError != nil { - require.True(t, errors.Is(err, tc.expectedError)) - } else { - require.NoError(t, err) - } - }) - } -} - -func TestUpdateBankAccountMetadata(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - bankAccountID string - req *UpdateBankAccountMetadataRequest - storageError error - expectedError error - } - - testCases := []testCase{ - { - name: "nominal", - bankAccountID: uuid.New().String(), - req: &UpdateBankAccountMetadataRequest{ - Metadata: map[string]string{ - "foo": "bar", - }, - }, - }, - { - name: "err not found from storage", - bankAccountID: uuid.New().String(), - req: &UpdateBankAccountMetadataRequest{ - Metadata: map[string]string{ - "foo": "bar", - }, - }, - storageError: storage.ErrNotFound, - expectedError: storage.ErrNotFound, - }, - { - name: "empty bank account id", - req: &UpdateBankAccountMetadataRequest{ - Metadata: map[string]string{ - "foo": "bar", - }, - }, - expectedError: ErrInvalidID, - }, - { - name: "invalid bank account id", - bankAccountID: "invalid", - req: &UpdateBankAccountMetadataRequest{ - Metadata: map[string]string{ - "foo": "bar", - }, - }, - expectedError: ErrInvalidID, - }, - } - - for _, tc := range testCases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - var handlers map[models.ConnectorProvider]*ConnectorHandlers - - store := &MockStore{} - if tc.storageError != nil { - store = store.WithError(tc.storageError) - } - service := New(store, &MockPublisher{}, messages.NewMessages(""), handlers) - - err := service.UpdateBankAccountMetadata(context.Background(), tc.bankAccountID, tc.req) - if tc.expectedError != nil { - require.True(t, errors.Is(err, tc.expectedError)) - } else { - require.NoError(t, err) - } - }) - } -} diff --git a/components/payments/cmd/connectors/internal/api/service/connector.go b/components/payments/cmd/connectors/internal/api/service/connector.go deleted file mode 100644 index fac2548ced..0000000000 --- a/components/payments/cmd/connectors/internal/api/service/connector.go +++ /dev/null @@ -1,12 +0,0 @@ -package service - -import ( - "context" - - "github.com/formancehq/payments/internal/models" -) - -func (s *Service) ListConnectors(ctx context.Context) ([]*models.Connector, error) { - connectors, err := s.store.ListConnectors(ctx) - return connectors, newStorageError(err, "listing connectors") -} diff --git a/components/payments/cmd/connectors/internal/api/service/ping.go b/components/payments/cmd/connectors/internal/api/service/ping.go deleted file mode 100644 index 8fbedef75f..0000000000 --- a/components/payments/cmd/connectors/internal/api/service/ping.go +++ /dev/null @@ -1,5 +0,0 @@ -package service - -func (s *Service) Ping() error { - return newStorageError(s.store.Ping(), "ping") -} diff --git a/components/payments/cmd/connectors/internal/api/service/service.go b/components/payments/cmd/connectors/internal/api/service/service.go deleted file mode 100644 index 9e59868037..0000000000 --- a/components/payments/cmd/connectors/internal/api/service/service.go +++ /dev/null @@ -1,66 +0,0 @@ -package service - -import ( - "context" - - "github.com/ThreeDotsLabs/watermill/message" - "github.com/formancehq/payments/internal/messages" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" -) - -type InitiatePaymentHandler func(ctx context.Context, transfer *models.TransferInitiation) error -type ReversePaymentHandler func(ctx context.Context, transfer *models.TransferReversal) error -type BankAccountHandler func(ctx context.Context, connectorID models.ConnectorID, bankAccount *models.BankAccount) error - -type Store interface { - Ping() error - - GetConnector(ctx context.Context, connectorID models.ConnectorID) (*models.Connector, error) - ListConnectors(ctx context.Context) ([]*models.Connector, error) - - UpsertAccounts(ctx context.Context, accounts []*models.Account) ([]models.AccountID, error) - GetAccount(ctx context.Context, id string) (*models.Account, error) - - CreateBankAccount(ctx context.Context, account *models.BankAccount) error - UpdateBankAccountMetadata(ctx context.Context, id uuid.UUID, metadata map[string]string) error - GetBankAccount(ctx context.Context, id uuid.UUID, expand bool) (*models.BankAccount, error) - GetBankAccountRelatedAccounts(ctx context.Context, id uuid.UUID) ([]*models.BankAccountRelatedAccount, error) - - ListConnectorsByProvider(ctx context.Context, provider models.ConnectorProvider) ([]*models.Connector, error) - IsInstalledByConnectorID(ctx context.Context, connectorID models.ConnectorID) (bool, error) - - CreateTransferInitiation(ctx context.Context, transferInitiation *models.TransferInitiation) error - ReadTransferInitiation(ctx context.Context, id models.TransferInitiationID) (*models.TransferInitiation, error) - UpdateTransferInitiationPaymentsStatus(ctx context.Context, id models.TransferInitiationID, paymentID *models.PaymentID, adjustment *models.TransferInitiationAdjustment) error - DeleteTransferInitiation(ctx context.Context, id models.TransferInitiationID) error - - CreateTransferReversal(ctx context.Context, transferReversal *models.TransferReversal) error -} - -type Service struct { - store Store - publisher message.Publisher - messages *messages.Messages - connectorHandlers map[models.ConnectorProvider]*ConnectorHandlers -} - -type ConnectorHandlers struct { - InitiatePaymentHandler InitiatePaymentHandler - ReversePaymentHandler ReversePaymentHandler - BankAccountHandler BankAccountHandler -} - -func New( - store Store, - publisher message.Publisher, - messages *messages.Messages, - connectorHandlers map[models.ConnectorProvider]*ConnectorHandlers, -) *Service { - return &Service{ - store: store, - publisher: publisher, - connectorHandlers: connectorHandlers, - messages: messages, - } -} diff --git a/components/payments/cmd/connectors/internal/api/service/service_test.go b/components/payments/cmd/connectors/internal/api/service/service_test.go deleted file mode 100644 index b17269fe53..0000000000 --- a/components/payments/cmd/connectors/internal/api/service/service_test.go +++ /dev/null @@ -1,284 +0,0 @@ -package service - -import ( - "context" - "math/big" - "time" - - "github.com/ThreeDotsLabs/watermill/message" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" -) - -var ( - connectorDummyPay = models.Connector{ - ID: models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - }, - Name: "c1", - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Provider: models.ConnectorProviderDummyPay, - } - - connectorBankingCircle = models.Connector{ - ID: models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderBankingCircle, - }, - Name: "c2", - CreatedAt: time.Date(2023, 11, 22, 9, 0, 0, 0, time.UTC), - Provider: models.ConnectorProviderBankingCircle, - } - - transferInitiationWaiting = models.TransferInitiation{ - ID: models.TransferInitiationID{ - Reference: "ref1", - ConnectorID: connectorDummyPay.ID, - }, - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - ScheduledAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Description: "test", - Type: models.TransferInitiationTypeTransfer, - SourceAccountID: &models.AccountID{ - Reference: "acc1", - ConnectorID: connectorDummyPay.ID, - }, - DestinationAccountID: models.AccountID{ - Reference: "acc2", - ConnectorID: connectorDummyPay.ID, - }, - Provider: models.ConnectorProviderDummyPay, - ConnectorID: connectorDummyPay.ID, - Amount: big.NewInt(100), - InitialAmount: big.NewInt(100), - Asset: "EUR/2", - RelatedAdjustments: []*models.TransferInitiationAdjustment{ - { - ID: uuid.New(), - TransferInitiationID: models.TransferInitiationID{ - Reference: "ref1", - ConnectorID: connectorDummyPay.ID, - }, - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Status: models.TransferInitiationStatusWaitingForValidation, - }, - }, - } - - transferInitiationFailed = models.TransferInitiation{ - ID: models.TransferInitiationID{ - Reference: "ref2", - ConnectorID: connectorDummyPay.ID, - }, - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - ScheduledAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Description: "test", - Type: models.TransferInitiationTypeTransfer, - SourceAccountID: &models.AccountID{ - Reference: "acc1", - ConnectorID: connectorDummyPay.ID, - }, - DestinationAccountID: models.AccountID{ - Reference: "acc2", - ConnectorID: connectorDummyPay.ID, - }, - Provider: models.ConnectorProviderDummyPay, - ConnectorID: connectorDummyPay.ID, - Amount: big.NewInt(100), - InitialAmount: big.NewInt(100), - Asset: "EUR/2", - RelatedAdjustments: []*models.TransferInitiationAdjustment{ - { - ID: uuid.New(), - TransferInitiationID: models.TransferInitiationID{ - Reference: "ref2", - ConnectorID: connectorDummyPay.ID, - }, - CreatedAt: time.Date(2023, 11, 22, 9, 0, 0, 0, time.UTC), - Status: models.TransferInitiationStatusFailed, - Error: "some error", - }, - { - ID: uuid.New(), - TransferInitiationID: models.TransferInitiationID{ - Reference: "ref2", - ConnectorID: connectorDummyPay.ID, - }, - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Status: models.TransferInitiationStatusWaitingForValidation, - }, - }, - } - - sourceAccountID = models.AccountID{ - Reference: "acc1", - ConnectorID: connectorDummyPay.ID, - } - - destinationAccountID = models.AccountID{ - Reference: "acc2", - ConnectorID: connectorDummyPay.ID, - } - - destinationExternalAccountID = models.AccountID{ - Reference: "acc3", - ConnectorID: connectorDummyPay.ID, - } -) - -type MockStore struct { - errorToSend error - listConnectorsNB int - bankAccountRelatedAccounts []*models.BankAccountRelatedAccount -} - -func (m *MockStore) WithError(err error) *MockStore { - m.errorToSend = err - return m -} - -func (m *MockStore) WithListConnectorsNB(nb int) *MockStore { - m.listConnectorsNB = nb - return m -} - -func (m *MockStore) WithBankAccountRelatedAccounts(relatedAccounts []*models.BankAccountRelatedAccount) *MockStore { - m.bankAccountRelatedAccounts = relatedAccounts - return m -} - -func (m *MockStore) Ping() error { - return nil -} - -func (m *MockStore) GetConnector(ctx context.Context, connectorID models.ConnectorID) (*models.Connector, error) { - if connectorID == connectorDummyPay.ID { - return &connectorDummyPay, nil - } else if connectorID == connectorBankingCircle.ID { - return &connectorBankingCircle, nil - } - - return nil, storage.ErrNotFound -} - -func (m *MockStore) ListConnectors(ctx context.Context) ([]*models.Connector, error) { - return []*models.Connector{&connectorDummyPay, &connectorBankingCircle}, nil -} - -func (m *MockStore) UpsertAccounts(ctx context.Context, accounts []*models.Account) ([]models.AccountID, error) { - return nil, nil -} - -func (m *MockStore) GetAccount(ctx context.Context, id string) (*models.Account, error) { - switch id { - case sourceAccountID.String(): - return &models.Account{ - ID: sourceAccountID, - Type: models.AccountTypeInternal, - }, nil - case destinationAccountID.String(): - return &models.Account{ - ID: destinationAccountID, - Type: models.AccountTypeInternal, - }, nil - case destinationExternalAccountID.String(): - return &models.Account{ - ID: destinationAccountID, - Type: models.AccountTypeExternal, - }, nil - } - - return nil, nil -} - -func (m *MockStore) CreateBankAccount(ctx context.Context, account *models.BankAccount) error { - account.ID = uuid.New() - return nil -} - -func (m *MockStore) UpdateBankAccountMetadata(ctx context.Context, id uuid.UUID, metadata map[string]string) error { - return m.errorToSend -} - -func (m *MockStore) GetBankAccount(ctx context.Context, id uuid.UUID, expand bool) (*models.BankAccount, error) { - return &models.BankAccount{ - ID: id, - CreatedAt: time.Now().UTC(), - Name: "test", - IBAN: "FR7630006000011234567890189", - SwiftBicCode: "HBUKGB4B", - Country: "FR", - Metadata: map[string]string{}, - RelatedAccounts: m.bankAccountRelatedAccounts, - }, nil -} - -func (m *MockStore) GetBankAccountRelatedAccounts(ctx context.Context, id uuid.UUID) ([]*models.BankAccountRelatedAccount, error) { - return nil, nil -} - -func (m *MockStore) ListConnectorsByProvider(ctx context.Context, provider models.ConnectorProvider) ([]*models.Connector, error) { - switch m.listConnectorsNB { - case 0: - return []*models.Connector{}, nil - case 1: - return []*models.Connector{&connectorDummyPay}, nil - default: - return []*models.Connector{&connectorDummyPay, &connectorBankingCircle}, nil - } -} - -func (m *MockStore) IsInstalledByConnectorID(ctx context.Context, connectorID models.ConnectorID) (bool, error) { - if connectorID == connectorDummyPay.ID { - return true, nil - } - - return false, nil -} - -func (m *MockStore) CreateTransferInitiation(ctx context.Context, transferInitiation *models.TransferInitiation) error { - return nil -} - -func (m *MockStore) ReadTransferInitiation(ctx context.Context, id models.TransferInitiationID) (*models.TransferInitiation, error) { - if id == transferInitiationWaiting.ID { - tc := transferInitiationWaiting - return &tc, nil - } else if id == transferInitiationFailed.ID { - tc := transferInitiationFailed - return &tc, nil - } - - return nil, storage.ErrNotFound -} - -func (m *MockStore) UpdateTransferInitiationPaymentsStatus(ctx context.Context, id models.TransferInitiationID, paymentID *models.PaymentID, adjustment *models.TransferInitiationAdjustment) error { - return nil -} - -func (m *MockStore) DeleteTransferInitiation(ctx context.Context, id models.TransferInitiationID) error { - return nil -} - -func (m *MockStore) CreateTransferReversal(ctx context.Context, transferReversal *models.TransferReversal) error { - return nil -} - -type MockPublisher struct { - errorToSend error -} - -func (m *MockPublisher) WithError(err error) *MockPublisher { - m.errorToSend = err - return m -} - -func (m *MockPublisher) Publish(topic string, messages ...*message.Message) error { - return m.errorToSend -} - -func (m *MockPublisher) Close() error { - return nil -} diff --git a/components/payments/cmd/connectors/internal/api/service/transfer_initiation.go b/components/payments/cmd/connectors/internal/api/service/transfer_initiation.go deleted file mode 100644 index a487a99e98..0000000000 --- a/components/payments/cmd/connectors/internal/api/service/transfer_initiation.go +++ /dev/null @@ -1,394 +0,0 @@ -package service - -import ( - "context" - "fmt" - "math/big" - "time" - - "github.com/formancehq/go-libs/publish" - manager "github.com/formancehq/payments/cmd/connectors/internal/api/connectors_manager" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/pkg/events" - "github.com/google/uuid" - "github.com/pkg/errors" -) - -type CreateTransferInitiationRequest struct { - Reference string `json:"reference"` - ScheduledAt time.Time `json:"scheduledAt"` - Description string `json:"description"` - SourceAccountID string `json:"sourceAccountID"` - DestinationAccountID string `json:"destinationAccountID"` - ConnectorID string `json:"connectorID"` - Provider string `json:"provider"` - Type string `json:"type"` - Amount *big.Int `json:"amount"` - Asset string `json:"asset"` - Validated bool `json:"validated"` - Metadata map[string]string `json:"metadata"` -} - -func (r *CreateTransferInitiationRequest) Validate() error { - if r.Reference == "" { - return errors.New("reference is required") - } - - if r.SourceAccountID != "" { - _, err := models.AccountIDFromString(r.SourceAccountID) - if err != nil { - return err - } - } - - _, err := models.AccountIDFromString(r.DestinationAccountID) - if err != nil { - return err - } - - _, err = models.TransferInitiationTypeFromString(r.Type) - if err != nil { - return err - } - - if r.Amount == nil { - return errors.New("amount is required") - } - - if r.Asset == "" { - return errors.New("asset is required") - } - - return nil -} - -func (s *Service) CreateTransferInitiation(ctx context.Context, req *CreateTransferInitiationRequest) (*models.TransferInitiation, error) { - status := models.TransferInitiationStatusWaitingForValidation - if req.Validated { - status = models.TransferInitiationStatusValidated - } - - var connectorID models.ConnectorID - if req.ConnectorID == "" { - provider, err := models.ConnectorProviderFromString(req.Provider) - if err != nil { - return nil, errors.Wrap(ErrValidation, err.Error()) - } - - connectors, err := s.store.ListConnectorsByProvider(ctx, provider) - if err != nil { - return nil, newStorageError(err, "listing connectors") - } - - if len(connectors) == 0 { - return nil, errors.Wrap(ErrValidation, fmt.Sprintf("no connector found for provider %s", provider)) - } - - if len(connectors) > 1 { - return nil, errors.Wrap(ErrValidation, fmt.Sprintf("multiple connectors found for provider %s", provider)) - } - - connectorID = connectors[0].ID - } else { - var err error - connectorID, err = models.ConnectorIDFromString(req.ConnectorID) - if err != nil { - return nil, errors.Wrap(ErrValidation, err.Error()) - } - } - - isInstalled, _ := s.store.IsInstalledByConnectorID(ctx, connectorID) - if !isInstalled { - return nil, errors.Wrap(ErrValidation, fmt.Sprintf("connector %s is not installed", req.ConnectorID)) - } - - if req.SourceAccountID != "" { - _, err := s.store.GetAccount(ctx, req.SourceAccountID) - if err != nil { - return nil, newStorageError(err, "getting source account") - } - } - - destinationAccount, err := s.store.GetAccount(ctx, req.DestinationAccountID) - if err != nil { - return nil, newStorageError(err, "getting destination account") - } - - transferType := models.MustTransferInitiationTypeFromString(req.Type) - - switch transferType { - case models.TransferInitiationTypeTransfer: - if destinationAccount.Type != models.AccountTypeInternal { - // account should be internal when doing a transfer, return an error - return nil, errors.Wrap(ErrValidation, "destination account must be internal when doing a transfer") - } - case models.TransferInitiationTypePayout: - switch destinationAccount.Type { - case models.AccountTypeExternal, models.AccountTypeExternalFormance: - default: - // account should be external when doing a payout, return an error - return nil, errors.Wrap(ErrValidation, "destination account must be external when doing a payout") - } - } - - id := models.TransferInitiationID{ - Reference: req.Reference, - ConnectorID: connectorID, - } - - // Always insert timestamp as UTC - createdAt := time.Now().UTC() - tf := &models.TransferInitiation{ - ID: id, - CreatedAt: createdAt, - ScheduledAt: req.ScheduledAt, - Description: req.Description, - DestinationAccountID: models.MustAccountIDFromString(req.DestinationAccountID), - ConnectorID: connectorID, - Provider: connectorID.Provider, - Type: transferType, - Amount: req.Amount, - InitialAmount: req.Amount, - Asset: models.Asset(req.Asset), - Metadata: req.Metadata, - RelatedAdjustments: []*models.TransferInitiationAdjustment{ - { - ID: uuid.New(), - TransferInitiationID: id, - CreatedAt: createdAt, - Status: status, - }, - }, - } - - if req.SourceAccountID != "" { - sID := models.MustAccountIDFromString(req.SourceAccountID) - tf.SourceAccountID = &sID - } - - if err := s.store.CreateTransferInitiation(ctx, tf); err != nil { - return nil, newStorageError(err, "creating transfer initiation") - } - - if err := s.publisher.Publish( - events.TopicPayments, - publish.NewMessage( - ctx, - s.messages.NewEventSavedTransferInitiations(tf), - ), - ); err != nil { - return nil, errors.Wrap(ErrPublish, err.Error()) - } - - if status == models.TransferInitiationStatusValidated { - connector, err := s.store.GetConnector(ctx, connectorID) - if err != nil { - return nil, newStorageError(err, "getting connector") - } - - handlers, ok := s.connectorHandlers[connector.Provider] - if !ok { - return nil, errors.Wrap(ErrValidation, fmt.Sprintf("no payment handler for provider %v", connector.Provider)) - } - - err = handlers.InitiatePaymentHandler(ctx, tf) - if err != nil { - switch { - case errors.Is(err, manager.ErrValidation): - return nil, errors.Wrap(ErrValidation, err.Error()) - case errors.Is(err, manager.ErrConnectorNotFound): - return nil, errors.Wrap(ErrValidation, err.Error()) - default: - return nil, err - } - } - } - - return tf, nil -} - -type UpdateTransferInitiationStatusRequest struct { - Status string `json:"status"` -} - -func (r *UpdateTransferInitiationStatusRequest) Validate() error { - if r.Status == "" { - return errors.New("status is required") - } - - return nil -} - -func (s *Service) UpdateTransferInitiationStatus(ctx context.Context, id string, req *UpdateTransferInitiationStatusRequest) error { - status, err := models.TransferInitiationStatusFromString(req.Status) - if err != nil { - return errors.Wrap(ErrValidation, err.Error()) - } - - switch status { - case models.TransferInitiationStatusWaitingForValidation: - return errors.Wrap(ErrValidation, "cannot set back transfer initiation status to waiting for validation") - case models.TransferInitiationStatusFailed, - models.TransferInitiationStatusProcessed, - models.TransferInitiationStatusProcessing: - return errors.Wrap(ErrValidation, "either VALIDATED or REJECTED status can be set") - default: - } - - transferID, err := models.TransferInitiationIDFromString(id) - if err != nil { - return errors.Wrap(ErrInvalidID, err.Error()) - } - - previousTransferInitiation, err := s.store.ReadTransferInitiation(ctx, transferID) - if err != nil { - return newStorageError(err, "reading transfer initiation") - } - - // Check last status - if len(previousTransferInitiation.RelatedAdjustments) == 0 || - previousTransferInitiation.RelatedAdjustments[0].Status != models.TransferInitiationStatusWaitingForValidation { - return errors.Wrap(ErrValidation, "only waiting for validation transfer initiation can be updated") - } - - adjustment := &models.TransferInitiationAdjustment{ - ID: uuid.New(), - TransferInitiationID: transferID, - CreatedAt: time.Now(), - Status: status, - Error: "", - } - - previousTransferInitiation.RelatedAdjustments = append(previousTransferInitiation.RelatedAdjustments, adjustment) - previousTransferInitiation.SortRelatedAdjustments() - - err = s.store.UpdateTransferInitiationPaymentsStatus(ctx, transferID, nil, adjustment) - if err != nil { - return newStorageError(err, "updating transfer initiation payments status") - } - - if err := s.publisher.Publish( - events.TopicPayments, - publish.NewMessage( - ctx, - s.messages.NewEventSavedTransferInitiations(previousTransferInitiation), - ), - ); err != nil { - return errors.Wrap(ErrPublish, err.Error()) - } - - if status == models.TransferInitiationStatusValidated { - handlers, ok := s.connectorHandlers[previousTransferInitiation.Provider] - if !ok { - return errors.Wrap(ErrValidation, fmt.Sprintf("no payment handler for provider %v", previousTransferInitiation.Provider)) - } - - err = handlers.InitiatePaymentHandler(ctx, previousTransferInitiation) - if err != nil { - switch { - case errors.Is(err, manager.ErrValidation): - return errors.Wrap(ErrValidation, err.Error()) - case errors.Is(err, manager.ErrConnectorNotFound): - return errors.Wrap(ErrValidation, err.Error()) - default: - return err - } - } - } - - return nil -} - -func (s *Service) RetryTransferInitiation(ctx context.Context, id string) error { - transferID, err := models.TransferInitiationIDFromString(id) - if err != nil { - return errors.Wrap(ErrInvalidID, err.Error()) - } - - previousTransferInitiation, err := s.store.ReadTransferInitiation(ctx, transferID) - if err != nil { - return newStorageError(err, "reading transfer initiation") - } - - if len(previousTransferInitiation.RelatedAdjustments) == 0 || - previousTransferInitiation.RelatedAdjustments[0].Status != models.TransferInitiationStatusFailed { - return errors.Wrap(ErrValidation, "only failed transfer initiation can be retried") - } - - adjustment := &models.TransferInitiationAdjustment{ - ID: uuid.New(), - TransferInitiationID: transferID, - CreatedAt: time.Now(), - Status: models.TransferInitiationStatusAskRetried, - Error: "", - Metadata: map[string]string{}, - } - - err = s.store.UpdateTransferInitiationPaymentsStatus(ctx, transferID, nil, adjustment) - if err != nil { - return newStorageError(err, "updating transfer initiation payments status") - } - - if err := s.publisher.Publish( - events.TopicPayments, - publish.NewMessage( - ctx, - s.messages.NewEventSavedTransferInitiations(previousTransferInitiation), - ), - ); err != nil { - return errors.Wrap(ErrPublish, err.Error()) - } - - handlers, ok := s.connectorHandlers[previousTransferInitiation.Provider] - if !ok { - return errors.Wrap(ErrValidation, fmt.Sprintf("no payment handler for provider %v", previousTransferInitiation.Provider)) - } - - err = handlers.InitiatePaymentHandler(ctx, previousTransferInitiation) - if err != nil { - switch { - case errors.Is(err, manager.ErrValidation): - return errors.Wrap(ErrValidation, err.Error()) - case errors.Is(err, manager.ErrConnectorNotFound): - return errors.Wrap(ErrValidation, err.Error()) - default: - return err - } - } - - return nil -} - -func (s *Service) DeleteTransferInitiation(ctx context.Context, id string) error { - transferID, err := models.TransferInitiationIDFromString(id) - if err != nil { - return errors.Wrap(ErrInvalidID, err.Error()) - } - - tf, err := s.store.ReadTransferInitiation(ctx, transferID) - if err != nil { - return newStorageError(err, "reading transfer initiation") - } - - if len(tf.RelatedAdjustments) == 0 || - tf.RelatedAdjustments[0].Status != models.TransferInitiationStatusWaitingForValidation { - return errors.Wrap(ErrValidation, "only waiting for validation transfer initiation can be deleted") - } - - err = s.store.DeleteTransferInitiation(ctx, transferID) - if err != nil { - return newStorageError(err, "deleting transfer initiation") - } - - if err := s.publisher.Publish( - events.TopicPayments, - publish.NewMessage( - ctx, - s.messages.NewEventDeleteTransferInitiation(tf.ID), - ), - ); err != nil { - return errors.Wrap(ErrPublish, err.Error()) - } - - return nil -} diff --git a/components/payments/cmd/connectors/internal/api/service/transfer_initiation_test.go b/components/payments/cmd/connectors/internal/api/service/transfer_initiation_test.go deleted file mode 100644 index 943547540d..0000000000 --- a/components/payments/cmd/connectors/internal/api/service/transfer_initiation_test.go +++ /dev/null @@ -1,715 +0,0 @@ -package service - -import ( - "context" - "math/big" - "testing" - "time" - - manager "github.com/formancehq/payments/cmd/connectors/internal/api/connectors_manager" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/internal/messages" - "github.com/formancehq/payments/internal/models" - "github.com/pkg/errors" - "github.com/stretchr/testify/require" -) - -func TestCreateTransferInitiation(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - req *CreateTransferInitiationRequest - expectedTF *models.TransferInitiation - listConnectorLength int - errorPublish bool - errorPaymentHandler error - noPaymentsHandler bool - expectedError error - } - - sourceAccountID := models.AccountID{ - Reference: "acc1", - ConnectorID: connectorDummyPay.ID, - } - - destinationAccountID := models.AccountID{ - Reference: "acc2", - ConnectorID: connectorDummyPay.ID, - } - - testCases := []testCase{ - { - name: "nominal", - req: &CreateTransferInitiationRequest{ - Reference: "ref1", - ScheduledAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), - Description: "test", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - ConnectorID: connectorDummyPay.ID.String(), - Provider: string(models.ConnectorProviderDummyPay), - Type: models.TransferInitiationTypeTransfer.String(), - Amount: big.NewInt(100), - Asset: "EUR/2", - Validated: false, - }, - expectedTF: &models.TransferInitiation{ - ID: models.TransferInitiationID{ - Reference: "ref1", - ConnectorID: connectorDummyPay.ID, - }, - ScheduledAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), - Description: "test", - DestinationAccountID: destinationAccountID, - SourceAccountID: &sourceAccountID, - ConnectorID: connectorDummyPay.ID, - Provider: models.ConnectorProviderDummyPay, - Type: models.TransferInitiationTypeTransfer, - Amount: big.NewInt(100), - InitialAmount: big.NewInt(100), - Asset: models.Asset("EUR/2"), - RelatedAdjustments: []*models.TransferInitiationAdjustment{ - { - TransferInitiationID: models.TransferInitiationID{ - Reference: "ref1", - ConnectorID: connectorDummyPay.ID, - }, - Status: models.TransferInitiationStatusWaitingForValidation, - }, - }, - }, - }, - { - name: "nominal without description", - req: &CreateTransferInitiationRequest{ - Reference: "ref1", - ScheduledAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - ConnectorID: connectorDummyPay.ID.String(), - Provider: string(models.ConnectorProviderDummyPay), - Type: models.TransferInitiationTypeTransfer.String(), - Amount: big.NewInt(100), - Asset: "EUR/2", - Validated: false, - }, - expectedTF: &models.TransferInitiation{ - ID: models.TransferInitiationID{ - Reference: "ref1", - ConnectorID: connectorDummyPay.ID, - }, - ScheduledAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), - DestinationAccountID: destinationAccountID, - SourceAccountID: &sourceAccountID, - ConnectorID: connectorDummyPay.ID, - Provider: models.ConnectorProviderDummyPay, - Type: models.TransferInitiationTypeTransfer, - Amount: big.NewInt(100), - InitialAmount: big.NewInt(100), - Asset: models.Asset("EUR/2"), - RelatedAdjustments: []*models.TransferInitiationAdjustment{ - { - TransferInitiationID: models.TransferInitiationID{ - Reference: "ref1", - ConnectorID: connectorDummyPay.ID, - }, - Status: models.TransferInitiationStatusWaitingForValidation, - }, - }, - }, - }, - { - name: "nominal with status changed", - req: &CreateTransferInitiationRequest{ - Reference: "ref1", - ScheduledAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), - Description: "test", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - ConnectorID: connectorDummyPay.ID.String(), - Provider: string(models.ConnectorProviderDummyPay), - Type: models.TransferInitiationTypeTransfer.String(), - Amount: big.NewInt(100), - Asset: "EUR/2", - Validated: true, - }, - expectedTF: &models.TransferInitiation{ - ID: models.TransferInitiationID{ - Reference: "ref1", - ConnectorID: connectorDummyPay.ID, - }, - ScheduledAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), - Description: "test", - DestinationAccountID: destinationAccountID, - SourceAccountID: &sourceAccountID, - ConnectorID: connectorDummyPay.ID, - Provider: models.ConnectorProviderDummyPay, - Type: models.TransferInitiationTypeTransfer, - Amount: big.NewInt(100), - InitialAmount: big.NewInt(100), - Asset: models.Asset("EUR/2"), - RelatedAdjustments: []*models.TransferInitiationAdjustment{ - { - TransferInitiationID: models.TransferInitiationID{ - Reference: "ref1", - ConnectorID: connectorDummyPay.ID, - }, - Status: models.TransferInitiationStatusValidated, - }, - }, - }, - }, - { - name: "transfer with external account as destination", - req: &CreateTransferInitiationRequest{ - Reference: "ref1", - ScheduledAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), - Description: "test", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationExternalAccountID.String(), - ConnectorID: connectorDummyPay.ID.String(), - Provider: string(models.ConnectorProviderDummyPay), - Type: models.TransferInitiationTypeTransfer.String(), - Amount: big.NewInt(100), - Asset: "EUR/2", - Validated: false, - }, - expectedError: ErrValidation, - }, - { - name: "payout with internal account as destination", - req: &CreateTransferInitiationRequest{ - Reference: "ref1", - ScheduledAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), - Description: "test", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - ConnectorID: connectorDummyPay.ID.String(), - Provider: string(models.ConnectorProviderDummyPay), - Type: models.TransferInitiationTypePayout.String(), - Amount: big.NewInt(100), - Asset: "EUR/2", - Validated: false, - }, - expectedError: ErrValidation, - }, - { - name: "invalid connector id", - req: &CreateTransferInitiationRequest{ - Reference: "ref1", - ScheduledAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), - Description: "test", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - ConnectorID: "invalid", - Provider: string(models.ConnectorProviderDummyPay), - Type: models.TransferInitiationTypeTransfer.String(), - Amount: big.NewInt(100), - Asset: "EUR/2", - Validated: false, - }, - expectedError: ErrValidation, - }, - { - name: "invalid provider", - req: &CreateTransferInitiationRequest{ - Reference: "ref1", - ScheduledAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), - Description: "test", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - Provider: "invalid", - Type: models.TransferInitiationTypeTransfer.String(), - Amount: big.NewInt(100), - Asset: "EUR/2", - Validated: false, - }, - expectedError: ErrValidation, - }, - { - name: "too many connectors list", - req: &CreateTransferInitiationRequest{ - Reference: "ref1", - ScheduledAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), - Description: "test", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - Provider: string(models.ConnectorProviderDummyPay), - Type: models.TransferInitiationTypeTransfer.String(), - Amount: big.NewInt(100), - Asset: "EUR/2", - Validated: false, - }, - listConnectorLength: 2, - expectedError: ErrValidation, - }, - { - name: "no connectors in connectors list", - req: &CreateTransferInitiationRequest{ - Reference: "ref1", - ScheduledAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), - Description: "test", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - Provider: string(models.ConnectorProviderDummyPay), - Type: models.TransferInitiationTypeTransfer.String(), - Amount: big.NewInt(100), - Asset: "EUR/2", - Validated: false, - }, - listConnectorLength: 0, - expectedError: ErrValidation, - }, - { - name: "connector not installed", - req: &CreateTransferInitiationRequest{ - Reference: "ref1", - ScheduledAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), - Description: "test", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - ConnectorID: connectorBankingCircle.ID.String(), - Type: models.TransferInitiationTypeTransfer.String(), - Amount: big.NewInt(100), - Asset: "EUR/2", - Validated: false, - }, - expectedError: ErrValidation, - }, - { - name: "error publishing", - req: &CreateTransferInitiationRequest{ - Reference: "ref1", - ScheduledAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), - Description: "test", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - ConnectorID: connectorDummyPay.ID.String(), - Provider: string(models.ConnectorProviderDummyPay), - Type: models.TransferInitiationTypeTransfer.String(), - Amount: big.NewInt(100), - Asset: "EUR/2", - Validated: false, - }, - errorPublish: true, - expectedError: ErrPublish, - }, - { - name: "no payments handler found", - req: &CreateTransferInitiationRequest{ - Reference: "ref1", - ScheduledAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), - Description: "test", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - ConnectorID: connectorDummyPay.ID.String(), - Provider: string(models.ConnectorProviderDummyPay), - Type: models.TransferInitiationTypeTransfer.String(), - Amount: big.NewInt(100), - Asset: "EUR/2", - Validated: true, - }, - noPaymentsHandler: true, - expectedError: ErrValidation, - }, - { - name: "error in payments handler", - req: &CreateTransferInitiationRequest{ - Reference: "ref1", - ScheduledAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), - Description: "test", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - ConnectorID: connectorDummyPay.ID.String(), - Provider: string(models.ConnectorProviderDummyPay), - Type: models.TransferInitiationTypeTransfer.String(), - Amount: big.NewInt(100), - Asset: "EUR/2", - Validated: true, - }, - errorPaymentHandler: manager.ErrValidation, - expectedError: ErrValidation, - }, - { - name: "error in payments handler", - req: &CreateTransferInitiationRequest{ - Reference: "ref1", - ScheduledAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), - Description: "test", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - ConnectorID: connectorDummyPay.ID.String(), - Provider: string(models.ConnectorProviderDummyPay), - Type: models.TransferInitiationTypeTransfer.String(), - Amount: big.NewInt(100), - Asset: "EUR/2", - Validated: true, - }, - errorPaymentHandler: manager.ErrConnectorNotFound, - expectedError: ErrValidation, - }, - } - - for _, tc := range testCases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - m := &MockPublisher{} - s := &MockStore{} - - var errPublish error - if tc.errorPublish { - errPublish = errors.New("publish error") - } - - var handlers map[models.ConnectorProvider]*ConnectorHandlers - if !tc.noPaymentsHandler { - handlers = map[models.ConnectorProvider]*ConnectorHandlers{ - models.ConnectorProviderDummyPay: { - InitiatePaymentHandler: func(ctx context.Context, transfer *models.TransferInitiation) error { - if tc.errorPaymentHandler != nil { - return tc.errorPaymentHandler - } - - return nil - }, - }, - } - } - service := New(s.WithListConnectorsNB(tc.listConnectorLength), m.WithError(errPublish), messages.NewMessages(""), handlers) - - tf, err := service.CreateTransferInitiation(context.Background(), tc.req) - if tc.expectedError != nil { - require.True(t, errors.Is(err, tc.expectedError)) - } else { - require.NoError(t, err) - tc.expectedTF.CreatedAt = tf.CreatedAt - require.Len(t, tf.RelatedAdjustments, 1) - tc.expectedTF.RelatedAdjustments[0].CreatedAt = tf.RelatedAdjustments[0].CreatedAt - tc.expectedTF.RelatedAdjustments[0].ID = tf.RelatedAdjustments[0].ID - require.Equal(t, tc.expectedTF, tf) - } - }) - } -} - -func TestUpdateTransferInitiationStatus(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - transferID string - req *UpdateTransferInitiationStatusRequest - errorPublish bool - errorPaymentHandler error - noPaymentsHandler bool - expectedError error - } - - tfNotFoundID := models.TransferInitiationID{ - Reference: "not_found", - ConnectorID: connectorDummyPay.ID, - } - - testCases := []testCase{ - { - name: "nominal validated", - req: &UpdateTransferInitiationStatusRequest{ - Status: "VALIDATED", - }, - transferID: transferInitiationWaiting.ID.String(), - }, - { - name: "nominal rejected", - req: &UpdateTransferInitiationStatusRequest{ - Status: "REJECTED", - }, - transferID: transferInitiationWaiting.ID.String(), - }, - { - name: "unknown status", - req: &UpdateTransferInitiationStatusRequest{ - Status: "INVALID", - }, - transferID: transferInitiationWaiting.ID.String(), - expectedError: ErrValidation, - }, - { - name: "invalid status waiting for validation", - req: &UpdateTransferInitiationStatusRequest{ - Status: "WAITING_FOR_VALIDATION", - }, - transferID: transferInitiationWaiting.ID.String(), - expectedError: ErrValidation, - }, - { - name: "invalid status failed", - req: &UpdateTransferInitiationStatusRequest{ - Status: "FAILED", - }, - transferID: transferInitiationWaiting.ID.String(), - expectedError: ErrValidation, - }, - { - name: "invalid status processed", - req: &UpdateTransferInitiationStatusRequest{ - Status: "PROCESSED", - }, - transferID: transferInitiationWaiting.ID.String(), - expectedError: ErrValidation, - }, - { - name: "invalid status processing", - req: &UpdateTransferInitiationStatusRequest{ - Status: "PROCESSING", - }, - transferID: transferInitiationWaiting.ID.String(), - expectedError: ErrValidation, - }, - { - name: "invalid transfer id", - req: &UpdateTransferInitiationStatusRequest{ - Status: "VALIDATED", - }, - transferID: "invalid", - expectedError: ErrInvalidID, - }, - { - name: "transfer not found", - req: &UpdateTransferInitiationStatusRequest{ - Status: "VALIDATED", - }, - transferID: tfNotFoundID.String(), - expectedError: storage.ErrNotFound, - }, - { - name: "previous transfer with wrong status", - req: &UpdateTransferInitiationStatusRequest{ - Status: "VALIDATED", - }, - transferID: transferInitiationFailed.ID.String(), - expectedError: ErrValidation, - }, - { - name: "error publishing", - req: &UpdateTransferInitiationStatusRequest{ - Status: "VALIDATED", - }, - transferID: transferInitiationWaiting.ID.String(), - errorPublish: true, - expectedError: ErrPublish, - }, - { - name: "error publishing", - req: &UpdateTransferInitiationStatusRequest{ - Status: "VALIDATED", - }, - transferID: transferInitiationWaiting.ID.String(), - noPaymentsHandler: true, - expectedError: ErrValidation, - }, - { - name: "error in payments handler", - req: &UpdateTransferInitiationStatusRequest{ - Status: "VALIDATED", - }, - transferID: transferInitiationWaiting.ID.String(), - errorPaymentHandler: manager.ErrValidation, - expectedError: ErrValidation, - }, - { - name: "error in payments handler", - req: &UpdateTransferInitiationStatusRequest{ - Status: "VALIDATED", - }, - transferID: transferInitiationWaiting.ID.String(), - errorPaymentHandler: manager.ErrConnectorNotFound, - expectedError: ErrValidation, - }, - } - - for _, tc := range testCases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - m := &MockPublisher{} - - var errPublish error - if tc.errorPublish { - errPublish = errors.New("publish error") - } - - var handlers map[models.ConnectorProvider]*ConnectorHandlers - if !tc.noPaymentsHandler { - handlers = map[models.ConnectorProvider]*ConnectorHandlers{ - models.ConnectorProviderDummyPay: { - InitiatePaymentHandler: func(ctx context.Context, transfer *models.TransferInitiation) error { - if tc.errorPaymentHandler != nil { - return tc.errorPaymentHandler - } - - return nil - }, - }, - } - } - service := New(&MockStore{}, m.WithError(errPublish), messages.NewMessages(""), handlers) - - err := service.UpdateTransferInitiationStatus(context.Background(), tc.transferID, tc.req) - if tc.expectedError != nil { - require.True(t, errors.Is(err, tc.expectedError)) - } else { - require.NoError(t, err) - } - }) - } -} - -func TestRetryTransferInitiation(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - transferID string - errorPublish bool - errorPaymentHandler error - noPaymentsHandler bool - expectedError error - } - - testCases := []testCase{ - { - name: "nominal", - transferID: transferInitiationFailed.ID.String(), - }, - { - name: "invalid transfer id", - transferID: "invalid", - expectedError: ErrInvalidID, - }, - { - name: "invalid previous transfer status", - transferID: transferInitiationWaiting.ID.String(), - expectedError: ErrValidation, - }, - { - name: "error publishing", - transferID: transferInitiationFailed.ID.String(), - noPaymentsHandler: true, - expectedError: ErrValidation, - }, - { - name: "error in payments handler", - transferID: transferInitiationFailed.ID.String(), - errorPaymentHandler: manager.ErrValidation, - expectedError: ErrValidation, - }, - { - name: "error in payments handler", - transferID: transferInitiationFailed.ID.String(), - errorPaymentHandler: manager.ErrConnectorNotFound, - expectedError: ErrValidation, - }, - } - - for _, tc := range testCases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - m := &MockPublisher{} - - var errPublish error - if tc.errorPublish { - errPublish = errors.New("publish error") - } - - var handlers map[models.ConnectorProvider]*ConnectorHandlers - if !tc.noPaymentsHandler { - handlers = map[models.ConnectorProvider]*ConnectorHandlers{ - models.ConnectorProviderDummyPay: { - InitiatePaymentHandler: func(ctx context.Context, transfer *models.TransferInitiation) error { - if tc.errorPaymentHandler != nil { - return tc.errorPaymentHandler - } - - return nil - }, - }, - } - } - service := New(&MockStore{}, m.WithError(errPublish), messages.NewMessages(""), handlers) - - err := service.RetryTransferInitiation(context.Background(), tc.transferID) - if tc.expectedError != nil { - require.True(t, errors.Is(err, tc.expectedError)) - } else { - require.NoError(t, err) - } - }) - } -} - -func TestDeleteTransferInitiation(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - transferID string - errorPublish bool - expectedError error - } - - testCases := []testCase{ - { - name: "nominal", - transferID: transferInitiationWaiting.ID.String(), - }, - { - name: "invalid transfer id", - transferID: "invalid", - expectedError: ErrInvalidID, - }, - { - name: "invalid previous transfer initiation status", - transferID: transferInitiationFailed.ID.String(), - expectedError: ErrValidation, - }, - { - name: "error publishing", - transferID: transferInitiationWaiting.ID.String(), - errorPublish: true, - expectedError: ErrPublish, - }, - } - - for _, tc := range testCases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - m := &MockPublisher{} - - var errPublish error - if tc.errorPublish { - errPublish = errors.New("publish error") - } - - service := New(&MockStore{}, m.WithError(errPublish), messages.NewMessages(""), nil) - - err := service.DeleteTransferInitiation(context.Background(), tc.transferID) - if tc.expectedError != nil { - require.True(t, errors.Is(err, tc.expectedError)) - } else { - require.NoError(t, err) - } - }) - } -} diff --git a/components/payments/cmd/connectors/internal/api/service/transfer_reversal.go b/components/payments/cmd/connectors/internal/api/service/transfer_reversal.go deleted file mode 100644 index 7bf526e500..0000000000 --- a/components/payments/cmd/connectors/internal/api/service/transfer_reversal.go +++ /dev/null @@ -1,122 +0,0 @@ -package service - -import ( - "context" - "fmt" - "math/big" - "time" - - manager "github.com/formancehq/payments/cmd/connectors/internal/api/connectors_manager" - "github.com/formancehq/payments/internal/models" - "github.com/pkg/errors" -) - -type ReverseTransferInitiationRequest struct { - Reference string `json:"reference"` - Description string `json:"description"` - Amount *big.Int `json:"amount"` - Asset string `json:"asset"` - Metadata map[string]string `json:"metadata"` -} - -func (r *ReverseTransferInitiationRequest) Validate() error { - if r.Reference == "" { - return errors.New("reference is required") - } - - if r.Amount == nil { - return errors.New("amount is required") - } - - if r.Asset == "" { - return errors.New("asset is required") - } - - return nil -} - -func checkIfReversalIsValid(transfer *models.TransferInitiation, req *ReverseTransferInitiationRequest) error { - finalAmount := new(big.Int) - finalAmount.Sub(transfer.Amount, req.Amount) - switch finalAmount.Cmp(big.NewInt(0)) { - case 0, 1: - // Nothing to do, requested reversed amount if less than or equal to the transfer amount - case -1: - return errors.New("reversed amount is greater than the transfer amount") - } - - if transfer.Type == models.TransferInitiationTypePayout { - return errors.New("payouts cannot be reversed") - } - - foundProcessed := false - for _, adjustment := range transfer.RelatedAdjustments { - if adjustment.Status == models.TransferInitiationStatusProcessed { - foundProcessed = true - break - } - } - - if !foundProcessed { - // transfer was never processed, so we can't reverse it - return errors.New("transfer was never processed") - } - - return nil -} - -func (s *Service) ReverseTransferInitiation(ctx context.Context, transferID string, req *ReverseTransferInitiationRequest) (*models.TransferReversal, error) { - transferInitiationID, err := models.TransferInitiationIDFromString(transferID) - if err != nil { - return nil, ErrInvalidID - } - - transfer, err := s.store.ReadTransferInitiation(ctx, transferInitiationID) - if err != nil { - return nil, newStorageError(err, "fetching transfer initiation") - } - - if err := checkIfReversalIsValid(transfer, req); err != nil { - return nil, errors.Wrap(ErrValidation, err.Error()) - } - - now := time.Now().UTC() - reversal := &models.TransferReversal{ - ID: models.TransferReversalID{ - Reference: req.Reference, - ConnectorID: transfer.ConnectorID, - }, - TransferInitiationID: transferInitiationID, - CreatedAt: now, - UpdatedAt: now, - Description: req.Description, - ConnectorID: transfer.ConnectorID, - Amount: req.Amount, - Asset: models.Asset(req.Asset), - Status: models.TransferReversalStatusProcessing, - Error: "", - Metadata: req.Metadata, - } - - if err := s.store.CreateTransferReversal(ctx, reversal); err != nil { - return nil, newStorageError(err, "creating transfer reversal") - } - - handlers, ok := s.connectorHandlers[transfer.Provider] - if !ok { - return nil, errors.Wrap(ErrValidation, fmt.Sprintf("no reverse payment handler for provider %v", transfer.Provider)) - } - - if err := handlers.ReversePaymentHandler(ctx, reversal); err != nil { - switch { - case errors.Is(err, manager.ErrValidation): - return nil, errors.Wrap(ErrValidation, err.Error()) - case errors.Is(err, manager.ErrConnectorNotFound): - return nil, errors.Wrap(ErrValidation, err.Error()) - default: - return nil, err - } - } - - return nil, nil -} diff --git a/components/payments/cmd/connectors/internal/api/transfer_initiation.go b/components/payments/cmd/connectors/internal/api/transfer_initiation.go deleted file mode 100644 index 869f825bbe..0000000000 --- a/components/payments/cmd/connectors/internal/api/transfer_initiation.go +++ /dev/null @@ -1,206 +0,0 @@ -package api - -import ( - "encoding/json" - "math/big" - "net/http" - "time" - - "github.com/formancehq/go-libs/api" - "github.com/formancehq/payments/cmd/connectors/internal/api/backend" - "github.com/formancehq/payments/cmd/connectors/internal/api/service" - "github.com/formancehq/payments/internal/otel" - "github.com/gorilla/mux" - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -type transferInitiationResponse struct { - ID string `json:"id"` - Reference string `json:"reference"` - CreatedAt time.Time `json:"createdAt"` - ScheduledAt time.Time `json:"scheduledAt"` - Description string `json:"description"` - SourceAccountID string `json:"sourceAccountID"` - DestinationAccountID string `json:"destinationAccountID"` - ConnectorID string `json:"connectorID"` - Type string `json:"type"` - Amount *big.Int `json:"amount"` - InitialAmount *big.Int `json:"initialAmount"` - Asset string `json:"asset"` - Status string `json:"status"` - Error string `json:"error"` - Metadata map[string]string `json:"metadata"` -} - -func createTransferInitiationHandler(b backend.ServiceBackend) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "createTransferInitiationHandler") - defer span.End() - - w.Header().Set("Content-Type", "application/json") - - payload := &service.CreateTransferInitiationRequest{} - if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrMissingOrInvalidBody, err) - return - } - - setSpanAttributesFromRequest(span, payload) - - if err := payload.Validate(); err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrValidation, err) - return - } - - tf, err := b.GetService().CreateTransferInitiation(ctx, payload) - if err != nil { - otel.RecordError(span, err) - handleServiceErrors(w, r, err) - return - } - - span.SetAttributes( - attribute.String("transfer.id", tf.ID.String()), - attribute.String("transfer.createdAt", tf.CreatedAt.String()), - attribute.String("connectorID", tf.ConnectorID.String()), - ) - - data := &transferInitiationResponse{ - ID: tf.ID.String(), - Reference: tf.ID.Reference, - CreatedAt: tf.CreatedAt, - ScheduledAt: tf.ScheduledAt, - Description: tf.Description, - SourceAccountID: tf.SourceAccountID.String(), - DestinationAccountID: tf.DestinationAccountID.String(), - ConnectorID: tf.ConnectorID.String(), - Type: tf.Type.String(), - Amount: tf.Amount, - InitialAmount: tf.InitialAmount, - Asset: tf.Asset.String(), - Metadata: tf.Metadata, - } - - if len(tf.RelatedAdjustments) > 0 { - // Take the status and error from the last adjustment - data.Status = tf.RelatedAdjustments[0].Status.String() - data.Error = tf.RelatedAdjustments[0].Error - } - - err = json.NewEncoder(w).Encode(api.BaseResponse[transferInitiationResponse]{ - Data: data, - }) - if err != nil { - otel.RecordError(span, err) - api.InternalServerError(w, r, err) - return - } - } -} - -func updateTransferInitiationStatusHandler(b backend.ServiceBackend) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "updateTransferInitiationStatusHandler") - defer span.End() - - payload := &service.UpdateTransferInitiationStatusRequest{} - if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrMissingOrInvalidBody, err) - return - } - - span.SetAttributes(attribute.String("request.status", payload.Status)) - - if err := payload.Validate(); err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrValidation, err) - return - } - - transferID, ok := mux.Vars(r)["transferID"] - if !ok { - otel.RecordError(span, errors.New("missing transferID")) - api.BadRequest(w, ErrInvalidID, errors.New("missing transferID")) - return - } - - span.SetAttributes(attribute.String("transfer.id", transferID)) - - if err := b.GetService().UpdateTransferInitiationStatus(ctx, transferID, payload); err != nil { - otel.RecordError(span, err) - handleServiceErrors(w, r, err) - return - } - - w.WriteHeader(http.StatusNoContent) - } -} - -func retryTransferInitiationHandler(b backend.ServiceBackend) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "retryTransferInitiationHandler") - defer span.End() - - transferID, ok := mux.Vars(r)["transferID"] - if !ok { - otel.RecordError(span, errors.New("missing transferID")) - api.BadRequest(w, ErrInvalidID, errors.New("missing transferID")) - return - } - - span.SetAttributes(attribute.String("transfer.id", transferID)) - - if err := b.GetService().RetryTransferInitiation(ctx, transferID); err != nil { - otel.RecordError(span, err) - handleServiceErrors(w, r, err) - return - } - - w.WriteHeader(http.StatusNoContent) - } -} - -func deleteTransferInitiationHandler(b backend.ServiceBackend) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "deleteTransferInitiationHandler") - defer span.End() - - transferID, ok := mux.Vars(r)["transferID"] - if !ok { - otel.RecordError(span, errors.New("missing transferID")) - api.BadRequest(w, ErrInvalidID, errors.New("missing transferID")) - return - } - - span.SetAttributes(attribute.String("transfer.id", transferID)) - - if err := b.GetService().DeleteTransferInitiation(ctx, transferID); err != nil { - otel.RecordError(span, err) - handleServiceErrors(w, r, err) - return - } - - w.WriteHeader(http.StatusNoContent) - } -} - -func setSpanAttributesFromRequest(span trace.Span, transfer *service.CreateTransferInitiationRequest) { - span.SetAttributes( - attribute.String("request.reference", transfer.Reference), - attribute.String("request.scheduledAt", transfer.ScheduledAt.String()), - attribute.String("request.description", transfer.Description), - attribute.String("request.sourceAccountID", transfer.SourceAccountID), - attribute.String("request.destinationAccountID", transfer.DestinationAccountID), - attribute.String("request.connectorID", transfer.ConnectorID), - attribute.String("request.provider", transfer.Provider), - attribute.String("request.type", transfer.Type), - attribute.String("request.amount", transfer.Amount.String()), - attribute.String("request.asset", transfer.Asset), - attribute.String("request.validated", transfer.Asset), - ) -} diff --git a/components/payments/cmd/connectors/internal/api/transfer_initiation_test.go b/components/payments/cmd/connectors/internal/api/transfer_initiation_test.go deleted file mode 100644 index bfd49906d4..0000000000 --- a/components/payments/cmd/connectors/internal/api/transfer_initiation_test.go +++ /dev/null @@ -1,762 +0,0 @@ -package api - -import ( - "bytes" - "encoding/json" - "fmt" - "math/big" - "net/http" - "net/http/httptest" - "testing" - "time" - - sharedapi "github.com/formancehq/go-libs/api" - "github.com/formancehq/go-libs/auth" - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/api/service" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/pkg/errors" - "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" -) - -func TestCreateTransferInitiations(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - req *service.CreateTransferInitiationRequest - expectedStatusCode int - expectedErrorCode string - serviceError error - } - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - - sourceAccountID := models.AccountID{ - Reference: "acc1", - ConnectorID: connectorID, - } - - destinationAccountID := models.AccountID{ - Reference: "acc2", - ConnectorID: connectorID, - } - - testCases := []testCase{ - { - name: "nominal", - req: &service.CreateTransferInitiationRequest{ - Reference: "ref1", - ScheduledAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Description: "test_nominal", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - ConnectorID: connectorID.String(), - Provider: models.ConnectorProviderDummyPay.String(), - Type: models.TransferInitiationTypeTransfer.String(), - Amount: big.NewInt(100), - Asset: "EUR/2", - Validated: false, - Metadata: map[string]string{ - "foo": "bar", - }, - }, - }, - { - name: "nominal without description", - req: &service.CreateTransferInitiationRequest{ - Reference: "ref1", - ScheduledAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - ConnectorID: connectorID.String(), - Provider: models.ConnectorProviderDummyPay.String(), - Type: models.TransferInitiationTypeTransfer.String(), - Amount: big.NewInt(100), - Asset: "EUR/2", - Validated: false, - Metadata: map[string]string{ - "foo": "bar", - }, - }, - }, - { - name: "missing reference", - req: &service.CreateTransferInitiationRequest{ - ScheduledAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Description: "test_nominal", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - ConnectorID: connectorID.String(), - Provider: models.ConnectorProviderDummyPay.String(), - Type: models.TransferInitiationTypeTransfer.String(), - Amount: big.NewInt(100), - Asset: "EUR/2", - Validated: false, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "missing destination account id", - req: &service.CreateTransferInitiationRequest{ - Reference: "ref1", - ScheduledAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Description: "test_nominal", - SourceAccountID: sourceAccountID.String(), - ConnectorID: connectorID.String(), - Provider: models.ConnectorProviderDummyPay.String(), - Type: models.TransferInitiationTypeTransfer.String(), - Amount: big.NewInt(100), - Asset: "EUR/2", - Validated: false, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "missing source account id, should not end in error", - req: &service.CreateTransferInitiationRequest{ - Reference: "ref1", - ScheduledAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Description: "test_nominal", - DestinationAccountID: destinationAccountID.String(), - ConnectorID: connectorID.String(), - Provider: models.ConnectorProviderDummyPay.String(), - Type: models.TransferInitiationTypeTransfer.String(), - Amount: big.NewInt(100), - Asset: "EUR/2", - Validated: false, - }, - }, - { - name: "wrong transfer initiation type", - req: &service.CreateTransferInitiationRequest{ - Reference: "ref1", - ScheduledAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Description: "test_nominal", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - ConnectorID: connectorID.String(), - Provider: models.ConnectorProviderDummyPay.String(), - Type: "invalid", - Amount: big.NewInt(100), - Asset: "EUR/2", - Validated: false, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "missing amount", - req: &service.CreateTransferInitiationRequest{ - Reference: "ref1", - ScheduledAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Description: "test_nominal", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - ConnectorID: connectorID.String(), - Provider: models.ConnectorProviderDummyPay.String(), - Type: models.TransferInitiationTypeTransfer.String(), - Asset: "EUR/2", - Validated: false, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "missing asset", - req: &service.CreateTransferInitiationRequest{ - Reference: "ref1", - ScheduledAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Description: "test_nominal", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - ConnectorID: connectorID.String(), - Provider: models.ConnectorProviderDummyPay.String(), - Type: models.TransferInitiationTypeTransfer.String(), - Amount: big.NewInt(100), - Validated: false, - }, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "no body", - req: nil, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrMissingOrInvalidBody, - }, - { - name: "service error duplicate key", - req: &service.CreateTransferInitiationRequest{ - Reference: "ref1", - ScheduledAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Description: "test_nominal", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - ConnectorID: connectorID.String(), - Provider: models.ConnectorProviderDummyPay.String(), - Type: models.TransferInitiationTypeTransfer.String(), - Amount: big.NewInt(100), - Asset: "EUR/2", - Validated: false, - }, - serviceError: storage.ErrDuplicateKeyValue, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - }, - { - name: "service error not found", - req: &service.CreateTransferInitiationRequest{ - Reference: "ref1", - ScheduledAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Description: "test_nominal", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - ConnectorID: connectorID.String(), - Provider: models.ConnectorProviderDummyPay.String(), - Type: models.TransferInitiationTypeTransfer.String(), - Amount: big.NewInt(100), - Asset: "EUR/2", - Validated: false, - }, - serviceError: storage.ErrNotFound, - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - }, - { - name: "service error validation", - req: &service.CreateTransferInitiationRequest{ - Reference: "ref1", - ScheduledAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Description: "test_nominal", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - ConnectorID: connectorID.String(), - Provider: models.ConnectorProviderDummyPay.String(), - Type: models.TransferInitiationTypeTransfer.String(), - Amount: big.NewInt(100), - Asset: "EUR/2", - Validated: false, - }, - serviceError: service.ErrValidation, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "service error invalid ID", - req: &service.CreateTransferInitiationRequest{ - Reference: "ref1", - ScheduledAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Description: "test_nominal", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - ConnectorID: connectorID.String(), - Provider: models.ConnectorProviderDummyPay.String(), - Type: models.TransferInitiationTypeTransfer.String(), - Amount: big.NewInt(100), - Asset: "EUR/2", - Validated: false, - }, - serviceError: service.ErrInvalidID, - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrInvalidID, - }, - { - name: "service error publish", - req: &service.CreateTransferInitiationRequest{ - Reference: "ref1", - ScheduledAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Description: "test_nominal", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - ConnectorID: connectorID.String(), - Provider: models.ConnectorProviderDummyPay.String(), - Type: models.TransferInitiationTypeTransfer.String(), - Amount: big.NewInt(100), - Asset: "EUR/2", - Validated: false, - }, - serviceError: service.ErrPublish, - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - { - name: "service error other errors", - req: &service.CreateTransferInitiationRequest{ - Reference: "ref1", - ScheduledAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Description: "test_nominal", - SourceAccountID: sourceAccountID.String(), - DestinationAccountID: destinationAccountID.String(), - ConnectorID: connectorID.String(), - Provider: models.ConnectorProviderDummyPay.String(), - Type: models.TransferInitiationTypeTransfer.String(), - Amount: big.NewInt(100), - Asset: "EUR/2", - Validated: false, - }, - serviceError: errors.New("some error"), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusOK - } - - createTransferInitiationResponse := models.TransferInitiation{ - ID: models.TransferInitiationID{ - Reference: "ref1", - ConnectorID: connectorID, - }, - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - ScheduledAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Description: "test_nominal", - Type: models.TransferInitiationTypeTransfer, - SourceAccountID: &sourceAccountID, - DestinationAccountID: destinationAccountID, - Provider: models.ConnectorProviderDummyPay, - ConnectorID: connectorID, - Amount: big.NewInt(100), - Asset: "EUR/2", - Metadata: map[string]string{ - "foo": "bar", - }, - RelatedAdjustments: []*models.TransferInitiationAdjustment{ - { - ID: uuid.New(), - TransferInitiationID: models.TransferInitiationID{ - Reference: "ref1", - ConnectorID: connectorID, - }, - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Status: models.TransferInitiationStatusProcessing, - }, - }, - } - - expectedCreateTransferInitiationResponse := &transferInitiationResponse{ - ID: models.TransferInitiationID{ - Reference: "ref1", - ConnectorID: connectorID, - }.String(), - Reference: "ref1", - CreatedAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - ScheduledAt: time.Date(2023, 11, 22, 8, 0, 0, 0, time.UTC), - Description: createTransferInitiationResponse.Description, - SourceAccountID: createTransferInitiationResponse.SourceAccountID.String(), - DestinationAccountID: createTransferInitiationResponse.DestinationAccountID.String(), - ConnectorID: createTransferInitiationResponse.ConnectorID.String(), - Type: createTransferInitiationResponse.Type.String(), - Amount: createTransferInitiationResponse.Amount, - Asset: createTransferInitiationResponse.Asset.String(), - Status: models.TransferInitiationStatusProcessing.String(), - Metadata: createTransferInitiationResponse.Metadata, - } - - backend, mockService := newServiceTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockService.EXPECT(). - CreateTransferInitiation(gomock.Any(), testCase.req). - Return(&createTransferInitiationResponse, nil) - } - if testCase.serviceError != nil { - mockService.EXPECT(). - CreateTransferInitiation(gomock.Any(), testCase.req). - Return(nil, testCase.serviceError) - } - - router := httpRouter(logging.Testing(), backend, sharedapi.ServiceInfo{}, auth.NewNoAuth(), nil, false) - - var body []byte - if testCase.req != nil { - var err error - body, err = json.Marshal(testCase.req) - require.NoError(t, err) - } - - req := httptest.NewRequest(http.MethodPost, "/transfer-initiations", bytes.NewReader(body)) - rec := httptest.NewRecorder() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - var resp sharedapi.BaseResponse[transferInitiationResponse] - sharedapi.Decode(t, rec.Body, &resp) - require.Equal(t, expectedCreateTransferInitiationResponse, resp.Data) - } else { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } -} - -func TestUpdateTransferInitiationStatus(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - req *service.UpdateTransferInitiationStatusRequest - transferID string - expectedStatusCode int - expectedErrorCode string - serviceError error - } - - transferID := models.TransferInitiationID{ - Reference: "ref1", - ConnectorID: models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - }, - } - - testCases := []testCase{ - { - name: "nominal", - req: &service.UpdateTransferInitiationStatusRequest{ - Status: "VALIDATED", - }, - transferID: transferID.String(), - }, - { - name: "missing body", - transferID: transferID.String(), - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrMissingOrInvalidBody, - }, - { - name: "service error duplicate key", - req: &service.UpdateTransferInitiationStatusRequest{ - Status: "VALIDATED", - }, - serviceError: storage.ErrDuplicateKeyValue, - transferID: transferID.String(), - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - }, - { - name: "service error not found", - req: &service.UpdateTransferInitiationStatusRequest{ - Status: "VALIDATED", - }, - serviceError: storage.ErrNotFound, - transferID: transferID.String(), - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - }, - { - name: "service error validation", - req: &service.UpdateTransferInitiationStatusRequest{ - Status: "VALIDATED", - }, - serviceError: service.ErrValidation, - transferID: transferID.String(), - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "service error invalid ID", - req: &service.UpdateTransferInitiationStatusRequest{ - Status: "VALIDATED", - }, - serviceError: service.ErrInvalidID, - transferID: transferID.String(), - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrInvalidID, - }, - { - name: "service error publish", - req: &service.UpdateTransferInitiationStatusRequest{ - Status: "VALIDATED", - }, - serviceError: service.ErrPublish, - transferID: transferID.String(), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - { - name: "service error other errors", - req: &service.UpdateTransferInitiationStatusRequest{ - Status: "VALIDATED", - }, - serviceError: errors.New("some error"), - transferID: transferID.String(), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusNoContent - } - - backend, mockService := newServiceTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockService.EXPECT(). - UpdateTransferInitiationStatus(gomock.Any(), testCase.transferID, testCase.req). - Return(nil) - } - if testCase.serviceError != nil { - mockService.EXPECT(). - UpdateTransferInitiationStatus(gomock.Any(), testCase.transferID, testCase.req). - Return(testCase.serviceError) - } - - router := httpRouter(logging.Testing(), backend, sharedapi.ServiceInfo{}, auth.NewNoAuth(), nil, false) - - var body []byte - if testCase.req != nil { - var err error - body, err = json.Marshal(testCase.req) - require.NoError(t, err) - } - - req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/transfer-initiations/%s/status", testCase.transferID), bytes.NewReader(body)) - rec := httptest.NewRecorder() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode >= 300 || testCase.expectedStatusCode < 200 { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } -} - -func TestRetryTransferInitiation(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - transferID string - expectedStatusCode int - expectedErrorCode string - serviceError error - } - - transferID := models.TransferInitiationID{ - Reference: "ref1", - ConnectorID: models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - }, - } - - testCases := []testCase{ - { - name: "nominal", - transferID: transferID.String(), - }, - { - name: "service error duplicate key", - serviceError: storage.ErrDuplicateKeyValue, - transferID: transferID.String(), - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - }, - { - name: "service error not found", - serviceError: storage.ErrNotFound, - transferID: transferID.String(), - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - }, - { - name: "service error validation", - serviceError: service.ErrValidation, - transferID: transferID.String(), - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "service error invalid ID", - serviceError: service.ErrInvalidID, - transferID: transferID.String(), - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrInvalidID, - }, - { - name: "service error publish", - serviceError: service.ErrPublish, - transferID: transferID.String(), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - { - name: "service error other errors", - serviceError: errors.New("some error"), - transferID: transferID.String(), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusNoContent - } - - backend, mockService := newServiceTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockService.EXPECT(). - RetryTransferInitiation(gomock.Any(), testCase.transferID). - Return(nil) - } - if testCase.serviceError != nil { - mockService.EXPECT(). - RetryTransferInitiation(gomock.Any(), testCase.transferID). - Return(testCase.serviceError) - } - - router := httpRouter(logging.Testing(), backend, sharedapi.ServiceInfo{}, auth.NewNoAuth(), nil, false) - - req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/transfer-initiations/%s/retry", testCase.transferID), nil) - rec := httptest.NewRecorder() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode >= 300 || testCase.expectedStatusCode < 200 { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } -} - -func TestDeleteTransferInitiation(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - transferID string - expectedStatusCode int - expectedErrorCode string - serviceError error - } - - transferID := models.TransferInitiationID{ - Reference: "ref1", - ConnectorID: models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - }, - } - - testCases := []testCase{ - { - name: "nominal", - transferID: transferID.String(), - }, - { - name: "service error duplicate key", - serviceError: storage.ErrDuplicateKeyValue, - transferID: transferID.String(), - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrUniqueReference, - }, - { - name: "service error not found", - serviceError: storage.ErrNotFound, - transferID: transferID.String(), - expectedStatusCode: http.StatusNotFound, - expectedErrorCode: ErrNotFound, - }, - { - name: "service error validation", - serviceError: service.ErrValidation, - transferID: transferID.String(), - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrValidation, - }, - { - name: "service error invalid ID", - serviceError: service.ErrInvalidID, - transferID: transferID.String(), - expectedStatusCode: http.StatusBadRequest, - expectedErrorCode: ErrInvalidID, - }, - { - name: "service error publish", - serviceError: service.ErrPublish, - transferID: transferID.String(), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - { - name: "service error other errors", - serviceError: errors.New("some error"), - transferID: transferID.String(), - expectedStatusCode: http.StatusInternalServerError, - expectedErrorCode: sharedapi.ErrorInternal, - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - - if testCase.expectedStatusCode == 0 { - testCase.expectedStatusCode = http.StatusNoContent - } - - backend, mockService := newServiceTestingBackend(t) - if testCase.expectedStatusCode < 300 && testCase.expectedStatusCode >= 200 { - mockService.EXPECT(). - DeleteTransferInitiation(gomock.Any(), testCase.transferID). - Return(nil) - } - if testCase.serviceError != nil { - mockService.EXPECT(). - DeleteTransferInitiation(gomock.Any(), testCase.transferID). - Return(testCase.serviceError) - } - - router := httpRouter(logging.Testing(), backend, sharedapi.ServiceInfo{}, auth.NewNoAuth(), nil, false) - - req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/transfer-initiations/%s", testCase.transferID), nil) - rec := httptest.NewRecorder() - - router.ServeHTTP(rec, req) - - require.Equal(t, testCase.expectedStatusCode, rec.Code) - if testCase.expectedStatusCode >= 300 || testCase.expectedStatusCode < 200 { - err := sharedapi.ErrorResponse{} - sharedapi.Decode(t, rec.Body, &err) - require.EqualValues(t, testCase.expectedErrorCode, err.ErrorCode) - } - }) - } -} diff --git a/components/payments/cmd/connectors/internal/api/transfer_reversal.go b/components/payments/cmd/connectors/internal/api/transfer_reversal.go deleted file mode 100644 index b49abbccd7..0000000000 --- a/components/payments/cmd/connectors/internal/api/transfer_reversal.go +++ /dev/null @@ -1,49 +0,0 @@ -package api - -import ( - "encoding/json" - "net/http" - - "github.com/formancehq/go-libs/api" - "github.com/formancehq/payments/cmd/connectors/internal/api/backend" - "github.com/formancehq/payments/cmd/connectors/internal/api/service" - "github.com/formancehq/payments/internal/otel" - "github.com/gorilla/mux" - "github.com/pkg/errors" -) - -func reverseTransferInitiationHandler(b backend.ServiceBackend) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "reverseTransferInitiationHandler") - defer span.End() - - payload := &service.ReverseTransferInitiationRequest{} - if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrMissingOrInvalidBody, err) - return - } - - if err := payload.Validate(); err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrValidation, err) - return - } - - transferID, ok := mux.Vars(r)["transferID"] - if !ok { - otel.RecordError(span, errors.New("missing transferID")) - api.BadRequest(w, ErrInvalidID, errors.New("missing transferID")) - return - } - - _, err := b.GetService().ReverseTransferInitiation(ctx, transferID, payload) - if err != nil { - otel.RecordError(span, err) - handleServiceErrors(w, r, err) - return - } - - api.NoContent(w) - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/adyen/client/accounts.go b/components/payments/cmd/connectors/internal/connectors/adyen/client/accounts.go deleted file mode 100644 index 3d309b5249..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/adyen/client/accounts.go +++ /dev/null @@ -1,30 +0,0 @@ -package client - -import ( - "context" - "fmt" - "time" - - "github.com/adyen/adyen-go-api-library/v7/src/management" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" -) - -func (c *Client) GetMerchantAccounts(ctx context.Context, pageNumber, pageSize int32) ([]management.Merchant, error) { - f := connectors.ClientMetrics(ctx, "adyen", "list_merchant_accounts") - now := time.Now() - defer f(ctx, now) - - listMerchantsResponse, raw, err := c.client.Management().AccountMerchantLevelApi.ListMerchantAccounts( - ctx, - c.client.Management().AccountMerchantLevelApi.ListMerchantAccountsInput().PageNumber(pageNumber).PageSize(pageSize), - ) - if err != nil { - return nil, err - } - - if raw.StatusCode >= 300 { - return nil, fmt.Errorf("failed to get merchant accounts: %d", raw.StatusCode) - } - - return listMerchantsResponse.Data, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/adyen/client/client.go b/components/payments/cmd/connectors/internal/connectors/adyen/client/client.go deleted file mode 100644 index e9237b618f..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/adyen/client/client.go +++ /dev/null @@ -1,37 +0,0 @@ -package client - -import ( - "github.com/adyen/adyen-go-api-library/v7/src/adyen" - "github.com/adyen/adyen-go-api-library/v7/src/common" - "github.com/formancehq/go-libs/logging" -) - -type Client struct { - client *adyen.APIClient - - HMACKey string - - logger logging.Logger -} - -func NewClient(apiKey, hmacKey, liveEndpointPrefix string, logger logging.Logger) (*Client, error) { - adyenConfig := &common.Config{ - ApiKey: apiKey, - Environment: common.TestEnv, - Debug: true, - } - - if liveEndpointPrefix != "" { - adyenConfig.Environment = common.LiveEnv - adyenConfig.LiveEndpointURLPrefix = liveEndpointPrefix - adyenConfig.Debug = false - } - - client := adyen.NewClient(adyenConfig) - - return &Client{ - client: client, - HMACKey: hmacKey, - logger: logger, - }, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/adyen/client/webhooks.go b/components/payments/cmd/connectors/internal/connectors/adyen/client/webhooks.go deleted file mode 100644 index c51b75aaee..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/adyen/client/webhooks.go +++ /dev/null @@ -1,7 +0,0 @@ -package client - -import "github.com/adyen/adyen-go-api-library/v7/src/webhook" - -func (c *Client) CreateWebhookForRequest(req string) (*webhook.Webhook, error) { - return webhook.HandleRequest(req) -} diff --git a/components/payments/cmd/connectors/internal/connectors/adyen/config.go b/components/payments/cmd/connectors/internal/connectors/adyen/config.go deleted file mode 100644 index b854af218f..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/adyen/config.go +++ /dev/null @@ -1,62 +0,0 @@ -package adyen - -import ( - "encoding/json" - "fmt" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/configtemplate" -) - -const ( - defaultPollingPeriod = 2 * time.Minute -) - -type Config struct { - Name string `json:"name" yaml:"name" bson:"name"` - APIKey string `json:"apiKey" yaml:"apiKey" bson:"apiKey"` - HMACKey string `json:"hmacKey" yaml:"hmacKey" bson:"hmacKey"` - LiveEndpointPrefix string `json:"liveEndpointPrefix" yaml:"liveEndpointPrefix" bson:"liveEndpointPrefix"` - PollingPeriod connectors.Duration `json:"pollingPeriod" yaml:"pollingPeriod" bson:"pollingPeriod"` -} - -func (c Config) String() string { - return fmt.Sprintf("liveEndpointPrefix=%s, apiKey=****, hmacKey=****", c.LiveEndpointPrefix) -} - -func (c Config) Validate() error { - if c.APIKey == "" { - return ErrMissingAPIKey - } - - if c.Name == "" { - return ErrMissingName - } - - if c.HMACKey == "" { - return ErrMissingHMACKey - } - - return nil -} - -func (c Config) Marshal() ([]byte, error) { - return json.Marshal(c) -} - -func (c Config) ConnectorName() string { - return c.Name -} - -func (c Config) BuildTemplate() (string, configtemplate.Config) { - cfg := configtemplate.NewConfig() - - cfg.AddParameter("name", configtemplate.TypeString, name.String(), false) - cfg.AddParameter("apiKey", configtemplate.TypeString, "", true) - cfg.AddParameter("hmacKey", configtemplate.TypeString, "", true) - cfg.AddParameter("liveEndpointPrefix", configtemplate.TypeString, "", false) - cfg.AddParameter("pollingPeriod", configtemplate.TypeDurationNs, defaultPollingPeriod.String(), false) - - return name.String(), cfg -} diff --git a/components/payments/cmd/connectors/internal/connectors/adyen/connector.go b/components/payments/cmd/connectors/internal/connectors/adyen/connector.go deleted file mode 100644 index 353ad192bb..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/adyen/connector.go +++ /dev/null @@ -1,112 +0,0 @@ -package adyen - -import ( - "context" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" -) - -const name = models.ConnectorProviderAdyen - -var ( - mainTaskDescriptor = TaskDescriptor{ - Name: "Main task to periodically fetch users and transactions", - Key: taskNameMain, - } -) - -type Connector struct { - logger logging.Logger - cfg Config -} - -func newConnector(logger logging.Logger, cfg Config) *Connector { - return &Connector{ - logger: logger.WithFields(map[string]any{ - "component": "connector", - }), - cfg: cfg, - } -} - -func (c *Connector) UpdateConfig(ctx task.ConnectorContext, config models.ConnectorConfigObject) error { - cfg, ok := config.(Config) - if !ok { - return connectors.ErrInvalidConfig - } - - restartTask := c.cfg.PollingPeriod.Duration != cfg.PollingPeriod.Duration - - c.cfg = cfg - - if restartTask { - // Restart the main task to use the new polling period. - taskDescriptor, err := models.EncodeTaskDescriptor(mainTaskDescriptor) - if err != nil { - return err - } - - return ctx.Scheduler().Schedule(ctx.Context(), taskDescriptor, models.TaskSchedulerOptions{ - // We want to polling every c.cfg.PollingPeriod.Duration seconds the users - // and their transactions. - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: c.cfg.PollingPeriod.Duration, - // No need to restart this task, since the connector is not existing or - // was uninstalled previously, the task does not exists in the database - RestartOption: models.OPTIONS_STOP_AND_RESTART, - }) - } - - return nil -} - -func (c *Connector) Install(ctx task.ConnectorContext) error { - taskDescriptor, err := models.EncodeTaskDescriptor(mainTaskDescriptor) - if err != nil { - return err - } - - return ctx.Scheduler().Schedule(ctx.Context(), taskDescriptor, models.TaskSchedulerOptions{ - // We want to polling every c.cfg.PollingPeriod.Duration seconds the users - // and their transactions. - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: c.cfg.PollingPeriod.Duration, - // No need to restart this task, since the connector is not existing or - // was uninstalled previously, the task does not exists in the database - RestartOption: models.OPTIONS_RESTART_NEVER, - }) -} - -func (c *Connector) Uninstall(ctx context.Context) error { - return nil -} - -func (c *Connector) Resolve(descriptor models.TaskDescriptor) task.Task { - taskDescriptor, err := models.DecodeTaskDescriptor[TaskDescriptor](descriptor) - if err != nil { - panic(err) - } - - return resolveTasks(c.logger, c.cfg)(taskDescriptor) -} - -func (c *Connector) SupportedCurrenciesAndDecimals() map[string]int { - return supportedCurrenciesWithDecimal -} - -func (c *Connector) InitiatePayment(ctx task.ConnectorContext, transfer *models.TransferInitiation) error { - return connectors.ErrNotImplemented -} - -func (c *Connector) ReversePayment(ctx task.ConnectorContext, transferReversal *models.TransferReversal) error { - return connectors.ErrNotImplemented -} - -func (c *Connector) CreateExternalBankAccount(ctx task.ConnectorContext, bankAccount *models.BankAccount) error { - return connectors.ErrNotImplemented -} - -var _ connectors.Connector = &Connector{} diff --git a/components/payments/cmd/connectors/internal/connectors/adyen/currencies.go b/components/payments/cmd/connectors/internal/connectors/adyen/currencies.go deleted file mode 100644 index 31e5bef7c1..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/adyen/currencies.go +++ /dev/null @@ -1,7 +0,0 @@ -package adyen - -import "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - -var ( - supportedCurrenciesWithDecimal = currency.ISO4217Currencies -) diff --git a/components/payments/cmd/connectors/internal/connectors/adyen/errors.go b/components/payments/cmd/connectors/internal/connectors/adyen/errors.go deleted file mode 100644 index 2b27cc229b..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/adyen/errors.go +++ /dev/null @@ -1,20 +0,0 @@ -package adyen - -import "errors" - -var ( - // ErrMissingTask is returned when the task is missing. - ErrMissingTask = errors.New("task is not implemented") - - // ErrMissingAPIKey is returned when the apiKey is missing. - ErrMissingAPIKey = errors.New("missing apiKey from config") - - // ErrMissingEndpoint is returned when the endpoint is missing. - ErrMissingLiveEndpointPrefix = errors.New("missing live endpoint prefix from config") - - // ErrMissingName is returned when the name is missing. - ErrMissingName = errors.New("missing name from config") - - // ErrMissingHMACKey is returned when the hmacKey is missing. - ErrMissingHMACKey = errors.New("missing hmacKey from config") -) diff --git a/components/payments/cmd/connectors/internal/connectors/adyen/loader.go b/components/payments/cmd/connectors/internal/connectors/adyen/loader.go deleted file mode 100644 index ab284974e2..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/adyen/loader.go +++ /dev/null @@ -1,54 +0,0 @@ -package adyen - -import ( - "net/http" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/gorilla/mux" -) - -type Loader struct{} - -const allowedTasks = 50 - -func (l *Loader) AllowTasks() int { - return allowedTasks -} - -func (l *Loader) Name() models.ConnectorProvider { - return name -} - -func (l *Loader) Load(logger logging.Logger, config Config) connectors.Connector { - return newConnector(logger, config) -} - -func (l *Loader) ApplyDefaults(cfg Config) Config { - if cfg.PollingPeriod.Duration == 0 { - cfg.PollingPeriod.Duration = defaultPollingPeriod - } - - if cfg.Name == "" { - cfg.Name = name.String() - } - - return cfg -} - -func (l *Loader) Router(_ *storage.Storage) *mux.Router { - // storage is not used in this connector - - r := mux.NewRouter() - - r.Path("/").Methods(http.MethodPost).Handler(handleStandardWebhooks()) - - return r -} - -// NewLoader creates a new loader. -func NewLoader() *Loader { - return &Loader{} -} diff --git a/components/payments/cmd/connectors/internal/connectors/adyen/task_fetch_merchants_accounts.go b/components/payments/cmd/connectors/internal/connectors/adyen/task_fetch_merchants_accounts.go deleted file mode 100644 index c96cc11ae8..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/adyen/task_fetch_merchants_accounts.go +++ /dev/null @@ -1,114 +0,0 @@ -package adyen - -import ( - "context" - "encoding/json" - "time" - - "github.com/adyen/adyen-go-api-library/v7/src/management" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/adyen/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "go.opentelemetry.io/otel/attribute" -) - -const ( - pageSize = 100 -) - -func taskFetchAccounts(client *client.Client) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "adyen.taskFetchAccounts", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - if err := fetchAccounts(ctx, client, connectorID, ingester, scheduler); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func fetchAccounts( - ctx context.Context, - client *client.Client, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, -) error { - for page := 1; ; page++ { - pagedAccounts, err := client.GetMerchantAccounts(ctx, int32(page), pageSize) - if err != nil { - return err - } - - if err := ingestAccountsBatch(ctx, connectorID, ingester, pagedAccounts); err != nil { - return err - } - - if len(pagedAccounts) < pageSize { - break - } - } - - return nil -} - -func ingestAccountsBatch( - ctx context.Context, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - accounts []management.Merchant, -) error { - if len(accounts) == 0 { - return nil - } - - batch := ingestion.AccountBatch{} - for _, account := range accounts { - raw, err := json.Marshal(account) - if err != nil { - return err - } - - a := &models.Account{ - ID: models.AccountID{ - Reference: *account.Id, - ConnectorID: connectorID, - }, - // Moneycorp does not send the opening date of the account - CreatedAt: time.Now(), - Reference: *account.Id, - ConnectorID: connectorID, - Type: models.AccountTypeInternal, - RawData: raw, - } - - if account.Name != nil { - a.AccountName = *account.Name - } - - batch = append(batch, a) - } - - if err := ingester.IngestAccounts(ctx, batch); err != nil { - return err - } - - return nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/adyen/task_main.go b/components/payments/cmd/connectors/internal/connectors/adyen/task_main.go deleted file mode 100644 index 54a1e51410..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/adyen/task_main.go +++ /dev/null @@ -1,49 +0,0 @@ -package adyen - -import ( - "context" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" -) - -func taskMain() task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - scheduler task.Scheduler, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "adyen.taskMain", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - taskAccounts, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch accounts from client", - Key: taskNameFetchAccounts, - }) - if err != nil { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - err = scheduler.Schedule(ctx, taskAccounts, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - return nil - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/adyen/task_resolve.go b/components/payments/cmd/connectors/internal/connectors/adyen/task_resolve.go deleted file mode 100644 index e95385f26b..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/adyen/task_resolve.go +++ /dev/null @@ -1,57 +0,0 @@ -package adyen - -import ( - "fmt" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/adyen/client" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/google/uuid" -) - -const ( - taskNameMain = "main" - taskNameFetchAccounts = "fetch-accounts" - taskNameHandleWebhook = "handle-webhook" -) - -type TaskDescriptor struct { - Name string `json:"name" yaml:"name" bson:"name"` - Key string `json:"key" yaml:"key" bson:"key"` - PollingPeriod int `json:"pollingPeriod" yaml:"pollingPeriod" bson:"pollingPeriod"` - WebhookID uuid.UUID `json:"webhookId" yaml:"webhookId" bson:"webhookId"` -} - -func resolveTasks(logger logging.Logger, config Config) func(taskDefinition TaskDescriptor) task.Task { - adyenClient, err := client.NewClient( - config.APIKey, - config.HMACKey, - config.LiveEndpointPrefix, - logger, - ) - if err != nil { - logger.Error(err) - - return func(taskDescriptor TaskDescriptor) task.Task { - return func() error { - return fmt.Errorf("cannot build adyen client: %w", err) - } - } - } - - return func(taskDescriptor TaskDescriptor) task.Task { - switch taskDescriptor.Key { - case taskNameMain: - return taskMain() - case taskNameFetchAccounts: - return taskFetchAccounts(adyenClient) - case taskNameHandleWebhook: - return taskHandleStandardWebhooks(adyenClient, taskDescriptor.WebhookID) - } - - // This should never happen. - return func() error { - return fmt.Errorf("key '%s': %w", taskDescriptor.Key, ErrMissingTask) - } - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/adyen/task_standard_webhooks.go b/components/payments/cmd/connectors/internal/connectors/adyen/task_standard_webhooks.go deleted file mode 100644 index 4f80f50974..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/adyen/task_standard_webhooks.go +++ /dev/null @@ -1,619 +0,0 @@ -package adyen - -import ( - "context" - "encoding/json" - "errors" - "math/big" - "net/http" - "strings" - - "github.com/adyen/adyen-go-api-library/v7/src/hmacvalidator" - "github.com/adyen/adyen-go-api-library/v7/src/webhook" - "github.com/formancehq/go-libs/api" - "github.com/formancehq/go-libs/contextutil" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/adyen/client" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/google/uuid" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -func handleStandardWebhooks() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - connectorContext := task.ConnectorContextFromContext(r.Context()) - webhookID := connectors.WebhookIDFromContext(r.Context()) - span := trace.SpanFromContext(r.Context()) - - // Detach the context since we're launching an async task and we're mostly - // coming from a HTTP request. - detachedCtx, _ := contextutil.Detached(r.Context()) - taskDescriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "handle webhook", - Key: taskNameHandleWebhook, - WebhookID: webhookID, - }) - if err != nil { - otel.RecordError(span, err) - api.InternalServerError(w, r, err) - return - } - - err = connectorContext.Scheduler().Schedule(detachedCtx, taskDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - otel.RecordError(span, err) - api.InternalServerError(w, r, err) - return - } - - w.WriteHeader(http.StatusOK) - w.Write([]byte("[accepted]")) - } -} - -func taskHandleStandardWebhooks(client *client.Client, webhookID uuid.UUID) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - storageReader storage.Reader, - ingester ingestion.Ingester, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "adyen.taskHandleStandardWebhooks", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("webhookID", webhookID.String()), - ) - defer span.End() - - w, err := storageReader.GetWebhook(ctx, webhookID) - if err != nil { - otel.RecordError(span, err) - return err - } - - webhooks, err := client.CreateWebhookForRequest(string(w.RequestBody)) - if err != nil { - otel.RecordError(span, err) - return err - } - - for _, item := range *webhooks.NotificationItems { - if !hmacvalidator.ValidateHmac(item.NotificationRequestItem, client.HMACKey) { - // Record error without setting the status to error since we - // continue the execution. - span.RecordError(err) - continue - } - - if err := handleNotificationRequestItem( - ctx, - connectorID, - storageReader, - ingester, - item.NotificationRequestItem, - ); err != nil { - otel.RecordError(span, err) - return err - } - } - - return nil - } -} - -func handleNotificationRequestItem( - ctx context.Context, - connectorID models.ConnectorID, - storageReader storage.Reader, - ingester ingestion.Ingester, - item webhook.NotificationRequestItem, -) error { - switch item.EventCode { - case webhook.EventCodeAuthorisation: - return handleAuthorisation(ctx, connectorID, ingester, item) - case webhook.EventCodeAuthorisationAdjustment: - return handleAuthorisationAdjustment(ctx, connectorID, storageReader, ingester, item) - case webhook.EventCodeCancellation: - return handleCancellation(ctx, connectorID, storageReader, ingester, item) - case webhook.EventCodeCapture: - return handleCapture(ctx, connectorID, storageReader, ingester, item) - case webhook.EventCodeCaptureFailed: - return handleCaptureFailed(ctx, connectorID, storageReader, ingester, item) - case webhook.EventCodeRefund: - return handleRefund(ctx, connectorID, storageReader, ingester, item) - case webhook.EventCodeRefundFailed: - return handleRefundFailed() - case webhook.EventCodeRefundedReversed: - return handleRefundedReversed(ctx, connectorID, storageReader, ingester, item) - case webhook.EventCodeRefundWithData: - return handleRefundWithData(ctx, connectorID, storageReader, ingester, item) - case webhook.EventCodePayoutThirdparty: - return handlePayoutThirdparty(ctx, connectorID, ingester, item) - case webhook.EventCodePayoutDecline: - return handlePayoutDecline(ctx, connectorID, ingester, item) - case webhook.EventCodePayoutExpire: - return handlePayoutExpire(ctx, connectorID, ingester, item) - } - - return nil -} - -func handleAuthorisation( - ctx context.Context, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - item webhook.NotificationRequestItem, -) error { - raw, err := json.Marshal(item) - if err != nil { - return err - } - - status := models.PaymentStatusPending - if item.Success == "false" { - status = models.PaymentStatusFailed - } - - payment := &models.Payment{ - ID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: item.PspReference, - Type: models.PaymentTypePayIn, - }, - ConnectorID: connectorID, - }, - ConnectorID: connectorID, - CreatedAt: *item.EventDate, - Reference: item.PspReference, - Amount: big.NewInt(item.Amount.Value), - InitialAmount: big.NewInt(item.Amount.Value), - Type: models.PaymentTypePayIn, - Status: status, - Scheme: parseScheme(item.PaymentMethod), - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, item.Amount.Currency), - RawData: raw, - DestinationAccountID: &models.AccountID{ - Reference: item.MerchantAccountCode, - ConnectorID: connectorID, - }, - } - - if err := ingester.IngestPayments( - ctx, - ingestion.PaymentBatch{{Payment: payment}}, - ); err != nil { - return err - } - - return nil -} - -func handleAuthorisationAdjustment( - ctx context.Context, - connectorID models.ConnectorID, - storageReader storage.Reader, - ingester ingestion.Ingester, - item webhook.NotificationRequestItem, -) error { - if item.Success == "true" { - payment, err := storageReader.GetPayment(ctx, models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: item.OriginalReference, - Type: models.PaymentTypePayIn, - }, - ConnectorID: connectorID, - }.String()) - if err != nil { - return err - } - - payment.Amount = big.NewInt(item.Amount.Value) - payment.InitialAmount = big.NewInt(item.Amount.Value) - - if err := ingester.IngestPayments( - ctx, - ingestion.PaymentBatch{{Payment: payment}}, - ); err != nil { - return err - } - } - - return nil -} - -func handleCancellation( - ctx context.Context, - connectorID models.ConnectorID, - storageReader storage.Reader, - ingester ingestion.Ingester, - item webhook.NotificationRequestItem, -) error { - if item.Success == "true" { - payment, err := storageReader.GetPayment(ctx, models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: item.OriginalReference, - Type: models.PaymentTypePayIn, - }, - ConnectorID: connectorID, - }.String()) - if err != nil { - return err - } - - payment.Status = models.PaymentStatusCancelled - - if err := ingester.IngestPayments( - ctx, - ingestion.PaymentBatch{{Payment: payment}}, - ); err != nil { - return err - } - } - - return nil -} - -func handleCapture( - ctx context.Context, - connectorID models.ConnectorID, - storageReader storage.Reader, - ingester ingestion.Ingester, - item webhook.NotificationRequestItem, -) error { - if item.Success == "true" { - payment, err := storageReader.GetPayment(ctx, models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: item.OriginalReference, - Type: models.PaymentTypePayIn, - }, - ConnectorID: connectorID, - }.String()) - if err != nil { - return err - } - - payment.Status = models.PaymentStatusSucceeded - payment.Amount = big.NewInt(item.Amount.Value) - - if err := ingester.IngestPayments( - ctx, - ingestion.PaymentBatch{{Payment: payment}}, - ); err != nil { - return err - } - } - - return nil -} - -func handleCaptureFailed( - ctx context.Context, - connectorID models.ConnectorID, - storageReader storage.Reader, - ingester ingestion.Ingester, - item webhook.NotificationRequestItem, -) error { - if item.Success == "true" { - payment, err := storageReader.GetPayment(ctx, models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: item.OriginalReference, - Type: models.PaymentTypePayIn, - }, - ConnectorID: connectorID, - }.String()) - if err != nil { - return err - } - - payment.Status = models.PaymentStatusFailed - - if err := ingester.IngestPayments( - ctx, - ingestion.PaymentBatch{{Payment: payment}}, - ); err != nil { - return err - } - } - - return nil -} - -func handleRefund( - ctx context.Context, - connectorID models.ConnectorID, - storageReader storage.Reader, - ingester ingestion.Ingester, - item webhook.NotificationRequestItem, -) error { - if item.Success == "true" { - payment, err := storageReader.GetPayment(ctx, models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: item.OriginalReference, - Type: models.PaymentTypePayIn, - }, - ConnectorID: connectorID, - }.String()) - if err != nil { - return err - } - - payment.Amount = payment.Amount.Sub(payment.Amount, big.NewInt(item.Amount.Value)) - if payment.Amount.Cmp(big.NewInt(0)) == 0 { - payment.Status = models.PaymentStatusRefunded - } - - if err := ingester.IngestPayments( - ctx, - ingestion.PaymentBatch{{Payment: payment}}, - ); err != nil { - return err - } - } - - return nil -} - -func handleRefundFailed() error { - // Nothing to do for now (while waiting to enhance the payment adjustment model) - return nil -} - -func handleRefundedReversed( - ctx context.Context, - connectorID models.ConnectorID, - storageReader storage.Reader, - ingester ingestion.Ingester, - item webhook.NotificationRequestItem, -) error { - if item.Success == "true" { - payment, err := storageReader.GetPayment(ctx, models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: item.PspReference, - Type: models.PaymentTypePayIn, - }, - ConnectorID: connectorID, - }.String()) - if err != nil { - return err - } - - payment.Amount = payment.Amount.Add(payment.Amount, big.NewInt(item.Amount.Value)) - - if err := ingester.IngestPayments( - ctx, - ingestion.PaymentBatch{{Payment: payment}}, - ); err != nil { - return err - } - } - - return nil -} - -func handleRefundWithData( - ctx context.Context, - connectorID models.ConnectorID, - storageReader storage.Reader, - ingester ingestion.Ingester, - item webhook.NotificationRequestItem, -) error { - if item.Success == "true" { - payment, err := storageReader.GetPayment(ctx, models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: item.OriginalReference, - Type: models.PaymentTypePayIn, - }, - ConnectorID: connectorID, - }.String()) - if err != nil { - return err - } - - payment.Amount = payment.Amount.Sub(payment.Amount, big.NewInt(item.Amount.Value)) - if payment.Amount.Cmp(big.NewInt(0)) == 0 { - payment.Status = models.PaymentStatusRefunded - } - - if err := ingester.IngestPayments( - ctx, - ingestion.PaymentBatch{{Payment: payment}}, - ); err != nil { - return err - } - } - - return nil -} - -func handlePayoutThirdparty( - ctx context.Context, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - item webhook.NotificationRequestItem, -) error { - raw, err := json.Marshal(item) - if err != nil { - return err - } - - status := models.PaymentStatusSucceeded - if item.Success == "false" { - status = models.PaymentStatusFailed - } - - payment := &models.Payment{ - ID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: item.PspReference, - Type: models.PaymentTypePayOut, - }, - ConnectorID: connectorID, - }, - ConnectorID: connectorID, - CreatedAt: *item.EventDate, - Reference: item.PspReference, - Amount: big.NewInt(item.Amount.Value), - InitialAmount: big.NewInt(item.Amount.Value), - Type: models.PaymentTypePayOut, - Status: status, - Scheme: parseScheme(item.PaymentMethod), - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, item.Amount.Currency), - RawData: raw, - SourceAccountID: &models.AccountID{ - Reference: item.MerchantAccountCode, - ConnectorID: connectorID, - }, - } - - if err := ingester.IngestPayments( - ctx, - ingestion.PaymentBatch{{Payment: payment}}, - ); err != nil { - return err - } - - return nil -} - -func handlePayoutDecline( - ctx context.Context, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - item webhook.NotificationRequestItem, -) error { - if item.Success != "true" { - return nil - } - - raw, err := json.Marshal(item) - if err != nil { - return err - } - - payment := &models.Payment{ - ID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: item.PspReference, - Type: models.PaymentTypePayOut, - }, - ConnectorID: connectorID, - }, - ConnectorID: connectorID, - CreatedAt: *item.EventDate, - Reference: item.PspReference, - Amount: big.NewInt(item.Amount.Value), - InitialAmount: big.NewInt(item.Amount.Value), - Type: models.PaymentTypePayOut, - Status: models.PaymentStatusCancelled, - Scheme: parseScheme(item.PaymentMethod), - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, item.Amount.Currency), - RawData: raw, - SourceAccountID: &models.AccountID{ - Reference: item.MerchantAccountCode, - ConnectorID: connectorID, - }, - } - - if err := ingester.IngestPayments( - ctx, - ingestion.PaymentBatch{{Payment: payment}}, - ); err != nil { - return err - } - - return nil -} - -func handlePayoutExpire( - ctx context.Context, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - item webhook.NotificationRequestItem, -) error { - if item.Success != "true" { - return nil - } - - raw, err := json.Marshal(item) - if err != nil { - return err - } - - payment := &models.Payment{ - ID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: item.PspReference, - Type: models.PaymentTypePayOut, - }, - ConnectorID: connectorID, - }, - ConnectorID: connectorID, - CreatedAt: *item.EventDate, - Reference: item.PspReference, - Amount: big.NewInt(item.Amount.Value), - InitialAmount: big.NewInt(item.Amount.Value), - Type: models.PaymentTypePayOut, - Status: models.PaymentStatusExpired, - Scheme: parseScheme(item.PaymentMethod), - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, item.Amount.Currency), - RawData: raw, - SourceAccountID: &models.AccountID{ - Reference: item.MerchantAccountCode, - ConnectorID: connectorID, - }, - } - - if err := ingester.IngestPayments( - ctx, - ingestion.PaymentBatch{{Payment: payment}}, - ); err != nil { - return err - } - - return nil -} - -func parseScheme(scheme string) models.PaymentScheme { - switch { - case strings.HasPrefix(scheme, "visa"): - return models.PaymentSchemeCardVisa - case strings.HasPrefix(scheme, "electron"): - return models.PaymentSchemeCardVisa - case strings.HasPrefix(scheme, "amex"): - return models.PaymentSchemeCardAmex - case strings.HasPrefix(scheme, "alipay"): - return models.PaymentSchemeCardAlipay - case strings.HasPrefix(scheme, "cup"): - return models.PaymentSchemeCardCUP - case strings.HasPrefix(scheme, "discover"): - return models.PaymentSchemeCardDiscover - case strings.HasPrefix(scheme, "doku"): - return models.PaymentSchemeDOKU - case strings.HasPrefix(scheme, "dragonpay"): - return models.PaymentSchemeDragonPay - case strings.HasPrefix(scheme, "jcb"): - return models.PaymentSchemeCardJCB - case strings.HasPrefix(scheme, "maestro"): - return models.PaymentSchemeMaestro - case strings.HasPrefix(scheme, "mc"): - return models.PaymentSchemeCardMasterCard - case strings.HasPrefix(scheme, "molpay"): - return models.PaymentSchemeMolPay - case strings.HasPrefix(scheme, "diners"): - return models.PaymentSchemeCardDiners - default: - return models.PaymentSchemeUnknown - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/atlar/Insomnium.json b/components/payments/cmd/connectors/internal/connectors/atlar/Insomnium.json deleted file mode 100644 index 65c54cc39b..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/atlar/Insomnium.json +++ /dev/null @@ -1 +0,0 @@ -{"_type":"export","__export_format":4,"__export_date":"2023-11-28T15:46:25.856Z","__export_source":"insomnia.desktop.app:v0.2.3","resources":[{"_id":"req_e9545a9d7ffd4e44b982e2ddb8b15e83","parentId":"wrk_9ca49e64d481426cb9e1831e73b552ba","modified":1700756567874,"created":1700665329960,"url":"{{ _.baseUrlApi }}/_info","name":"Info","description":"","method":"GET","body":{},"parameters":[],"headers":[{"name":"User-Agent","value":"insomnium/0.2.3"}],"authentication":{},"metaSortKey":-1700665329960,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","segmentParams":[],"_type":"request"},{"_id":"wrk_9ca49e64d481426cb9e1831e73b552ba","parentId":null,"modified":1700663660087,"created":1700663635161,"name":"Local Formance Payments Atlar","description":"","scope":"collection","_type":"workspace"},{"_id":"req_05b0176074ef4c37a7a1be0926635e79","parentId":"wrk_9ca49e64d481426cb9e1831e73b552ba","modified":1700732400128,"created":1700732368940,"url":"{{ _.baseUrl }}/connectors/configs","name":"List the configs of each available connector","description":"","method":"GET","body":{},"parameters":[],"headers":[{"name":"User-Agent","value":"insomnium/0.2.3"}],"authentication":{},"metaSortKey":-1700664496984.5,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","segmentParams":[],"_type":"request"},{"_id":"req_545f3ec751574e399e31b9ff899cf77d","parentId":"wrk_9ca49e64d481426cb9e1831e73b552ba","modified":1700756760593,"created":1700755772447,"url":"{{ _.baseUrlApi }}/accounts","name":"List accounts","description":"","method":"GET","body":{},"parameters":[],"headers":[{"name":"User-Agent","value":"insomnium/0.2.3"}],"authentication":{},"metaSortKey":-1700664392862.5625,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","segmentParams":[],"_type":"request"},{"_id":"req_c133874ab7094fcf85ecdc255b3f138b","parentId":"wrk_9ca49e64d481426cb9e1831e73b552ba","modified":1701094404853,"created":1700755638397,"url":"{{ _.baseUrlApi }}/payments","name":"List payments","description":"","method":"GET","body":{},"parameters":[{"id":"pair_629ff5e5c1be4c54a90b8664ea656b0a","name":"","value":"","description":""}],"headers":[{"name":"User-Agent","value":"insomnium/0.2.3"}],"authentication":{},"metaSortKey":-1700664288740.625,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","segmentParams":[],"_type":"request"},{"_id":"req_6088705ca6e44f9693b07dea3d0aad11","parentId":"wrk_9ca49e64d481426cb9e1831e73b552ba","modified":1700756584625,"created":1700745901467,"url":"{{ _.baseUrlConnectors }}/connectors","name":"List all installed connectors","description":"","method":"GET","body":{},"parameters":[],"headers":[{"name":"User-Agent","value":"insomnium/0.2.3"}],"authentication":{},"metaSortKey":-1700664080496.75,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","segmentParams":[],"_type":"request"},{"_id":"req_4da5e386b56141889cb7769e8846f3cd","parentId":"wrk_9ca49e64d481426cb9e1831e73b552ba","modified":1700756591790,"created":1700746001449,"url":"{{ _.baseUrlConnectors }}/connectors/configs","name":"List the configs of each available connector","description":"","method":"GET","body":{},"parameters":[],"headers":[{"name":"User-Agent","value":"insomnium/0.2.3"}],"authentication":{},"metaSortKey":-1700663872252.875,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","segmentParams":[],"_type":"request"},{"_id":"req_a20a27621407489cb8998b4e3238af6e","parentId":"wrk_9ca49e64d481426cb9e1831e73b552ba","modified":1700756596678,"created":1700663664009,"url":"{{ _.baseUrlConnectors }}/connectors/atlar","name":"Install Atlar Connector","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Test\",\n\t\"pollingPeriod\": \"10s\",\n\t\"baseUrl\": \"https://api.atlar.com\",\n\t\"accessKey\": \"{{ _.atlar_accessKey }}\",\n\t\"secret\": \"{{ _.atlar_secret }}\"\n}\n"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json"},{"name":"User-Agent","value":"insomnium/0.2.3"}],"authentication":{},"metaSortKey":-1700663664009,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","segmentParams":[],"_type":"request"},{"_id":"req_a678810335464e0bba10f57ee3bc9f95","parentId":"wrk_9ca49e64d481426cb9e1831e73b552ba","modified":1700756602079,"created":1700743647460,"url":"{{ _.baseUrlConnectors }}/connectors/atlar/:connectorID/reset","name":"Reset Atlar Connector","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Test\",\n\t\"pollingPeriod\": \"10s\",\n\t\"baseUrl\": \"https://api.atlar.com\",\n\t\"accessKey\": \"{{ _.atlar_accessKey }}\",\n\t\"secret\": \"{{ _.atlar_secret }}\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json"},{"name":"User-Agent","value":"insomnium/0.2.3"}],"authentication":{},"metaSortKey":-1700534131609,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","segmentParams":[{"name":"connectorID","value":"{% response 'body', 'req_a20a27621407489cb8998b4e3238af6e', 'b64::JC5kYXRhLmNvbm5lY3RvcklE::46b', 'never', 60 %}","disabled":false,"id":"pair_519cdcc16fad41d8afccf27bd428eb3aending0","fileName":""}],"_type":"request"},{"_id":"req_4419fe631b61412d9ecbb80ea3ecd29c","parentId":"wrk_9ca49e64d481426cb9e1831e73b552ba","modified":1700756607072,"created":1700736368735,"url":"{{ _.baseUrlConnectors }}/connectors/atlar/:connectorID","name":"Uninstall Atlar Connector","description":"","method":"DELETE","body":{"mimeType":"application/json","text":""},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json"},{"name":"User-Agent","value":"insomnium/0.2.3"}],"authentication":{},"metaSortKey":-1700501748509,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","segmentParams":[{"name":"connectorID","value":"{% response 'body', 'req_a20a27621407489cb8998b4e3238af6e', 'b64::JC5kYXRhLmNvbm5lY3RvcklE::46b', 'never', 60 %}","disabled":false,"id":"pair_519cdcc16fad41d8afccf27bd428eb3aending0","fileName":""}],"_type":"request"},{"_id":"req_e8c035af9b6e48fda8410b089764c8f6","parentId":"wrk_9ca49e64d481426cb9e1831e73b552ba","modified":1700756613381,"created":1700747923012,"url":"{{ _.baseUrlConnectors }}/connectors/atlar/:connectorID/tasks","name":"List tasks from a connector","description":"","method":"GET","body":{"mimeType":"application/json","text":""},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json"},{"name":"User-Agent","value":"insomnium/0.2.3"}],"authentication":{},"metaSortKey":-1700469365409,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","segmentParams":[{"name":"connectorID","value":"{% response 'body', 'req_a20a27621407489cb8998b4e3238af6e', 'b64::JC5kYXRhLmNvbm5lY3RvcklE::46b', 'never', 60 %}","disabled":false,"id":"pair_519cdcc16fad41d8afccf27bd428eb3aending0","fileName":""}],"_type":"request"},{"_id":"env_90638f53ef3039b5d60b8dfc5744952b4e2de8c7","parentId":"wrk_9ca49e64d481426cb9e1831e73b552ba","modified":1700756537786,"created":1700663635163,"name":"Base Environment","data":{"baseUrlApi":"http://localhost:8080","baseUrlConnectors":"http://localhost:8081","atlar_accessKey":"","atlar_secret":""},"dataPropertyOrder":{"&":["baseUrlApi","baseUrlConnectors","atlar_accessKey","atlar_secret"]},"color":null,"isPrivate":false,"metaSortKey":1700663635163,"_type":"environment"},{"_id":"jar_90638f53ef3039b5d60b8dfc5744952b4e2de8c7","parentId":"wrk_9ca49e64d481426cb9e1831e73b552ba","modified":1700663635163,"created":1700663635163,"name":"Default Jar","cookies":[],"_type":"cookie_jar"}]} \ No newline at end of file diff --git a/components/payments/cmd/connectors/internal/connectors/atlar/account_utils.go b/components/payments/cmd/connectors/internal/connectors/atlar/account_utils.go deleted file mode 100644 index 82e2fbefd6..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/atlar/account_utils.go +++ /dev/null @@ -1,93 +0,0 @@ -package atlar - -import ( - "encoding/json" - "fmt" - - "github.com/formancehq/go-libs/metadata" - "github.com/formancehq/payments/internal/models" - atlar_models "github.com/get-momo/atlar-v1-go-client/models" -) - -type AtlarExternalAccountAndCounterparty struct { - ExternalAccount atlar_models.ExternalAccount `json:"externalAccount" yaml:"externalAccount" bson:"externalAccount"` - Counterparty atlar_models.Counterparty `json:"counterparty" yaml:"counterparty" bson:"counterparty"` -} - -func ExternalAccountFromAtlarData( - connectorID models.ConnectorID, - externalAccount *atlar_models.ExternalAccount, - counterparty *atlar_models.Counterparty, -) (*models.Account, error) { - raw, err := json.Marshal(AtlarExternalAccountAndCounterparty{ExternalAccount: *externalAccount, Counterparty: *counterparty}) - if err != nil { - return nil, err - } - - createdAt, err := ParseAtlarTimestamp(externalAccount.Created) - if err != nil { - return nil, fmt.Errorf("failed to parse opening date: %w", err) - } - - return &models.Account{ - ID: models.AccountID{ - Reference: externalAccount.ID, - ConnectorID: connectorID, - }, - CreatedAt: createdAt, - Reference: externalAccount.ID, - ConnectorID: connectorID, - // DefaultAsset: left empty because the information is not provided by Atlar, - AccountName: counterparty.Name, // TODO: is that okay? External accounts do not have a name at Atlar. - Type: models.AccountTypeExternal, - Metadata: extractExternalAccountAndCounterpartyMetadata(externalAccount, counterparty), - RawData: raw, - }, nil -} - -func ExtractAccountMetadata(account *atlar_models.Account, bank *atlar_models.ThirdParty) metadata.Metadata { - result := metadata.Metadata{} - result = result.Merge(ComputeAccountMetadataBool("fictive", account.Fictive)) - result = result.Merge(ComputeAccountMetadata("bank/id", bank.ID)) - result = result.Merge(ComputeAccountMetadata("bank/name", bank.Name)) - result = result.Merge(ComputeAccountMetadata("bank/bic", account.Bank.Bic)) - result = result.Merge(IdentifiersToMetadata(account.Identifiers)) - result = result.Merge(ComputeAccountMetadata("alias", account.Alias)) - result = result.Merge(ComputeAccountMetadata("owner/name", account.Owner.Name)) - return result -} - -func IdentifiersToMetadata(identifiers []*atlar_models.AccountIdentifier) metadata.Metadata { - result := metadata.Metadata{} - for _, i := range identifiers { - result = result.Merge(ComputeAccountMetadata( - fmt.Sprintf("identifier/%s/%s", *i.Market, *i.Type), - *i.Number, - )) - if *i.Type == "IBAN" { - result = result.Merge(ComputeAccountMetadata( - fmt.Sprintf("identifier/%s", *i.Type), - *i.Number, - )) - } - } - return result -} - -func extractExternalAccountAndCounterpartyMetadata(externalAccount *atlar_models.ExternalAccount, counterparty *atlar_models.Counterparty) metadata.Metadata { - result := metadata.Metadata{} - result = result.Merge(ComputeAccountMetadata("bank/id", externalAccount.Bank.ID)) - result = result.Merge(ComputeAccountMetadata("bank/name", externalAccount.Bank.Name)) - result = result.Merge(ComputeAccountMetadata("bank/bic", externalAccount.Bank.Bic)) - result = result.Merge(IdentifiersToMetadata(externalAccount.Identifiers)) - result = result.Merge(ComputeAccountMetadata("owner/name", counterparty.Name)) - result = result.Merge(ComputeAccountMetadata("owner/type", counterparty.PartyType)) - result = result.Merge(ComputeAccountMetadata("owner/contact/email", counterparty.ContactDetails.Email)) - result = result.Merge(ComputeAccountMetadata("owner/contact/phone", counterparty.ContactDetails.Phone)) - result = result.Merge(ComputeAccountMetadata("owner/contact/address/streetName", counterparty.ContactDetails.Address.StreetName)) - result = result.Merge(ComputeAccountMetadata("owner/contact/address/streetNumber", counterparty.ContactDetails.Address.StreetNumber)) - result = result.Merge(ComputeAccountMetadata("owner/contact/address/city", counterparty.ContactDetails.Address.City)) - result = result.Merge(ComputeAccountMetadata("owner/contact/address/postalCode", counterparty.ContactDetails.Address.PostalCode)) - result = result.Merge(ComputeAccountMetadata("owner/contact/address/country", counterparty.ContactDetails.Address.Country)) - return result -} diff --git a/components/payments/cmd/connectors/internal/connectors/atlar/client/accounts.go b/components/payments/cmd/connectors/internal/connectors/atlar/client/accounts.go deleted file mode 100644 index 07ee30f461..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/atlar/client/accounts.go +++ /dev/null @@ -1,36 +0,0 @@ -package client - -import ( - "context" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/get-momo/atlar-v1-go-client/client/accounts" -) - -func (c *Client) GetV1AccountsID(ctx context.Context, id string) (*accounts.GetV1AccountsIDOK, error) { - f := connectors.ClientMetrics(ctx, "atlar", "list_accounts") - now := time.Now() - defer f(ctx, now) - - accountsParams := accounts.GetV1AccountsIDParams{ - Context: ctx, - ID: id, - } - - return c.client.Accounts.GetV1AccountsID(&accountsParams) -} - -func (c *Client) GetV1Accounts(ctx context.Context, token string, pageSize int64) (*accounts.GetV1AccountsOK, error) { - f := connectors.ClientMetrics(ctx, "atlar", "list_accounts") - now := time.Now() - defer f(ctx, now) - - accountsParams := accounts.GetV1AccountsParams{ - Limit: &pageSize, - Context: ctx, - Token: &token, - } - - return c.client.Accounts.GetV1Accounts(&accountsParams) -} diff --git a/components/payments/cmd/connectors/internal/connectors/atlar/client/client.go b/components/payments/cmd/connectors/internal/connectors/atlar/client/client.go deleted file mode 100644 index 887a33219f..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/atlar/client/client.go +++ /dev/null @@ -1,33 +0,0 @@ -package client - -import ( - "net/url" - - "github.com/go-openapi/strfmt" - - atlar_client "github.com/get-momo/atlar-v1-go-client/client" - - httptransport "github.com/go-openapi/runtime/client" -) - -type Client struct { - client *atlar_client.Rest -} - -func NewClient(baseURL url.URL, accessKey, secret string) *Client { - return &Client{ - client: createAtlarClient(baseURL, accessKey, secret), - } -} - -func createAtlarClient(baseURL url.URL, accessKey, secret string) *atlar_client.Rest { - transport := httptransport.New( - baseURL.Host, - baseURL.Path, - []string{baseURL.Scheme}, - ) - basicAuth := httptransport.BasicAuth(accessKey, secret) - transport.DefaultAuthentication = basicAuth - client := atlar_client.New(transport, strfmt.Default) - return client -} diff --git a/components/payments/cmd/connectors/internal/connectors/atlar/client/counter_parties.go b/components/payments/cmd/connectors/internal/connectors/atlar/client/counter_parties.go deleted file mode 100644 index 0ac1b199a8..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/atlar/client/counter_parties.go +++ /dev/null @@ -1,110 +0,0 @@ -package client - -import ( - "context" - "errors" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/internal/models" - "github.com/get-momo/atlar-v1-go-client/client/counterparties" - atlar_models "github.com/get-momo/atlar-v1-go-client/models" -) - -func (c *Client) GetV1CounterpartiesID(ctx context.Context, counterPartyID string) (*counterparties.GetV1CounterpartiesIDOK, error) { - f := connectors.ClientMetrics(ctx, "atlar", "get_counterparty") - now := time.Now() - defer f(ctx, now) - - getCounterpartyParams := counterparties.GetV1CounterpartiesIDParams{ - Context: ctx, - ID: counterPartyID, - } - counterpartyResponse, err := c.client.Counterparties.GetV1CounterpartiesID(&getCounterpartyParams) - if err != nil { - return nil, err - } - - return counterpartyResponse, nil -} - -func (c *Client) CreateCounterParty(ctx context.Context, newExternalBankAccount *models.BankAccount) (*string, error) { - f := connectors.ClientMetrics(ctx, "atlar", "create_counterparty") - now := time.Now() - defer f(ctx, now) - - // TODO: make sure an account with that IBAN does not already exist (Atlar API v2 needed, v1 lacks the filters) - // alternatively we could query the local DB - - createCounterpartyRequest := atlar_models.CreateCounterpartyRequest{ - Name: ExtractNamespacedMetadataIgnoreEmpty(newExternalBankAccount.Metadata, "owner/name"), - PartyType: *ExtractNamespacedMetadataIgnoreEmpty(newExternalBankAccount.Metadata, "owner/type"), - ContactDetails: &atlar_models.ContactDetails{ - Email: *ExtractNamespacedMetadataIgnoreEmpty(newExternalBankAccount.Metadata, "owner/contact/email"), - Phone: *ExtractNamespacedMetadataIgnoreEmpty(newExternalBankAccount.Metadata, "owner/contact/phone"), - Address: &atlar_models.Address{ - StreetName: *ExtractNamespacedMetadataIgnoreEmpty(newExternalBankAccount.Metadata, "owner/contact/address/streetName"), - StreetNumber: *ExtractNamespacedMetadataIgnoreEmpty(newExternalBankAccount.Metadata, "owner/contact/address/streetNumber"), - City: *ExtractNamespacedMetadataIgnoreEmpty(newExternalBankAccount.Metadata, "owner/contact/address/city"), - PostalCode: *ExtractNamespacedMetadataIgnoreEmpty(newExternalBankAccount.Metadata, "owner/contact/address/postalCode"), - Country: *ExtractNamespacedMetadataIgnoreEmpty(newExternalBankAccount.Metadata, "owner/contact/address/country"), - }, - }, - ExternalAccounts: []*atlar_models.CreateEmbeddedExternalAccountRequest{ - { - // ExternalID could cause problems when synchronizing with Accounts[type=external] - Bank: &atlar_models.UpdatableBank{ - Bic: newExternalBankAccount.SwiftBicCode, - }, - Identifiers: extractAtlarAccountIdentifiersFromBankAccount(newExternalBankAccount), - }, - }, - } - postCounterpartiesParams := counterparties.PostV1CounterpartiesParams{ - Context: ctx, - Counterparty: &createCounterpartyRequest, - } - postCounterpartiesResponse, err := c.client.Counterparties.PostV1Counterparties(&postCounterpartiesParams) - if err != nil { - return nil, err - } - - if len(postCounterpartiesResponse.Payload.ExternalAccounts) != 1 { - // should never occur, but when in case it happens it's nice to have an error to search for - return nil, errors.New("counterparty was not created with exactly one account") - } - - externalAccountID := postCounterpartiesResponse.Payload.ExternalAccounts[0].ID - - return &externalAccountID, nil -} - -func extractAtlarAccountIdentifiersFromBankAccount(bankAccount *models.BankAccount) []*atlar_models.AccountIdentifier { - ownerName := bankAccount.Metadata[atlarMetadataSpecNamespace+"owner/name"] - ibanType := "IBAN" - accountIdentifiers := []*atlar_models.AccountIdentifier{{ - HolderName: &ownerName, - Market: &bankAccount.Country, - Type: &ibanType, - Number: &bankAccount.IBAN, - }} - for k := range bankAccount.Metadata { - // check whether the key has format com.atlar.spec/identifier// - identifierData, err := metadataToIdentifierData(k, bankAccount.Metadata[k]) - if err != nil { - // matadata does not describe an identifier - continue - } - if identifierData.Market == bankAccount.Country && identifierData.Type == "IBAN" { - // avoid duplicate identifiers - continue - } - accountIdentifiers = append(accountIdentifiers, &atlar_models.AccountIdentifier{ - HolderName: &ownerName, - Market: &identifierData.Market, - Type: &identifierData.Type, - Number: &identifierData.Number, - }) - } - return accountIdentifiers -} diff --git a/components/payments/cmd/connectors/internal/connectors/atlar/client/external_accounts.go b/components/payments/cmd/connectors/internal/connectors/atlar/client/external_accounts.go deleted file mode 100644 index fab92c05ab..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/atlar/client/external_accounts.go +++ /dev/null @@ -1,41 +0,0 @@ -package client - -import ( - "context" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/get-momo/atlar-v1-go-client/client/external_accounts" -) - -func (c *Client) GetV1ExternalAccountsID(ctx context.Context, externalAccountID string) (*external_accounts.GetV1ExternalAccountsIDOK, error) { - f := connectors.ClientMetrics(ctx, "atlar", "get_external_account") - now := time.Now() - defer f(ctx, now) - - getExternalAccountParams := external_accounts.GetV1ExternalAccountsIDParams{ - Context: ctx, - ID: externalAccountID, - } - - externalAccountResponse, err := c.client.ExternalAccounts.GetV1ExternalAccountsID(&getExternalAccountParams) - if err != nil { - return nil, err - } - - return externalAccountResponse, nil -} - -func (c *Client) GetV1ExternalAccounts(ctx context.Context, token string, pageSize int64) (*external_accounts.GetV1ExternalAccountsOK, error) { - f := connectors.ClientMetrics(ctx, "atlar", "list_external_accounts") - now := time.Now() - defer f(ctx, now) - - externalAccountsParams := external_accounts.GetV1ExternalAccountsParams{ - Limit: &pageSize, - Context: ctx, - Token: &token, - } - - return c.client.ExternalAccounts.GetV1ExternalAccounts(&externalAccountsParams) -} diff --git a/components/payments/cmd/connectors/internal/connectors/atlar/client/third_parties.go b/components/payments/cmd/connectors/internal/connectors/atlar/client/third_parties.go deleted file mode 100644 index 8a04ce0213..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/atlar/client/third_parties.go +++ /dev/null @@ -1,22 +0,0 @@ -package client - -import ( - "context" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/get-momo/atlar-v1-go-client/client/third_parties" -) - -func (c *Client) GetV1BetaThirdPartiesID(ctx context.Context, id string) (*third_parties.GetV1betaThirdPartiesIDOK, error) { - f := connectors.ClientMetrics(ctx, "atlar", "list_third_parties") - now := time.Now() - defer f(ctx, now) - - params := third_parties.GetV1betaThirdPartiesIDParams{ - Context: ctx, - ID: id, - } - - return c.client.ThirdParties.GetV1betaThirdPartiesID(¶ms) -} diff --git a/components/payments/cmd/connectors/internal/connectors/atlar/client/transactions.go b/components/payments/cmd/connectors/internal/connectors/atlar/client/transactions.go deleted file mode 100644 index 733d6c1fb6..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/atlar/client/transactions.go +++ /dev/null @@ -1,36 +0,0 @@ -package client - -import ( - "context" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/get-momo/atlar-v1-go-client/client/transactions" -) - -func (c *Client) GetV1Transactions(ctx context.Context, token string, pageSize int64) (*transactions.GetV1TransactionsOK, error) { - f := connectors.ClientMetrics(ctx, "atlar", "list_transactions") - now := time.Now() - defer f(ctx, now) - - params := transactions.GetV1TransactionsParams{ - Limit: &pageSize, - Context: ctx, - Token: &token, - } - - return c.client.Transactions.GetV1Transactions(¶ms) -} - -func (c *Client) GetV1TransactionsID(ctx context.Context, id string) (*transactions.GetV1TransactionsIDOK, error) { - f := connectors.ClientMetrics(ctx, "atlar", "list_transactions") - now := time.Now() - defer f(ctx, now) - - params := transactions.GetV1TransactionsIDParams{ - Context: ctx, - ID: id, - } - - return c.client.Transactions.GetV1TransactionsID(¶ms) -} diff --git a/components/payments/cmd/connectors/internal/connectors/atlar/client/transfers.go b/components/payments/cmd/connectors/internal/connectors/atlar/client/transfers.go deleted file mode 100644 index 5aa3f92489..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/atlar/client/transfers.go +++ /dev/null @@ -1,37 +0,0 @@ -package client - -import ( - "context" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/get-momo/atlar-v1-go-client/client/credit_transfers" - atlar_models "github.com/get-momo/atlar-v1-go-client/models" -) - -func (c *Client) PostV1CreditTransfers(ctx context.Context, req *atlar_models.CreatePaymentRequest) (*credit_transfers.PostV1CreditTransfersCreated, error) { - f := connectors.ClientMetrics(ctx, "atlar", "create_credit_transfer") - now := time.Now() - defer f(ctx, now) - - postCreditTransfersParams := credit_transfers.PostV1CreditTransfersParams{ - Context: ctx, - CreditTransfer: req, - } - - return c.client.CreditTransfers.PostV1CreditTransfers(&postCreditTransfersParams) - -} - -func (c *Client) GetV1CreditTransfersGetByExternalIDExternalID(ctx context.Context, externalID string) (*credit_transfers.GetV1CreditTransfersGetByExternalIDExternalIDOK, error) { - f := connectors.ClientMetrics(ctx, "atlar", "get_credit_transfer") - now := time.Now() - defer f(ctx, now) - - getCreditTransferParams := credit_transfers.GetV1CreditTransfersGetByExternalIDExternalIDParams{ - Context: ctx, - ExternalID: externalID, - } - - return c.client.CreditTransfers.GetV1CreditTransfersGetByExternalIDExternalID(&getCreditTransferParams) -} diff --git a/components/payments/cmd/connectors/internal/connectors/atlar/client/utils.go b/components/payments/cmd/connectors/internal/connectors/atlar/client/utils.go deleted file mode 100644 index 761b572aa5..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/atlar/client/utils.go +++ /dev/null @@ -1,38 +0,0 @@ -package client - -import ( - "errors" - "regexp" -) - -const ( - atlarMetadataSpecNamespace = "com.atlar.spec/" -) - -func ExtractNamespacedMetadataIgnoreEmpty(metadata map[string]string, key string) *string { - value := metadata[atlarMetadataSpecNamespace+key] - return &value -} - -type IdentifierData struct { - Market string - Type string - Number string -} - -var identifierMetadataRegex = regexp.MustCompile(`^com\.atlar\.spec/identifier/([^/]+)/([^/]+)$`) - -func metadataToIdentifierData(key, value string) (*IdentifierData, error) { - // Find matches in the input string - matches := identifierMetadataRegex.FindStringSubmatch(key) - if matches == nil { - return nil, errors.New("input does not match the expected format") - } - - // Extract values from the matched groups - return &IdentifierData{ - Market: matches[1], - Type: matches[2], - Number: value, - }, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/atlar/client/utils_test.go b/components/payments/cmd/connectors/internal/connectors/atlar/client/utils_test.go deleted file mode 100644 index a5b73791ad..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/atlar/client/utils_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package client - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestMetadataToIdentifierData(t *testing.T) { - t.Parallel() - - _, err := metadataToIdentifierData("not_valid", "test") - if assert.Error(t, err) { - assert.Equal(t, errors.New("input does not match the expected format"), err) - } - _, err = metadataToIdentifierData(atlarMetadataSpecNamespace+"not_valid", "test") - if assert.Error(t, err) { - assert.Equal(t, errors.New("input does not match the expected format"), err) - } - _, err = metadataToIdentifierData(atlarMetadataSpecNamespace+"identifier/not_valid", "test") - if assert.Error(t, err) { - assert.Equal(t, errors.New("input does not match the expected format"), err) - } - identifier, err := metadataToIdentifierData(atlarMetadataSpecNamespace+"identifier/DE/IBAN", "DE02700100800030876808") - if assert.Nil(t, err) { - assert.Equal(t, IdentifierData{ - Market: "DE", - Type: "IBAN", - Number: "DE02700100800030876808", - }, *identifier) - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/atlar/config.go b/components/payments/cmd/connectors/internal/connectors/atlar/config.go deleted file mode 100644 index 7f9ea4c79e..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/atlar/config.go +++ /dev/null @@ -1,113 +0,0 @@ -package atlar - -import ( - "encoding/json" - "errors" - "fmt" - "net/url" - "strconv" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors/configtemplate" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" -) - -var ( - //"https://api.atlar.com" - defaultURLValue = url.URL{ - Scheme: "https", - Host: "api.atlar.com", - } - defaultPollingPeriod = 2 * time.Minute - defaultPageSize uint64 = 25 -) - -type Config struct { - Name string `json:"name" yaml:"name" bson:"name"` - PollingPeriod connectors.Duration `json:"pollingPeriod" yaml:"pollingPeriod" bson:"pollingPeriod"` - TransferInitiationStatusPollingPeriod connectors.Duration `json:"transferInitiationStatusPollingPeriod" yaml:"transferInitiationStatusPollingPeriod" bson:"transferInitiationStatusPollingPeriod"` - BaseUrl url.URL `json:"-" yaml:"-" bson:"-"` // Already marshalled as string in the MarshalJson function - AccessKey string `json:"accessKey" yaml:"accessKey" bson:"accessKey"` - Secret string `json:"secret" yaml:"secret" bson:"secret"` - ApiConfig `bson:",inline"` -} - -// String obfuscates sensitive fields and returns a string representation of the config. -// This is used for logging. -func (c Config) String() string { - return fmt.Sprintf("baseUrl=%s, pollingPeriod=%s, transferInitiationStatusPollingPeriod=%s, pageSize=%d, accessKey=%s, secret=****", - c.BaseUrl.String(), c.PollingPeriod, c.TransferInitiationStatusPollingPeriod, c.PageSize, c.AccessKey) -} - -func (c Config) Validate() error { - if c.AccessKey == "" { - return errors.New("missing api access key") - } - - if c.Secret == "" { - return errors.New("missing api secret") - } - - return nil -} - -func (c Config) ConnectorName() string { - return c.Name -} - -func (c Config) Marshal() ([]byte, error) { - type CopyType Config - - basicConfig := struct { - BaseUrl string `json:"baseUrl"` - CopyType - }{ - BaseUrl: c.BaseUrl.String(), - CopyType: (CopyType)(c), - } - - return json.Marshal(basicConfig) -} - -func (c *Config) UnmarshalJSON(data []byte) error { - type CopyType Config - - tmp := struct { - BaseUrl string `json:"baseUrl"` - *CopyType - }{ - CopyType: (*CopyType)(c), - } - - err := json.Unmarshal(data, &tmp) - if err != nil { - return err - } - - baseUrl, err := url.Parse(tmp.BaseUrl) - if err != nil { - return err - } - c.BaseUrl = *baseUrl - - return nil -} - -type ApiConfig struct { - PageSize uint64 `json:"pageSize" yaml:"pageSize" bson:"pageSize"` -} - -func (c Config) BuildTemplate() (string, configtemplate.Config) { - cfg := configtemplate.NewConfig() - - cfg.AddParameter("name", configtemplate.TypeString, name.String(), false) - cfg.AddParameter("baseUrl", configtemplate.TypeString, defaultURLValue.String(), false) - cfg.AddParameter("accessKey", configtemplate.TypeString, "", true) - cfg.AddParameter("secret", configtemplate.TypeString, "", true) - cfg.AddParameter("pollingPeriod", configtemplate.TypeDurationNs, defaultPollingPeriod.String(), false) - cfg.AddParameter("transferInitiationStatusPollingPeriod", configtemplate.TypeDurationNs, defaultPollingPeriod.String(), false) - cfg.AddParameter("pageSize", configtemplate.TypeDurationUnsignedInteger, strconv.Itoa(int(defaultPageSize)), false) - - return name.String(), cfg -} diff --git a/components/payments/cmd/connectors/internal/connectors/atlar/connector.go b/components/payments/cmd/connectors/internal/connectors/atlar/connector.go deleted file mode 100644 index 056131c413..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/atlar/connector.go +++ /dev/null @@ -1,151 +0,0 @@ -package atlar - -import ( - "context" - "errors" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" -) - -const name = models.ConnectorProviderAtlar - -var ( - mainTaskDescriptor = TaskDescriptor{ - Name: "Main task to periodically fetch transactions", - Main: true, - } -) - -type Connector struct { - logger logging.Logger - cfg Config -} - -func newConnector(logger logging.Logger, cfg Config) *Connector { - return &Connector{ - logger: logger.WithFields(map[string]any{ - "component": "connector", - }), - cfg: cfg, - } -} - -func (c *Connector) UpdateConfig(ctx task.ConnectorContext, config models.ConnectorConfigObject) error { - cfg, ok := config.(Config) - if !ok { - return connectors.ErrInvalidConfig - } - - restartTask := c.cfg.PollingPeriod.Duration != cfg.PollingPeriod.Duration - - c.cfg = cfg - - if restartTask { - descriptor, err := models.EncodeTaskDescriptor(mainTaskDescriptor) - if err != nil { - return err - } - - return ctx.Scheduler().Schedule(ctx.Context(), descriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: c.cfg.PollingPeriod.Duration, - // No need to restart this task, since the connector is not existing or - // was uninstalled previously, the task does not exists in the database - RestartOption: models.OPTIONS_STOP_AND_RESTART, - }) - } - - return nil -} - -func (c *Connector) Install(ctx task.ConnectorContext) error { - descriptor, err := models.EncodeTaskDescriptor(mainTaskDescriptor) - if err != nil { - return err - } - - return ctx.Scheduler().Schedule(ctx.Context(), descriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: c.cfg.PollingPeriod.Duration, - // No need to restart this task, since the connector is not existing or - // was uninstalled previously, the task does not exists in the database - RestartOption: models.OPTIONS_RESTART_NEVER, - }) -} - -func (c *Connector) Uninstall(ctx context.Context) error { - return nil -} - -func (c *Connector) SupportedCurrenciesAndDecimals() map[string]int { - return supportedCurrenciesWithDecimal -} - -func (c *Connector) Resolve(descriptor models.TaskDescriptor) task.Task { - taskDescriptor, err := models.DecodeTaskDescriptor[TaskDescriptor](descriptor) - if err != nil { - panic(err) - } - - return resolveTasks(c.logger, c.cfg)(taskDescriptor) -} - -func (c *Connector) InitiatePayment(ctx task.ConnectorContext, transfer *models.TransferInitiation) error { - err := ValidateTransferInitiation(transfer) - if err != nil { - return err - } - - descriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Initiate payment", - Key: taskNameInitiatePayment, - TransferID: transfer.ID.String(), - }) - if err != nil { - return err - } - - scheduleOption := models.OPTIONS_RUN_NOW_SYNC - scheduledAt := transfer.ScheduledAt - if !scheduledAt.IsZero() { - scheduleOption = models.OPTIONS_RUN_SCHEDULED_AT - } - - if err := ctx.Scheduler().Schedule(ctx.Context(), descriptor, models.TaskSchedulerOptions{ - ScheduleOption: scheduleOption, - ScheduleAt: scheduledAt, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }); err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return err - } - - return nil -} - -func (c *Connector) ReversePayment(ctx task.ConnectorContext, transferReversal *models.TransferReversal) error { - return connectors.ErrNotImplemented -} - -func (c *Connector) CreateExternalBankAccount(ctx task.ConnectorContext, bankAccount *models.BankAccount) error { - descriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Create external bank account", - Key: taskNameCreateExternalBankAccount, - BankAccount: bankAccount, - }) - if err != nil { - return err - } - if err := ctx.Scheduler().Schedule(ctx.Context(), descriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW_SYNC, - }); err != nil { - return err - } - - // TODO: it might make sense to return the external account ID so the client can use it for initiating a payment - return nil -} - -var _ connectors.Connector = &Connector{} diff --git a/components/payments/cmd/connectors/internal/connectors/atlar/currencies.go b/components/payments/cmd/connectors/internal/connectors/atlar/currencies.go deleted file mode 100644 index 9f6470890c..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/atlar/currencies.go +++ /dev/null @@ -1,10 +0,0 @@ -package atlar - -import "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - -var ( - supportedCurrenciesWithDecimal = map[string]int{ - "EUR": currency.ISO4217Currencies["EUR"], // Euro - "DKK": currency.ISO4217Currencies["DKK"], - } -) diff --git a/components/payments/cmd/connectors/internal/connectors/atlar/loader.go b/components/payments/cmd/connectors/internal/connectors/atlar/loader.go deleted file mode 100644 index 77be83a7f9..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/atlar/loader.go +++ /dev/null @@ -1,63 +0,0 @@ -package atlar - -import ( - "net/url" - - "github.com/formancehq/payments/internal/models" - "github.com/gorilla/mux" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/storage" -) - -type Loader struct{} - -const allowedTasks = 50 - -func (l *Loader) AllowTasks() int { - return allowedTasks -} - -func (l *Loader) Name() models.ConnectorProvider { - return name -} - -func (l *Loader) Load(logger logging.Logger, config Config) connectors.Connector { - return newConnector(logger, config) -} - -func (l *Loader) ApplyDefaults(cfg Config) Config { - emptyUrl := url.URL{} - if cfg.BaseUrl == emptyUrl { - //"https://api.atlar.com" - cfg.BaseUrl = defaultURLValue - } - - if cfg.PageSize == 0 { - cfg.PageSize = defaultPageSize - } - - if cfg.PollingPeriod.Duration == 0 { - cfg.PollingPeriod = connectors.Duration{Duration: defaultPollingPeriod} - } - - if cfg.TransferInitiationStatusPollingPeriod.Duration == 0 { - cfg.TransferInitiationStatusPollingPeriod = connectors.Duration{Duration: defaultPollingPeriod} - } - - if cfg.Name == "" { - cfg.Name = name.String() - } - - return cfg -} - -func (l *Loader) Router(_ *storage.Storage) *mux.Router { - // Webhooks are not implemented yet - return nil -} - -func NewLoader() *Loader { - return &Loader{} -} diff --git a/components/payments/cmd/connectors/internal/connectors/atlar/loader_test.go b/components/payments/cmd/connectors/internal/connectors/atlar/loader_test.go deleted file mode 100644 index 144dd323c3..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/atlar/loader_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package atlar - -import ( - "context" - "net/url" - "testing" - "time" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/stretchr/testify/assert" -) - -// TestLoader tests the loader. -func TestLoader(t *testing.T) { - t.Parallel() - - config := Config{} - logger := logging.FromContext(context.TODO()) - - loader := NewLoader() - - assert.Equal(t, name, loader.Name()) - assert.Equal(t, 50, loader.AllowTasks()) - - baseUrl, err := url.Parse("https://api.atlar.com") - assert.Nil(t, err) - - assert.Equal(t, Config{ - Name: "ATLAR", - BaseUrl: *baseUrl, - PollingPeriod: connectors.Duration{Duration: 2 * time.Minute}, - TransferInitiationStatusPollingPeriod: connectors.Duration{Duration: 2 * time.Minute}, - ApiConfig: ApiConfig{PageSize: 25}, - }, loader.ApplyDefaults(config)) - - assert.EqualValues(t, newConnector(logger, config), loader.Load(logger, config)) -} diff --git a/components/payments/cmd/connectors/internal/connectors/atlar/metadata.go b/components/payments/cmd/connectors/internal/connectors/atlar/metadata.go deleted file mode 100644 index cd1a8398d7..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/atlar/metadata.go +++ /dev/null @@ -1,56 +0,0 @@ -package atlar - -import ( - "fmt" - "time" - - "github.com/formancehq/go-libs/metadata" - "github.com/formancehq/payments/internal/models" -) - -const ( - atlarMetadataSpecNamespace = "com.atlar.spec/" - valueTRUE = "TRUE" - valueFALSE = "FALSE" -) - -func ComputeAccountMetadata(key, value string) metadata.Metadata { - namespacedKey := fmt.Sprintf("%s%s", atlarMetadataSpecNamespace, key) - return metadata.Metadata{ - namespacedKey: value, - } -} - -func ComputeAccountMetadataBool(key string, value bool) metadata.Metadata { - computedValue := valueFALSE - if value { - computedValue = valueTRUE - } - return ComputeAccountMetadata(key, computedValue) -} - -func ComputePaymentMetadata(paymentId models.PaymentID, key, value string) *models.PaymentMetadata { - namespacedKey := fmt.Sprintf("%s%s", atlarMetadataSpecNamespace, key) - return &models.PaymentMetadata{ - PaymentID: paymentId, - CreatedAt: time.Now(), - Key: namespacedKey, - Value: value, - } -} - -func ComputePaymentMetadataBool(paymentId models.PaymentID, key string, value bool) *models.PaymentMetadata { - computedValue := valueFALSE - if value { - computedValue = valueTRUE - } - return ComputePaymentMetadata(paymentId, key, computedValue) -} - -func ExtractNamespacedMetadata(metadata map[string]string, key string) (*string, error) { - value, ok := metadata[atlarMetadataSpecNamespace+key] - if !ok { - return nil, fmt.Errorf("unable to find metadata with key %s%s", atlarMetadataSpecNamespace, key) - } - return &value, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/atlar/task_create_external_bank_account.go b/components/payments/cmd/connectors/internal/connectors/atlar/task_create_external_bank_account.go deleted file mode 100644 index 1570ba7f60..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/atlar/task_create_external_bank_account.go +++ /dev/null @@ -1,126 +0,0 @@ -package atlar - -import ( - "context" - "errors" - "fmt" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/atlar/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "go.opentelemetry.io/otel/attribute" -) - -func CreateExternalBankAccountTask(config Config, client *client.Client, newExternalBankAccount *models.BankAccount) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "atlar.taskCreateExternalBankAccount", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("bankAccount.name", newExternalBankAccount.Name), - attribute.String("bankAccount.id", newExternalBankAccount.ID.String()), - ) - defer span.End() - - err := validateExternalBankAccount(newExternalBankAccount) - if err != nil { - otel.RecordError(span, err) - return err - } - - externalAccountID, err := createExternalBankAccount(ctx, client, newExternalBankAccount) - if err != nil { - otel.RecordError(span, err) - return err - } - if externalAccountID == nil { - err := errors.New("no external account id returned") - otel.RecordError(span, err) - return err - } - - err = ingestExternalAccountFromAtlar( - ctx, - connectorID, - ingester, - client, - newExternalBankAccount, - *externalAccountID, - ) - if err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -// TODO: validation (also metadata) needs to return a 400 -func validateExternalBankAccount(newExternalBankAccount *models.BankAccount) error { - _, err := ExtractNamespacedMetadata(newExternalBankAccount.Metadata, "owner/name") - if err != nil { - return fmt.Errorf("required metadata field %sowner/name is missing", atlarMetadataSpecNamespace) - } - ownerType, err := ExtractNamespacedMetadata(newExternalBankAccount.Metadata, "owner/type") - if err != nil { - return fmt.Errorf("required metadata field %sowner/type is missing", atlarMetadataSpecNamespace) - } - if *ownerType != "INDIVIDUAL" && *ownerType != "COMPANY" { - return fmt.Errorf("metadata field %sowner/type needs to be one of [ INDIVIDUAL COMPANY ]", atlarMetadataSpecNamespace) - } - - return nil -} - -func createExternalBankAccount(ctx context.Context, client *client.Client, newExternalBankAccount *models.BankAccount) (*string, error) { - return client.CreateCounterParty(ctx, newExternalBankAccount) -} - -func ingestExternalAccountFromAtlar( - ctx context.Context, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - client *client.Client, - formanceBankAccount *models.BankAccount, - externalAccountID string, -) error { - accountsBatch := ingestion.AccountBatch{} - - externalAccountResponse, err := client.GetV1ExternalAccountsID(ctx, externalAccountID) - if err != nil { - return err - } - - counterpartyResponse, err := client.GetV1CounterpartiesID(ctx, externalAccountResponse.Payload.CounterpartyID) - if err != nil { - return err - } - - newAccount, err := ExternalAccountFromAtlarData(connectorID, externalAccountResponse.Payload, counterpartyResponse.Payload) - if err != nil { - return err - } - - accountsBatch = append(accountsBatch, newAccount) - - err = ingester.IngestAccounts(ctx, accountsBatch) - if err != nil { - return err - } - - if err := ingester.LinkBankAccountWithAccount(ctx, formanceBankAccount, &newAccount.ID); err != nil { - return err - } - - return nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/atlar/task_fetch_accounts.go b/components/payments/cmd/connectors/internal/connectors/atlar/task_fetch_accounts.go deleted file mode 100644 index c2174ccf02..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/atlar/task_fetch_accounts.go +++ /dev/null @@ -1,220 +0,0 @@ -package atlar - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "math/big" - "time" - - "github.com/formancehq/go-libs/contextutil" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/atlar/client" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/get-momo/atlar-v1-go-client/client/accounts" - "github.com/get-momo/atlar-v1-go-client/client/external_accounts" - "go.opentelemetry.io/otel/attribute" -) - -func FetchAccountsTask(config Config, client *client.Client) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - scheduler task.Scheduler, - ingester ingestion.Ingester, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "atlar.taskFetchAccounts", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - // Pagination works by cursor token. - for token := ""; ; { - requestCtx, cancel := contextutil.DetachedWithTimeout(ctx, 30*time.Second) - defer cancel() - pagedAccounts, err := client.GetV1Accounts(requestCtx, token, int64(config.PageSize)) - if err != nil { - otel.RecordError(span, err) - return err - } - - token = pagedAccounts.Payload.NextToken - - if err := ingestAccountsBatch(ctx, connectorID, taskID, ingester, pagedAccounts, client); err != nil { - otel.RecordError(span, err) - return err - } - - if token == "" { - break - } - } - - // Pagination works by cursor token. - for token := ""; ; { - requestCtx, cancel := contextutil.DetachedWithTimeout(ctx, 30*time.Second) - defer cancel() - pagedExternalAccounts, err := client.GetV1ExternalAccounts(requestCtx, token, int64(config.PageSize)) - if err != nil { - otel.RecordError(span, err) - return err - } - - token = pagedExternalAccounts.Payload.NextToken - - if err := ingestExternalAccountsBatch(ctx, connectorID, ingester, pagedExternalAccounts, client); err != nil { - otel.RecordError(span, err) - return err - } - - if token == "" { - break - } - } - - // Fetch payments after inserting all accounts in order to link them correctly - taskPayments, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch payments from Atlar", - Key: taskNameFetchTransactions, - }) - if err != nil { - otel.RecordError(span, err) - return err - } - - err = scheduler.Schedule(ctx, taskPayments, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func ingestAccountsBatch( - ctx context.Context, - connectorID models.ConnectorID, - taskID models.TaskID, - ingester ingestion.Ingester, - pagedAccounts *accounts.GetV1AccountsOK, - client *client.Client, -) error { - ctx, span := connectors.StartSpan( - ctx, - "atlar.taskFetchAccounts.ingestAccountsBatch", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - accountsBatch := ingestion.AccountBatch{} - balanceBatch := ingestion.BalanceBatch{} - - for _, account := range pagedAccounts.Payload.Items { - raw, err := json.Marshal(account) - if err != nil { - return err - } - - createdAt, err := ParseAtlarTimestamp(account.Created) - if err != nil { - return fmt.Errorf("failed to parse opening date: %w", err) - } - - requestCtx, cancel := contextutil.DetachedWithTimeout(ctx, 30*time.Second) - defer cancel() - thirdPartyResponse, err := client.GetV1BetaThirdPartiesID(requestCtx, account.ThirdPartyID) - if err != nil { - otel.RecordError(span, err) - return err - } - - accountsBatch = append(accountsBatch, &models.Account{ - ID: models.AccountID{ - Reference: *account.ID, - ConnectorID: connectorID, - }, - CreatedAt: createdAt, - Reference: *account.ID, - ConnectorID: connectorID, - DefaultAsset: currency.FormatAsset(supportedCurrenciesWithDecimal, account.Currency), - AccountName: account.Name, - Type: models.AccountTypeInternal, - Metadata: ExtractAccountMetadata(account, thirdPartyResponse.Payload), - RawData: raw, - }) - - balance := account.Balance - balanceTimestamp, err := ParseAtlarTimestamp(balance.Timestamp) - if err != nil { - return err - } - balanceBatch = append(balanceBatch, &models.Balance{ - AccountID: models.AccountID{ - Reference: *account.ID, - ConnectorID: connectorID, - }, - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, *balance.Amount.Currency), - Balance: big.NewInt(*balance.Amount.Value), - CreatedAt: balanceTimestamp, - LastUpdatedAt: time.Now().UTC(), - ConnectorID: connectorID, - }) - } - - if err := ingester.IngestAccounts(ctx, accountsBatch); err != nil { - return err - } - - if err := ingester.IngestBalances(ctx, balanceBatch, false); err != nil { - return err - } - - return nil -} - -func ingestExternalAccountsBatch( - ctx context.Context, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - pagedExternalAccounts *external_accounts.GetV1ExternalAccountsOK, - client *client.Client, -) error { - accountsBatch := ingestion.AccountBatch{} - - for _, externalAccount := range pagedExternalAccounts.Payload.Items { - requestCtx, cancel := contextutil.DetachedWithTimeout(ctx, 30*time.Second) - defer cancel() - counterparty_response, err := client.GetV1CounterpartiesID(requestCtx, externalAccount.CounterpartyID) - if err != nil { - return err - } - counterparty := counterparty_response.Payload - - newAccount, err := ExternalAccountFromAtlarData(connectorID, externalAccount, counterparty) - if err != nil { - return err - } - - accountsBatch = append(accountsBatch, newAccount) - } - - if err := ingester.IngestAccounts(ctx, accountsBatch); err != nil { - return err - } - - return nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/atlar/task_fetch_transactions.go b/components/payments/cmd/connectors/internal/connectors/atlar/task_fetch_transactions.go deleted file mode 100644 index ec2f3dd4fe..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/atlar/task_fetch_transactions.go +++ /dev/null @@ -1,297 +0,0 @@ -package atlar - -import ( - "context" - "encoding/json" - "fmt" - "math/big" - "time" - - "github.com/formancehq/go-libs/contextutil" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/atlar/client" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/get-momo/atlar-v1-go-client/client/transactions" - atlar_models "github.com/get-momo/atlar-v1-go-client/models" - "go.opentelemetry.io/otel/attribute" -) - -func FetchTransactionsTask(config Config, client *client.Client) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - resolver task.StateResolver, - scheduler task.Scheduler, - ingester ingestion.Ingester, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "atlar.taskFetchTransactions", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - // Pagination works by cursor token. - for token := ""; ; { - requestCtx, cancel := contextutil.DetachedWithTimeout(ctx, 30*time.Second) - defer cancel() - pagedTransactions, err := client.GetV1Transactions(requestCtx, token, int64(config.PageSize)) - if err != nil { - otel.RecordError(span, err) - return err - } - - token = pagedTransactions.Payload.NextToken - - if err := ingestPaymentsBatch(ctx, connectorID, taskID, ingester, client, pagedTransactions); err != nil { - otel.RecordError(span, err) - return err - } - - if token == "" { - break - } - } - - return nil - } -} - -func ingestPaymentsBatch( - ctx context.Context, - connectorID models.ConnectorID, - taskID models.TaskID, - ingester ingestion.Ingester, - client *client.Client, - pagedTransactions *transactions.GetV1TransactionsOK, -) error { - batch := ingestion.PaymentBatch{} - - for _, item := range pagedTransactions.Payload.Items { - batchElement, err := atlarTransactionToPaymentBatchElement(ctx, connectorID, taskID, item, client) - if err != nil { - return err - } - if batchElement == nil { - continue - } - - batch = append(batch, *batchElement) - } - - if err := ingester.IngestPayments(ctx, batch); err != nil { - return err - } - - return nil -} - -func atlarTransactionToPaymentBatchElement( - ctx context.Context, - connectorID models.ConnectorID, - taskID models.TaskID, - transaction *atlar_models.Transaction, - client *client.Client, -) (*ingestion.PaymentBatchElement, error) { - ctx, span := connectors.StartSpan( - ctx, - "atlar.atlarTransactionToPaymentBatchElement", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - if _, ok := supportedCurrenciesWithDecimal[*transaction.Amount.Currency]; !ok { - // Discard transactions with unsupported currencies - return nil, nil - } - - raw, err := json.Marshal(transaction) - if err != nil { - return nil, err - } - - paymentType := determinePaymentType(transaction) - - itemAmount := transaction.Amount - amount, err := atlarTransactionAmountToPaymentAbsoluteAmount(*itemAmount.Value) - if err != nil { - return nil, err - } - - createdAt, err := ParseAtlarTimestamp(transaction.Created) - if err != nil { - return nil, err - } - - requestCtx, cancel := contextutil.DetachedWithTimeout(ctx, 30*time.Second) - defer cancel() - accountResponse, err := client.GetV1AccountsID(requestCtx, *transaction.Account.ID) - if err != nil { - otel.RecordError(span, err) - return nil, err - } - - requestCtx, cancel = contextutil.DetachedWithTimeout(ctx, 30*time.Second) - defer cancel() - thirdPartyResponse, err := client.GetV1BetaThirdPartiesID(requestCtx, *&accountResponse.Payload.ThirdPartyID) - if err != nil { - otel.RecordError(span, err) - return nil, err - } - - paymentId := models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: transaction.ID, - Type: paymentType, - }, - ConnectorID: connectorID, - } - - batchElement := ingestion.PaymentBatchElement{ - Payment: &models.Payment{ - ID: paymentId, - Reference: transaction.ID, - Type: paymentType, - ConnectorID: connectorID, - CreatedAt: createdAt, - Status: determinePaymentStatus(transaction), - Scheme: determinePaymentScheme(transaction), - Amount: amount, - InitialAmount: amount, - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, *transaction.Amount.Currency), - Metadata: ExtractPaymentMetadata(paymentId, transaction, accountResponse.Payload, thirdPartyResponse.Payload), - RawData: raw, - }, - } - - if *itemAmount.Value >= 0 { - // DEBIT - batchElement.Payment.DestinationAccountID = &models.AccountID{ - Reference: *transaction.Account.ID, - ConnectorID: connectorID, - } - } else { - // CREDIT - batchElement.Payment.SourceAccountID = &models.AccountID{ - Reference: *transaction.Account.ID, - ConnectorID: connectorID, - } - } - - return &batchElement, nil -} - -func determinePaymentType(item *atlar_models.Transaction) models.PaymentType { - if *item.Amount.Value >= 0 { - return models.PaymentTypePayIn - } else { - return models.PaymentTypePayOut - } -} - -func determinePaymentStatus(item *atlar_models.Transaction) models.PaymentStatus { - if item.Reconciliation.Status == atlar_models.ReconciliationDetailsStatusEXPECTED { - // A payment initiated by the owner of the accunt through the Atlar API, - // which was not yet reconciled with a payment from the statement - return models.PaymentStatusPending - } - if item.Reconciliation.Status == atlar_models.ReconciliationDetailsStatusBOOKED { - // A payment comissioned with the bank, which was not yet reconciled with a - // payment from the statement - return models.PaymentStatusSucceeded - } - if item.Reconciliation.Status == atlar_models.ReconciliationDetailsStatusRECONCILED { - return models.PaymentStatusSucceeded - } - return models.PaymentStatusOther -} - -func determinePaymentScheme(item *atlar_models.Transaction) models.PaymentScheme { - // item.Characteristics.BankTransactionCode.Domain - // item.Characteristics.BankTransactionCode.Family - // TODO: fees and interest -> models.PaymentSchemeOther with additional info on metadata. Will need example transactions for that - - if *item.Amount.Value > 0 { - return models.PaymentSchemeSepaDebit - } else if *item.Amount.Value < 0 { - return models.PaymentSchemeSepaCredit - } - return models.PaymentSchemeSepa -} - -func ExtractPaymentMetadata(paymentId models.PaymentID, transaction *atlar_models.Transaction, account *atlar_models.Account, bank *atlar_models.ThirdParty) []*models.PaymentMetadata { - result := []*models.PaymentMetadata{} - if transaction.Date != "" { - result = append(result, ComputePaymentMetadata(paymentId, "date", transaction.Date)) - } - if transaction.ValueDate != "" { - result = append(result, ComputePaymentMetadata(paymentId, "valueDate", transaction.ValueDate)) - } - result = append(result, ComputePaymentMetadata(paymentId, "remittanceInformation/type", *transaction.RemittanceInformation.Type)) - result = append(result, ComputePaymentMetadata(paymentId, "remittanceInformation/value", *transaction.RemittanceInformation.Value)) - result = append(result, ComputePaymentMetadata(paymentId, "bank/id", bank.ID)) - result = append(result, ComputePaymentMetadata(paymentId, "bank/name", bank.Name)) - result = append(result, ComputePaymentMetadata(paymentId, "bank/bic", account.Bank.Bic)) - result = append(result, ComputePaymentMetadata(paymentId, "btc/domain", transaction.Characteristics.BankTransactionCode.Domain)) - result = append(result, ComputePaymentMetadata(paymentId, "btc/family", transaction.Characteristics.BankTransactionCode.Family)) - result = append(result, ComputePaymentMetadata(paymentId, "btc/subfamily", transaction.Characteristics.BankTransactionCode.Subfamily)) - result = append(result, ComputePaymentMetadata(paymentId, "btc/description", transaction.Characteristics.BankTransactionCode.Description)) - result = append(result, ComputePaymentMetadataBool(paymentId, "returned", transaction.Characteristics.Returned)) - if transaction.CounterpartyDetails != nil && transaction.CounterpartyDetails.Name != "" { - result = append(result, ComputePaymentMetadata(paymentId, "counterparty/name", transaction.CounterpartyDetails.Name)) - if transaction.CounterpartyDetails.ExternalAccount != nil && transaction.CounterpartyDetails.ExternalAccount.Identifier != nil { - result = append(result, ComputePaymentMetadata(paymentId, "counterparty/bank/bic", transaction.CounterpartyDetails.ExternalAccount.Bank.Bic)) - result = append(result, ComputePaymentMetadata(paymentId, "counterparty/bank/name", transaction.CounterpartyDetails.ExternalAccount.Bank.Name)) - result = append(result, ComputePaymentMetadata(paymentId, - fmt.Sprintf("counterparty/identifier/%s", transaction.CounterpartyDetails.ExternalAccount.Identifier.Type), - transaction.CounterpartyDetails.ExternalAccount.Identifier.Number)) - } - } - if transaction.Characteristics.Returned { - result = append(result, ComputePaymentMetadata(paymentId, "returnReason/code", transaction.Characteristics.ReturnReason.Code)) - result = append(result, ComputePaymentMetadata(paymentId, "returnReason/description", transaction.Characteristics.ReturnReason.Description)) - result = append(result, ComputePaymentMetadata(paymentId, "returnReason/btc/domain", transaction.Characteristics.ReturnReason.OriginalBankTransactionCode.Domain)) - result = append(result, ComputePaymentMetadata(paymentId, "returnReason/btc/family", transaction.Characteristics.ReturnReason.OriginalBankTransactionCode.Family)) - result = append(result, ComputePaymentMetadata(paymentId, "returnReason/btc/subfamily", transaction.Characteristics.ReturnReason.OriginalBankTransactionCode.Subfamily)) - result = append(result, ComputePaymentMetadata(paymentId, "returnReason/btc/description", transaction.Characteristics.ReturnReason.OriginalBankTransactionCode.Description)) - } - if transaction.Characteristics.VirtualAccount != nil { - result = append(result, ComputePaymentMetadata(paymentId, "virtualAccount/market", transaction.Characteristics.VirtualAccount.Market)) - result = append(result, ComputePaymentMetadata(paymentId, "virtualAccount/rawIdentifier", transaction.Characteristics.VirtualAccount.RawIdentifier)) - result = append(result, ComputePaymentMetadata(paymentId, "virtualAccount/bank/id", transaction.Characteristics.VirtualAccount.Bank.ID)) - result = append(result, ComputePaymentMetadata(paymentId, "virtualAccount/bank/name", transaction.Characteristics.VirtualAccount.Bank.Name)) - result = append(result, ComputePaymentMetadata(paymentId, "virtualAccount/bank/bic", transaction.Characteristics.VirtualAccount.Bank.Bic)) - result = append(result, ComputePaymentMetadata(paymentId, "virtualAccount/identifier/holderName", *transaction.Characteristics.VirtualAccount.Identifier.HolderName)) - result = append(result, ComputePaymentMetadata(paymentId, "virtualAccount/identifier/market", transaction.Characteristics.VirtualAccount.Identifier.Market)) - result = append(result, ComputePaymentMetadata(paymentId, "virtualAccount/identifier/type", transaction.Characteristics.VirtualAccount.Identifier.Type)) - result = append(result, ComputePaymentMetadata(paymentId, "virtualAccount/identifier/number", transaction.Characteristics.VirtualAccount.Identifier.Number)) - } - result = append(result, ComputePaymentMetadata(paymentId, "reconciliation/status", transaction.Reconciliation.Status)) - result = append(result, ComputePaymentMetadata(paymentId, "reconciliation/transactableId", transaction.Reconciliation.TransactableID)) - result = append(result, ComputePaymentMetadata(paymentId, "reconciliation/transactableType", transaction.Reconciliation.TransactableType)) - if transaction.Characteristics.CurrencyExchange != nil { - result = append(result, ComputePaymentMetadata(paymentId, "currencyExchange/sourceCurrency", transaction.Characteristics.CurrencyExchange.SourceCurrency)) - result = append(result, ComputePaymentMetadata(paymentId, "currencyExchange/targetCurrency", transaction.Characteristics.CurrencyExchange.TargetCurrency)) - result = append(result, ComputePaymentMetadata(paymentId, "currencyExchange/exchangeRate", transaction.Characteristics.CurrencyExchange.ExchangeRate)) - result = append(result, ComputePaymentMetadata(paymentId, "currencyExchange/unitCurrency", transaction.Characteristics.CurrencyExchange.UnitCurrency)) - } - if transaction.CounterpartyDetails.MandateReference != "" { - result = append(result, ComputePaymentMetadata(paymentId, "mandateReference", transaction.CounterpartyDetails.MandateReference)) - } - - return result -} - -func atlarTransactionAmountToPaymentAbsoluteAmount(atlarAmount int64) (*big.Int, error) { - var amount big.Int - amountInt := amount.SetInt64(atlarAmount) - amountInt = amountInt.Abs(amountInt) - return amountInt, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/atlar/task_fetch_transactions_test.go b/components/payments/cmd/connectors/internal/connectors/atlar/task_fetch_transactions_test.go deleted file mode 100644 index 20599a183c..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/atlar/task_fetch_transactions_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package atlar - -import ( - "math/big" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestAtlarTransactionAmountToPaymentAbsoluteAmount(t *testing.T) { - t.Parallel() - var result *big.Int - var err error - - result, err = atlarTransactionAmountToPaymentAbsoluteAmount(30) - if assert.Nil(t, err) { - assert.Equal(t, *big.NewInt(30), *result) - } - - result, err = atlarTransactionAmountToPaymentAbsoluteAmount(330) - if assert.Nil(t, err) { - assert.Equal(t, *big.NewInt(330), *result) - } - - result, err = atlarTransactionAmountToPaymentAbsoluteAmount(330) - if assert.Nil(t, err) { - assert.Equal(t, *big.NewInt(330), *result) - } - - result, err = atlarTransactionAmountToPaymentAbsoluteAmount(-30) - if assert.Nil(t, err) { - assert.Equal(t, *big.NewInt(30), *result) - } - - result, err = atlarTransactionAmountToPaymentAbsoluteAmount(-330) - if assert.Nil(t, err) { - assert.Equal(t, *big.NewInt(330), *result) - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/atlar/task_main.go b/components/payments/cmd/connectors/internal/connectors/atlar/task_main.go deleted file mode 100644 index 35a2d87868..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/atlar/task_main.go +++ /dev/null @@ -1,52 +0,0 @@ -package atlar - -import ( - "context" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" -) - -// Launch accounts and payments tasks. -// Period between runs dictated by config.PollingPeriod. -func MainTask(logger logging.Logger) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - scheduler task.Scheduler, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "atlar.taskMain", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - taskAccounts, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch accounts from client", - Key: taskNameFetchAccounts, - }) - if err != nil { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - err = scheduler.Schedule(ctx, taskAccounts, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_ALWAYS, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - return nil - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/atlar/task_payments.go b/components/payments/cmd/connectors/internal/connectors/atlar/task_payments.go deleted file mode 100644 index d9969c896b..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/atlar/task_payments.go +++ /dev/null @@ -1,394 +0,0 @@ -package atlar - -import ( - "context" - "errors" - "fmt" - "math/big" - "regexp" - "strings" - "time" - - "github.com/formancehq/go-libs/contextutil" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/atlar/client" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/get-momo/atlar-v1-go-client/client/credit_transfers" - atlar_models "github.com/get-momo/atlar-v1-go-client/models" - "go.opentelemetry.io/otel/attribute" -) - -func InitiatePaymentTask(config Config, client *client.Client, transferID string) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - scheduler task.Scheduler, - storageReader storage.Reader, - ingester ingestion.Ingester, - ) error { - transferInitiationID := models.MustTransferInitiationIDFromString(transferID) - - ctx, span := connectors.StartSpan( - ctx, - "atlar.taskInitiatePayment", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("transferID", transferID), - attribute.String("reference", transferInitiationID.Reference), - ) - defer span.End() - - transfer, err := getTransfer(ctx, storageReader, transferInitiationID, true) - if err != nil { - return err - } - - var paymentID *models.PaymentID - defer func() { - if err != nil { - otel.RecordError(span, err) - ctx, cancel := contextutil.Detached(ctx) - defer cancel() - if err := ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusFailed, err.Error(), time.Now()); err != nil { - otel.RecordError(span, err) - } - } - }() - - err = ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusProcessing, "", time.Now()) - if err != nil { - return err - } - - if transfer.SourceAccount != nil { - if transfer.SourceAccount.Type == models.AccountTypeExternal { - err = errors.New("payin not implemented: source account must be an internal account") - return err - } - } - - currency, precision, err := currency.GetCurrencyAndPrecisionFromAsset(supportedCurrenciesWithDecimal, transfer.Asset) - if err != nil { - return err - } - - paymentSchemeType := "SCT" // SEPA Credit Transfer - remittanceInformationType := "UNSTRUCTURED" - remittanceInformationValue := transfer.Description - amount := atlar_models.AmountInput{ - Currency: ¤cy, - Value: transfer.Amount.Int64(), - StringValue: amountToString(*transfer.Amount, precision), - } - date := transfer.ScheduledAt - if date.IsZero() { - date = time.Now() - } - dateString := date.Format(time.DateOnly) - - createPaymentRequest := atlar_models.CreatePaymentRequest{ - SourceAccountID: &transfer.SourceAccount.Reference, - DestinationExternalAccountID: &transfer.DestinationAccount.Reference, - Amount: &amount, - Date: &dateString, - ExternalID: serializeAtlarPaymentExternalID(transfer.ID.Reference, transfer.CountRetries()), - PaymentSchemeType: &paymentSchemeType, - RemittanceInformation: &atlar_models.RemittanceInformation{ - Type: &remittanceInformationType, - Value: &remittanceInformationValue, - }, - } - - requestCtx, cancel := contextutil.DetachedWithTimeout(ctx, 30*time.Second) - defer cancel() - var postCreditTransferResponse *credit_transfers.PostV1CreditTransfersCreated - postCreditTransferResponse, err = client.PostV1CreditTransfers(requestCtx, &createPaymentRequest) - if err != nil { - return err - } - - paymentID = &models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: postCreditTransferResponse.Payload.Reconciliation.ExpectedTransactionID, - Type: models.PaymentTypePayOut, - }, - ConnectorID: connectorID, - } - - var taskDescriptor models.TaskDescriptor - taskDescriptor, err = models.EncodeTaskDescriptor(TaskDescriptor{ - Name: fmt.Sprintf("Update transfer initiation status of transfer %s", transfer.ID.String()), - Key: taskNameUpdatePaymentStatus, - TransferID: transfer.ID.String(), - PaymentID: paymentID.String(), - Attempt: 1, - }) - if err != nil { - return err - } - - err = scheduler.Schedule(ctx, taskDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return err - } - - return nil - } -} - -func ValidateTransferInitiation(transfer *models.TransferInitiation) error { - if transfer == nil { - return errors.New("transfer cannot be nil") - } - if transfer.Type.String() != "PAYOUT" { - return errors.New("this connector only supports type PAYOUT") - } - return nil -} - -func UpdatePaymentStatusTask( - config Config, - client *client.Client, - transferID string, - stringPaymentID string, - attempt int, -) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - scheduler task.Scheduler, - storageReader storage.Reader, - ingester ingestion.Ingester, - ) error { - paymentID := models.MustPaymentIDFromString(stringPaymentID) - transferInitiationID := models.MustTransferInitiationIDFromString(transferID) - - ctx, span := connectors.StartSpan( - ctx, - "atlar.taskUpdatePaymentStatus", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("transferID", transferID), - attribute.String("paymentID", stringPaymentID), - attribute.Int("attempt", attempt), - attribute.String("reference", transferInitiationID.Reference), - ) - defer span.End() - - transfer, err := getTransfer(ctx, storageReader, transferInitiationID, true) - if err != nil { - otel.RecordError(span, err) - return err - } - - requestCtx, cancel := contextutil.DetachedWithTimeout(ctx, 30*time.Second) - defer cancel() - getCreditTransferResponse, err := client.GetV1CreditTransfersGetByExternalIDExternalID( - requestCtx, - serializeAtlarPaymentExternalID(transfer.ID.Reference, transfer.CountRetries()), - ) - if err != nil { - otel.RecordError(span, err) - return err - } - - status := getCreditTransferResponse.Payload.Status - // Status docs: https://docs.atlar.com/docs/payment-details#payment-states--events - switch status { - case "CREATED", "APPROVED", "PENDING_SUBMISSION", "SENT", "PENDING_AT_BANK", "ACCEPTED", "EXECUTED": - taskDescriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: fmt.Sprintf("Update transfer initiation status of transfer %s", transfer.ID.String()), - Key: taskNameUpdatePaymentStatus, - TransferID: transfer.ID.String(), - PaymentID: paymentID.String(), - Attempt: attempt + 1, - }) - if err != nil { - otel.RecordError(span, err) - return err - } - - err = scheduler.Schedule(ctx, taskDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_IN_DURATION, - Duration: config.TransferInitiationStatusPollingPeriod.Duration, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - otel.RecordError(span, err) - return err - } - return nil - - case "RECONCILED": - err = ingestAtlarTransaction(ctx, - ingester, - connectorID, - taskID, - client, - getCreditTransferResponse.Payload.Reconciliation.BookedTransactionID, - ) - if err != nil { - otel.RecordError(span, err) - return err - } - - paymentID = &models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: getCreditTransferResponse.Payload.Reconciliation.BookedTransactionID, - Type: models.PaymentTypePayOut, - }, - ConnectorID: connectorID, - } - - err = ingester.UpdateTransferInitiationPayment(ctx, transfer, paymentID, models.TransferInitiationStatusProcessed, "", time.Now()) - if err != nil { - otel.RecordError(span, err) - return err - } - - return nil - - case "REJECTED", "FAILED", "RETURNED": - err = ingester.UpdateTransferInitiationPaymentsStatus( - ctx, transfer, paymentID, models.TransferInitiationStatusFailed, - fmt.Sprintf("paymant initiation status is \"%s\"", status), time.Now(), - ) - if err != nil { - otel.RecordError(span, err) - return err - } - - return nil - - default: - err := fmt.Errorf( - "unknown status \"%s\" encountered while fetching payment initiation status of payment \"%s\"", - status, getCreditTransferResponse.Payload.ID, - ) - otel.RecordError(span, err) - return err - } - } -} - -func amountToString(amount big.Int, precision int) string { - raw := amount.String() - if precision < 0 { - precision = 0 - } - insertPosition := len(raw) - precision - if insertPosition <= 0 { - return "0." + strings.Repeat("0", -insertPosition) + raw - } - return raw[:insertPosition] + "." + raw[insertPosition:] -} - -func getTransfer( - ctx context.Context, - reader storage.Reader, - transferID models.TransferInitiationID, - expand bool, -) (*models.TransferInitiation, error) { - transfer, err := reader.ReadTransferInitiation(ctx, transferID) - if err != nil { - return nil, err - } - - if expand { - if transfer.SourceAccountID != nil { - sourceAccount, err := reader.GetAccount(ctx, transfer.SourceAccountID.String()) - if err != nil { - return nil, err - } - transfer.SourceAccount = sourceAccount - } - - destinationAccount, err := reader.GetAccount(ctx, transfer.DestinationAccountID.String()) - if err != nil { - return nil, err - } - transfer.DestinationAccount = destinationAccount - } - - return transfer, nil -} - -func serializeAtlarPaymentExternalID(ID string, attempts int) string { - return fmt.Sprintf("%s_%d", ID, attempts) -} - -var deserializeAtlarPaymentExternalIDRegex = regexp.MustCompile(`^([^\_]+)_([0-9]+)$`) - -func deserializeAtlarPaymentExternalID(serialized string) (string, int, error) { - var attempts int - - // Find matches in the input string - matches := deserializeAtlarPaymentExternalIDRegex.FindStringSubmatch(serialized) - if matches == nil || len(matches) != 3 { - return "", 0, errors.New("cannot deserialize malformed externalID") - } - - parsed, err := fmt.Sscanf(matches[2], "%d", &attempts) - if err != nil { - return "", 0, errors.New("cannot deserialize malformed externalID") - } - if parsed != 1 { - return "", 0, errors.New("cannot deserialize malformed externalID") - } - return matches[1], attempts, nil -} - -func ingestAtlarTransaction( - ctx context.Context, - ingester ingestion.Ingester, - connectorID models.ConnectorID, - taskID models.TaskID, - client *client.Client, - transactionId string, -) error { - ctx, span := connectors.StartSpan( - ctx, - "atlar.taskUpdatePaymentStatus.ingestAtlarTransaction", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("transactionID", transactionId), - ) - defer span.End() - - requestCtx, cancel := contextutil.DetachedWithTimeout(ctx, 30*time.Second) - defer cancel() - transactionResponse, err := client.GetV1TransactionsID(requestCtx, transactionId) - if err != nil { - otel.RecordError(span, err) - return err - } - - batchElement, err := atlarTransactionToPaymentBatchElement(ctx, connectorID, taskID, transactionResponse.Payload, client) - if err != nil { - otel.RecordError(span, err) - return err - } - if batchElement == nil { - return nil - } - - batch := ingestion.PaymentBatch{*batchElement} - - err = ingester.IngestPayments(ctx, batch) - if err != nil { - otel.RecordError(span, err) - return err - } - - return nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/atlar/task_payments_test.go b/components/payments/cmd/connectors/internal/connectors/atlar/task_payments_test.go deleted file mode 100644 index b01621f5a1..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/atlar/task_payments_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package atlar - -import ( - "errors" - "math/big" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestAmountToString(t *testing.T) { - t.Parallel() - - assert.EqualValues(t, "0.032", amountToString(*big.NewInt(32), 3)) - assert.EqualValues(t, "0.32", amountToString(*big.NewInt(32), 2)) - assert.EqualValues(t, "3.2", amountToString(*big.NewInt(32), 1)) - assert.EqualValues(t, "5.432", amountToString(*big.NewInt(5432), 3)) - assert.EqualValues(t, "54.32", amountToString(*big.NewInt(5432), 2)) - assert.EqualValues(t, "543.2", amountToString(*big.NewInt(5432), 1)) -} - -func TestSerializeAtlarPaymentExternalID(t *testing.T) { - t.Parallel() - - assert.EqualValues(t, "testID_1", serializeAtlarPaymentExternalID("testID", 1)) - assert.EqualValues(t, "tqmbAGgV4S2pHics57BT5tV2_682", serializeAtlarPaymentExternalID("tqmbAGgV4S2pHics57BT5tV2", 682)) -} - -func TestDeserializeAtlarPaymentExternalID(t *testing.T) { - t.Parallel() - - var ID string - var attempts int - var err error - - ID, attempts, err = deserializeAtlarPaymentExternalID("testID_1") - if assert.Nil(t, err) { - assert.EqualValues(t, "testID", ID) - assert.EqualValues(t, 1, attempts) - } - - ID, attempts, err = deserializeAtlarPaymentExternalID("tqmbAGgV4S2pHics57BT5tV2_682") - if assert.Nil(t, err) { - assert.EqualValues(t, "tqmbAGgV4S2pHics57BT5tV2", ID) - assert.EqualValues(t, 682, attempts) - } - - _, _, err = deserializeAtlarPaymentExternalID("tqmbAGgV4S2pHics57BT5tV2_682_432") - if assert.Error(t, err) { - assert.Equal(t, errors.New("cannot deserialize malformed externalID"), err) - } - - _, _, err = deserializeAtlarPaymentExternalID("tqmbAGgV4S2pHics57BT5tV2_") - if assert.Error(t, err) { - assert.Equal(t, errors.New("cannot deserialize malformed externalID"), err) - } - - _, _, err = deserializeAtlarPaymentExternalID("tqmbAGgV4S2pHics57BT5tV2") - if assert.Error(t, err) { - assert.Equal(t, errors.New("cannot deserialize malformed externalID"), err) - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/atlar/task_resolve.go b/components/payments/cmd/connectors/internal/connectors/atlar/task_resolve.go deleted file mode 100644 index 1483d257a7..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/atlar/task_resolve.go +++ /dev/null @@ -1,52 +0,0 @@ -package atlar - -import ( - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/atlar/client" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" -) - -const ( - taskNameFetchAccounts = "fetch_accounts" - taskNameFetchTransactions = "fetch_transactions" - taskNameCreateExternalBankAccount = "create_external_bank_account" - taskNameInitiatePayment = "initiate_payment" - taskNameUpdatePaymentStatus = "update_payment_status" -) - -// TaskDescriptor is the definition of a task. -type TaskDescriptor struct { - Name string `json:"name" yaml:"name" bson:"name"` - Key string `json:"key" yaml:"key" bson:"key"` - Main bool `json:"main,omitempty" yaml:"main" bson:"main"` - BankAccount *models.BankAccount `json:"bankAccount,omitempty" yaml:"bankAccount" bson:"bankAccount"` - TransferID string `json:"transferId,omitempty" yaml:"transferId" bson:"transferId"` - PaymentID string `json:"paymentId,omitempty" yaml:"paymentId" bson:"paymentId"` - Attempt int `json:"attempt,omitempty" yaml:"attempt" bson:"attempt"` -} - -func resolveTasks(logger logging.Logger, config Config) func(taskDefinition TaskDescriptor) task.Task { - client := client.NewClient(config.BaseUrl, config.AccessKey, config.Secret) - - return func(taskDescriptor TaskDescriptor) task.Task { - if taskDescriptor.Main { - return MainTask(logger) - } - - switch taskDescriptor.Key { - case taskNameFetchAccounts: - return FetchAccountsTask(config, client) - case taskNameFetchTransactions: - return FetchTransactionsTask(config, client) - case taskNameCreateExternalBankAccount: - return CreateExternalBankAccountTask(config, client, taskDescriptor.BankAccount) - case taskNameInitiatePayment: - return InitiatePaymentTask(config, client, taskDescriptor.TransferID) - case taskNameUpdatePaymentStatus: - return UpdatePaymentStatusTask(config, client, taskDescriptor.TransferID, taskDescriptor.PaymentID, taskDescriptor.Attempt) - default: - return nil - } - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/atlar/time.go b/components/payments/cmd/connectors/internal/connectors/atlar/time.go deleted file mode 100644 index 8a90de7c9e..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/atlar/time.go +++ /dev/null @@ -1,16 +0,0 @@ -package atlar - -import "time" - -func ParseAtlarTimestamp(value string) (time.Time, error) { - return time.Parse(time.RFC3339Nano, value) -} - -func ParseAtlarDate(value string) (time.Time, error) { - return time.Parse(time.DateOnly, value) -} - -func TimeToAtlarTimestamp(input *time.Time) *string { - atlarTimestamp := input.Format(time.RFC3339Nano) - return &atlarTimestamp -} diff --git a/components/payments/cmd/connectors/internal/connectors/bankingcircle/config.go b/components/payments/cmd/connectors/internal/connectors/bankingcircle/config.go deleted file mode 100644 index c80b79a63e..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/bankingcircle/config.go +++ /dev/null @@ -1,94 +0,0 @@ -package bankingcircle - -import ( - "encoding/json" - "fmt" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/configtemplate" -) - -const ( - defaultPollingPeriod = 2 * time.Minute -) - -// PFX is not handle very well in Go if we have more than one certificate -// in the pfx data. -// To be safe for every user to pass the right data to the connector, let's -// use two config parameters instead of one: the user certificate and the key -// associated. -// To extract them for a pfx file, you can use the following commands: -// openssl pkcs12 -in PC20230412293693.pfx -clcerts -nokeys | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > clientcert.cer -// openssl pkcs12 -in PC20230412293693.pfx -nocerts -nodes | sed -ne '/-BEGIN PRIVATE KEY-/,/-END PRIVATE KEY-/p' > clientcert.key -type Config struct { - Name string `json:"name" yaml:"name" bson:"name"` - Username string `json:"username" yaml:"username" bson:"username"` - Password string `json:"password" yaml:"password" bson:"password"` - Endpoint string `json:"endpoint" yaml:"endpoint" bson:"endpoint"` - AuthorizationEndpoint string `json:"authorizationEndpoint" yaml:"authorizationEndpoint" bson:"authorizationEndpoint"` - UserCertificate string `json:"userCertificate" yaml:"userCertificate" bson:"userCertificate"` - UserCertificateKey string `json:"userCertificateKey" yaml:"userCertificateKey" bson:"userCertificateKey"` - PollingPeriod connectors.Duration `json:"pollingPeriod" yaml:"pollingPeriod" bson:"pollingPeriod"` -} - -// String obfuscates sensitive fields and returns a string representation of the config. -// This is used for logging. -func (c Config) String() string { - return fmt.Sprintf("username=%s, password=****, endpoint=%s, authorizationEndpoint=%s", c.Username, c.Endpoint, c.AuthorizationEndpoint) -} - -func (c Config) Validate() error { - if c.Username == "" { - return ErrMissingUsername - } - - if c.Password == "" { - return ErrMissingPassword - } - - if c.Endpoint == "" { - return ErrMissingEndpoint - } - - if c.AuthorizationEndpoint == "" { - return ErrMissingAuthorizationEndpoint - } - - if c.UserCertificate == "" { - return ErrMissingUserCertificate - } - - if c.UserCertificateKey == "" { - return ErrMissingUserCertificatePassphrase - } - - if c.Name == "" { - return ErrMissingName - } - - return nil -} - -func (c Config) Marshal() ([]byte, error) { - return json.Marshal(c) -} - -func (c Config) ConnectorName() string { - return c.Name -} - -func (c Config) BuildTemplate() (string, configtemplate.Config) { - cfg := configtemplate.NewConfig() - - cfg.AddParameter("name", configtemplate.TypeString, name.String(), false) - cfg.AddParameter("username", configtemplate.TypeString, "", true) - cfg.AddParameter("password", configtemplate.TypeString, "", true) - cfg.AddParameter("endpoint", configtemplate.TypeString, "", true) - cfg.AddParameter("authorizationEndpoint", configtemplate.TypeString, "", true) - cfg.AddParameter("userCertificate", configtemplate.TypeLongString, "", true) - cfg.AddParameter("userCertificateKey", configtemplate.TypeLongString, "", true) - cfg.AddParameter("pollingPeriod", configtemplate.TypeDurationNs, defaultPollingPeriod.String(), false) - - return name.String(), cfg -} diff --git a/components/payments/cmd/connectors/internal/connectors/bankingcircle/connector.go b/components/payments/cmd/connectors/internal/connectors/bankingcircle/connector.go deleted file mode 100644 index 6491a4b0da..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/bankingcircle/connector.go +++ /dev/null @@ -1,152 +0,0 @@ -package bankingcircle - -import ( - "context" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/pkg/errors" -) - -const name = models.ConnectorProviderBankingCircle - -var ( - mainTaskDescriptor = TaskDescriptor{ - Name: "Main task to periodically fetch payments", - Key: taskNameMain, - } -) - -type Connector struct { - logger logging.Logger - cfg Config -} - -func newConnector(logger logging.Logger, cfg Config) *Connector { - return &Connector{ - logger: logger.WithFields(map[string]any{ - "component": "connector", - }), - cfg: cfg, - } -} - -func (c *Connector) UpdateConfig(ctx task.ConnectorContext, config models.ConnectorConfigObject) error { - cfg, ok := config.(Config) - if !ok { - return connectors.ErrInvalidConfig - } - - restartTask := c.cfg.PollingPeriod.Duration != cfg.PollingPeriod.Duration - - c.cfg = cfg - - if restartTask { - taskDescriptor, err := models.EncodeTaskDescriptor(mainTaskDescriptor) - if err != nil { - return err - } - - return ctx.Scheduler().Schedule(ctx.Context(), taskDescriptor, models.TaskSchedulerOptions{ - // We want to polling every c.cfg.PollingPeriod.Duration seconds the users - // and their transactions. - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: c.cfg.PollingPeriod.Duration, - // No need to restart this task, since the connector is not existing or - // was uninstalled previously, the task does not exists in the database - RestartOption: models.OPTIONS_STOP_AND_RESTART, - }) - } - - return nil -} -func (c *Connector) Install(ctx task.ConnectorContext) error { - taskDescriptor, err := models.EncodeTaskDescriptor(mainTaskDescriptor) - if err != nil { - return err - } - - return ctx.Scheduler().Schedule(ctx.Context(), taskDescriptor, models.TaskSchedulerOptions{ - // We want to polling every c.cfg.PollingPeriod.Duration seconds the users - // and their transactions. - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: c.cfg.PollingPeriod.Duration, - // No need to restart this task, since the connector is not existing or - // was uninstalled previously, the task does not exists in the database - RestartOption: models.OPTIONS_RESTART_NEVER, - }) -} - -func (c *Connector) Uninstall(ctx context.Context) error { - return nil -} - -func (c *Connector) Resolve(descriptor models.TaskDescriptor) task.Task { - taskDescriptor, err := models.DecodeTaskDescriptor[TaskDescriptor](descriptor) - if err != nil { - panic(err) - } - - return resolveTasks(c.logger, c.cfg)(taskDescriptor) -} - -func (c *Connector) SupportedCurrenciesAndDecimals() map[string]int { - return supportedCurrenciesWithDecimal -} - -func (c *Connector) InitiatePayment(ctx task.ConnectorContext, transfer *models.TransferInitiation) error { - taskDescriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Initiate payment", - Key: taskNameInitiatePayment, - TransferID: transfer.ID.String(), - }) - if err != nil { - return err - } - - scheduleOption := models.OPTIONS_RUN_NOW_SYNC - scheduledAt := transfer.ScheduledAt - if !scheduledAt.IsZero() { - scheduleOption = models.OPTIONS_RUN_SCHEDULED_AT - } - - err = ctx.Scheduler().Schedule(ctx.Context(), taskDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: scheduleOption, - ScheduleAt: scheduledAt, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return err - } - - return nil -} - -func (c *Connector) ReversePayment(ctx task.ConnectorContext, transferReversal *models.TransferReversal) error { - return connectors.ErrNotImplemented -} - -func (c *Connector) CreateExternalBankAccount(ctx task.ConnectorContext, bankAccount *models.BankAccount) error { - taskDescriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Initiate bank account creation", - Key: taskNameCreateExternalAccount, - BankAccountID: bankAccount.ID, - }) - if err != nil { - return err - } - - err = ctx.Scheduler().Schedule(ctx.Context(), taskDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW_SYNC, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return err - } - - return nil -} - -var _ connectors.Connector = &Connector{} diff --git a/components/payments/cmd/connectors/internal/connectors/bankingcircle/errors.go b/components/payments/cmd/connectors/internal/connectors/bankingcircle/errors.go deleted file mode 100644 index 641171001a..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/bankingcircle/errors.go +++ /dev/null @@ -1,29 +0,0 @@ -package bankingcircle - -import "github.com/pkg/errors" - -var ( - // ErrMissingTask is returned when the task is missing. - ErrMissingTask = errors.New("task is not implemented") - - // ErrMissingUsername is returned when the username is missing. - ErrMissingUsername = errors.New("missing username from config") - - // ErrMissingPassword is returned when the password is missing. - ErrMissingPassword = errors.New("missing password from config") - - // ErrMissingEndpoint is returned when the endpoint is missing. - ErrMissingEndpoint = errors.New("missing endpoint from config") - - // ErrMissingAuthorizationEndpoint is returned when the authorization endpoint is missing. - ErrMissingAuthorizationEndpoint = errors.New("missing authorization endpoint from config") - - // ErrMissingUserCertificate is returned when the user certificate is missing. - ErrMissingUserCertificate = errors.New("missing user certificate from config") - - // ErrMissingUserCertificatePassphrase is returned when the user certificate passphrase is missing. - ErrMissingUserCertificatePassphrase = errors.New("missing user certificate passphrase from config") - - // ErrMissingClientCertificate is returned when the client certificate is missing. - ErrMissingName = errors.New("missing name from config") -) diff --git a/components/payments/cmd/connectors/internal/connectors/bankingcircle/loader.go b/components/payments/cmd/connectors/internal/connectors/bankingcircle/loader.go deleted file mode 100644 index 8df156b5fb..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/bankingcircle/loader.go +++ /dev/null @@ -1,47 +0,0 @@ -package bankingcircle - -import ( - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/gorilla/mux" -) - -type Loader struct{} - -const allowedTasks = 50 - -func (l *Loader) AllowTasks() int { - return allowedTasks -} - -func (l *Loader) Name() models.ConnectorProvider { - return name -} - -func (l *Loader) Load(logger logging.Logger, config Config) connectors.Connector { - return newConnector(logger, config) -} - -func (l *Loader) ApplyDefaults(cfg Config) Config { - if cfg.PollingPeriod.Duration == 0 { - cfg.PollingPeriod.Duration = defaultPollingPeriod - } - - if cfg.Name == "" { - cfg.Name = name.String() - } - - return cfg -} - -func (l *Loader) Router(_ *storage.Storage) *mux.Router { - // Webhooks are not implemented yet - return nil -} - -// NewLoader creates a new loader. -func NewLoader() *Loader { - return &Loader{} -} diff --git a/components/payments/cmd/connectors/internal/connectors/bankingcircle/task_create_external_account.go b/components/payments/cmd/connectors/internal/connectors/bankingcircle/task_create_external_account.go deleted file mode 100644 index 46e175b346..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/bankingcircle/task_create_external_account.go +++ /dev/null @@ -1,88 +0,0 @@ -package bankingcircle - -import ( - "context" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/bankingcircle/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/google/uuid" - "go.opentelemetry.io/otel/attribute" -) - -// No need to call any API for banking circle since it does not support it. -// We will just create an external accounts on our side linked to the -// bank account object. -func taskCreateExternalAccount( - client *client.Client, - bankAccountID uuid.UUID, -) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - storageReader storage.Reader, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "bankingcircle.taskCreateExternalAccount", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("bankAccount.id", bankAccountID.String()), - ) - defer span.End() - - bankAccount, err := storageReader.GetBankAccount(ctx, bankAccountID, false) - if err != nil { - otel.RecordError(span, err) - return err - } - - span.SetAttributes(attribute.String("bankAccount.name", bankAccount.Name)) - - if err := createExternalAccount(ctx, connectorID, ingester, storageReader, bankAccount); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func createExternalAccount( - ctx context.Context, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - storageReader storage.Reader, - bankAccount *models.BankAccount, -) error { - accountID := models.AccountID{ - Reference: bankAccount.ID.String(), - ConnectorID: connectorID, - } - - if err := ingester.IngestAccounts(ctx, ingestion.AccountBatch{ - { - ID: accountID, - CreatedAt: time.Now(), - Reference: bankAccount.ID.String(), - ConnectorID: connectorID, - AccountName: bankAccount.Name, - Type: models.AccountTypeExternalFormance, - }, - }); err != nil { - return err - } - - if err := ingester.LinkBankAccountWithAccount(ctx, bankAccount, &accountID); err != nil { - return err - } - - return nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/bankingcircle/task_fetch_accounts.go b/components/payments/cmd/connectors/internal/connectors/bankingcircle/task_fetch_accounts.go deleted file mode 100644 index e3959b52a1..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/bankingcircle/task_fetch_accounts.go +++ /dev/null @@ -1,158 +0,0 @@ -package bankingcircle - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/bankingcircle/client" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "go.opentelemetry.io/otel/attribute" -) - -func taskFetchAccounts( - client *client.Client, -) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - scheduler task.Scheduler, - ingester ingestion.Ingester, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "bankingcircle.taskFetchAccounts", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - if err := fetchAccount(ctx, client, connectorID, scheduler, ingester); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func fetchAccount( - ctx context.Context, - client *client.Client, - connectorID models.ConnectorID, - scheduler task.Scheduler, - ingester ingestion.Ingester, -) error { - for page := 1; ; page++ { - pagedAccounts, err := client.GetAccounts(ctx, page) - if err != nil { - return err - } - - if len(pagedAccounts) == 0 { - break - } - - if err := ingestAccountsBatch(ctx, connectorID, ingester, pagedAccounts); err != nil { - return err - } - } - - // We want to fetch payments after inserting all accounts in order to - // ling them correctly - taskPayments, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch payments from client", - Key: taskNameFetchPayments, - }) - if err != nil { - return err - } - - err = scheduler.Schedule(ctx, taskPayments, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return err - } - - return nil -} - -func ingestAccountsBatch( - ctx context.Context, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - accounts []*client.Account, -) error { - accountsBatch := ingestion.AccountBatch{} - balanceBatch := ingestion.BalanceBatch{} - - for _, account := range accounts { - raw, err := json.Marshal(account) - if err != nil { - return err - } - - openingDate, err := time.Parse("2006-01-02T15:04:05.999999999+00:00", account.OpeningDate) - if err != nil { - return fmt.Errorf("failed to parse opening date: %w", err) - } - - accountsBatch = append(accountsBatch, &models.Account{ - ID: models.AccountID{ - Reference: account.AccountID, - ConnectorID: connectorID, - }, - CreatedAt: openingDate, - Reference: account.AccountID, - ConnectorID: connectorID, - DefaultAsset: currency.FormatAsset(supportedCurrenciesWithDecimal, account.Currency), - AccountName: account.AccountDescription, - Type: models.AccountTypeInternal, - RawData: raw, - }) - - for _, balance := range account.Balances { - // No need to check if the currency is supported for accounts and - // balances. - precision := supportedCurrenciesWithDecimal[balance.Currency] - - amount, err := currency.GetAmountWithPrecisionFromString(balance.IntraDayAmount.String(), precision) - if err != nil { - return err - } - - now := time.Now() - balanceBatch = append(balanceBatch, &models.Balance{ - AccountID: models.AccountID{ - Reference: account.AccountID, - ConnectorID: connectorID, - }, - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, balance.Currency), - Balance: amount, - CreatedAt: now, - LastUpdatedAt: now, - ConnectorID: connectorID, - }) - } - } - - if err := ingester.IngestAccounts(ctx, accountsBatch); err != nil { - return err - } - - if err := ingester.IngestBalances(ctx, balanceBatch, false); err != nil { - return err - } - - return nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/bankingcircle/task_fetch_payments.go b/components/payments/cmd/connectors/internal/connectors/bankingcircle/task_fetch_payments.go deleted file mode 100644 index 3974e61f50..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/bankingcircle/task_fetch_payments.go +++ /dev/null @@ -1,165 +0,0 @@ -package bankingcircle - -import ( - "context" - "encoding/json" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/bankingcircle/client" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "go.opentelemetry.io/otel/attribute" -) - -func taskFetchPayments( - client *client.Client, -) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "bankingcircle.taskFetchPayments", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - if err := fetchPayments(ctx, client, connectorID, ingester); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func fetchPayments( - ctx context.Context, - client *client.Client, - connectorID models.ConnectorID, - ingester ingestion.Ingester, -) error { - for page := 1; ; page++ { - pagedPayments, err := client.GetPayments(ctx, page) - if err != nil { - return err - } - - if len(pagedPayments) == 0 { - break - } - - if err := ingestBatch(ctx, connectorID, ingester, pagedPayments); err != nil { - return err - } - } - - return nil -} - -func ingestBatch( - ctx context.Context, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - payments []*client.Payment, -) error { - batch := ingestion.PaymentBatch{} - - for _, paymentEl := range payments { - raw, err := json.Marshal(paymentEl) - if err != nil { - return err - } - - paymentType := matchPaymentType(paymentEl.Classification) - - precision, ok := supportedCurrenciesWithDecimal[paymentEl.Transfer.Amount.Currency] - if !ok { - continue - } - - amount, err := currency.GetAmountWithPrecisionFromString(paymentEl.Transfer.Amount.Amount.String(), precision) - if err != nil { - return err - } - - batchElement := ingestion.PaymentBatchElement{ - Payment: &models.Payment{ - ID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: paymentEl.PaymentID, - Type: paymentType, - }, - ConnectorID: connectorID, - }, - Reference: paymentEl.PaymentID, - Type: paymentType, - ConnectorID: connectorID, - Status: matchPaymentStatus(paymentEl.Status), - Scheme: models.PaymentSchemeOther, - Amount: amount, - InitialAmount: amount, - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, paymentEl.Transfer.Amount.Currency), - RawData: raw, - }, - } - - if paymentEl.DebtorInformation.AccountID != "" { - batchElement.Payment.SourceAccountID = &models.AccountID{ - Reference: paymentEl.DebtorInformation.AccountID, - ConnectorID: connectorID, - } - } - - if paymentEl.CreditorInformation.AccountID != "" { - batchElement.Payment.DestinationAccountID = &models.AccountID{ - Reference: paymentEl.CreditorInformation.AccountID, - ConnectorID: connectorID, - } - } - - batch = append(batch, batchElement) - } - - if err := ingester.IngestPayments(ctx, batch); err != nil { - return err - } - - return nil -} - -func matchPaymentStatus(paymentStatus string) models.PaymentStatus { - switch paymentStatus { - case "Processed": - return models.PaymentStatusSucceeded - // On MissingFunding - the payment is still in progress. - // If there will be funds available within 10 days - the payment will be processed. - // Otherwise - it will be cancelled. - case "PendingProcessing", "MissingFunding": - return models.PaymentStatusPending - case "Rejected", "Cancelled", "Reversed", "Returned": - return models.PaymentStatusFailed - } - - return models.PaymentStatusOther -} - -func matchPaymentType(paymentType string) models.PaymentType { - switch paymentType { - case "Incoming": - return models.PaymentTypePayIn - case "Outgoing": - return models.PaymentTypePayOut - case "Own": - return models.PaymentTypeTransfer - } - - return models.PaymentTypeOther -} diff --git a/components/payments/cmd/connectors/internal/connectors/bankingcircle/task_main.go b/components/payments/cmd/connectors/internal/connectors/bankingcircle/task_main.go deleted file mode 100644 index b4ea3fc865..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/bankingcircle/task_main.go +++ /dev/null @@ -1,50 +0,0 @@ -package bankingcircle - -import ( - "context" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" -) - -// taskMain is the main task of the connector. It launches the other tasks. -func taskMain() task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - scheduler task.Scheduler, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "bankingcircle.taskMain", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - taskAccounts, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch accounts from client", - Key: taskNameFetchAccounts, - }) - if err != nil { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - err = scheduler.Schedule(ctx, taskAccounts, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - return nil - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/bankingcircle/task_payments.go b/components/payments/cmd/connectors/internal/connectors/bankingcircle/task_payments.go deleted file mode 100644 index 043217c176..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/bankingcircle/task_payments.go +++ /dev/null @@ -1,377 +0,0 @@ -package bankingcircle - -import ( - "context" - "encoding/json" - "errors" - "time" - - "github.com/formancehq/go-libs/contextutil" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/bankingcircle/client" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "go.opentelemetry.io/otel/attribute" -) - -func taskInitiatePayment(bankingCircleClient *client.Client, transferID string) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - storageReader storage.Reader, - ) error { - transferInitiationID := models.MustTransferInitiationIDFromString(transferID) - - ctx, span := connectors.StartSpan( - ctx, - "bankingcircle.taskInitiatePayment", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("transferID", transferID), - attribute.String("reference", transferInitiationID.Reference), - ) - defer span.End() - - transfer, err := getTransfer(ctx, storageReader, transferInitiationID, true) - if err != nil { - otel.RecordError(span, err) - return err - } - - if err := initiatePayment(ctx, bankingCircleClient, transfer, connectorID, ingester, scheduler, storageReader); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func initiatePayment( - ctx context.Context, - bankingCircleClient *client.Client, - transfer *models.TransferInitiation, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - storageReader storage.Reader, -) error { - var err error - var paymentID *models.PaymentID - defer func() { - if err != nil { - ctx, cancel := contextutil.Detached(ctx) - defer cancel() - _ = ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusFailed, err.Error(), time.Now()) - } - }() - - err = ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusProcessing, "", time.Now()) - if err != nil { - return err - } - - if transfer.SourceAccount == nil { - err = errors.New("no source account provided") - return err - } - - if transfer.SourceAccount.Type == models.AccountTypeExternal { - err = errors.New("payin not implemented: source account must be an internal account") - return err - } - - var curr string - var precision int - curr, precision, err = currency.GetCurrencyAndPrecisionFromAsset(supportedCurrenciesWithDecimal, transfer.Asset) - if err != nil { - return err - } - - amount, err := currency.GetStringAmountFromBigIntWithPrecision(transfer.Amount, precision) - if err != nil { - return err - } - - var sourceAccount *client.Account - sourceAccount, err = bankingCircleClient.GetAccount(ctx, transfer.SourceAccountID.Reference) - if err != nil { - return err - } - if len(sourceAccount.AccountIdentifiers) == 0 { - err = errors.New("no source account identifiers provided") - return err - } - - var destinationAccount *client.Account - destinationAccount, err = bankingCircleClient.GetAccount(ctx, transfer.DestinationAccountID.Reference) - if err != nil { - return err - } - if len(destinationAccount.AccountIdentifiers) == 0 { - err = errors.New("no destination account identifiers provided") - return err - } - - var connectorPaymentID string - var paymentType models.PaymentType - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) - defer cancel() - switch transfer.DestinationAccount.Type { - case models.AccountTypeInternal: - // Transfer between internal accounts - var resp *client.PaymentResponse - resp, err = bankingCircleClient.InitiateTransferOrPayouts(ctx, &client.PaymentRequest{ - IdempotencyKey: transfer.ID.Reference, - RequestedExecutionDate: transfer.ScheduledAt, - DebtorAccount: client.PaymentAccount{ - Account: sourceAccount.AccountIdentifiers[0].Account, - FinancialInstitution: sourceAccount.AccountIdentifiers[0].FinancialInstitution, - Country: sourceAccount.AccountIdentifiers[0].Country, - }, - DebtorReference: transfer.Description, - CurrencyOfTransfer: curr, - Amount: struct { - Currency string "json:\"currency\"" - Amount json.Number "json:\"amount\"" - }{ - Currency: curr, - Amount: json.Number(amount), - }, - ChargeBearer: "SHA", - CreditorAccount: &client.PaymentAccount{ - Account: destinationAccount.AccountIdentifiers[0].Account, - FinancialInstitution: destinationAccount.AccountIdentifiers[0].FinancialInstitution, - Country: destinationAccount.AccountIdentifiers[0].Country, - }, - }) - if err != nil { - return err - } - - connectorPaymentID = resp.PaymentID - paymentType = models.PaymentTypeTransfer - case models.AccountTypeExternal: - // Payout to an external account - var resp *client.PaymentResponse - resp, err = bankingCircleClient.InitiateTransferOrPayouts(ctx, &client.PaymentRequest{ - IdempotencyKey: transfer.ID.Reference, - RequestedExecutionDate: transfer.ScheduledAt, - DebtorAccount: client.PaymentAccount{ - Account: sourceAccount.AccountIdentifiers[0].Account, - FinancialInstitution: sourceAccount.AccountIdentifiers[0].FinancialInstitution, - Country: sourceAccount.AccountIdentifiers[0].Country, - }, - DebtorReference: transfer.Description, - CurrencyOfTransfer: curr, - Amount: struct { - Currency string "json:\"currency\"" - Amount json.Number "json:\"amount\"" - }{ - Currency: curr, - Amount: json.Number(amount), - }, - ChargeBearer: "SHA", - CreditorAccount: &client.PaymentAccount{ - Account: destinationAccount.AccountIdentifiers[0].Account, - FinancialInstitution: destinationAccount.AccountIdentifiers[0].FinancialInstitution, - Country: destinationAccount.AccountIdentifiers[0].Country, - }, - }) - if err != nil { - return err - } - - connectorPaymentID = resp.PaymentID - paymentType = models.PaymentTypePayOut - } - - paymentID = &models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: connectorPaymentID, - Type: paymentType, - }, - ConnectorID: connectorID, - } - err = ingester.AddTransferInitiationPaymentID(ctx, transfer, paymentID, time.Now()) - if err != nil { - return err - } - - var taskDescriptor models.TaskDescriptor - taskDescriptor, err = models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Update transfer initiation status", - Key: taskNameUpdatePaymentStatus, - TransferID: transfer.ID.String(), - PaymentID: paymentID.String(), - Attempt: 1, - }) - if err != nil { - return err - } - - ctx, _ = contextutil.DetachedWithTimeout(ctx, 10*time.Second) - err = scheduler.Schedule(ctx, taskDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return err - } - - return nil -} - -func taskUpdatePaymentStatus( - bankingCircleClient *client.Client, - transferID string, - pID string, - attempt int, -) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - storageReader storage.Reader, - ) error { - paymentID := models.MustPaymentIDFromString(pID) - transferInitiationID := models.MustTransferInitiationIDFromString(transferID) - - ctx, span := connectors.StartSpan( - ctx, - "bankingcircle.taskUpdatePaymentStatus", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("transferID", transferID), - attribute.String("paymentID", pID), - attribute.Int("attempt", attempt), - attribute.String("reference", transferInitiationID.Reference), - ) - defer span.End() - - transfer, err := getTransfer(ctx, storageReader, transferInitiationID, false) - if err != nil { - otel.RecordError(span, err) - return err - } - - if err := updatePaymentStatus(ctx, bankingCircleClient, transfer, paymentID, attempt, ingester, scheduler, storageReader); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func updatePaymentStatus( - ctx context.Context, - bankingCircleClient *client.Client, - transfer *models.TransferInitiation, - paymentID *models.PaymentID, - attempt int, - ingester ingestion.Ingester, - scheduler task.Scheduler, - storageReader storage.Reader, -) error { - var err error - var status string - switch transfer.Type { - case models.TransferInitiationTypeTransfer: - var resp *client.StatusResponse - resp, err = bankingCircleClient.GetPaymentStatus(ctx, paymentID.Reference) - if err != nil { - return err - } - - status = resp.Status - case models.TransferInitiationTypePayout: - var resp *client.StatusResponse - resp, err = bankingCircleClient.GetPaymentStatus(ctx, paymentID.Reference) - if err != nil { - return err - } - - status = resp.Status - } - - switch status { - case "PendingApproval", "PendingProcessing", "Hold", "Approved", "ScaPending": - taskDescriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Update transfer initiation status", - Key: taskNameUpdatePaymentStatus, - TransferID: transfer.ID.String(), - PaymentID: paymentID.String(), - Attempt: attempt + 1, - }) - if err != nil { - return err - } - - err = scheduler.Schedule(ctx, taskDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_IN_DURATION, - Duration: 2 * time.Minute, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return err - } - case "Processed": - err = ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusProcessed, "", time.Now()) - if err != nil { - return err - } - - return nil - case "Unknown", "ScaExpired", "ScaFailed", "MissingFunding", - "PendingCancellation", "PendingCancellationApproval", "DeclinedByApprover", - "Rejected", "Cancelled", "Reversed", "ScaDeclined": - err = ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusFailed, "", time.Now()) - if err != nil { - return err - } - - return nil - } - - return nil -} - -func getTransfer( - ctx context.Context, - reader storage.Reader, - transferID models.TransferInitiationID, - expand bool, -) (*models.TransferInitiation, error) { - transfer, err := reader.ReadTransferInitiation(ctx, transferID) - if err != nil { - return nil, err - } - - if expand { - if transfer.SourceAccountID != nil { - sourceAccount, err := reader.GetAccount(ctx, transfer.SourceAccountID.String()) - if err != nil { - return nil, err - } - transfer.SourceAccount = sourceAccount - } - - destinationAccount, err := reader.GetAccount(ctx, transfer.DestinationAccountID.String()) - if err != nil { - return nil, err - } - transfer.DestinationAccount = destinationAccount - } - - return transfer, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/bankingcircle/task_resolve.go b/components/payments/cmd/connectors/internal/connectors/bankingcircle/task_resolve.go deleted file mode 100644 index 7d82466fe8..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/bankingcircle/task_resolve.go +++ /dev/null @@ -1,72 +0,0 @@ -package bankingcircle - -import ( - "fmt" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/bankingcircle/client" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/google/uuid" -) - -const ( - taskNameMain = "main" - taskNameFetchPayments = "fetch-payments" - taskNameFetchAccounts = "fetch-accounts" - taskNameInitiatePayment = "initiate-payment" - taskNameUpdatePaymentStatus = "update-payment-status" - taskNameCreateExternalAccount = "create-external-account" -) - -// TaskDescriptor is the definition of a task. -type TaskDescriptor struct { - Name string `json:"name" yaml:"name" bson:"name"` - Key string `json:"key" yaml:"key" bson:"key"` - TransferID string `json:"transferID" yaml:"transferID" bson:"transferID"` - PaymentID string `json:"paymentID" yaml:"paymentID" bson:"paymentID"` - BankAccountID uuid.UUID `json:"bankAccountID" yaml:"bankAccountID" bson:"bankAccountID"` - Attempt int `json:"attempt" yaml:"attempt" bson:"attempt"` -} - -func resolveTasks(logger logging.Logger, config Config) func(taskDefinition TaskDescriptor) task.Task { - bankingCircleClient, err := client.NewClient( - config.Username, - config.Password, - config.Endpoint, - config.AuthorizationEndpoint, - config.UserCertificate, - config.UserCertificateKey, - logger, - ) - if err != nil { - logger.Error(err) - - return func(taskDescriptor TaskDescriptor) task.Task { - return func() error { - return fmt.Errorf("cannot build banking circle client: %w", err) - } - } - } - - return func(taskDescriptor TaskDescriptor) task.Task { - switch taskDescriptor.Key { - case taskNameMain: - return taskMain() - case taskNameFetchPayments: - return taskFetchPayments(bankingCircleClient) - case taskNameFetchAccounts: - return taskFetchAccounts(bankingCircleClient) - case taskNameInitiatePayment: - return taskInitiatePayment(bankingCircleClient, taskDescriptor.TransferID) - case taskNameUpdatePaymentStatus: - return taskUpdatePaymentStatus(bankingCircleClient, taskDescriptor.TransferID, taskDescriptor.PaymentID, taskDescriptor.Attempt) - case taskNameCreateExternalAccount: - return taskCreateExternalAccount(bankingCircleClient, taskDescriptor.BankAccountID) - } - - // This should never happen. - return func() error { - return fmt.Errorf("key '%s': %w", taskDescriptor.Key, ErrMissingTask) - } - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/configtemplate/template.go b/components/payments/cmd/connectors/internal/connectors/configtemplate/template.go deleted file mode 100644 index 188dca93b2..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/configtemplate/template.go +++ /dev/null @@ -1,37 +0,0 @@ -package configtemplate - -type Configs map[string]Config - -type Config map[string]Parameter - -type Parameter struct { - DataType Type `json:"dataType"` - Required bool `json:"required"` - DefaultValue string `json:"defaultValue"` -} - -type TemplateBuilder interface { - BuildTemplate() (string, Config) -} - -func BuildConfigs(builders ...TemplateBuilder) Configs { - configs := make(map[string]Config) - for _, builder := range builders { - name, config := builder.BuildTemplate() - configs[name] = config - } - - return configs -} - -func NewConfig() Config { - return make(map[string]Parameter) -} - -func (c *Config) AddParameter(name string, dataType Type, defaultValue string, required bool) { - (*c)[name] = Parameter{ - DataType: dataType, - Required: required, - DefaultValue: defaultValue, - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/configtemplate/types.go b/components/payments/cmd/connectors/internal/connectors/configtemplate/types.go deleted file mode 100644 index ba1181459a..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/configtemplate/types.go +++ /dev/null @@ -1,11 +0,0 @@ -package configtemplate - -type Type string - -const ( - TypeLongString Type = "long string" - TypeString Type = "string" - TypeDurationNs Type = "duration ns" - TypeDurationUnsignedInteger Type = "unsigned integer" - TypeBoolean Type = "boolean" -) diff --git a/components/payments/cmd/connectors/internal/connectors/connector.go b/components/payments/cmd/connectors/internal/connectors/connector.go deleted file mode 100644 index 64f3332bd1..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/connector.go +++ /dev/null @@ -1,28 +0,0 @@ -package connectors - -import ( - "context" - - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" -) - -// Connector provide entry point to a payment provider. -type Connector interface { - // Install is used to start the connector. The implementation if in charge of scheduling all required resources. - Install(ctx task.ConnectorContext) error - // Uninstall is used to uninstall the connector. It has to close all related resources opened by the connector. - Uninstall(ctx context.Context) error - // UpdateConfig is used to update the configuration of the connector. - UpdateConfig(ctx task.ConnectorContext, config models.ConnectorConfigObject) error - // Resolve is used to recover state of a failed or restarted task - Resolve(descriptor models.TaskDescriptor) task.Task - // InitiateTransfer is used to initiate a transfer from the connector to a bank account. - InitiatePayment(ctx task.ConnectorContext, transfer *models.TransferInitiation) error - // ReverssePayment is used to reverse a transfer from the connector. - ReversePayment(ctx task.ConnectorContext, transferReversal *models.TransferReversal) error - // CreateExternalBankAccount is used to create a bank account on the connector side. - CreateExternalBankAccount(ctx task.ConnectorContext, bankAccount *models.BankAccount) error - // GetSupportedCurrenciesAndDecimals returns a map of supported currencies and their decimals. - SupportedCurrenciesAndDecimals() map[string]int -} diff --git a/components/payments/cmd/connectors/internal/connectors/context.go b/components/payments/cmd/connectors/internal/connectors/context.go deleted file mode 100644 index f685b71822..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/context.go +++ /dev/null @@ -1,19 +0,0 @@ -package connectors - -import ( - "context" - - "github.com/google/uuid" -) - -type webhookIDKey struct{} - -var _webhookIDKey = webhookIDKey{} - -func ContextWithWebhookID(ctx context.Context, id uuid.UUID) context.Context { - return context.WithValue(ctx, _webhookIDKey, id) -} - -func WebhookIDFromContext(ctx context.Context) uuid.UUID { - return ctx.Value(_webhookIDKey).(uuid.UUID) -} diff --git a/components/payments/cmd/connectors/internal/connectors/currencycloud/client/contact.go b/components/payments/cmd/connectors/internal/connectors/currencycloud/client/contact.go deleted file mode 100644 index e888bac6a8..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/currencycloud/client/contact.go +++ /dev/null @@ -1,57 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "net/url" - "strings" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" -) - -type Contact struct { - ID string `json:"id"` -} - -func (c *Client) GetContactID(ctx context.Context, accountID string) (*Contact, error) { - f := connectors.ClientMetrics(ctx, "currencycloud", "list_contacts") - now := time.Now() - defer f(ctx, now) - - form := url.Values{} - form.Set("account_id", accountID) - req, err := http.NewRequestWithContext(ctx, http.MethodPost, - c.buildEndpoint("/v2/contacts/find"), strings.NewReader(form.Encode())) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, unmarshalError(resp.StatusCode, resp.Body).Error() - } - - type Contacts struct { - Contacts []*Contact `json:"contacts"` - } - - var res Contacts - if err = json.NewDecoder(resp.Body).Decode(&res); err != nil { - return nil, err - } - - if len(res.Contacts) == 0 { - return nil, fmt.Errorf("no contact found for account %s", accountID) - } - - return res.Contacts[0], nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/currencycloud/client/payout.go b/components/payments/cmd/connectors/internal/connectors/currencycloud/client/payout.go deleted file mode 100644 index abcd640b5d..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/currencycloud/client/payout.go +++ /dev/null @@ -1,116 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "net/url" - "strings" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" -) - -type PayoutRequest struct { - OnBehalfOf string `json:"on_behalf_of"` - BeneficiaryID string `json:"beneficiary_id"` - Currency string `json:"currency"` - Amount json.Number `json:"amount"` - Reference string `json:"reference"` - UniqueRequestID string `json:"unique_request_id"` -} - -func (pr *PayoutRequest) ToFormData() url.Values { - form := url.Values{} - form.Set("on_behalf_of", pr.OnBehalfOf) - form.Set("beneficiary_id", pr.BeneficiaryID) - form.Set("currency", pr.Currency) - form.Set("amount", pr.Amount.String()) - form.Set("reference", pr.Reference) - if pr.UniqueRequestID != "" { - form.Set("unique_request_id", pr.UniqueRequestID) - } - - return form -} - -type PayoutResponse struct { - ID string `json:"id"` - Amount string `json:"amount"` - BeneficiaryID string `json:"beneficiary_id"` - Currency string `json:"currency"` - Reference string `json:"reference"` - Status string `json:"status"` - Reason string `json:"reason"` - CreatorContactID string `json:"creator_contact_id"` - PaymentType string `json:"payment_type"` - TransferredAt string `json:"transferred_at"` - PaymentDate string `json:"payment_date"` - FailureReason string `json:"failure_reason"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` - UniqueRequestID string `json:"unique_request_id"` -} - -func (c *Client) InitiatePayout(ctx context.Context, payoutRequest *PayoutRequest) (*PayoutResponse, error) { - f := connectors.ClientMetrics(ctx, "currencycloud", "initiate_payout") - now := time.Now() - defer f(ctx, now) - - form := payoutRequest.ToFormData() - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, - c.buildEndpoint("v2/payments/create"), strings.NewReader(form.Encode())) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, unmarshalError(resp.StatusCode, resp.Body).Error() - } - - var payoutResponse PayoutResponse - if err = json.NewDecoder(resp.Body).Decode(&payoutResponse); err != nil { - return nil, err - } - - return &payoutResponse, nil -} - -func (c *Client) GetPayout(ctx context.Context, payoutID string) (*PayoutResponse, error) { - f := connectors.ClientMetrics(ctx, "currencycloud", "get_payment") - now := time.Now() - defer f(ctx, now) - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, - c.buildEndpoint("v2/payments/%s", payoutID), http.NoBody) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - req.Header.Add("Accept", "application/json") - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, unmarshalError(resp.StatusCode, resp.Body).Error() - } - - var payoutResponse PayoutResponse - if err = json.NewDecoder(resp.Body).Decode(&payoutResponse); err != nil { - return nil, err - } - - return &payoutResponse, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/currencycloud/client/transfer.go b/components/payments/cmd/connectors/internal/connectors/currencycloud/client/transfer.go deleted file mode 100644 index 6f75ae6836..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/currencycloud/client/transfer.go +++ /dev/null @@ -1,116 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "net/url" - "strings" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" -) - -type TransferRequest struct { - SourceAccountID string `json:"source_account_id"` - DestinationAccountID string `json:"destination_account_id"` - Currency string `json:"currency"` - Amount json.Number `json:"amount"` - Reason string `json:"reason,omitempty"` - UniqueRequestID string `json:"unique_request_id,omitempty"` -} - -func (tr *TransferRequest) ToFormData() url.Values { - form := url.Values{} - form.Set("source_account_id", tr.SourceAccountID) - form.Set("destination_account_id", tr.DestinationAccountID) - form.Set("currency", tr.Currency) - form.Set("amount", fmt.Sprintf("%v", tr.Amount)) - if tr.Reason != "" { - form.Set("reason", tr.Reason) - } - if tr.UniqueRequestID != "" { - form.Set("unique_request_id", tr.UniqueRequestID) - } - - return form -} - -type TransferResponse struct { - ID string `json:"id"` - ShortReference string `json:"short_reference"` - SourceAccountID string `json:"source_account_id"` - DestinationAccountID string `json:"destination_account_id"` - Currency string `json:"currency"` - Amount string `json:"amount"` - Status string `json:"status"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` - CompletedAt string `json:"completed_at"` - CreatorAccountID string `json:"creator_account_id"` - CreatorContactID string `json:"creator_contact_id"` - Reason string `json:"reason"` - UniqueRequestID string `json:"unique_request_id"` -} - -func (c *Client) InitiateTransfer(ctx context.Context, transferRequest *TransferRequest) (*TransferResponse, error) { - f := connectors.ClientMetrics(ctx, "currencycloud", "initiate_transfer") - now := time.Now() - defer f(ctx, now) - - form := transferRequest.ToFormData() - req, err := http.NewRequestWithContext(ctx, http.MethodPost, - c.buildEndpoint("v2/transfers/create"), strings.NewReader(form.Encode())) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, unmarshalError(resp.StatusCode, resp.Body).Error() - } - - var res TransferResponse - if err = json.NewDecoder(resp.Body).Decode(&res); err != nil { - return nil, err - } - - return &res, nil -} - -func (c *Client) GetTransfer(ctx context.Context, transferID string) (*TransferResponse, error) { - f := connectors.ClientMetrics(ctx, "currencycloud", "get_transfer") - now := time.Now() - defer f(ctx, now) - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, - c.buildEndpoint("v2/transfers/%s", transferID), http.NoBody) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - req.Header.Add("Accept", "application/json") - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, unmarshalError(resp.StatusCode, resp.Body).Error() - } - - var res TransferResponse - if err = json.NewDecoder(resp.Body).Decode(&res); err != nil { - return nil, err - } - - return &res, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/currencycloud/config.go b/components/payments/cmd/connectors/internal/connectors/currencycloud/config.go deleted file mode 100644 index 1512801aba..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/currencycloud/config.go +++ /dev/null @@ -1,65 +0,0 @@ -package currencycloud - -import ( - "encoding/json" - "fmt" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/configtemplate" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currencycloud/client" -) - -const ( - defaultPollingDuration = 2 * time.Minute -) - -type Config struct { - Name string `json:"name" bson:"name"` - LoginID string `json:"loginID" bson:"loginID"` - APIKey string `json:"apiKey" bson:"apiKey"` - Endpoint string `json:"endpoint" bson:"endpoint"` - PollingPeriod connectors.Duration `json:"pollingPeriod" bson:"pollingPeriod"` -} - -// String obfuscates sensitive fields and returns a string representation of the config. -// This is used for logging. -func (c Config) String() string { - return fmt.Sprintf("loginID=%s, endpoint=%s, pollingPeriod=%s, apiKey=****", c.LoginID, c.Endpoint, c.PollingPeriod.String()) -} - -func (c Config) Validate() error { - if c.APIKey == "" { - return ErrMissingAPIKey - } - - if c.LoginID == "" { - return ErrMissingLoginID - } - - if c.Name == "" { - return ErrMissingName - } - - return nil -} - -func (c Config) Marshal() ([]byte, error) { - return json.Marshal(c) -} - -func (c Config) ConnectorName() string { - return c.Name -} - -func (c Config) BuildTemplate() (string, configtemplate.Config) { - cfg := configtemplate.NewConfig() - - cfg.AddParameter("name", configtemplate.TypeString, name.String(), false) - cfg.AddParameter("loginID", configtemplate.TypeString, "", true) - cfg.AddParameter("apiKey", configtemplate.TypeString, "", true) - cfg.AddParameter("endpoint", configtemplate.TypeString, client.DevAPIEndpoint, false) - cfg.AddParameter("pollingPeriod", configtemplate.TypeDurationNs, defaultPollingDuration.String(), false) - - return name.String(), cfg -} diff --git a/components/payments/cmd/connectors/internal/connectors/currencycloud/connector.go b/components/payments/cmd/connectors/internal/connectors/currencycloud/connector.go deleted file mode 100644 index afa1d45fa7..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/currencycloud/connector.go +++ /dev/null @@ -1,136 +0,0 @@ -package currencycloud - -import ( - "context" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/pkg/errors" -) - -const name = models.ConnectorProviderCurrencyCloud - -var ( - mainTaskDescriptor = TaskDescriptor{ - Name: "Main task to periodically fetch users and transactions", - Key: taskNameMain, - } -) - -type Connector struct { - logger logging.Logger - cfg Config -} - -func newConnector(logger logging.Logger, cfg Config) *Connector { - return &Connector{ - logger: logger.WithFields(map[string]any{ - "component": "connector", - }), - cfg: cfg, - } -} - -func (c *Connector) UpdateConfig(ctx task.ConnectorContext, config models.ConnectorConfigObject) error { - cfg, ok := config.(Config) - if !ok { - return connectors.ErrInvalidConfig - } - - restartTask := c.cfg.PollingPeriod.Duration != cfg.PollingPeriod.Duration - - c.cfg = cfg - - if restartTask { - taskDescriptor, err := models.EncodeTaskDescriptor(mainTaskDescriptor) - if err != nil { - return err - } - - return ctx.Scheduler().Schedule(ctx.Context(), taskDescriptor, models.TaskSchedulerOptions{ - // We want to polling every c.cfg.PollingPeriod.Duration seconds the users - // and their transactions. - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: c.cfg.PollingPeriod.Duration, - // No need to restart this task, since the connector is not existing or - // was uninstalled previously, the task does not exists in the database - RestartOption: models.OPTIONS_STOP_AND_RESTART, - }) - } - - return nil -} - -func (c *Connector) Install(ctx task.ConnectorContext) error { - taskDescriptor, err := models.EncodeTaskDescriptor(mainTaskDescriptor) - if err != nil { - return err - } - - return ctx.Scheduler().Schedule(ctx.Context(), taskDescriptor, models.TaskSchedulerOptions{ - // We want to polling every c.cfg.PollingPeriod.Duration seconds the users - // and their transactions. - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: c.cfg.PollingPeriod.Duration, - // No need to restart this task, since the connector is not existing or - // was uninstalled previously, the task does not exists in the database - RestartOption: models.OPTIONS_RESTART_NEVER, - }) -} - -func (c *Connector) Uninstall(ctx context.Context) error { - return nil -} - -func (c *Connector) Resolve(descriptor models.TaskDescriptor) task.Task { - taskDescriptor, err := models.DecodeTaskDescriptor[TaskDescriptor](descriptor) - if err != nil { - panic(err) - } - - return resolveTasks(c.cfg)(taskDescriptor) -} - -func (c *Connector) SupportedCurrenciesAndDecimals() map[string]int { - return supportedCurrenciesWithDecimal -} - -func (c *Connector) InitiatePayment(ctx task.ConnectorContext, transfer *models.TransferInitiation) error { - taskDescriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Initiate payment", - Key: taskNameInitiatePayment, - TransferID: transfer.ID.String(), - }) - if err != nil { - return err - } - - scheduleOption := models.OPTIONS_RUN_NOW_SYNC - scheduledAt := transfer.ScheduledAt - if !scheduledAt.IsZero() { - scheduleOption = models.OPTIONS_RUN_SCHEDULED_AT - } - - err = ctx.Scheduler().Schedule(ctx.Context(), taskDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: scheduleOption, - ScheduleAt: scheduledAt, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return err - } - - return nil -} - -func (c *Connector) ReversePayment(ctx task.ConnectorContext, transfer *models.TransferReversal) error { - return connectors.ErrNotImplemented -} - -func (c *Connector) CreateExternalBankAccount(ctx task.ConnectorContext, bankAccount *models.BankAccount) error { - return connectors.ErrNotImplemented -} - -var _ connectors.Connector = &Connector{} diff --git a/components/payments/cmd/connectors/internal/connectors/currencycloud/errors.go b/components/payments/cmd/connectors/internal/connectors/currencycloud/errors.go deleted file mode 100644 index 4c4b5b9681..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/currencycloud/errors.go +++ /dev/null @@ -1,23 +0,0 @@ -package currencycloud - -import "github.com/pkg/errors" - -var ( - // ErrMissingTask is returned when the task is missing. - ErrMissingTask = errors.New("task is not implemented") - - // ErrMissingAPIKey is returned when the api key is missing from config. - ErrMissingAPIKey = errors.New("missing apiKey from config") - - // ErrMissingLoginID is returned when the login id is missing from config. - ErrMissingLoginID = errors.New("missing loginID from config") - - // ErrMissingPollingPeriod is returned when the polling period is missing from config. - ErrMissingPollingPeriod = errors.New("missing pollingPeriod from config") - - // ErrDurationInvalid is returned when the duration is invalid. - ErrDurationInvalid = errors.New("duration is invalid") - - // ErrMissingName is returned when the name is missing from config. - ErrMissingName = errors.New("missing name from config") -) diff --git a/components/payments/cmd/connectors/internal/connectors/currencycloud/loader.go b/components/payments/cmd/connectors/internal/connectors/currencycloud/loader.go deleted file mode 100644 index e42c2fa6a2..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/currencycloud/loader.go +++ /dev/null @@ -1,49 +0,0 @@ -package currencycloud - -import ( - "time" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/gorilla/mux" -) - -type Loader struct{} - -const allowedTasks = 50 - -func (l *Loader) AllowTasks() int { - return allowedTasks -} - -func (l *Loader) Name() models.ConnectorProvider { - return name -} - -func (l *Loader) Load(logger logging.Logger, config Config) connectors.Connector { - return newConnector(logger, config) -} - -func (l *Loader) ApplyDefaults(cfg Config) Config { - if cfg.PollingPeriod.Duration == 0 { - cfg.PollingPeriod.Duration = 2 * time.Minute - } - - if cfg.Name == "" { - cfg.Name = name.String() - } - - return cfg -} - -func (l *Loader) Router(_ *storage.Storage) *mux.Router { - // Webhooks are not implemented yet - return nil -} - -// NewLoader creates a new loader. -func NewLoader() *Loader { - return &Loader{} -} diff --git a/components/payments/cmd/connectors/internal/connectors/currencycloud/task_fetch_accounts.go b/components/payments/cmd/connectors/internal/connectors/currencycloud/task_fetch_accounts.go deleted file mode 100644 index 012fe99301..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/currencycloud/task_fetch_accounts.go +++ /dev/null @@ -1,160 +0,0 @@ -package currencycloud - -import ( - "context" - "encoding/json" - "errors" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currencycloud/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "go.opentelemetry.io/otel/attribute" -) - -type fetchAccountsState struct { - LastPage int - LastCreatedAt time.Time -} - -func taskFetchAccounts( - client *client.Client, -) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - resolver task.StateResolver, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "currencycloud.taskFetchAccounts", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - state := task.MustResolveTo(ctx, resolver, fetchAccountsState{}) - if state.LastPage == 0 { - // First run, the first page for currencycloud starts at 1 and not 0 - state.LastPage = 1 - } - - newState, err := fetchAccount(ctx, client, connectorID, ingester, scheduler, state) - if err != nil { - otel.RecordError(span, err) - return err - } - - if err := ingester.UpdateTaskState(ctx, newState); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func fetchAccount( - ctx context.Context, - client *client.Client, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - state fetchAccountsState, -) (fetchAccountsState, error) { - newState := fetchAccountsState{ - LastPage: state.LastPage, - LastCreatedAt: state.LastCreatedAt, - } - - page := state.LastPage - for { - if page < 0 { - break - } - - pagedAccounts, nextPage, err := client.GetAccounts(ctx, page) - if err != nil { - return fetchAccountsState{}, err - } - - page = nextPage - - batch := ingestion.AccountBatch{} - for _, account := range pagedAccounts { - switch account.CreatedAt.Compare(state.LastCreatedAt) { - case -1, 0: - // Account already ingested, skip - continue - default: - } - - raw, err := json.Marshal(account) - if err != nil { - return fetchAccountsState{}, err - } - - batch = append(batch, &models.Account{ - ID: models.AccountID{ - Reference: account.ID, - ConnectorID: connectorID, - }, - // Moneycorp does not send the opening date of the account - CreatedAt: account.CreatedAt, - Reference: account.ID, - ConnectorID: connectorID, - AccountName: account.AccountName, - Type: models.AccountTypeInternal, - RawData: raw, - }) - - newState.LastCreatedAt = account.CreatedAt - } - - if err := ingester.IngestAccounts(ctx, batch); err != nil { - return fetchAccountsState{}, err - } - } - - newState.LastPage = page - - taskTransactions, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch transactions from client", - Key: taskNameFetchTransactions, - }) - if err != nil { - return fetchAccountsState{}, err - } - - err = scheduler.Schedule(ctx, taskTransactions, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return fetchAccountsState{}, err - } - - taskBalances, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch balances from client", - Key: taskNameFetchBalances, - }) - if err != nil { - return fetchAccountsState{}, err - } - - err = scheduler.Schedule(ctx, taskBalances, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return fetchAccountsState{}, err - } - - return newState, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/currencycloud/task_fetch_balances.go b/components/payments/cmd/connectors/internal/connectors/currencycloud/task_fetch_balances.go deleted file mode 100644 index f7ab4f5b6f..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/currencycloud/task_fetch_balances.go +++ /dev/null @@ -1,105 +0,0 @@ -package currencycloud - -import ( - "context" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currencycloud/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "go.opentelemetry.io/otel/attribute" -) - -func taskFetchBalances( - client *client.Client, -) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "currencycloud.taskFetchBalances", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - if err := fetchBalances(ctx, client, connectorID, ingester); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func fetchBalances( - ctx context.Context, - client *client.Client, - connectorID models.ConnectorID, - ingester ingestion.Ingester, -) error { - page := 1 - for { - if page < 0 { - break - } - - pagedBalances, nextPage, err := client.GetBalances(ctx, page) - if err != nil { - return err - } - - page = nextPage - - if err := ingestBalancesBatch(ctx, connectorID, ingester, pagedBalances); err != nil { - return err - } - } - - return nil -} - -func ingestBalancesBatch( - ctx context.Context, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - balances []*client.Balance, -) error { - batch := ingestion.BalanceBatch{} - for _, balance := range balances { - // No need to check if the currency is supported for accounts and balances. - precision := supportedCurrenciesWithDecimal[balance.Currency] - - amount, err := currency.GetAmountWithPrecisionFromString(balance.Amount.String(), precision) - if err != nil { - return err - } - - now := time.Now() - batch = append(batch, &models.Balance{ - AccountID: models.AccountID{ - Reference: balance.AccountID, - ConnectorID: connectorID, - }, - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, balance.Currency), - Balance: amount, - CreatedAt: now, - LastUpdatedAt: now, - ConnectorID: connectorID, - }) - } - - if err := ingester.IngestBalances(ctx, batch, true); err != nil { - return err - } - - return nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/currencycloud/task_fetch_beneficiaries.go b/components/payments/cmd/connectors/internal/connectors/currencycloud/task_fetch_beneficiaries.go deleted file mode 100644 index 451c26539b..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/currencycloud/task_fetch_beneficiaries.go +++ /dev/null @@ -1,127 +0,0 @@ -package currencycloud - -import ( - "context" - "encoding/json" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currencycloud/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "go.opentelemetry.io/otel/attribute" -) - -type fetchBeneficiariesState struct { - LastPage int - LastCreatedAt time.Time -} - -func taskFetchBeneficiaries( - client *client.Client, -) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - resolver task.StateResolver, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "currencycloud.taskFetchBeneficiaries", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - state := task.MustResolveTo(ctx, resolver, fetchBeneficiariesState{}) - if state.LastPage == 0 { - // First run, the first page for currencycloud starts at 1 and not 0 - state.LastPage = 1 - } - - newState, err := fetchBeneficiaries(ctx, client, connectorID, ingester, scheduler, state) - if err != nil { - otel.RecordError(span, err) - return err - } - - if err := ingester.UpdateTaskState(ctx, newState); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func fetchBeneficiaries( - ctx context.Context, - client *client.Client, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - state fetchBeneficiariesState, -) (fetchBeneficiariesState, error) { - newState := fetchBeneficiariesState{ - LastPage: state.LastPage, - LastCreatedAt: state.LastCreatedAt, - } - - page := state.LastPage - for { - if page < 0 { - break - } - - pagedBeneficiaries, nextPage, err := client.GetBeneficiaries(ctx, page) - if err != nil { - return fetchBeneficiariesState{}, err - } - - page = nextPage - - batch := ingestion.AccountBatch{} - for _, beneficiary := range pagedBeneficiaries { - switch beneficiary.CreatedAt.Compare(state.LastCreatedAt) { - case -1, 0: - // Account already ingested, skip - continue - default: - } - - raw, err := json.Marshal(beneficiary) - if err != nil { - return fetchBeneficiariesState{}, err - } - - batch = append(batch, &models.Account{ - ID: models.AccountID{ - Reference: beneficiary.ID, - ConnectorID: connectorID, - }, - // Moneycorp does not send the opening date of the account - CreatedAt: beneficiary.CreatedAt, - Reference: beneficiary.ID, - ConnectorID: connectorID, - AccountName: beneficiary.Name, - Type: models.AccountTypeExternal, - RawData: raw, - }) - - newState.LastCreatedAt = beneficiary.CreatedAt - } - - if err := ingester.IngestAccounts(ctx, batch); err != nil { - return fetchBeneficiariesState{}, err - } - } - - newState.LastPage = page - - return newState, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/currencycloud/task_fetch_transactions.go b/components/payments/cmd/connectors/internal/connectors/currencycloud/task_fetch_transactions.go deleted file mode 100644 index e1fbb5d91c..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/currencycloud/task_fetch_transactions.go +++ /dev/null @@ -1,179 +0,0 @@ -package currencycloud - -import ( - "context" - "encoding/json" - "fmt" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currencycloud/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "go.opentelemetry.io/otel/attribute" -) - -type fetchTransactionsState struct { - LastUpdatedAt time.Time -} - -func taskFetchTransactions(client *client.Client, config Config) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - resolver task.StateResolver, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "currencycloud.taskFetchTransactions", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - state := task.MustResolveTo(ctx, resolver, fetchTransactionsState{}) - - newState, err := ingestTransactions(ctx, connectorID, client, ingester, state) - if err != nil { - otel.RecordError(span, err) - return err - } - - if err := ingester.UpdateTaskState(ctx, newState); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func ingestTransactions( - ctx context.Context, - connectorID models.ConnectorID, - client *client.Client, - ingester ingestion.Ingester, - state fetchTransactionsState, -) (fetchTransactionsState, error) { - newState := fetchTransactionsState{ - LastUpdatedAt: state.LastUpdatedAt, - } - - page := 1 - for { - if page < 0 { - break - } - - transactions, nextPage, err := client.GetTransactions(ctx, page, state.LastUpdatedAt) - if err != nil { - return fetchTransactionsState{}, err - } - - page = nextPage - - batch := ingestion.PaymentBatch{} - - for _, transaction := range transactions { - switch transaction.UpdatedAt.Compare(state.LastUpdatedAt) { - case -1, 0: - continue - default: - } - - precision, ok := supportedCurrenciesWithDecimal[transaction.Currency] - if !ok { - continue - } - - amount, err := currency.GetAmountWithPrecisionFromString(transaction.Amount.String(), precision) - if err != nil { - return fetchTransactionsState{}, err - } - - var rawData json.RawMessage - - rawData, err = json.Marshal(transaction) - if err != nil { - return fetchTransactionsState{}, fmt.Errorf("failed to marshal transaction: %w", err) - } - - paymentType := matchTransactionType(transaction.Type) - - batchElement := ingestion.PaymentBatchElement{ - Payment: &models.Payment{ - ID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: transaction.ID, - Type: paymentType, - }, - ConnectorID: connectorID, - }, - Reference: transaction.ID, - CreatedAt: transaction.CreatedAt, - Type: paymentType, - ConnectorID: connectorID, - Status: matchTransactionStatus(transaction.Status), - Scheme: models.PaymentSchemeOther, - Amount: amount, - InitialAmount: amount, - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transaction.Currency), - RawData: rawData, - }, - } - - switch paymentType { - case models.PaymentTypePayOut: - batchElement.Payment.SourceAccountID = &models.AccountID{ - Reference: transaction.AccountID, - ConnectorID: connectorID, - } - default: - batchElement.Payment.DestinationAccountID = &models.AccountID{ - Reference: transaction.AccountID, - ConnectorID: connectorID, - } - } - - batch = append(batch, batchElement) - - newState.LastUpdatedAt = transaction.UpdatedAt - } - - err = ingester.IngestPayments(ctx, batch) - if err != nil { - return fetchTransactionsState{}, err - } - } - - return newState, nil -} - -func matchTransactionType(transactionType string) models.PaymentType { - switch transactionType { - case "credit": - return models.PaymentTypePayIn - case "debit": - return models.PaymentTypePayOut - } - - return models.PaymentTypeOther -} - -func matchTransactionStatus(transactionStatus string) models.PaymentStatus { - switch transactionStatus { - case "completed": - return models.PaymentStatusSucceeded - case "pending": - return models.PaymentStatusPending - case "deleted": - return models.PaymentStatusFailed - } - - return models.PaymentStatusOther -} diff --git a/components/payments/cmd/connectors/internal/connectors/currencycloud/task_main.go b/components/payments/cmd/connectors/internal/connectors/currencycloud/task_main.go deleted file mode 100644 index 2c994ea8af..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/currencycloud/task_main.go +++ /dev/null @@ -1,68 +0,0 @@ -package currencycloud - -import ( - "context" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" -) - -// taskMain is the main task of the connector. It launches the other tasks. -func taskMain() task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - scheduler task.Scheduler, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "currencycloud.taskMain", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - taskAccounts, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch accounts from client", - Key: taskNameFetchAccounts, - }) - if err != nil { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - err = scheduler.Schedule(ctx, taskAccounts, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - taskBeneficiaries, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch beneficiaries from client", - Key: taskNameFetchBeneficiaries, - }) - if err != nil { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - err = scheduler.Schedule(ctx, taskBeneficiaries, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - return nil - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/currencycloud/task_payments.go b/components/payments/cmd/connectors/internal/connectors/currencycloud/task_payments.go deleted file mode 100644 index 30970e0ac0..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/currencycloud/task_payments.go +++ /dev/null @@ -1,328 +0,0 @@ -package currencycloud - -import ( - "context" - "encoding/json" - "fmt" - "time" - - "github.com/formancehq/go-libs/contextutil" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currencycloud/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" -) - -func taskInitiatePayment(currencyCloudClient *client.Client, transferID string) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - storageReader storage.Reader, - ) error { - transferInitiationID := models.MustTransferInitiationIDFromString(transferID) - - ctx, span := connectors.StartSpan( - ctx, - "currencycloud.taskInitiatePayment", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("transferInitiationID", transferInitiationID.String()), - attribute.String("reference", transferInitiationID.Reference), - ) - defer span.End() - - transfer, err := getTransfer(ctx, storageReader, transferInitiationID, true) - if err != nil { - otel.RecordError(span, err) - return err - } - - if err := initiatePayment(ctx, currencyCloudClient, transfer, connectorID, ingester, scheduler, storageReader); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func initiatePayment( - ctx context.Context, - currencyCloudClient *client.Client, - transfer *models.TransferInitiation, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - storageReader storage.Reader, -) error { - var err error - var paymentID *models.PaymentID - defer func() { - if err != nil { - ctx, cancel := contextutil.Detached(ctx) - defer cancel() - _ = ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusFailed, err.Error(), time.Now()) - } - }() - - err = ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusProcessing, "", time.Now()) - if err != nil { - return err - } - - if transfer.SourceAccount == nil { - err = errors.New("no source account provided") - return err - } - - if transfer.SourceAccount.Type == models.AccountTypeExternal { - err = errors.New("payin not implemented: source account must be an internal account") - return err - } - - var curr string - var precision int - curr, precision, err = currency.GetCurrencyAndPrecisionFromAsset(supportedCurrenciesWithDecimal, transfer.Asset) - if err != nil { - return err - } - - amount, err := currency.GetStringAmountFromBigIntWithPrecision(transfer.Amount, precision) - if err != nil { - return err - } - - var connectorPaymentID string - var paymentType models.PaymentType - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) - defer cancel() - switch transfer.DestinationAccount.Type { - case models.AccountTypeInternal: - // Transfer between internal accounts - var resp *client.TransferResponse - resp, err = currencyCloudClient.InitiateTransfer(ctx, &client.TransferRequest{ - SourceAccountID: transfer.SourceAccountID.Reference, - DestinationAccountID: transfer.DestinationAccountID.Reference, - Currency: curr, - Amount: json.Number(amount), - Reason: transfer.Description, - UniqueRequestID: fmt.Sprintf("%s_%d", transfer.ID.Reference, len(transfer.RelatedAdjustments)), - }) - if err != nil { - return err - } - - connectorPaymentID = resp.ID - paymentType = models.PaymentTypeTransfer - case models.AccountTypeExternal: - // Payout to an external account - contact, err := currencyCloudClient.GetContactID(ctx, transfer.SourceAccount.ID.Reference) - if err != nil { - return err - } - - var resp *client.PayoutResponse - resp, err = currencyCloudClient.InitiatePayout(ctx, &client.PayoutRequest{ - OnBehalfOf: contact.ID, - BeneficiaryID: transfer.DestinationAccount.Reference, - Currency: curr, - Amount: json.Number(amount), - Reference: transfer.Description, - UniqueRequestID: fmt.Sprintf("%s_%d", transfer.ID.Reference, len(transfer.RelatedAdjustments)), - }) - if err != nil { - return err - } - - connectorPaymentID = resp.ID - paymentType = models.PaymentTypePayOut - } - - paymentID = &models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: connectorPaymentID, - Type: paymentType, - }, - ConnectorID: connectorID, - } - err = ingester.AddTransferInitiationPaymentID(ctx, transfer, paymentID, time.Now()) - if err != nil { - return err - } - - taskDescriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Update transfer initiation status", - Key: taskNameUpdatePaymentStatus, - TransferID: transfer.ID.String(), - PaymentID: paymentID.String(), - Attempt: 1, - }) - if err != nil { - return err - } - - ctx, _ = contextutil.DetachedWithTimeout(ctx, 10*time.Second) - err = scheduler.Schedule(ctx, taskDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return err - } - - return nil -} - -func taskUpdatePaymentStatus( - currencyCloudClient *client.Client, - transferID string, - pID string, - attempt int, -) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - storageReader storage.Reader, - ) error { - paymentID := models.MustPaymentIDFromString(pID) - transferInitiationID := models.MustTransferInitiationIDFromString(transferID) - - ctx, span := connectors.StartSpan( - ctx, - "currencycloud.taskUpdatePaymentStatus", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("transferInitiationID", transferInitiationID.String()), - attribute.String("paymentID", paymentID.String()), - attribute.Int("attempt", attempt), - attribute.String("reference", paymentID.Reference), - ) - defer span.End() - - transfer, err := getTransfer(ctx, storageReader, transferInitiationID, false) - if err != nil { - otel.RecordError(span, err) - return err - } - - if err := updatePaymentStatus(ctx, currencyCloudClient, transfer, paymentID, attempt, ingester, scheduler, storageReader); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func updatePaymentStatus( - ctx context.Context, - currencyCloudClient *client.Client, - transfer *models.TransferInitiation, - paymentID *models.PaymentID, - attempt int, - ingester ingestion.Ingester, - scheduler task.Scheduler, - storageReader storage.Reader, -) error { - var status string - var resultMessage string - switch transfer.Type { - case models.TransferInitiationTypeTransfer: - resp, err := currencyCloudClient.GetTransfer(ctx, paymentID.Reference) - if err != nil { - return err - } - - status = resp.Status - resultMessage = resp.Reason - case models.TransferInitiationTypePayout: - resp, err := currencyCloudClient.GetPayout(ctx, paymentID.Reference) - if err != nil { - return err - } - - status = resp.Status - resultMessage = resp.Reason - } - - switch status { - case "pending": - taskDescriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Update transfer initiation status", - Key: taskNameUpdatePaymentStatus, - TransferID: transfer.ID.String(), - PaymentID: paymentID.String(), - Attempt: attempt + 1, - }) - if err != nil { - return err - } - - err = scheduler.Schedule(ctx, taskDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_IN_DURATION, - Duration: 2 * time.Minute, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return err - } - case "completed": - err := ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusProcessed, "", time.Now()) - if err != nil { - return err - } - - return nil - case "cancelled": - err := ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusFailed, resultMessage, time.Now()) - if err != nil { - return err - } - - return nil - } - - return nil -} - -func getTransfer( - ctx context.Context, - reader storage.Reader, - transferID models.TransferInitiationID, - expand bool, -) (*models.TransferInitiation, error) { - transfer, err := reader.ReadTransferInitiation(ctx, transferID) - if err != nil { - return nil, err - } - - if expand { - if transfer.SourceAccountID != nil { - sourceAccount, err := reader.GetAccount(ctx, transfer.SourceAccountID.String()) - if err != nil { - return nil, err - } - transfer.SourceAccount = sourceAccount - } - - destinationAccount, err := reader.GetAccount(ctx, transfer.DestinationAccountID.String()) - if err != nil { - return nil, err - } - transfer.DestinationAccount = destinationAccount - } - - return transfer, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/currencycloud/task_resolve.go b/components/payments/cmd/connectors/internal/connectors/currencycloud/task_resolve.go deleted file mode 100644 index f4cdbad536..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/currencycloud/task_resolve.go +++ /dev/null @@ -1,68 +0,0 @@ -package currencycloud - -import ( - "fmt" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currencycloud/client" - "github.com/formancehq/payments/cmd/connectors/internal/task" -) - -const ( - taskNameMain = "main" - taskNameFetchTransactions = "fetch-transactions" - taskNameFetchAccounts = "fetch-accounts" - taskNameFetchBeneficiaries = "fetch-beneficiaries" - taskNameFetchBalances = "fetch-balances" - taskNameInitiatePayment = "initiate-payment" - taskNameUpdatePaymentStatus = "update-payment-status" -) - -// TaskDescriptor is the definition of a task. -type TaskDescriptor struct { - Name string `json:"name" yaml:"name" bson:"name"` - Key string `json:"key" yaml:"key" bson:"key"` - TransferID string `json:"transferID" yaml:"transferID" bson:"transferID"` - PaymentID string `json:"paymentID" yaml:"paymentID" bson:"paymentID"` - Attempt int `json:"attempt" yaml:"attempt" bson:"attempt"` -} - -func resolveTasks(config Config) func(taskDefinition TaskDescriptor) task.Task { - currencyCloudClient, err := client.NewClient(config.LoginID, config.APIKey, config.Endpoint) - if err != nil { - return func(taskDefinition TaskDescriptor) task.Task { - return func() error { - return fmt.Errorf("failed to initiate client: %w", err) - } - } - } - - return func(taskDescriptor TaskDescriptor) task.Task { - if taskDescriptor.Key == "" { - // Keep the compatibility with previous version if the connector. - // If the key is empty, use the name as the key. - taskDescriptor.Key = taskDescriptor.Name - } - - switch taskDescriptor.Key { - case taskNameMain: - return taskMain() - case taskNameFetchAccounts: - return taskFetchAccounts(currencyCloudClient) - case taskNameFetchBeneficiaries: - return taskFetchBeneficiaries(currencyCloudClient) - case taskNameFetchTransactions: - return taskFetchTransactions(currencyCloudClient, config) - case taskNameFetchBalances: - return taskFetchBalances(currencyCloudClient) - case taskNameInitiatePayment: - return taskInitiatePayment(currencyCloudClient, taskDescriptor.TransferID) - case taskNameUpdatePaymentStatus: - return taskUpdatePaymentStatus(currencyCloudClient, taskDescriptor.TransferID, taskDescriptor.PaymentID, taskDescriptor.Attempt) - } - - // This should never happen. - return func() error { - return fmt.Errorf("key '%s': %w", taskDescriptor.Name, ErrMissingTask) - } - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/dummypay/config.go b/components/payments/cmd/connectors/internal/connectors/dummypay/config.go deleted file mode 100644 index 148dad4fbc..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/dummypay/config.go +++ /dev/null @@ -1,76 +0,0 @@ -package dummypay - -import ( - "encoding/json" - "fmt" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors/configtemplate" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" -) - -// Config is the configuration for the dummy payment connector. -type Config struct { - Name string `json:"name" yaml:"name" bson:"name"` - - // Directory is the directory where the files are stored. - Directory string `json:"directory" yaml:"directory" bson:"directory"` - - // FilePollingPeriod is the period between file polling. - FilePollingPeriod connectors.Duration `json:"filePollingPeriod" yaml:"filePollingPeriod" bson:"filePollingPeriod"` - - // PrefixFileToIngest is the prefix of the file to ingest. - PrefixFileToIngest string `json:"prefixFileToIngest" yaml:"prefixFileToIngest" bson:"prefixFileToIngest"` - - // NumberOfAccountsPreGenerated is the number of accounts to pre-generate. - NumberOfAccountsPreGenerated int `json:"numberOfAccountsPreGenerated" yaml:"numberOfAccountsPreGenerated" bson:"numberOfAccountsPreGenerated"` - // NumberOfPaymentsPreGenerated is the number of payments to pre-generate. - NumberOfPaymentsPreGenerated int `json:"numberOfPaymentsPreGenerated" yaml:"numberOfPaymentsPreGenerated" bson:"numberOfPaymentsPreGenerated"` -} - -// String returns a string representation of the configuration. -func (c Config) String() string { - return fmt.Sprintf("directory=%s, filePollingPeriod=%s", - c.Directory, c.FilePollingPeriod.String()) -} - -func (c Config) Marshal() ([]byte, error) { - return json.Marshal(c) -} - -func (c Config) ConnectorName() string { - return c.Name -} - -// Validate validates the configuration. -func (c Config) Validate() error { - // require directory path to be present - if c.Directory == "" { - return ErrMissingDirectory - } - - // check if file polling period is set properly - if c.FilePollingPeriod.Duration <= 0 { - return fmt.Errorf("filePollingPeriod must be greater than 0: %w", - ErrFilePollingPeriodInvalid) - } - - if c.Name == "" { - return fmt.Errorf("name must be set: %w", ErrMissingName) - } - - return nil -} - -func (c Config) BuildTemplate() (string, configtemplate.Config) { - cfg := configtemplate.NewConfig() - - cfg.AddParameter("name", configtemplate.TypeString, name.String(), false) - cfg.AddParameter("directory", configtemplate.TypeString, "", true) - cfg.AddParameter("filePollingPeriod", configtemplate.TypeDurationNs, "", true) - cfg.AddParameter("prefixFileToIngest", configtemplate.TypeString, "", false) - cfg.AddParameter("numberOfAccountsPreGenerated", configtemplate.TypeDurationUnsignedInteger, "0", false) - cfg.AddParameter("numberOfPaymentsPreGenerated", configtemplate.TypeDurationUnsignedInteger, "0", false) - - return name.String(), cfg -} diff --git a/components/payments/cmd/connectors/internal/connectors/dummypay/config_test.go b/components/payments/cmd/connectors/internal/connectors/dummypay/config_test.go deleted file mode 100644 index 224746a030..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/dummypay/config_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package dummypay - -import ( - "os" - "testing" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/stretchr/testify/assert" -) - -// TestConfigString tests the string representation of the config. -func TestConfigString(t *testing.T) { - t.Parallel() - - config := Config{ - Directory: "test", - FilePollingPeriod: connectors.Duration{Duration: time.Second}, - } - - assert.Equal(t, "directory=test, filePollingPeriod=1s", config.String()) -} - -// TestConfigValidate tests the validation of the config. -func TestConfigValidate(t *testing.T) { - t.Parallel() - - var config Config - - config.Name = "test1" - - // fail on missing directory - assert.EqualError(t, config.Validate(), ErrMissingDirectory.Error()) - - // fail on missing RW access to directory - config.Directory = "/non-existing" - assert.Error(t, config.Validate()) - - // set directory with RW access - userHomeDir, err := os.UserHomeDir() - if err != nil { - t.Error(err) - } - - config.Directory = userHomeDir - - // fail on invalid file polling period - config.FilePollingPeriod.Duration = -1 - assert.ErrorIs(t, config.Validate(), ErrFilePollingPeriodInvalid) - - // success - config.FilePollingPeriod.Duration = 1 - assert.NoError(t, config.Validate()) -} diff --git a/components/payments/cmd/connectors/internal/connectors/dummypay/connector.go b/components/payments/cmd/connectors/internal/connectors/dummypay/connector.go deleted file mode 100644 index a1d201b5d7..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/dummypay/connector.go +++ /dev/null @@ -1,121 +0,0 @@ -package dummypay - -import ( - "context" - "fmt" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" -) - -// name is the name of the connector. -const name = models.ConnectorProviderDummyPay - -// Connector is the connector for the dummy payment connector. -type Connector struct { - logger logging.Logger - cfg Config - fs fs -} - -func newConnector(logger logging.Logger, cfg Config, fs fs) *Connector { - return &Connector{ - logger: logger.WithFields(map[string]any{ - "component": "connector", - }), - cfg: cfg, - fs: fs, - } -} - -func (c *Connector) UpdateConfig(ctx task.ConnectorContext, config models.ConnectorConfigObject) error { - cfg, ok := config.(Config) - if !ok { - return connectors.ErrInvalidConfig - } - - c.cfg = cfg - - return nil -} - -// Install executes post-installation steps to read and generate files. -// It is called after the connector is installed. -func (c *Connector) Install(ctx task.ConnectorContext) error { - initDirectoryDescriptor, err := models.EncodeTaskDescriptor(newTaskGenerateFiles()) - if err != nil { - return fmt.Errorf("failed to create generate files task descriptor: %w", err) - } - - if err = ctx.Scheduler().Schedule(ctx.Context(), initDirectoryDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW_SYNC, - RestartOption: models.OPTIONS_RESTART_NEVER, - }); err != nil { - return fmt.Errorf("failed to schedule task to generate files: %w", err) - } - - readFilesDescriptor, err := models.EncodeTaskDescriptor(newTaskReadFiles()) - if err != nil { - return fmt.Errorf("failed to create read files task descriptor: %w", err) - } - - if err = ctx.Scheduler().Schedule(ctx.Context(), readFilesDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: c.cfg.FilePollingPeriod.Duration, - // No need to restart this task, since the connector is not existing or - // was uninstalled previously, the task does not exists in the database - RestartOption: models.OPTIONS_RESTART_NEVER, - }); err != nil { - return fmt.Errorf("failed to schedule task to read files: %w", err) - } - - return nil -} - -// Uninstall executes pre-uninstallation steps to remove the generated files. -// It is called before the connector is uninstalled. -func (c *Connector) Uninstall(ctx context.Context) error { - c.logger.Infof("Removing generated files from '%s'...", c.cfg.Directory) - - err := removeFiles(c.cfg, c.fs) - if err != nil { - return fmt.Errorf("failed to remove generated files: %w", err) - } - - return nil -} - -// Resolve resolves a task execution request based on the task descriptor. -func (c *Connector) Resolve(descriptor models.TaskDescriptor) task.Task { - taskDescriptor, err := models.DecodeTaskDescriptor[TaskDescriptor](descriptor) - if err != nil { - panic(err) - } - - c.logger.Infof("Executing '%s' task...", taskDescriptor.Key) - - return handleResolve(c.cfg, taskDescriptor, c.fs) -} - -func (c *Connector) SupportedCurrenciesAndDecimals() map[string]int { - return supportedCurrenciesWithDecimal -} - -func (c *Connector) InitiatePayment(ctx task.ConnectorContext, transfer *models.TransferInitiation) error { - // TODO implement me - return connectors.ErrNotImplemented -} - -func (c *Connector) ReversePayment(ctx task.ConnectorContext, transferReversal *models.TransferReversal) error { - // TODO implement me - return connectors.ErrNotImplemented -} - -func (c *Connector) CreateExternalBankAccount(ctx task.ConnectorContext, bankAccount *models.BankAccount) error { - // TODO implement me - return connectors.ErrNotImplemented -} - -var _ connectors.Connector = &Connector{} diff --git a/components/payments/cmd/connectors/internal/connectors/dummypay/connector_test.go b/components/payments/cmd/connectors/internal/connectors/dummypay/connector_test.go deleted file mode 100644 index bf685f539a..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/dummypay/connector_test.go +++ /dev/null @@ -1,121 +0,0 @@ -package dummypay - -import ( - "context" - "reflect" - "testing" - "time" - - "github.com/formancehq/payments/internal/models" - - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - - "github.com/formancehq/go-libs/logging" - "github.com/stretchr/testify/assert" -) - -// Create a minimal mock for connector installation. -type ( - mockConnectorContext struct { - ctx context.Context - } - mockScheduler struct{} -) - -func (mcc *mockConnectorContext) Context() context.Context { - return mcc.ctx -} - -func (mcc mockScheduler) Schedule(ctx context.Context, p models.TaskDescriptor, options models.TaskSchedulerOptions) error { - return nil -} - -func (mcc *mockConnectorContext) Scheduler() task.Scheduler { - return mockScheduler{} -} - -func TestConnector(t *testing.T) { - t.Parallel() - - config := Config{} - logger := logging.FromContext(context.TODO()) - - fileSystem := newTestFS() - - connector := newConnector(logger, config, fileSystem) - - err := connector.Install(new(mockConnectorContext)) - assert.NoErrorf(t, err, "Install() failed") - - testCases := []struct { - key taskKey - task task.Task - }{ - {taskKeyReadFiles, taskReadFiles(config, fileSystem)}, - {taskKeyInitDirectory, taskGenerateFiles(config, fileSystem)}, - {taskKeyIngest, taskIngest(config, TaskDescriptor{}, fileSystem)}, - } - - for _, testCase := range testCases { - var taskDescriptor models.TaskDescriptor - - taskDescriptor, err = models.EncodeTaskDescriptor(TaskDescriptor{Key: testCase.key}) - assert.NoErrorf(t, err, "EncodeTaskDescriptor() failed") - - assert.EqualValues(t, - reflect.ValueOf(testCase.task).String(), - reflect.ValueOf(connector.Resolve(taskDescriptor)).String(), - ) - } - - taskDescriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{Key: "test"}) - assert.NoErrorf(t, err, "EncodeTaskDescriptor() failed") - - assert.EqualValues(t, - reflect.ValueOf(func() error { return nil }).String(), - reflect.ValueOf(connector.Resolve(taskDescriptor)).String(), - ) - - assert.NoError(t, connector.Uninstall(context.TODO())) -} - -type MockIngester struct{} - -func (m *MockIngester) IngestAccounts(ctx context.Context, batch ingestion.AccountBatch) error { - return nil -} - -func (m *MockIngester) IngestPayments(ctx context.Context, batch ingestion.PaymentBatch) error { - return nil -} - -func (m *MockIngester) IngestBalances(ctx context.Context, batch ingestion.BalanceBatch, checkIfAccountExists bool) error { - return nil -} - -func (m *MockIngester) UpdateTaskState(ctx context.Context, state any) error { - return nil -} - -func (m *MockIngester) UpdateTransferInitiationPayment(ctx context.Context, tf *models.TransferInitiation, paymentID *models.PaymentID, status models.TransferInitiationStatus, errorMessage string, updatedAt time.Time) error { - return nil -} - -func (m *MockIngester) UpdateTransferInitiationPaymentsStatus(ctx context.Context, tf *models.TransferInitiation, paymentID *models.PaymentID, status models.TransferInitiationStatus, errorMessage string, updatedAt time.Time) error { - return nil -} - -func (m *MockIngester) AddTransferInitiationPaymentID(ctx context.Context, tf *models.TransferInitiation, paymentID *models.PaymentID, updatedAt time.Time) error { - return nil -} - -func (m *MockIngester) UpdateTransferReversalStatus(ctx context.Context, transfer *models.TransferInitiation, transferReversal *models.TransferReversal) error { - return nil -} - -func (m *MockIngester) LinkBankAccountWithAccount(ctx context.Context, bankAccount *models.BankAccount, accountID *models.AccountID) error { - return nil -} - -var _ ingestion.Ingester = (*MockIngester)(nil) diff --git a/components/payments/cmd/connectors/internal/connectors/dummypay/currencies.go b/components/payments/cmd/connectors/internal/connectors/dummypay/currencies.go deleted file mode 100644 index 75b0ef3d35..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/dummypay/currencies.go +++ /dev/null @@ -1,7 +0,0 @@ -package dummypay - -import "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - -var ( - supportedCurrenciesWithDecimal = currency.ISO4217Currencies -) diff --git a/components/payments/cmd/connectors/internal/connectors/dummypay/errors.go b/components/payments/cmd/connectors/internal/connectors/dummypay/errors.go deleted file mode 100644 index 9529b82ce0..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/dummypay/errors.go +++ /dev/null @@ -1,23 +0,0 @@ -package dummypay - -import "github.com/pkg/errors" - -var ( - // ErrMissingDirectory is returned when the directory is missing. - ErrMissingDirectory = errors.New("missing directory to watch") - - // ErrFilePollingPeriodInvalid is returned when the file polling period is invalid. - ErrFilePollingPeriodInvalid = errors.New("file polling period is invalid") - - // ErrFileGenerationPeriodInvalid is returned when the file generation period is invalid. - ErrFileGenerationPeriodInvalid = errors.New("file generation period is invalid") - - // ErrMissingTask is returned when the task is missing. - ErrMissingTask = errors.New("task is not implemented") - - // ErrDurationInvalid is returned when the duration is invalid. - ErrDurationInvalid = errors.New("duration is invalid") - - // ErrMissingName is returned when the name is missing. - ErrMissingName = errors.New("missing name") -) diff --git a/components/payments/cmd/connectors/internal/connectors/dummypay/fs.go b/components/payments/cmd/connectors/internal/connectors/dummypay/fs.go deleted file mode 100644 index 5224ccda1d..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/dummypay/fs.go +++ /dev/null @@ -1,12 +0,0 @@ -package dummypay - -import ( - "github.com/spf13/afero" -) - -type fs afero.Fs - -// newFS creates a new file system access point. -func newFS() fs { - return afero.NewOsFs() -} diff --git a/components/payments/cmd/connectors/internal/connectors/dummypay/fs_test.go b/components/payments/cmd/connectors/internal/connectors/dummypay/fs_test.go deleted file mode 100644 index 36d374a586..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/dummypay/fs_test.go +++ /dev/null @@ -1,7 +0,0 @@ -package dummypay - -import "github.com/spf13/afero" - -func newTestFS() fs { - return afero.NewMemMapFs() -} diff --git a/components/payments/cmd/connectors/internal/connectors/dummypay/loader.go b/components/payments/cmd/connectors/internal/connectors/dummypay/loader.go deleted file mode 100644 index 20702fd3f4..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/dummypay/loader.go +++ /dev/null @@ -1,57 +0,0 @@ -package dummypay - -import ( - "time" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/gorilla/mux" -) - -type Loader struct{} - -// Name returns the name of the connector. -func (l *Loader) Name() models.ConnectorProvider { - return name -} - -// AllowTasks returns the amount of tasks that are allowed to be scheduled. -func (l *Loader) AllowTasks() int { - return 10 -} - -const ( - // defaultFilePollingPeriod is the default period between file polling. - defaultFilePollingPeriod = 10 * time.Second -) - -// ApplyDefaults applies default values to the configuration. -func (l *Loader) ApplyDefaults(cfg Config) Config { - if cfg.FilePollingPeriod.Duration == 0 { - cfg.FilePollingPeriod = connectors.Duration{Duration: defaultFilePollingPeriod} - } - - if cfg.Name == "" { - cfg.Name = name.String() - } - - return cfg -} - -// Load returns the connector. -func (l *Loader) Load(logger logging.Logger, config Config) connectors.Connector { - return newConnector(logger, config, newFS()) -} - -// Router returns the router. -func (l *Loader) Router(_ *storage.Storage) *mux.Router { - // Webhooks are not implemented yet - return nil -} - -// NewLoader creates a new loader. -func NewLoader() *Loader { - return &Loader{} -} diff --git a/components/payments/cmd/connectors/internal/connectors/dummypay/loader_test.go b/components/payments/cmd/connectors/internal/connectors/dummypay/loader_test.go deleted file mode 100644 index c8128a1b32..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/dummypay/loader_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package dummypay - -import ( - "context" - "testing" - "time" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/stretchr/testify/assert" -) - -// TestLoader tests the loader. -func TestLoader(t *testing.T) { - t.Parallel() - - config := Config{} - logger := logging.FromContext(context.TODO()) - - loader := NewLoader() - - assert.Equal(t, name, loader.Name()) - assert.Equal(t, 10, loader.AllowTasks()) - assert.Equal(t, Config{ - Name: "DUMMY-PAY", - FilePollingPeriod: connectors.Duration{Duration: 10 * time.Second}, - }, loader.ApplyDefaults(config)) - - assert.EqualValues(t, newConnector(logger, config, newFS()), loader.Load(logger, config)) -} diff --git a/components/payments/cmd/connectors/internal/connectors/dummypay/models.go b/components/payments/cmd/connectors/internal/connectors/dummypay/models.go deleted file mode 100644 index 34fe8a9369..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/dummypay/models.go +++ /dev/null @@ -1,42 +0,0 @@ -package dummypay - -import ( - "math/big" - "time" -) - -type Kind string - -const ( - KindPayment Kind = "payment" - KindAccount Kind = "account" -) - -type object struct { - Kind Kind `json:"kind"` - Payment *payment `json:"payment,omitempty"` - Account *account `json:"account,omitempty"` -} - -// payment represents a payment structure used in the generated files. -type payment struct { - Reference string `json:"reference"` - CreatedAt time.Time `json:"createdAt"` - Amount *big.Int `json:"amount"` - Asset string `json:"asset"` - Type string `json:"type"` - Status string `json:"status"` - Scheme string `json:"scheme"` - SourceAccountID string `json:"sourceAccountId"` - DestinationAccountID string `json:"destinationAccountId"` - Metadata map[string]string `json:"metadata"` -} - -type account struct { - Reference string `json:"reference"` - CreatedAt time.Time `json:"createdAt"` - DefaultAsset string `json:"defaultAsset"` - AccountName string `json:"accountName"` - Type string `json:"type"` - Metadata map[string]string `json:"metadata"` -} diff --git a/components/payments/cmd/connectors/internal/connectors/dummypay/remove_files.go b/components/payments/cmd/connectors/internal/connectors/dummypay/remove_files.go deleted file mode 100644 index 06e2eb1a80..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/dummypay/remove_files.go +++ /dev/null @@ -1,39 +0,0 @@ -package dummypay - -import ( - "fmt" - "os" - "strings" - - "github.com/spf13/afero" -) - -// removeFiles removes all files from the given directory. -// Only removes files that has generatedFilePrefix in the name. -func removeFiles(config Config, fs fs) error { - dir, err := afero.ReadDir(fs, config.Directory) - if err != nil { - if os.IsNotExist(err) { - return nil - } - return fmt.Errorf("failed to open directory '%s': %w", config.Directory, err) - } - - // iterate over all files in the directory - for _, file := range dir { - // skip files that do not match the generatedFilePrefix - if config.PrefixFileToIngest != "" { - if !strings.HasPrefix(file.Name(), generatedFilePrefix) && !strings.HasPrefix(file.Name(), config.PrefixFileToIngest) { - continue - } - } - - // remove the file - err = fs.Remove(fmt.Sprintf("%s/%s", config.Directory, file.Name())) - if err != nil { - return fmt.Errorf("failed to remove file '%s': %w", file.Name(), err) - } - } - - return nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/dummypay/task_descriptor.go b/components/payments/cmd/connectors/internal/connectors/dummypay/task_descriptor.go deleted file mode 100644 index 20cb02be8d..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/dummypay/task_descriptor.go +++ /dev/null @@ -1,34 +0,0 @@ -package dummypay - -import ( - "fmt" - - "github.com/formancehq/payments/cmd/connectors/internal/task" -) - -// taskKey defines a unique key of the task. -type taskKey string - -// TaskDescriptor represents a task descriptor. -type TaskDescriptor struct { - Name string `json:"name" bson:"name" yaml:"name"` - Key taskKey `json:"key" bson:"key" yaml:"key"` - FileName string `json:"fileName" bson:"fileName" yaml:"fileName"` -} - -// handleResolve resolves a task execution request based on the task descriptor. -func handleResolve(config Config, descriptor TaskDescriptor, fs fs) task.Task { - switch descriptor.Key { - case taskKeyReadFiles: - return taskReadFiles(config, fs) - case taskKeyIngest: - return taskIngest(config, descriptor, fs) - case taskKeyInitDirectory: - return taskGenerateFiles(config, fs) - } - - // This should never happen. - return func() error { - return fmt.Errorf("key '%s': %w", descriptor.Key, ErrMissingTask) - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/dummypay/task_ingest.go b/components/payments/cmd/connectors/internal/connectors/dummypay/task_ingest.go deleted file mode 100644 index 34d20f4439..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/dummypay/task_ingest.go +++ /dev/null @@ -1,172 +0,0 @@ -package dummypay - -import ( - "context" - "encoding/json" - "fmt" - "path/filepath" - - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" -) - -const ( - taskKeyIngest = "ingest" -) - -// newTaskIngest returns a new task descriptor for the ingest task. -func newTaskIngest(filePath string) TaskDescriptor { - return TaskDescriptor{ - Name: "Ingest accounts and payments from read files", - Key: taskKeyIngest, - FileName: filePath, - } -} - -// taskIngest ingests a payment file. -func taskIngest(config Config, descriptor TaskDescriptor, fs fs) task.Task { - return func( - ctx context.Context, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - ) error { - err := handleFile(ctx, connectorID, ingester, config, descriptor, fs) - if err != nil { - return fmt.Errorf("failed to handle file '%s': %w", descriptor.FileName, err) - } - - return nil - } -} - -func handleFile( - ctx context.Context, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - config Config, - descriptor TaskDescriptor, - fs fs, -) error { - object, err := getObject(config, descriptor, fs) - if err != nil { - return err - } - - switch object.Kind { - case KindAccount: - batch, err := handleAccount(connectorID, object.Account) - if err != nil { - return err - } - - err = ingester.IngestAccounts(ctx, batch) - if err != nil { - return fmt.Errorf("failed to ingest file '%s': %w", descriptor.FileName, err) - } - case KindPayment: - batch, err := handlePayment(connectorID, object.Payment) - if err != nil { - return err - } - - err = ingester.IngestPayments(ctx, batch) - if err != nil { - return fmt.Errorf("failed to ingest file '%s': %w", descriptor.FileName, err) - } - default: - return fmt.Errorf("unknown object kind '%s'", object.Kind) - } - - return nil -} - -func getObject(config Config, descriptor TaskDescriptor, fs fs) (*object, error) { - file, err := fs.Open(filepath.Join(config.Directory, descriptor.FileName)) - if err != nil { - return nil, fmt.Errorf("failed to open file '%s': %w", descriptor.FileName, err) - } - defer file.Close() - - var objectElement object - err = json.NewDecoder(file).Decode(&objectElement) - if err != nil { - return nil, fmt.Errorf("failed to decode file '%s': %w", descriptor.FileName, err) - } - - return &objectElement, nil -} - -func handleAccount(connectorID models.ConnectorID, accountElement *account) (ingestion.AccountBatch, error) { - accountType, err := models.AccountTypeFromString(accountElement.Type) - if err != nil { - return nil, fmt.Errorf("failed to parse account type: %w", err) - } - - raw, err := json.Marshal(accountElement) - if err != nil { - return nil, fmt.Errorf("failed to marshal payment: %w", err) - } - - ingestionPayload := ingestion.AccountBatch{&models.Account{ - ID: models.AccountID{ - Reference: accountElement.Reference, - ConnectorID: connectorID, - }, - ConnectorID: connectorID, - CreatedAt: accountElement.CreatedAt, - Reference: accountElement.Reference, - DefaultAsset: models.Asset(accountElement.DefaultAsset), - AccountName: accountElement.AccountName, - Type: accountType, - Metadata: map[string]string{}, - RawData: raw, - }} - - return ingestionPayload, nil -} - -func handlePayment(connectorID models.ConnectorID, paymentElement *payment) (ingestion.PaymentBatch, error) { - paymentType, err := models.PaymentTypeFromString(paymentElement.Type) - if err != nil { - return nil, fmt.Errorf("failed to parse payment type '%s': %w", paymentElement.Type, err) - } - - paymentStatus, err := models.PaymentStatusFromString(paymentElement.Status) - if err != nil { - return nil, fmt.Errorf("failed to parse payment status '%s': %w", paymentElement.Status, err) - } - - paymentScheme, err := models.PaymentSchemeFromString(paymentElement.Scheme) - if err != nil { - return nil, fmt.Errorf("failed to parse payment scheme '%s': %w", paymentElement.Scheme, err) - } - - raw, err := json.Marshal(paymentElement) - if err != nil { - return nil, fmt.Errorf("failed to marshal payment: %w", err) - } - - ingestionPayload := ingestion.PaymentBatch{ingestion.PaymentBatchElement{ - Payment: &models.Payment{ - ID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: paymentElement.Reference, - Type: paymentType, - }, - ConnectorID: connectorID, - }, - Reference: paymentElement.Reference, - ConnectorID: connectorID, - Amount: paymentElement.Amount, - InitialAmount: paymentElement.Amount, - Type: paymentType, - Status: paymentStatus, - Scheme: paymentScheme, - Asset: models.Asset(paymentElement.Asset), - RawData: raw, - }, - }} - - return ingestionPayload, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/dummypay/task_init_directory.go b/components/payments/cmd/connectors/internal/connectors/dummypay/task_init_directory.go deleted file mode 100644 index a8c93782c4..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/dummypay/task_init_directory.go +++ /dev/null @@ -1,302 +0,0 @@ -package dummypay - -import ( - "context" - "encoding/json" - "fmt" - "math/big" - "math/rand" - "os" - "time" - - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" -) - -const ( - taskKeyInitDirectory = "init-directory" - asset = "DUMMYCOIN" - generatedFilePrefix = "dummypay-generated-file" -) - -// newTaskGenerateFiles returns a new task descriptor for the task that generates files. -func newTaskGenerateFiles() TaskDescriptor { - return TaskDescriptor{ - Name: "Generate files into a directory", - Key: taskKeyInitDirectory, - } -} - -// taskGenerateFiles generates payment files to a given directory. -func taskGenerateFiles(config Config, fs fs) task.Task { - return func(ctx context.Context, ingester ingestion.Ingester, connectorID models.ConnectorID) error { - err := fs.Mkdir(config.Directory, 0o777) //nolint:gomnd - if err != nil && !os.IsExist(err) { - return fmt.Errorf( - "failed to create dummypay config directory '%s': %w", config.Directory, err) - } - - var accountIDs []*models.AccountID - for i := 0; i < config.NumberOfAccountsPreGenerated; i++ { - accountID, err := generateAccountsFile(ctx, connectorID, ingester, config, fs) - if err != nil { - return fmt.Errorf("failed to generate accounts file: %w", err) - } - - accountIDs = append(accountIDs, accountID) - } - - for i := 0; i < config.NumberOfPaymentsPreGenerated; i++ { - if err := generatePaymentsFile(ctx, generatedFilePrefix, connectorID, ingester, accountIDs, config, fs); err != nil { - return fmt.Errorf("failed to generate payments files: %w", err) - } - } - - return nil - } -} - -func generateAccountsFile( - ctx context.Context, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - config Config, - fs fs, -) (*models.AccountID, error) { - name := fmt.Sprintf("account-%d", time.Now().UnixNano()) - key := fmt.Sprintf("%s-%s", generatedFilePrefix, name) - fileKey := fmt.Sprintf("%s/%s.json", config.Directory, key) - - generatedAccount := account{ - Reference: uuid.New().String(), - CreatedAt: time.Now(), - DefaultAsset: asset, - AccountName: name, - Type: generateRandomAccountType().String(), - Metadata: map[string]string{}, - } - - file, err := fs.Create(fileKey) - if err != nil { - return nil, fmt.Errorf("failed to create file: %w", err) - } - defer file.Close() - - // Encode the payment object as JSON to a new file. - err = json.NewEncoder(file).Encode(&object{ - Kind: KindAccount, - Account: &generatedAccount, - }) - if err != nil { - return nil, fmt.Errorf("failed to encode json into file: %w", err) - } - - raw, err := json.Marshal(generatedAccount) - if err != nil { - return nil, fmt.Errorf("failed to marshal account: %w", err) - } - - accountID := models.AccountID{ - Reference: generatedAccount.Reference, - ConnectorID: connectorID, - } - if err := ingester.IngestAccounts(ctx, ingestion.AccountBatch{ - { - ID: accountID, - ConnectorID: connectorID, - CreatedAt: generatedAccount.CreatedAt, - Reference: generatedAccount.Reference, - DefaultAsset: asset, - AccountName: name, - Type: models.AccountType(generatedAccount.Type), - Metadata: map[string]string{}, - RawData: raw, - }, - }); err != nil { - return nil, fmt.Errorf("failed to ingest accounts: %w", err) - } - - return &accountID, nil -} - -func generatePaymentsFile( - ctx context.Context, - prefix string, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - accountIDs []*models.AccountID, - config Config, - fs fs, -) error { - name := fmt.Sprintf("payment-%d", time.Now().UnixNano()) - key := name - if prefix != "" { - key = fmt.Sprintf("%s-%s", generatedFilePrefix, name) - } - fileKey := fmt.Sprintf("%s/%s.json", config.Directory, key) - - generatedPayment := payment{ - Reference: uuid.New().String(), - CreatedAt: time.Now(), - Amount: big.NewInt(int64(rand.Intn(10000))), - Asset: asset, - Type: generateRandomPaymentType().String(), - Status: generateRandomStatus().String(), - Scheme: generateRandomScheme().String(), - Metadata: map[string]string{}, - } - - var sourceAccountID, destinationAccountID *models.AccountID - if len(accountIDs) != 0 { - if generateRandomNumber() > nMax/2 { - sourceAccountID = accountIDs[generateRandomNumber()%len(accountIDs)] - generatedPayment.SourceAccountID = sourceAccountID.String() - } else { - destinationAccountID = accountIDs[generateRandomNumber()%len(accountIDs)] - generatedPayment.DestinationAccountID = destinationAccountID.String() - } - } - - file, err := fs.Create(fileKey) - if err != nil { - return fmt.Errorf("failed to create file: %w", err) - } - defer file.Close() - - // Encode the payment object as JSON to a new file. - err = json.NewEncoder(file).Encode(&object{ - Kind: KindPayment, - Payment: &generatedPayment, - }) - if err != nil { - return fmt.Errorf("failed to encode json into file: %w", err) - } - - raw, err := json.Marshal(generatedPayment) - if err != nil { - return fmt.Errorf("failed to marshal payment: %w", err) - } - if err := ingester.IngestPayments(ctx, ingestion.PaymentBatch{ - { - Payment: &models.Payment{ - ID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: generatedPayment.Reference, - Type: models.PaymentType(generatedPayment.Type), - }, - ConnectorID: connectorID, - }, - ConnectorID: connectorID, - CreatedAt: generatedPayment.CreatedAt, - Reference: generatedPayment.Reference, - Amount: generatedPayment.Amount, - InitialAmount: generatedPayment.Amount, - Type: models.PaymentType(generatedPayment.Type), - Status: models.PaymentStatus(generatedPayment.Status), - Scheme: models.PaymentScheme(generatedPayment.Scheme), - Asset: asset, - RawData: raw, - SourceAccountID: sourceAccountID, - DestinationAccountID: destinationAccountID, - }, - }, - }); err != nil { - return fmt.Errorf("failed to ingest payments: %w", err) - } - - return nil -} - -// nMax is the maximum number that can be generated -// with the minimum being 0. -const nMax = 10000 - -func generateRandomAccountType() models.AccountType { - // 50% chance. - accountType := models.AccountTypeInternal - - // 50% chance. - if generateRandomNumber() > nMax/2 { - accountType = models.AccountTypeExternal - } - - return accountType -} - -// generateRandomNumber generates a random number between 0 and nMax. -func generateRandomNumber() int { - rand.Seed(time.Now().UnixNano()) - - //nolint:gosec // allow weak random number generator as it is not used for security - value := rand.Intn(nMax) - - return value -} - -// generateRandomType generates a random payment type. -func generateRandomPaymentType() models.PaymentType { - paymentType := models.PaymentTypePayIn - - num := generateRandomNumber() - switch { - case num < nMax/4: // 25% chance - paymentType = models.PaymentTypePayOut - case num < nMax/3: // ~9% chance - paymentType = models.PaymentTypeTransfer - } - - return paymentType -} - -// generateRandomStatus generates a random payment status. -func generateRandomStatus() models.PaymentStatus { - // ~50% chance. - paymentStatus := models.PaymentStatusSucceeded - - num := generateRandomNumber() - - switch { - case num < nMax/4: // 25% chance - paymentStatus = models.PaymentStatusPending - case num < nMax/3: // ~9% chance - paymentStatus = models.PaymentStatusFailed - case num < nMax/2: // ~16% chance - paymentStatus = models.PaymentStatusCancelled - } - - return paymentStatus -} - -// generateRandomScheme generates a random payment scheme. -func generateRandomScheme() models.PaymentScheme { - num := generateRandomNumber() / 1000 //nolint:gomnd // allow for random number - - paymentScheme := models.PaymentSchemeCardMasterCard - - availableSchemes := []models.PaymentScheme{ - models.PaymentSchemeCardMasterCard, - models.PaymentSchemeCardVisa, - models.PaymentSchemeCardDiscover, - models.PaymentSchemeCardJCB, - models.PaymentSchemeCardUnionPay, - models.PaymentSchemeCardAmex, - models.PaymentSchemeCardDiners, - models.PaymentSchemeSepaDebit, - models.PaymentSchemeSepaCredit, - models.PaymentSchemeApplePay, - models.PaymentSchemeGooglePay, - models.PaymentSchemeA2A, - models.PaymentSchemeACHDebit, - models.PaymentSchemeACH, - models.PaymentSchemeRTP, - } - - if num < len(availableSchemes) { - paymentScheme = availableSchemes[num] - } - - return paymentScheme -} diff --git a/components/payments/cmd/connectors/internal/connectors/dummypay/task_read_files.go b/components/payments/cmd/connectors/internal/connectors/dummypay/task_read_files.go deleted file mode 100644 index 88f30ca5e3..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/dummypay/task_read_files.go +++ /dev/null @@ -1,92 +0,0 @@ -package dummypay - -import ( - "context" - "fmt" - "os" - "strings" - - "github.com/formancehq/payments/internal/models" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/spf13/afero" -) - -const ( - taskKeyReadFiles = "read-files" -) - -// newTaskReadFiles creates a new task descriptor for the taskReadFiles task. -func newTaskReadFiles() TaskDescriptor { - return TaskDescriptor{ - Name: "Read Files from directory", - Key: taskKeyReadFiles, - } -} - -// taskReadFiles creates a task that reads files from a given directory. -// Only reads files with the generatedFilePrefix in their name. -func taskReadFiles(config Config, fs fs) task.Task { - return func(ctx context.Context, logger logging.Logger, - scheduler task.Scheduler, - ) error { - err := fs.Mkdir(config.Directory, 0o777) //nolint:gomnd - if err != nil && !os.IsExist(err) { - return fmt.Errorf( - "failed to create dummypay config directory '%s': %w", config.Directory, err) - } - - files, err := parseFilesToIngest(config, fs) - if err != nil { - return fmt.Errorf("error parsing files to ingest: %w", err) - } - - for _, file := range files { - descriptor, err := models.EncodeTaskDescriptor(newTaskIngest(file)) - if err != nil { - return err - } - - // schedule a task to ingest the file into the payments system. - err = scheduler.Schedule(ctx, descriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_NEVER, - }) - if err != nil { - return fmt.Errorf("failed to schedule task to ingest file '%s': %w", file, err) - } - - } - - return nil - } -} - -func parseFilesToIngest(config Config, fs fs) ([]string, error) { - dir, err := afero.ReadDir(fs, config.Directory) - if err != nil { - return nil, fmt.Errorf("error reading directory '%s': %w", config.Directory, err) - } - - var files []string //nolint:prealloc // length is unknown - - // iterate over all files in the directory. - for _, file := range dir { - // skip files that match the generatedFilePrefix because they were already ingested. - if strings.HasPrefix(file.Name(), generatedFilePrefix) { - continue - } - - if config.PrefixFileToIngest != "" { - // skip files that do not match the toIngestFilePrefix. - if !strings.HasPrefix(file.Name(), config.PrefixFileToIngest) { - continue - } - } - - files = append(files, file.Name()) - } - - return files, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/dummypay/task_test.go b/components/payments/cmd/connectors/internal/connectors/dummypay/task_test.go deleted file mode 100644 index f7b79a4413..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/dummypay/task_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package dummypay - -import ( - "context" - "testing" - - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/spf13/afero" - "github.com/stretchr/testify/assert" -) - -func TestTasks(t *testing.T) { - t.Parallel() - - config := Config{Directory: "/tmp"} - fs := newTestFS() - - // test generating files - err := generatePaymentsFile(context.Background(), "", models.ConnectorID{}, &MockIngester{}, []*models.AccountID{}, config, fs) - assert.NoError(t, err) - - files, err := afero.ReadDir(fs, config.Directory) - assert.NoError(t, err) - assert.Len(t, files, 1) - - // test reading files - filesList, err := parseFilesToIngest(config, fs) - assert.NoError(t, err) - assert.Len(t, filesList, 1) - - // test getting object - object, err := getObject(config, TaskDescriptor{Key: taskKeyIngest, FileName: files[0].Name()}, fs) - assert.NoError(t, err) - assert.NotNil(t, object) - assert.NotNil(t, object.Payment) - - // test ingesting files - payload, err := handlePayment(models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - }, object.Payment) - assert.NoError(t, err) - assert.Len(t, payload, 1) - - // test removing files - err = removeFiles(config, fs) - assert.NoError(t, err) - - files, err = afero.ReadDir(fs, config.Directory) - assert.NoError(t, err) - assert.Len(t, files, 0) -} diff --git a/components/payments/cmd/connectors/internal/connectors/duration.go b/components/payments/cmd/connectors/internal/connectors/duration.go deleted file mode 100644 index c6e5ee31d1..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/duration.go +++ /dev/null @@ -1,45 +0,0 @@ -package connectors - -import ( - "encoding/json" - "fmt" - "time" -) - -type Duration struct { - time.Duration `json:"duration"` -} - -func (d *Duration) MarshalJSON() ([]byte, error) { - return json.Marshal(d.Duration.String()) -} - -func (d *Duration) UnmarshalJSON(b []byte) error { - var rawValue any - - if err := json.Unmarshal(b, &rawValue); err != nil { - return fmt.Errorf("custom Duration UnmarshalJSON: %w", err) - } - - switch value := rawValue.(type) { - case string: - var err error - d.Duration, err = time.ParseDuration(value) - if err != nil { - return fmt.Errorf("custom Duration UnmarshalJSON: time.ParseDuration: %w", err) - } - - return nil - case map[string]interface{}: - switch val := value["duration"].(type) { - case float64: - d.Duration = time.Duration(int64(val)) - - return nil - default: - return fmt.Errorf("custom Duration UnmarshalJSON from map: invalid type: value:%v, type:%T", val, val) - } - default: - return fmt.Errorf("custom Duration UnmarshalJSON: invalid type: value:%v, type:%T", value, value) - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/errors.go b/components/payments/cmd/connectors/internal/connectors/errors.go deleted file mode 100644 index 2f4b28d47e..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/errors.go +++ /dev/null @@ -1,8 +0,0 @@ -package connectors - -import "errors" - -var ( - ErrNotImplemented = errors.New("not implemented") - ErrInvalidConfig = errors.New("invalid config") -) diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/accounts.go b/components/payments/cmd/connectors/internal/connectors/generic/client/accounts.go deleted file mode 100644 index b2cc74ffdc..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/accounts.go +++ /dev/null @@ -1,27 +0,0 @@ -package client - -import ( - "context" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/genericclient" -) - -func (c *Client) ListAccounts(ctx context.Context, page, pageSize int64) ([]genericclient.Account, error) { - f := connectors.ClientMetrics(ctx, "generic", "list_accounts") - now := time.Now() - defer f(ctx, now) - - req := c.apiClient.DefaultApi. - GetAccounts(ctx). - Page(page). - PageSize(pageSize) - - accounts, _, err := req.Execute() - if err != nil { - return nil, err - } - - return accounts, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/balances.go b/components/payments/cmd/connectors/internal/connectors/generic/client/balances.go deleted file mode 100644 index ce5f085b0c..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/balances.go +++ /dev/null @@ -1,24 +0,0 @@ -package client - -import ( - "context" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/genericclient" -) - -func (c *Client) GetBalances(ctx context.Context, accountID string) (*genericclient.Balances, error) { - f := connectors.ClientMetrics(ctx, "generic", "get_balance") - now := time.Now() - defer f(ctx, now) - - req := c.apiClient.DefaultApi.GetAccountBalances(ctx, accountID) - - balances, _, err := req.Execute() - if err != nil { - return nil, err - } - - return balances, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/beneficiaries.go b/components/payments/cmd/connectors/internal/connectors/generic/client/beneficiaries.go deleted file mode 100644 index 5e3c2196b0..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/beneficiaries.go +++ /dev/null @@ -1,31 +0,0 @@ -package client - -import ( - "context" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/genericclient" -) - -func (c *Client) ListBeneficiaries(ctx context.Context, page, pageSize int64, createdAtFrom time.Time) ([]genericclient.Beneficiary, error) { - f := connectors.ClientMetrics(ctx, "generic", "list_beneficiaries") - now := time.Now() - defer f(ctx, now) - - req := c.apiClient.DefaultApi. - GetBeneficiaries(ctx). - Page(page). - PageSize(pageSize) - - if !createdAtFrom.IsZero() { - req = req.CreatedAtFrom(createdAtFrom) - } - - beneficiaries, _, err := req.Execute() - if err != nil { - return nil, err - } - - return beneficiaries, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/client.go b/components/payments/cmd/connectors/internal/connectors/generic/client/client.go deleted file mode 100644 index 7388a96d68..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/client.go +++ /dev/null @@ -1,44 +0,0 @@ -package client - -import ( - "fmt" - "net/http" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/genericclient" - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" -) - -type apiTransport struct { - APIKey string - underlying http.RoundTripper -} - -func (t *apiTransport) RoundTrip(req *http.Request) (*http.Response, error) { - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", t.APIKey)) - - return t.underlying.RoundTrip(req) -} - -type Client struct { - apiClient *genericclient.APIClient -} - -func NewClient(apiKey, baseURL string, logger logging.Logger) *Client { - httpClient := &http.Client{ - Transport: &apiTransport{ - APIKey: apiKey, - underlying: otelhttp.NewTransport(http.DefaultTransport), - }, - } - - configuration := genericclient.NewConfiguration() - configuration.HTTPClient = httpClient - configuration.Servers[0].URL = baseURL - - genericClient := genericclient.NewAPIClient(configuration) - - return &Client{ - apiClient: genericClient, - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/.gitignore b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/.gitignore deleted file mode 100644 index daf913b1b3..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Compiled Object files, Static and Dynamic libs (Shared Objects) -*.o -*.a -*.so - -# Folders -_obj -_test - -# Architecture specific extensions/prefixes -*.[568vq] -[568vq].out - -*.cgo1.go -*.cgo2.c -_cgo_defun.c -_cgo_gotypes.go -_cgo_export.* - -_testmain.go - -*.exe -*.test -*.prof diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/.openapi-generator-ignore b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/.openapi-generator-ignore deleted file mode 100644 index 7484ee590a..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/.openapi-generator-ignore +++ /dev/null @@ -1,23 +0,0 @@ -# OpenAPI Generator Ignore -# Generated by openapi-generator https://github.com/openapitools/openapi-generator - -# Use this file to prevent files from being overwritten by the generator. -# The patterns follow closely to .gitignore or .dockerignore. - -# As an example, the C# client generator defines ApiClient.cs. -# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: -#ApiClient.cs - -# You can match any string of characters against a directory, file or extension with a single asterisk (*): -#foo/*/qux -# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux - -# You can recursively match patterns against a directory, file or extension with a double asterisk (**): -#foo/**/qux -# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux - -# You can also negate patterns with an exclamation (!). -# For example, you can ignore all files in a docs folder with the file extension .md: -#docs/*.md -# Then explicitly reverse the ignore rule for a single file: -#!docs/README.md diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/.openapi-generator/FILES b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/.openapi-generator/FILES deleted file mode 100644 index ba9823d79a..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/.openapi-generator/FILES +++ /dev/null @@ -1,31 +0,0 @@ -.gitignore -.openapi-generator-ignore -.travis.yml -README.md -api/openapi.yaml -api_default.go -client.go -configuration.go -docs/Account.md -docs/Balance.md -docs/Balances.md -docs/Beneficiary.md -docs/DefaultApi.md -docs/Error.md -docs/Transaction.md -docs/TransactionStatus.md -docs/TransactionType.md -git_push.sh -go.mod -go.sum -model_account.go -model_balance.go -model_balances.go -model_beneficiary.go -model_error.go -model_transaction.go -model_transaction_status.go -model_transaction_type.go -response.go -test/api_default_test.go -utils.go diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/.openapi-generator/VERSION b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/.openapi-generator/VERSION deleted file mode 100644 index cd802a1ec4..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/.openapi-generator/VERSION +++ /dev/null @@ -1 +0,0 @@ -6.6.0 \ No newline at end of file diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/.travis.yml b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/.travis.yml deleted file mode 100644 index f5cb2ce9a5..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: go - -install: - - go get -d -v . - -script: - - go build -v ./ - diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/README.md b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/README.md deleted file mode 100644 index b67617e452..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/README.md +++ /dev/null @@ -1,122 +0,0 @@ -# Go API client for genericclient - -No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - -## Overview -This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [OpenAPI-spec](https://www.openapis.org/) from a remote server, you can easily generate an API client. - -- API version: v0.1 -- Package version: latest -- Build package: org.openapitools.codegen.languages.GoClientCodegen - -## Installation - -Install the following dependencies: - -```shell -go get github.com/stretchr/testify/assert -go get golang.org/x/net/context -``` - -Put the package under your project folder and add the following in import: - -```golang -import genericclient "github.com/formancehq/payments/genericclient" -``` - -To use a proxy, set the environment variable `HTTP_PROXY`: - -```golang -os.Setenv("HTTP_PROXY", "http://proxy_name:proxy_port") -``` - -## Configuration of Server URL - -Default configuration comes with `Servers` field that contains server objects as defined in the OpenAPI specification. - -### Select Server Configuration - -For using other server than the one defined on index 0 set context value `sw.ContextServerIndex` of type `int`. - -```golang -ctx := context.WithValue(context.Background(), genericclient.ContextServerIndex, 1) -``` - -### Templated Server URL - -Templated server URL is formatted using default variables from configuration or from context value `sw.ContextServerVariables` of type `map[string]string`. - -```golang -ctx := context.WithValue(context.Background(), genericclient.ContextServerVariables, map[string]string{ - "basePath": "v2", -}) -``` - -Note, enum values are always validated and all unused variables are silently ignored. - -### URLs Configuration per Operation - -Each operation can use different server URL defined using `OperationServers` map in the `Configuration`. -An operation is uniquely identified by `"{classname}Service.{nickname}"` string. -Similar rules for overriding default operation server index and variables applies by using `sw.ContextOperationServerIndices` and `sw.ContextOperationServerVariables` context maps. - -```golang -ctx := context.WithValue(context.Background(), genericclient.ContextOperationServerIndices, map[string]int{ - "{classname}Service.{nickname}": 2, -}) -ctx = context.WithValue(context.Background(), genericclient.ContextOperationServerVariables, map[string]map[string]string{ - "{classname}Service.{nickname}": { - "port": "8443", - }, -}) -``` - -## Documentation for API Endpoints - -All URIs are relative to *http://localhost* - -Class | Method | HTTP request | Description ------------- | ------------- | ------------- | ------------- -*DefaultApi* | [**GetAccountBalances**](docs/DefaultApi.md#getaccountbalances) | **Get** /accounts/{accountId}/balances | Get account balance -*DefaultApi* | [**GetAccounts**](docs/DefaultApi.md#getaccounts) | **Get** /accounts | Get all accounts -*DefaultApi* | [**GetBeneficiaries**](docs/DefaultApi.md#getbeneficiaries) | **Get** /beneficiaries | Get all beneficiaries -*DefaultApi* | [**GetTransactions**](docs/DefaultApi.md#gettransactions) | **Get** /transactions | Get all transactions - - -## Documentation For Models - - - [Account](docs/Account.md) - - [Balance](docs/Balance.md) - - [Balances](docs/Balances.md) - - [Beneficiary](docs/Beneficiary.md) - - [Error](docs/Error.md) - - [Transaction](docs/Transaction.md) - - [TransactionStatus](docs/TransactionStatus.md) - - [TransactionType](docs/TransactionType.md) - - -## Documentation For Authorization - -Endpoints do not require authorization. - - -## Documentation for Utility Methods - -Due to the fact that model structure members are all pointers, this package contains -a number of utility functions to easily obtain pointers to values of basic types. -Each of these functions takes a value of the given basic type and returns a pointer to it: - -* `PtrBool` -* `PtrInt` -* `PtrInt32` -* `PtrInt64` -* `PtrFloat` -* `PtrFloat32` -* `PtrFloat64` -* `PtrString` -* `PtrTime` - -## Author - - - diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/api/openapi.yaml b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/api/openapi.yaml deleted file mode 100644 index 7f79fb8276..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/api/openapi.yaml +++ /dev/null @@ -1,496 +0,0 @@ -openapi: 3.0.3 -info: - title: GENERIC connector API - version: v0.1 -servers: -- url: / -paths: - /accounts: - get: - operationId: getAccounts - parameters: - - description: Number of items per page - example: 100 - explode: true - in: query - name: pageSize - required: false - schema: - default: 100 - format: int64 - minimum: 1 - type: integer - style: form - - description: Page number - example: 1 - explode: true - in: query - name: page - required: false - schema: - default: 1 - format: int64 - minimum: 1 - type: integer - style: form - - description: Sort order - example: createdAt:asc - explode: true - in: query - name: sort - required: false - schema: - type: string - style: form - - description: Filter by created at date - explode: true - in: query - name: createdAtFrom - required: false - schema: - format: date-time - type: string - style: form - responses: - "200": - content: - application/json: - schema: - items: - $ref: '#/components/schemas/Account' - type: array - description: List of accounts - default: - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - description: General error - summary: Get all accounts - /accounts/{accountId}/balances: - get: - operationId: getAccountBalances - parameters: - - explode: false - in: path - name: accountId - required: true - schema: - type: string - style: simple - responses: - "200": - content: - application/json: - schema: - $ref: '#/components/schemas/Balances' - description: Account balances - default: - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - description: General error - summary: Get account balance - /beneficiaries: - get: - operationId: getBeneficiaries - parameters: - - description: Number of items per page - example: 100 - explode: true - in: query - name: pageSize - required: false - schema: - default: 100 - format: int64 - minimum: 1 - type: integer - style: form - - description: Page number - example: 1 - explode: true - in: query - name: page - required: false - schema: - default: 1 - format: int64 - minimum: 1 - type: integer - style: form - - description: Sort order - example: createdAt:asc - explode: true - in: query - name: sort - required: false - schema: - type: string - style: form - - description: Filter by created at date - explode: true - in: query - name: createdAtFrom - required: false - schema: - format: date-time - type: string - style: form - responses: - "200": - content: - application/json: - schema: - items: - $ref: '#/components/schemas/Beneficiary' - type: array - description: List of beneficiaries - default: - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - description: General error - summary: Get all beneficiaries - /transactions: - get: - operationId: getTransactions - parameters: - - description: Number of items per page - example: 100 - explode: true - in: query - name: pageSize - required: false - schema: - default: 100 - format: int64 - minimum: 1 - type: integer - style: form - - description: Page number - example: 1 - explode: true - in: query - name: page - required: false - schema: - default: 1 - format: int64 - minimum: 1 - type: integer - style: form - - description: Sort order - example: createdAt:asc - explode: true - in: query - name: sort - required: false - schema: - type: string - style: form - - description: Filter by updated at date - explode: true - in: query - name: updatedAtFrom - required: false - schema: - format: date-time - type: string - style: form - responses: - "200": - content: - application/json: - schema: - items: - $ref: '#/components/schemas/Transaction' - type: array - description: List of transactions - default: - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - description: General error - summary: Get all transactions -components: - parameters: - AccountId: - explode: false - in: path - name: accountId - required: true - schema: - type: string - style: simple - PageSize: - description: Number of items per page - example: 100 - explode: true - in: query - name: pageSize - required: false - schema: - default: 100 - format: int64 - minimum: 1 - type: integer - style: form - Page: - description: Page number - example: 1 - explode: true - in: query - name: page - required: false - schema: - default: 1 - format: int64 - minimum: 1 - type: integer - style: form - Sort: - description: Sort order - example: createdAt:asc - explode: true - in: query - name: sort - required: false - schema: - type: string - style: form - CreatedAtFrom: - description: Filter by created at date - explode: true - in: query - name: createdAtFrom - required: false - schema: - format: date-time - type: string - style: form - UpdatedAtFrom: - description: Filter by updated at date - explode: true - in: query - name: updatedAtFrom - required: false - schema: - format: date-time - type: string - style: form - responses: - NoContent: - description: No content - ErrorResponse: - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - description: General error - Accounts: - content: - application/json: - schema: - items: - $ref: '#/components/schemas/Account' - type: array - description: List of accounts - Balances: - content: - application/json: - schema: - $ref: '#/components/schemas/Balances' - description: Account balances - Beneficiaries: - content: - application/json: - schema: - items: - $ref: '#/components/schemas/Beneficiary' - type: array - description: List of beneficiaries - Transactions: - content: - application/json: - schema: - items: - $ref: '#/components/schemas/Transaction' - type: array - description: List of transactions - schemas: - Error: - properties: - Title: - type: string - Detail: - type: string - required: - - Detail - - Title - type: object - Account: - example: - createdAt: 2000-01-23T04:56:07.000+00:00 - metadata: - key: metadata - accountName: accountName - id: id - properties: - id: - type: string - accountName: - type: string - createdAt: - format: date-time - type: string - metadata: - additionalProperties: - type: string - nullable: true - type: object - required: - - accountName - - createdAt - - id - type: object - Balances: - example: - accountID: accountID - balances: - - amount: amount - currency: currency - - amount: amount - currency: currency - at: 2000-01-23T04:56:07.000+00:00 - id: id - properties: - id: - type: string - accountID: - type: string - at: - format: date-time - type: string - balances: - items: - $ref: '#/components/schemas/Balance' - type: array - required: - - accountID - - at - - balances - - id - type: object - Balance: - example: - amount: amount - currency: currency - properties: - amount: - type: string - currency: - type: string - required: - - amount - - currency - type: object - Beneficiary: - example: - createdAt: 2000-01-23T04:56:07.000+00:00 - metadata: - key: metadata - ownerName: ownerName - id: id - properties: - id: - type: string - createdAt: - format: date-time - type: string - ownerName: - type: string - metadata: - additionalProperties: - type: string - nullable: true - type: object - required: - - createdAt - - id - - ownerName - type: object - Transaction: - example: - createdAt: 2000-01-23T04:56:07.000+00:00 - amount: amount - sourceAccountID: sourceAccountID - metadata: - key: metadata - scheme: scheme - currency: currency - id: id - relatedTransactionID: relatedTransactionID - type: null - destinationAccountID: destinationAccountID - updatedAt: 2000-01-23T04:56:07.000+00:00 - status: null - properties: - id: - type: string - relatedTransactionID: - type: string - createdAt: - format: date-time - type: string - updatedAt: - format: date-time - type: string - currency: - type: string - scheme: - type: string - type: - $ref: '#/components/schemas/TransactionType' - status: - $ref: '#/components/schemas/TransactionStatus' - amount: - type: string - sourceAccountID: - type: string - destinationAccountID: - type: string - metadata: - additionalProperties: - type: string - nullable: true - type: object - required: - - amount - - createdAt - - currency - - id - - status - - type - - updatedAt - type: object - Metadata: - additionalProperties: - type: string - nullable: true - type: object - TransactionType: - enum: - - PAYIN - - PAYOUT - - TRANSFER - type: string - TransactionStatus: - enum: - - PENDING - - SUCCEEDED - - FAILED - type: string diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/api_default.go b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/api_default.go deleted file mode 100644 index e196970c4d..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/api_default.go +++ /dev/null @@ -1,569 +0,0 @@ -/* -GENERIC connector API - -No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - -API version: v0.1 -*/ - -// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. - -package genericclient - -import ( - "bytes" - "context" - "io" - "net/http" - "net/url" - "strings" - "time" -) - - -// DefaultApiService DefaultApi service -type DefaultApiService service - -type ApiGetAccountBalancesRequest struct { - ctx context.Context - ApiService *DefaultApiService - accountId string -} - -func (r ApiGetAccountBalancesRequest) Execute() (*Balances, *http.Response, error) { - return r.ApiService.GetAccountBalancesExecute(r) -} - -/* -GetAccountBalances Get account balance - - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param accountId - @return ApiGetAccountBalancesRequest -*/ -func (a *DefaultApiService) GetAccountBalances(ctx context.Context, accountId string) ApiGetAccountBalancesRequest { - return ApiGetAccountBalancesRequest{ - ApiService: a, - ctx: ctx, - accountId: accountId, - } -} - -// Execute executes the request -// @return Balances -func (a *DefaultApiService) GetAccountBalancesExecute(r ApiGetAccountBalancesRequest) (*Balances, *http.Response, error) { - var ( - localVarHTTPMethod = http.MethodGet - localVarPostBody interface{} - formFiles []formFile - localVarReturnValue *Balances - ) - - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "DefaultApiService.GetAccountBalances") - if err != nil { - return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} - } - - localVarPath := localBasePath + "/accounts/{accountId}/balances" - localVarPath = strings.Replace(localVarPath, "{"+"accountId"+"}", url.PathEscape(parameterValueToString(r.accountId, "accountId")), -1) - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} - localVarFormParams := url.Values{} - - // to determine the Content-Type header - localVarHTTPContentTypes := []string{} - - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType - } - - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} - - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept - } - req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) - if err != nil { - return localVarReturnValue, nil, err - } - - localVarHTTPResponse, err := a.client.callAPI(req) - if err != nil || localVarHTTPResponse == nil { - return localVarReturnValue, localVarHTTPResponse, err - } - - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) - localVarHTTPResponse.Body.Close() - localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) - if err != nil { - return localVarReturnValue, localVarHTTPResponse, err - } - - if localVarHTTPResponse.StatusCode >= 300 { - newErr := &GenericOpenAPIError{ - body: localVarBody, - error: localVarHTTPResponse.Status, - } - var v Error - err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) - if err != nil { - newErr.error = err.Error() - return localVarReturnValue, localVarHTTPResponse, newErr - } - newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) - newErr.model = v - return localVarReturnValue, localVarHTTPResponse, newErr - } - - err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) - if err != nil { - newErr := &GenericOpenAPIError{ - body: localVarBody, - error: err.Error(), - } - return localVarReturnValue, localVarHTTPResponse, newErr - } - - return localVarReturnValue, localVarHTTPResponse, nil -} - -type ApiGetAccountsRequest struct { - ctx context.Context - ApiService *DefaultApiService - pageSize *int64 - page *int64 - sort *string - createdAtFrom *time.Time -} - -// Number of items per page -func (r ApiGetAccountsRequest) PageSize(pageSize int64) ApiGetAccountsRequest { - r.pageSize = &pageSize - return r -} - -// Page number -func (r ApiGetAccountsRequest) Page(page int64) ApiGetAccountsRequest { - r.page = &page - return r -} - -// Sort order -func (r ApiGetAccountsRequest) Sort(sort string) ApiGetAccountsRequest { - r.sort = &sort - return r -} - -// Filter by created at date -func (r ApiGetAccountsRequest) CreatedAtFrom(createdAtFrom time.Time) ApiGetAccountsRequest { - r.createdAtFrom = &createdAtFrom - return r -} - -func (r ApiGetAccountsRequest) Execute() ([]Account, *http.Response, error) { - return r.ApiService.GetAccountsExecute(r) -} - -/* -GetAccounts Get all accounts - - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @return ApiGetAccountsRequest -*/ -func (a *DefaultApiService) GetAccounts(ctx context.Context) ApiGetAccountsRequest { - return ApiGetAccountsRequest{ - ApiService: a, - ctx: ctx, - } -} - -// Execute executes the request -// @return []Account -func (a *DefaultApiService) GetAccountsExecute(r ApiGetAccountsRequest) ([]Account, *http.Response, error) { - var ( - localVarHTTPMethod = http.MethodGet - localVarPostBody interface{} - formFiles []formFile - localVarReturnValue []Account - ) - - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "DefaultApiService.GetAccounts") - if err != nil { - return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} - } - - localVarPath := localBasePath + "/accounts" - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} - localVarFormParams := url.Values{} - - if r.pageSize != nil { - parameterAddToHeaderOrQuery(localVarQueryParams, "pageSize", r.pageSize, "") - } - if r.page != nil { - parameterAddToHeaderOrQuery(localVarQueryParams, "page", r.page, "") - } - if r.sort != nil { - parameterAddToHeaderOrQuery(localVarQueryParams, "sort", r.sort, "") - } - if r.createdAtFrom != nil { - parameterAddToHeaderOrQuery(localVarQueryParams, "createdAtFrom", r.createdAtFrom, "") - } - // to determine the Content-Type header - localVarHTTPContentTypes := []string{} - - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType - } - - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} - - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept - } - req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) - if err != nil { - return localVarReturnValue, nil, err - } - - localVarHTTPResponse, err := a.client.callAPI(req) - if err != nil || localVarHTTPResponse == nil { - return localVarReturnValue, localVarHTTPResponse, err - } - - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) - localVarHTTPResponse.Body.Close() - localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) - if err != nil { - return localVarReturnValue, localVarHTTPResponse, err - } - - if localVarHTTPResponse.StatusCode >= 300 { - newErr := &GenericOpenAPIError{ - body: localVarBody, - error: localVarHTTPResponse.Status, - } - var v Error - err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) - if err != nil { - newErr.error = err.Error() - return localVarReturnValue, localVarHTTPResponse, newErr - } - newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) - newErr.model = v - return localVarReturnValue, localVarHTTPResponse, newErr - } - - err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) - if err != nil { - newErr := &GenericOpenAPIError{ - body: localVarBody, - error: err.Error(), - } - return localVarReturnValue, localVarHTTPResponse, newErr - } - - return localVarReturnValue, localVarHTTPResponse, nil -} - -type ApiGetBeneficiariesRequest struct { - ctx context.Context - ApiService *DefaultApiService - pageSize *int64 - page *int64 - sort *string - createdAtFrom *time.Time -} - -// Number of items per page -func (r ApiGetBeneficiariesRequest) PageSize(pageSize int64) ApiGetBeneficiariesRequest { - r.pageSize = &pageSize - return r -} - -// Page number -func (r ApiGetBeneficiariesRequest) Page(page int64) ApiGetBeneficiariesRequest { - r.page = &page - return r -} - -// Sort order -func (r ApiGetBeneficiariesRequest) Sort(sort string) ApiGetBeneficiariesRequest { - r.sort = &sort - return r -} - -// Filter by created at date -func (r ApiGetBeneficiariesRequest) CreatedAtFrom(createdAtFrom time.Time) ApiGetBeneficiariesRequest { - r.createdAtFrom = &createdAtFrom - return r -} - -func (r ApiGetBeneficiariesRequest) Execute() ([]Beneficiary, *http.Response, error) { - return r.ApiService.GetBeneficiariesExecute(r) -} - -/* -GetBeneficiaries Get all beneficiaries - - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @return ApiGetBeneficiariesRequest -*/ -func (a *DefaultApiService) GetBeneficiaries(ctx context.Context) ApiGetBeneficiariesRequest { - return ApiGetBeneficiariesRequest{ - ApiService: a, - ctx: ctx, - } -} - -// Execute executes the request -// @return []Beneficiary -func (a *DefaultApiService) GetBeneficiariesExecute(r ApiGetBeneficiariesRequest) ([]Beneficiary, *http.Response, error) { - var ( - localVarHTTPMethod = http.MethodGet - localVarPostBody interface{} - formFiles []formFile - localVarReturnValue []Beneficiary - ) - - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "DefaultApiService.GetBeneficiaries") - if err != nil { - return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} - } - - localVarPath := localBasePath + "/beneficiaries" - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} - localVarFormParams := url.Values{} - - if r.pageSize != nil { - parameterAddToHeaderOrQuery(localVarQueryParams, "pageSize", r.pageSize, "") - } - if r.page != nil { - parameterAddToHeaderOrQuery(localVarQueryParams, "page", r.page, "") - } - if r.sort != nil { - parameterAddToHeaderOrQuery(localVarQueryParams, "sort", r.sort, "") - } - if r.createdAtFrom != nil { - parameterAddToHeaderOrQuery(localVarQueryParams, "createdAtFrom", r.createdAtFrom, "") - } - // to determine the Content-Type header - localVarHTTPContentTypes := []string{} - - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType - } - - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} - - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept - } - req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) - if err != nil { - return localVarReturnValue, nil, err - } - - localVarHTTPResponse, err := a.client.callAPI(req) - if err != nil || localVarHTTPResponse == nil { - return localVarReturnValue, localVarHTTPResponse, err - } - - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) - localVarHTTPResponse.Body.Close() - localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) - if err != nil { - return localVarReturnValue, localVarHTTPResponse, err - } - - if localVarHTTPResponse.StatusCode >= 300 { - newErr := &GenericOpenAPIError{ - body: localVarBody, - error: localVarHTTPResponse.Status, - } - var v Error - err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) - if err != nil { - newErr.error = err.Error() - return localVarReturnValue, localVarHTTPResponse, newErr - } - newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) - newErr.model = v - return localVarReturnValue, localVarHTTPResponse, newErr - } - - err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) - if err != nil { - newErr := &GenericOpenAPIError{ - body: localVarBody, - error: err.Error(), - } - return localVarReturnValue, localVarHTTPResponse, newErr - } - - return localVarReturnValue, localVarHTTPResponse, nil -} - -type ApiGetTransactionsRequest struct { - ctx context.Context - ApiService *DefaultApiService - pageSize *int64 - page *int64 - sort *string - updatedAtFrom *time.Time -} - -// Number of items per page -func (r ApiGetTransactionsRequest) PageSize(pageSize int64) ApiGetTransactionsRequest { - r.pageSize = &pageSize - return r -} - -// Page number -func (r ApiGetTransactionsRequest) Page(page int64) ApiGetTransactionsRequest { - r.page = &page - return r -} - -// Sort order -func (r ApiGetTransactionsRequest) Sort(sort string) ApiGetTransactionsRequest { - r.sort = &sort - return r -} - -// Filter by updated at date -func (r ApiGetTransactionsRequest) UpdatedAtFrom(updatedAtFrom time.Time) ApiGetTransactionsRequest { - r.updatedAtFrom = &updatedAtFrom - return r -} - -func (r ApiGetTransactionsRequest) Execute() ([]Transaction, *http.Response, error) { - return r.ApiService.GetTransactionsExecute(r) -} - -/* -GetTransactions Get all transactions - - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @return ApiGetTransactionsRequest -*/ -func (a *DefaultApiService) GetTransactions(ctx context.Context) ApiGetTransactionsRequest { - return ApiGetTransactionsRequest{ - ApiService: a, - ctx: ctx, - } -} - -// Execute executes the request -// @return []Transaction -func (a *DefaultApiService) GetTransactionsExecute(r ApiGetTransactionsRequest) ([]Transaction, *http.Response, error) { - var ( - localVarHTTPMethod = http.MethodGet - localVarPostBody interface{} - formFiles []formFile - localVarReturnValue []Transaction - ) - - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "DefaultApiService.GetTransactions") - if err != nil { - return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} - } - - localVarPath := localBasePath + "/transactions" - - localVarHeaderParams := make(map[string]string) - localVarQueryParams := url.Values{} - localVarFormParams := url.Values{} - - if r.pageSize != nil { - parameterAddToHeaderOrQuery(localVarQueryParams, "pageSize", r.pageSize, "") - } - if r.page != nil { - parameterAddToHeaderOrQuery(localVarQueryParams, "page", r.page, "") - } - if r.sort != nil { - parameterAddToHeaderOrQuery(localVarQueryParams, "sort", r.sort, "") - } - if r.updatedAtFrom != nil { - parameterAddToHeaderOrQuery(localVarQueryParams, "updatedAtFrom", r.updatedAtFrom, "") - } - // to determine the Content-Type header - localVarHTTPContentTypes := []string{} - - // set Content-Type header - localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) - if localVarHTTPContentType != "" { - localVarHeaderParams["Content-Type"] = localVarHTTPContentType - } - - // to determine the Accept header - localVarHTTPHeaderAccepts := []string{"application/json"} - - // set Accept header - localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) - if localVarHTTPHeaderAccept != "" { - localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept - } - req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) - if err != nil { - return localVarReturnValue, nil, err - } - - localVarHTTPResponse, err := a.client.callAPI(req) - if err != nil || localVarHTTPResponse == nil { - return localVarReturnValue, localVarHTTPResponse, err - } - - localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) - localVarHTTPResponse.Body.Close() - localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) - if err != nil { - return localVarReturnValue, localVarHTTPResponse, err - } - - if localVarHTTPResponse.StatusCode >= 300 { - newErr := &GenericOpenAPIError{ - body: localVarBody, - error: localVarHTTPResponse.Status, - } - var v Error - err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) - if err != nil { - newErr.error = err.Error() - return localVarReturnValue, localVarHTTPResponse, newErr - } - newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) - newErr.model = v - return localVarReturnValue, localVarHTTPResponse, newErr - } - - err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) - if err != nil { - newErr := &GenericOpenAPIError{ - body: localVarBody, - error: err.Error(), - } - return localVarReturnValue, localVarHTTPResponse, newErr - } - - return localVarReturnValue, localVarHTTPResponse, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/client.go b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/client.go deleted file mode 100644 index 8670ac161c..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/client.go +++ /dev/null @@ -1,656 +0,0 @@ -/* -GENERIC connector API - -No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - -API version: v0.1 -*/ - -// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. - -package genericclient - -import ( - "bytes" - "context" - "encoding/json" - "encoding/xml" - "errors" - "fmt" - "io" - "log" - "mime/multipart" - "net/http" - "net/http/httputil" - "net/url" - "os" - "path/filepath" - "reflect" - "regexp" - "strconv" - "strings" - "time" - "unicode/utf8" - -) - -var ( - jsonCheck = regexp.MustCompile(`(?i:(?:application|text)/(?:vnd\.[^;]+\+)?json)`) - xmlCheck = regexp.MustCompile(`(?i:(?:application|text)/xml)`) - queryParamSplit = regexp.MustCompile(`(^|&)([^&]+)`) - queryDescape = strings.NewReplacer( "%5B", "[", "%5D", "]" ) -) - -// APIClient manages communication with the GENERIC connector API API vv0.1 -// In most cases there should be only one, shared, APIClient. -type APIClient struct { - cfg *Configuration - common service // Reuse a single struct instead of allocating one for each service on the heap. - - // API Services - - DefaultApi *DefaultApiService -} - -type service struct { - client *APIClient -} - -// NewAPIClient creates a new API client. Requires a userAgent string describing your application. -// optionally a custom http.Client to allow for advanced features such as caching. -func NewAPIClient(cfg *Configuration) *APIClient { - if cfg.HTTPClient == nil { - cfg.HTTPClient = http.DefaultClient - } - - c := &APIClient{} - c.cfg = cfg - c.common.client = c - - // API Services - c.DefaultApi = (*DefaultApiService)(&c.common) - - return c -} - -func atoi(in string) (int, error) { - return strconv.Atoi(in) -} - -// selectHeaderContentType select a content type from the available list. -func selectHeaderContentType(contentTypes []string) string { - if len(contentTypes) == 0 { - return "" - } - if contains(contentTypes, "application/json") { - return "application/json" - } - return contentTypes[0] // use the first content type specified in 'consumes' -} - -// selectHeaderAccept join all accept types and return -func selectHeaderAccept(accepts []string) string { - if len(accepts) == 0 { - return "" - } - - if contains(accepts, "application/json") { - return "application/json" - } - - return strings.Join(accepts, ",") -} - -// contains is a case insensitive match, finding needle in a haystack -func contains(haystack []string, needle string) bool { - for _, a := range haystack { - if strings.EqualFold(a, needle) { - return true - } - } - return false -} - -// Verify optional parameters are of the correct type. -func typeCheckParameter(obj interface{}, expected string, name string) error { - // Make sure there is an object. - if obj == nil { - return nil - } - - // Check the type is as expected. - if reflect.TypeOf(obj).String() != expected { - return fmt.Errorf("expected %s to be of type %s but received %s", name, expected, reflect.TypeOf(obj).String()) - } - return nil -} - -func parameterValueToString( obj interface{}, key string ) string { - if reflect.TypeOf(obj).Kind() != reflect.Ptr { - return fmt.Sprintf("%v", obj) - } - var param,ok = obj.(MappedNullable) - if !ok { - return "" - } - dataMap,err := param.ToMap() - if err != nil { - return "" - } - return fmt.Sprintf("%v", dataMap[key]) -} - -// parameterAddToHeaderOrQuery adds the provided object to the request header or url query -// supporting deep object syntax -func parameterAddToHeaderOrQuery(headerOrQueryParams interface{}, keyPrefix string, obj interface{}, collectionType string) { - var v = reflect.ValueOf(obj) - var value = "" - if v == reflect.ValueOf(nil) { - value = "null" - } else { - switch v.Kind() { - case reflect.Invalid: - value = "invalid" - - case reflect.Struct: - if t,ok := obj.(MappedNullable); ok { - dataMap,err := t.ToMap() - if err != nil { - return - } - parameterAddToHeaderOrQuery(headerOrQueryParams, keyPrefix, dataMap, collectionType) - return - } - if t, ok := obj.(time.Time); ok { - parameterAddToHeaderOrQuery(headerOrQueryParams, keyPrefix, t.Format(time.RFC3339), collectionType) - return - } - value = v.Type().String() + " value" - case reflect.Slice: - var indValue = reflect.ValueOf(obj) - if indValue == reflect.ValueOf(nil) { - return - } - var lenIndValue = indValue.Len() - for i:=0;i 0 || (len(formFiles) > 0) { - if body != nil { - return nil, errors.New("Cannot specify postBody and multipart form at the same time.") - } - body = &bytes.Buffer{} - w := multipart.NewWriter(body) - - for k, v := range formParams { - for _, iv := range v { - if strings.HasPrefix(k, "@") { // file - err = addFile(w, k[1:], iv) - if err != nil { - return nil, err - } - } else { // form value - w.WriteField(k, iv) - } - } - } - for _, formFile := range formFiles { - if len(formFile.fileBytes) > 0 && formFile.fileName != "" { - w.Boundary() - part, err := w.CreateFormFile(formFile.formFileName, filepath.Base(formFile.fileName)) - if err != nil { - return nil, err - } - _, err = part.Write(formFile.fileBytes) - if err != nil { - return nil, err - } - } - } - - // Set the Boundary in the Content-Type - headerParams["Content-Type"] = w.FormDataContentType() - - // Set Content-Length - headerParams["Content-Length"] = fmt.Sprintf("%d", body.Len()) - w.Close() - } - - if strings.HasPrefix(headerParams["Content-Type"], "application/x-www-form-urlencoded") && len(formParams) > 0 { - if body != nil { - return nil, errors.New("Cannot specify postBody and x-www-form-urlencoded form at the same time.") - } - body = &bytes.Buffer{} - body.WriteString(formParams.Encode()) - // Set Content-Length - headerParams["Content-Length"] = fmt.Sprintf("%d", body.Len()) - } - - // Setup path and query parameters - url, err := url.Parse(path) - if err != nil { - return nil, err - } - - // Override request host, if applicable - if c.cfg.Host != "" { - url.Host = c.cfg.Host - } - - // Override request scheme, if applicable - if c.cfg.Scheme != "" { - url.Scheme = c.cfg.Scheme - } - - // Adding Query Param - query := url.Query() - for k, v := range queryParams { - for _, iv := range v { - query.Add(k, iv) - } - } - - // Encode the parameters. - url.RawQuery = queryParamSplit.ReplaceAllStringFunc(query.Encode(), func(s string) string { - pieces := strings.Split(s, "=") - pieces[0] = queryDescape.Replace(pieces[0]) - return strings.Join(pieces, "=") - }) - - // Generate a new request - if body != nil { - localVarRequest, err = http.NewRequest(method, url.String(), body) - } else { - localVarRequest, err = http.NewRequest(method, url.String(), nil) - } - if err != nil { - return nil, err - } - - // add header parameters, if any - if len(headerParams) > 0 { - headers := http.Header{} - for h, v := range headerParams { - headers[h] = []string{v} - } - localVarRequest.Header = headers - } - - // Add the user agent to the request. - localVarRequest.Header.Add("User-Agent", c.cfg.UserAgent) - - if ctx != nil { - // add context to the request - localVarRequest = localVarRequest.WithContext(ctx) - - // Walk through any authentication. - - } - - for header, value := range c.cfg.DefaultHeader { - localVarRequest.Header.Add(header, value) - } - return localVarRequest, nil -} - -func (c *APIClient) decode(v interface{}, b []byte, contentType string) (err error) { - if len(b) == 0 { - return nil - } - if s, ok := v.(*string); ok { - *s = string(b) - return nil - } - if f, ok := v.(*os.File); ok { - f, err = os.CreateTemp("", "HttpClientFile") - if err != nil { - return - } - _, err = f.Write(b) - if err != nil { - return - } - _, err = f.Seek(0, io.SeekStart) - return - } - if f, ok := v.(**os.File); ok { - *f, err = os.CreateTemp("", "HttpClientFile") - if err != nil { - return - } - _, err = (*f).Write(b) - if err != nil { - return - } - _, err = (*f).Seek(0, io.SeekStart) - return - } - if xmlCheck.MatchString(contentType) { - if err = xml.Unmarshal(b, v); err != nil { - return err - } - return nil - } - if jsonCheck.MatchString(contentType) { - if actualObj, ok := v.(interface{ GetActualInstance() interface{} }); ok { // oneOf, anyOf schemas - if unmarshalObj, ok := actualObj.(interface{ UnmarshalJSON([]byte) error }); ok { // make sure it has UnmarshalJSON defined - if err = unmarshalObj.UnmarshalJSON(b); err != nil { - return err - } - } else { - return errors.New("Unknown type with GetActualInstance but no unmarshalObj.UnmarshalJSON defined") - } - } else if err = json.Unmarshal(b, v); err != nil { // simple model - return err - } - return nil - } - return errors.New("undefined response type") -} - -// Add a file to the multipart request -func addFile(w *multipart.Writer, fieldName, path string) error { - file, err := os.Open(filepath.Clean(path)) - if err != nil { - return err - } - err = file.Close() - if err != nil { - return err - } - - part, err := w.CreateFormFile(fieldName, filepath.Base(path)) - if err != nil { - return err - } - _, err = io.Copy(part, file) - - return err -} - -// Prevent trying to import "fmt" -func reportError(format string, a ...interface{}) error { - return fmt.Errorf(format, a...) -} - -// A wrapper for strict JSON decoding -func newStrictDecoder(data []byte) *json.Decoder { - dec := json.NewDecoder(bytes.NewBuffer(data)) - dec.DisallowUnknownFields() - return dec -} - -// Set request body from an interface{} -func setBody(body interface{}, contentType string) (bodyBuf *bytes.Buffer, err error) { - if bodyBuf == nil { - bodyBuf = &bytes.Buffer{} - } - - if reader, ok := body.(io.Reader); ok { - _, err = bodyBuf.ReadFrom(reader) - } else if fp, ok := body.(*os.File); ok { - _, err = bodyBuf.ReadFrom(fp) - } else if b, ok := body.([]byte); ok { - _, err = bodyBuf.Write(b) - } else if s, ok := body.(string); ok { - _, err = bodyBuf.WriteString(s) - } else if s, ok := body.(*string); ok { - _, err = bodyBuf.WriteString(*s) - } else if jsonCheck.MatchString(contentType) { - err = json.NewEncoder(bodyBuf).Encode(body) - } else if xmlCheck.MatchString(contentType) { - err = xml.NewEncoder(bodyBuf).Encode(body) - } - - if err != nil { - return nil, err - } - - if bodyBuf.Len() == 0 { - err = fmt.Errorf("invalid body type %s\n", contentType) - return nil, err - } - return bodyBuf, nil -} - -// detectContentType method is used to figure out `Request.Body` content type for request header -func detectContentType(body interface{}) string { - contentType := "text/plain; charset=utf-8" - kind := reflect.TypeOf(body).Kind() - - switch kind { - case reflect.Struct, reflect.Map, reflect.Ptr: - contentType = "application/json; charset=utf-8" - case reflect.String: - contentType = "text/plain; charset=utf-8" - default: - if b, ok := body.([]byte); ok { - contentType = http.DetectContentType(b) - } else if kind == reflect.Slice { - contentType = "application/json; charset=utf-8" - } - } - - return contentType -} - -// Ripped from https://github.com/gregjones/httpcache/blob/master/httpcache.go -type cacheControl map[string]string - -func parseCacheControl(headers http.Header) cacheControl { - cc := cacheControl{} - ccHeader := headers.Get("Cache-Control") - for _, part := range strings.Split(ccHeader, ",") { - part = strings.Trim(part, " ") - if part == "" { - continue - } - if strings.ContainsRune(part, '=') { - keyval := strings.Split(part, "=") - cc[strings.Trim(keyval[0], " ")] = strings.Trim(keyval[1], ",") - } else { - cc[part] = "" - } - } - return cc -} - -// CacheExpires helper function to determine remaining time before repeating a request. -func CacheExpires(r *http.Response) time.Time { - // Figure out when the cache expires. - var expires time.Time - now, err := time.Parse(time.RFC1123, r.Header.Get("date")) - if err != nil { - return time.Now() - } - respCacheControl := parseCacheControl(r.Header) - - if maxAge, ok := respCacheControl["max-age"]; ok { - lifetime, err := time.ParseDuration(maxAge + "s") - if err != nil { - expires = now - } else { - expires = now.Add(lifetime) - } - } else { - expiresHeader := r.Header.Get("Expires") - if expiresHeader != "" { - expires, err = time.Parse(time.RFC1123, expiresHeader) - if err != nil { - expires = now - } - } - } - return expires -} - -func strlen(s string) int { - return utf8.RuneCountInString(s) -} - -// GenericOpenAPIError Provides access to the body, error and model on returned errors. -type GenericOpenAPIError struct { - body []byte - error string - model interface{} -} - -// Error returns non-empty string if there was an error. -func (e GenericOpenAPIError) Error() string { - return e.error -} - -// Body returns the raw bytes of the response -func (e GenericOpenAPIError) Body() []byte { - return e.body -} - -// Model returns the unpacked model of the error -func (e GenericOpenAPIError) Model() interface{} { - return e.model -} - -// format error message using title and detail when model implements rfc7807 -func formatErrorMessage(status string, v interface{}) string { - str := "" - metaValue := reflect.ValueOf(v).Elem() - - if metaValue.Kind() == reflect.Struct { - field := metaValue.FieldByName("Title") - if field != (reflect.Value{}) { - str = fmt.Sprintf("%s", field.Interface()) - } - - field = metaValue.FieldByName("Detail") - if field != (reflect.Value{}) { - str = fmt.Sprintf("%s (%s)", str, field.Interface()) - } - } - - return strings.TrimSpace(fmt.Sprintf("%s %s", status, str)) -} diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/configuration.go b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/configuration.go deleted file mode 100644 index 27b99ff25b..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/configuration.go +++ /dev/null @@ -1,215 +0,0 @@ -/* -GENERIC connector API - -No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - -API version: v0.1 -*/ - -// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. - -package genericclient - -import ( - "context" - "fmt" - "net/http" - "strings" -) - -// contextKeys are used to identify the type of value in the context. -// Since these are string, it is possible to get a short description of the -// context key for logging and debugging using key.String(). - -type contextKey string - -func (c contextKey) String() string { - return "auth " + string(c) -} - -var ( - // ContextServerIndex uses a server configuration from the index. - ContextServerIndex = contextKey("serverIndex") - - // ContextOperationServerIndices uses a server configuration from the index mapping. - ContextOperationServerIndices = contextKey("serverOperationIndices") - - // ContextServerVariables overrides a server configuration variables. - ContextServerVariables = contextKey("serverVariables") - - // ContextOperationServerVariables overrides a server configuration variables using operation specific values. - ContextOperationServerVariables = contextKey("serverOperationVariables") -) - -// BasicAuth provides basic http authentication to a request passed via context using ContextBasicAuth -type BasicAuth struct { - UserName string `json:"userName,omitempty"` - Password string `json:"password,omitempty"` -} - -// APIKey provides API key based authentication to a request passed via context using ContextAPIKey -type APIKey struct { - Key string - Prefix string -} - -// ServerVariable stores the information about a server variable -type ServerVariable struct { - Description string - DefaultValue string - EnumValues []string -} - -// ServerConfiguration stores the information about a server -type ServerConfiguration struct { - URL string - Description string - Variables map[string]ServerVariable -} - -// ServerConfigurations stores multiple ServerConfiguration items -type ServerConfigurations []ServerConfiguration - -// Configuration stores the configuration of the API client -type Configuration struct { - Host string `json:"host,omitempty"` - Scheme string `json:"scheme,omitempty"` - DefaultHeader map[string]string `json:"defaultHeader,omitempty"` - UserAgent string `json:"userAgent,omitempty"` - Debug bool `json:"debug,omitempty"` - Servers ServerConfigurations - OperationServers map[string]ServerConfigurations - HTTPClient *http.Client -} - -// NewConfiguration returns a new Configuration object -func NewConfiguration() *Configuration { - cfg := &Configuration{ - DefaultHeader: make(map[string]string), - UserAgent: "OpenAPI-Generator/latest/go", - Debug: false, - Servers: ServerConfigurations{ - { - URL: "", - Description: "No description provided", - }, - }, - OperationServers: map[string]ServerConfigurations{ - }, - } - return cfg -} - -// AddDefaultHeader adds a new HTTP header to the default header in the request -func (c *Configuration) AddDefaultHeader(key string, value string) { - c.DefaultHeader[key] = value -} - -// URL formats template on a index using given variables -func (sc ServerConfigurations) URL(index int, variables map[string]string) (string, error) { - if index < 0 || len(sc) <= index { - return "", fmt.Errorf("index %v out of range %v", index, len(sc)-1) - } - server := sc[index] - url := server.URL - - // go through variables and replace placeholders - for name, variable := range server.Variables { - if value, ok := variables[name]; ok { - found := bool(len(variable.EnumValues) == 0) - for _, enumValue := range variable.EnumValues { - if value == enumValue { - found = true - } - } - if !found { - return "", fmt.Errorf("the variable %s in the server URL has invalid value %v. Must be %v", name, value, variable.EnumValues) - } - url = strings.Replace(url, "{"+name+"}", value, -1) - } else { - url = strings.Replace(url, "{"+name+"}", variable.DefaultValue, -1) - } - } - return url, nil -} - -// ServerURL returns URL based on server settings -func (c *Configuration) ServerURL(index int, variables map[string]string) (string, error) { - return c.Servers.URL(index, variables) -} - -func getServerIndex(ctx context.Context) (int, error) { - si := ctx.Value(ContextServerIndex) - if si != nil { - if index, ok := si.(int); ok { - return index, nil - } - return 0, reportError("Invalid type %T should be int", si) - } - return 0, nil -} - -func getServerOperationIndex(ctx context.Context, endpoint string) (int, error) { - osi := ctx.Value(ContextOperationServerIndices) - if osi != nil { - if operationIndices, ok := osi.(map[string]int); !ok { - return 0, reportError("Invalid type %T should be map[string]int", osi) - } else { - index, ok := operationIndices[endpoint] - if ok { - return index, nil - } - } - } - return getServerIndex(ctx) -} - -func getServerVariables(ctx context.Context) (map[string]string, error) { - sv := ctx.Value(ContextServerVariables) - if sv != nil { - if variables, ok := sv.(map[string]string); ok { - return variables, nil - } - return nil, reportError("ctx value of ContextServerVariables has invalid type %T should be map[string]string", sv) - } - return nil, nil -} - -func getServerOperationVariables(ctx context.Context, endpoint string) (map[string]string, error) { - osv := ctx.Value(ContextOperationServerVariables) - if osv != nil { - if operationVariables, ok := osv.(map[string]map[string]string); !ok { - return nil, reportError("ctx value of ContextOperationServerVariables has invalid type %T should be map[string]map[string]string", osv) - } else { - variables, ok := operationVariables[endpoint] - if ok { - return variables, nil - } - } - } - return getServerVariables(ctx) -} - -// ServerURLWithContext returns a new server URL given an endpoint -func (c *Configuration) ServerURLWithContext(ctx context.Context, endpoint string) (string, error) { - sc, ok := c.OperationServers[endpoint] - if !ok { - sc = c.Servers - } - - if ctx == nil { - return sc.URL(0, nil) - } - - index, err := getServerOperationIndex(ctx, endpoint) - if err != nil { - return "", err - } - - variables, err := getServerOperationVariables(ctx, endpoint) - if err != nil { - return "", err - } - - return sc.URL(index, variables) -} diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/docs/Account.md b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/docs/Account.md deleted file mode 100644 index 57157a528f..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/docs/Account.md +++ /dev/null @@ -1,129 +0,0 @@ -# Account - -## Properties - -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**Id** | **string** | | -**AccountName** | **string** | | -**CreatedAt** | **time.Time** | | -**Metadata** | Pointer to **map[string]string** | | [optional] - -## Methods - -### NewAccount - -`func NewAccount(id string, accountName string, createdAt time.Time, ) *Account` - -NewAccount instantiates a new Account object -This constructor will assign default values to properties that have it defined, -and makes sure properties required by API are set, but the set of arguments -will change when the set of required properties is changed - -### NewAccountWithDefaults - -`func NewAccountWithDefaults() *Account` - -NewAccountWithDefaults instantiates a new Account object -This constructor will only assign default values to properties that have it defined, -but it doesn't guarantee that properties required by API are set - -### GetId - -`func (o *Account) GetId() string` - -GetId returns the Id field if non-nil, zero value otherwise. - -### GetIdOk - -`func (o *Account) GetIdOk() (*string, bool)` - -GetIdOk returns a tuple with the Id field if it's non-nil, zero value otherwise -and a boolean to check if the value has been set. - -### SetId - -`func (o *Account) SetId(v string)` - -SetId sets Id field to given value. - - -### GetAccountName - -`func (o *Account) GetAccountName() string` - -GetAccountName returns the AccountName field if non-nil, zero value otherwise. - -### GetAccountNameOk - -`func (o *Account) GetAccountNameOk() (*string, bool)` - -GetAccountNameOk returns a tuple with the AccountName field if it's non-nil, zero value otherwise -and a boolean to check if the value has been set. - -### SetAccountName - -`func (o *Account) SetAccountName(v string)` - -SetAccountName sets AccountName field to given value. - - -### GetCreatedAt - -`func (o *Account) GetCreatedAt() time.Time` - -GetCreatedAt returns the CreatedAt field if non-nil, zero value otherwise. - -### GetCreatedAtOk - -`func (o *Account) GetCreatedAtOk() (*time.Time, bool)` - -GetCreatedAtOk returns a tuple with the CreatedAt field if it's non-nil, zero value otherwise -and a boolean to check if the value has been set. - -### SetCreatedAt - -`func (o *Account) SetCreatedAt(v time.Time)` - -SetCreatedAt sets CreatedAt field to given value. - - -### GetMetadata - -`func (o *Account) GetMetadata() map[string]string` - -GetMetadata returns the Metadata field if non-nil, zero value otherwise. - -### GetMetadataOk - -`func (o *Account) GetMetadataOk() (*map[string]string, bool)` - -GetMetadataOk returns a tuple with the Metadata field if it's non-nil, zero value otherwise -and a boolean to check if the value has been set. - -### SetMetadata - -`func (o *Account) SetMetadata(v map[string]string)` - -SetMetadata sets Metadata field to given value. - -### HasMetadata - -`func (o *Account) HasMetadata() bool` - -HasMetadata returns a boolean if a field has been set. - -### SetMetadataNil - -`func (o *Account) SetMetadataNil(b bool)` - - SetMetadataNil sets the value for Metadata to be an explicit nil - -### UnsetMetadata -`func (o *Account) UnsetMetadata()` - -UnsetMetadata ensures that no value is present for Metadata, not even an explicit nil - -[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/docs/Balance.md b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/docs/Balance.md deleted file mode 100644 index 10aef2295e..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/docs/Balance.md +++ /dev/null @@ -1,72 +0,0 @@ -# Balance - -## Properties - -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**Amount** | **string** | | -**Currency** | **string** | | - -## Methods - -### NewBalance - -`func NewBalance(amount string, currency string, ) *Balance` - -NewBalance instantiates a new Balance object -This constructor will assign default values to properties that have it defined, -and makes sure properties required by API are set, but the set of arguments -will change when the set of required properties is changed - -### NewBalanceWithDefaults - -`func NewBalanceWithDefaults() *Balance` - -NewBalanceWithDefaults instantiates a new Balance object -This constructor will only assign default values to properties that have it defined, -but it doesn't guarantee that properties required by API are set - -### GetAmount - -`func (o *Balance) GetAmount() string` - -GetAmount returns the Amount field if non-nil, zero value otherwise. - -### GetAmountOk - -`func (o *Balance) GetAmountOk() (*string, bool)` - -GetAmountOk returns a tuple with the Amount field if it's non-nil, zero value otherwise -and a boolean to check if the value has been set. - -### SetAmount - -`func (o *Balance) SetAmount(v string)` - -SetAmount sets Amount field to given value. - - -### GetCurrency - -`func (o *Balance) GetCurrency() string` - -GetCurrency returns the Currency field if non-nil, zero value otherwise. - -### GetCurrencyOk - -`func (o *Balance) GetCurrencyOk() (*string, bool)` - -GetCurrencyOk returns a tuple with the Currency field if it's non-nil, zero value otherwise -and a boolean to check if the value has been set. - -### SetCurrency - -`func (o *Balance) SetCurrency(v string)` - -SetCurrency sets Currency field to given value. - - - -[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/docs/Balances.md b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/docs/Balances.md deleted file mode 100644 index dd764e805a..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/docs/Balances.md +++ /dev/null @@ -1,114 +0,0 @@ -# Balances - -## Properties - -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**Id** | **string** | | -**AccountID** | **string** | | -**At** | **time.Time** | | -**Balances** | [**[]Balance**](Balance.md) | | - -## Methods - -### NewBalances - -`func NewBalances(id string, accountID string, at time.Time, balances []Balance, ) *Balances` - -NewBalances instantiates a new Balances object -This constructor will assign default values to properties that have it defined, -and makes sure properties required by API are set, but the set of arguments -will change when the set of required properties is changed - -### NewBalancesWithDefaults - -`func NewBalancesWithDefaults() *Balances` - -NewBalancesWithDefaults instantiates a new Balances object -This constructor will only assign default values to properties that have it defined, -but it doesn't guarantee that properties required by API are set - -### GetId - -`func (o *Balances) GetId() string` - -GetId returns the Id field if non-nil, zero value otherwise. - -### GetIdOk - -`func (o *Balances) GetIdOk() (*string, bool)` - -GetIdOk returns a tuple with the Id field if it's non-nil, zero value otherwise -and a boolean to check if the value has been set. - -### SetId - -`func (o *Balances) SetId(v string)` - -SetId sets Id field to given value. - - -### GetAccountID - -`func (o *Balances) GetAccountID() string` - -GetAccountID returns the AccountID field if non-nil, zero value otherwise. - -### GetAccountIDOk - -`func (o *Balances) GetAccountIDOk() (*string, bool)` - -GetAccountIDOk returns a tuple with the AccountID field if it's non-nil, zero value otherwise -and a boolean to check if the value has been set. - -### SetAccountID - -`func (o *Balances) SetAccountID(v string)` - -SetAccountID sets AccountID field to given value. - - -### GetAt - -`func (o *Balances) GetAt() time.Time` - -GetAt returns the At field if non-nil, zero value otherwise. - -### GetAtOk - -`func (o *Balances) GetAtOk() (*time.Time, bool)` - -GetAtOk returns a tuple with the At field if it's non-nil, zero value otherwise -and a boolean to check if the value has been set. - -### SetAt - -`func (o *Balances) SetAt(v time.Time)` - -SetAt sets At field to given value. - - -### GetBalances - -`func (o *Balances) GetBalances() []Balance` - -GetBalances returns the Balances field if non-nil, zero value otherwise. - -### GetBalancesOk - -`func (o *Balances) GetBalancesOk() (*[]Balance, bool)` - -GetBalancesOk returns a tuple with the Balances field if it's non-nil, zero value otherwise -and a boolean to check if the value has been set. - -### SetBalances - -`func (o *Balances) SetBalances(v []Balance)` - -SetBalances sets Balances field to given value. - - - -[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/docs/Beneficiary.md b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/docs/Beneficiary.md deleted file mode 100644 index d967209a9b..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/docs/Beneficiary.md +++ /dev/null @@ -1,129 +0,0 @@ -# Beneficiary - -## Properties - -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**Id** | **string** | | -**CreatedAt** | **time.Time** | | -**OwnerName** | **string** | | -**Metadata** | Pointer to **map[string]string** | | [optional] - -## Methods - -### NewBeneficiary - -`func NewBeneficiary(id string, createdAt time.Time, ownerName string, ) *Beneficiary` - -NewBeneficiary instantiates a new Beneficiary object -This constructor will assign default values to properties that have it defined, -and makes sure properties required by API are set, but the set of arguments -will change when the set of required properties is changed - -### NewBeneficiaryWithDefaults - -`func NewBeneficiaryWithDefaults() *Beneficiary` - -NewBeneficiaryWithDefaults instantiates a new Beneficiary object -This constructor will only assign default values to properties that have it defined, -but it doesn't guarantee that properties required by API are set - -### GetId - -`func (o *Beneficiary) GetId() string` - -GetId returns the Id field if non-nil, zero value otherwise. - -### GetIdOk - -`func (o *Beneficiary) GetIdOk() (*string, bool)` - -GetIdOk returns a tuple with the Id field if it's non-nil, zero value otherwise -and a boolean to check if the value has been set. - -### SetId - -`func (o *Beneficiary) SetId(v string)` - -SetId sets Id field to given value. - - -### GetCreatedAt - -`func (o *Beneficiary) GetCreatedAt() time.Time` - -GetCreatedAt returns the CreatedAt field if non-nil, zero value otherwise. - -### GetCreatedAtOk - -`func (o *Beneficiary) GetCreatedAtOk() (*time.Time, bool)` - -GetCreatedAtOk returns a tuple with the CreatedAt field if it's non-nil, zero value otherwise -and a boolean to check if the value has been set. - -### SetCreatedAt - -`func (o *Beneficiary) SetCreatedAt(v time.Time)` - -SetCreatedAt sets CreatedAt field to given value. - - -### GetOwnerName - -`func (o *Beneficiary) GetOwnerName() string` - -GetOwnerName returns the OwnerName field if non-nil, zero value otherwise. - -### GetOwnerNameOk - -`func (o *Beneficiary) GetOwnerNameOk() (*string, bool)` - -GetOwnerNameOk returns a tuple with the OwnerName field if it's non-nil, zero value otherwise -and a boolean to check if the value has been set. - -### SetOwnerName - -`func (o *Beneficiary) SetOwnerName(v string)` - -SetOwnerName sets OwnerName field to given value. - - -### GetMetadata - -`func (o *Beneficiary) GetMetadata() map[string]string` - -GetMetadata returns the Metadata field if non-nil, zero value otherwise. - -### GetMetadataOk - -`func (o *Beneficiary) GetMetadataOk() (*map[string]string, bool)` - -GetMetadataOk returns a tuple with the Metadata field if it's non-nil, zero value otherwise -and a boolean to check if the value has been set. - -### SetMetadata - -`func (o *Beneficiary) SetMetadata(v map[string]string)` - -SetMetadata sets Metadata field to given value. - -### HasMetadata - -`func (o *Beneficiary) HasMetadata() bool` - -HasMetadata returns a boolean if a field has been set. - -### SetMetadataNil - -`func (o *Beneficiary) SetMetadataNil(b bool)` - - SetMetadataNil sets the value for Metadata to be an explicit nil - -### UnsetMetadata -`func (o *Beneficiary) UnsetMetadata()` - -UnsetMetadata ensures that no value is present for Metadata, not even an explicit nil - -[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/docs/DefaultApi.md b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/docs/DefaultApi.md deleted file mode 100644 index 1c8673441d..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/docs/DefaultApi.md +++ /dev/null @@ -1,293 +0,0 @@ -# \DefaultApi - -All URIs are relative to *http://localhost* - -Method | HTTP request | Description -------------- | ------------- | ------------- -[**GetAccountBalances**](DefaultApi.md#GetAccountBalances) | **Get** /accounts/{accountId}/balances | Get account balance -[**GetAccounts**](DefaultApi.md#GetAccounts) | **Get** /accounts | Get all accounts -[**GetBeneficiaries**](DefaultApi.md#GetBeneficiaries) | **Get** /beneficiaries | Get all beneficiaries -[**GetTransactions**](DefaultApi.md#GetTransactions) | **Get** /transactions | Get all transactions - - - -## GetAccountBalances - -> Balances GetAccountBalances(ctx, accountId).Execute() - -Get account balance - -### Example - -```go -package main - -import ( - "context" - "fmt" - "os" - openapiclient "github.com/formancehq/payments/genericclient" -) - -func main() { - accountId := "accountId_example" // string | - - configuration := openapiclient.NewConfiguration() - apiClient := openapiclient.NewAPIClient(configuration) - resp, r, err := apiClient.DefaultApi.GetAccountBalances(context.Background(), accountId).Execute() - if err != nil { - fmt.Fprintf(os.Stderr, "Error when calling `DefaultApi.GetAccountBalances``: %v\n", err) - fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) - } - // response from `GetAccountBalances`: Balances - fmt.Fprintf(os.Stdout, "Response from `DefaultApi.GetAccountBalances`: %v\n", resp) -} -``` - -### Path Parameters - - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- -**ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. -**accountId** | **string** | | - -### Other Parameters - -Other parameters are passed through a pointer to a apiGetAccountBalancesRequest struct via the builder pattern - - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - - -### Return type - -[**Balances**](Balances.md) - -### Authorization - -No authorization required - -### HTTP request headers - -- **Content-Type**: Not defined -- **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) -[[Back to Model list]](../README.md#documentation-for-models) -[[Back to README]](../README.md) - - -## GetAccounts - -> []Account GetAccounts(ctx).PageSize(pageSize).Page(page).Sort(sort).CreatedAtFrom(createdAtFrom).Execute() - -Get all accounts - -### Example - -```go -package main - -import ( - "context" - "fmt" - "os" - "time" - openapiclient "github.com/formancehq/payments/genericclient" -) - -func main() { - pageSize := int64(100) // int64 | Number of items per page (optional) (default to 100) - page := int64(1) // int64 | Page number (optional) (default to 1) - sort := "createdAt:asc" // string | Sort order (optional) - createdAtFrom := time.Now() // time.Time | Filter by created at date (optional) - - configuration := openapiclient.NewConfiguration() - apiClient := openapiclient.NewAPIClient(configuration) - resp, r, err := apiClient.DefaultApi.GetAccounts(context.Background()).PageSize(pageSize).Page(page).Sort(sort).CreatedAtFrom(createdAtFrom).Execute() - if err != nil { - fmt.Fprintf(os.Stderr, "Error when calling `DefaultApi.GetAccounts``: %v\n", err) - fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) - } - // response from `GetAccounts`: []Account - fmt.Fprintf(os.Stdout, "Response from `DefaultApi.GetAccounts`: %v\n", resp) -} -``` - -### Path Parameters - - - -### Other Parameters - -Other parameters are passed through a pointer to a apiGetAccountsRequest struct via the builder pattern - - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **pageSize** | **int64** | Number of items per page | [default to 100] - **page** | **int64** | Page number | [default to 1] - **sort** | **string** | Sort order | - **createdAtFrom** | **time.Time** | Filter by created at date | - -### Return type - -[**[]Account**](Account.md) - -### Authorization - -No authorization required - -### HTTP request headers - -- **Content-Type**: Not defined -- **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) -[[Back to Model list]](../README.md#documentation-for-models) -[[Back to README]](../README.md) - - -## GetBeneficiaries - -> []Beneficiary GetBeneficiaries(ctx).PageSize(pageSize).Page(page).Sort(sort).CreatedAtFrom(createdAtFrom).Execute() - -Get all beneficiaries - -### Example - -```go -package main - -import ( - "context" - "fmt" - "os" - "time" - openapiclient "github.com/formancehq/payments/genericclient" -) - -func main() { - pageSize := int64(100) // int64 | Number of items per page (optional) (default to 100) - page := int64(1) // int64 | Page number (optional) (default to 1) - sort := "createdAt:asc" // string | Sort order (optional) - createdAtFrom := time.Now() // time.Time | Filter by created at date (optional) - - configuration := openapiclient.NewConfiguration() - apiClient := openapiclient.NewAPIClient(configuration) - resp, r, err := apiClient.DefaultApi.GetBeneficiaries(context.Background()).PageSize(pageSize).Page(page).Sort(sort).CreatedAtFrom(createdAtFrom).Execute() - if err != nil { - fmt.Fprintf(os.Stderr, "Error when calling `DefaultApi.GetBeneficiaries``: %v\n", err) - fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) - } - // response from `GetBeneficiaries`: []Beneficiary - fmt.Fprintf(os.Stdout, "Response from `DefaultApi.GetBeneficiaries`: %v\n", resp) -} -``` - -### Path Parameters - - - -### Other Parameters - -Other parameters are passed through a pointer to a apiGetBeneficiariesRequest struct via the builder pattern - - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **pageSize** | **int64** | Number of items per page | [default to 100] - **page** | **int64** | Page number | [default to 1] - **sort** | **string** | Sort order | - **createdAtFrom** | **time.Time** | Filter by created at date | - -### Return type - -[**[]Beneficiary**](Beneficiary.md) - -### Authorization - -No authorization required - -### HTTP request headers - -- **Content-Type**: Not defined -- **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) -[[Back to Model list]](../README.md#documentation-for-models) -[[Back to README]](../README.md) - - -## GetTransactions - -> []Transaction GetTransactions(ctx).PageSize(pageSize).Page(page).Sort(sort).UpdatedAtFrom(updatedAtFrom).Execute() - -Get all transactions - -### Example - -```go -package main - -import ( - "context" - "fmt" - "os" - "time" - openapiclient "github.com/formancehq/payments/genericclient" -) - -func main() { - pageSize := int64(100) // int64 | Number of items per page (optional) (default to 100) - page := int64(1) // int64 | Page number (optional) (default to 1) - sort := "createdAt:asc" // string | Sort order (optional) - updatedAtFrom := time.Now() // time.Time | Filter by updated at date (optional) - - configuration := openapiclient.NewConfiguration() - apiClient := openapiclient.NewAPIClient(configuration) - resp, r, err := apiClient.DefaultApi.GetTransactions(context.Background()).PageSize(pageSize).Page(page).Sort(sort).UpdatedAtFrom(updatedAtFrom).Execute() - if err != nil { - fmt.Fprintf(os.Stderr, "Error when calling `DefaultApi.GetTransactions``: %v\n", err) - fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) - } - // response from `GetTransactions`: []Transaction - fmt.Fprintf(os.Stdout, "Response from `DefaultApi.GetTransactions`: %v\n", resp) -} -``` - -### Path Parameters - - - -### Other Parameters - -Other parameters are passed through a pointer to a apiGetTransactionsRequest struct via the builder pattern - - -Name | Type | Description | Notes -------------- | ------------- | ------------- | ------------- - **pageSize** | **int64** | Number of items per page | [default to 100] - **page** | **int64** | Page number | [default to 1] - **sort** | **string** | Sort order | - **updatedAtFrom** | **time.Time** | Filter by updated at date | - -### Return type - -[**[]Transaction**](Transaction.md) - -### Authorization - -No authorization required - -### HTTP request headers - -- **Content-Type**: Not defined -- **Accept**: application/json - -[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) -[[Back to Model list]](../README.md#documentation-for-models) -[[Back to README]](../README.md) - diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/docs/Error.md b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/docs/Error.md deleted file mode 100644 index a6b1b02c9b..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/docs/Error.md +++ /dev/null @@ -1,72 +0,0 @@ -# Error - -## Properties - -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**Title** | **string** | | -**Detail** | **string** | | - -## Methods - -### NewError - -`func NewError(title string, detail string, ) *Error` - -NewError instantiates a new Error object -This constructor will assign default values to properties that have it defined, -and makes sure properties required by API are set, but the set of arguments -will change when the set of required properties is changed - -### NewErrorWithDefaults - -`func NewErrorWithDefaults() *Error` - -NewErrorWithDefaults instantiates a new Error object -This constructor will only assign default values to properties that have it defined, -but it doesn't guarantee that properties required by API are set - -### GetTitle - -`func (o *Error) GetTitle() string` - -GetTitle returns the Title field if non-nil, zero value otherwise. - -### GetTitleOk - -`func (o *Error) GetTitleOk() (*string, bool)` - -GetTitleOk returns a tuple with the Title field if it's non-nil, zero value otherwise -and a boolean to check if the value has been set. - -### SetTitle - -`func (o *Error) SetTitle(v string)` - -SetTitle sets Title field to given value. - - -### GetDetail - -`func (o *Error) GetDetail() string` - -GetDetail returns the Detail field if non-nil, zero value otherwise. - -### GetDetailOk - -`func (o *Error) GetDetailOk() (*string, bool)` - -GetDetailOk returns a tuple with the Detail field if it's non-nil, zero value otherwise -and a boolean to check if the value has been set. - -### SetDetail - -`func (o *Error) SetDetail(v string)` - -SetDetail sets Detail field to given value. - - - -[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/docs/Transaction.md b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/docs/Transaction.md deleted file mode 100644 index 834e33b9e8..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/docs/Transaction.md +++ /dev/null @@ -1,317 +0,0 @@ -# Transaction - -## Properties - -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- -**Id** | **string** | | -**RelatedTransactionID** | Pointer to **string** | | [optional] -**CreatedAt** | **time.Time** | | -**UpdatedAt** | **time.Time** | | -**Currency** | **string** | | -**Scheme** | Pointer to **string** | | [optional] -**Type** | [**TransactionType**](TransactionType.md) | | -**Status** | [**TransactionStatus**](TransactionStatus.md) | | -**Amount** | **string** | | -**SourceAccountID** | Pointer to **string** | | [optional] -**DestinationAccountID** | Pointer to **string** | | [optional] -**Metadata** | Pointer to **map[string]string** | | [optional] - -## Methods - -### NewTransaction - -`func NewTransaction(id string, createdAt time.Time, updatedAt time.Time, currency string, type_ TransactionType, status TransactionStatus, amount string, ) *Transaction` - -NewTransaction instantiates a new Transaction object -This constructor will assign default values to properties that have it defined, -and makes sure properties required by API are set, but the set of arguments -will change when the set of required properties is changed - -### NewTransactionWithDefaults - -`func NewTransactionWithDefaults() *Transaction` - -NewTransactionWithDefaults instantiates a new Transaction object -This constructor will only assign default values to properties that have it defined, -but it doesn't guarantee that properties required by API are set - -### GetId - -`func (o *Transaction) GetId() string` - -GetId returns the Id field if non-nil, zero value otherwise. - -### GetIdOk - -`func (o *Transaction) GetIdOk() (*string, bool)` - -GetIdOk returns a tuple with the Id field if it's non-nil, zero value otherwise -and a boolean to check if the value has been set. - -### SetId - -`func (o *Transaction) SetId(v string)` - -SetId sets Id field to given value. - - -### GetRelatedTransactionID - -`func (o *Transaction) GetRelatedTransactionID() string` - -GetRelatedTransactionID returns the RelatedTransactionID field if non-nil, zero value otherwise. - -### GetRelatedTransactionIDOk - -`func (o *Transaction) GetRelatedTransactionIDOk() (*string, bool)` - -GetRelatedTransactionIDOk returns a tuple with the RelatedTransactionID field if it's non-nil, zero value otherwise -and a boolean to check if the value has been set. - -### SetRelatedTransactionID - -`func (o *Transaction) SetRelatedTransactionID(v string)` - -SetRelatedTransactionID sets RelatedTransactionID field to given value. - -### HasRelatedTransactionID - -`func (o *Transaction) HasRelatedTransactionID() bool` - -HasRelatedTransactionID returns a boolean if a field has been set. - -### GetCreatedAt - -`func (o *Transaction) GetCreatedAt() time.Time` - -GetCreatedAt returns the CreatedAt field if non-nil, zero value otherwise. - -### GetCreatedAtOk - -`func (o *Transaction) GetCreatedAtOk() (*time.Time, bool)` - -GetCreatedAtOk returns a tuple with the CreatedAt field if it's non-nil, zero value otherwise -and a boolean to check if the value has been set. - -### SetCreatedAt - -`func (o *Transaction) SetCreatedAt(v time.Time)` - -SetCreatedAt sets CreatedAt field to given value. - - -### GetUpdatedAt - -`func (o *Transaction) GetUpdatedAt() time.Time` - -GetUpdatedAt returns the UpdatedAt field if non-nil, zero value otherwise. - -### GetUpdatedAtOk - -`func (o *Transaction) GetUpdatedAtOk() (*time.Time, bool)` - -GetUpdatedAtOk returns a tuple with the UpdatedAt field if it's non-nil, zero value otherwise -and a boolean to check if the value has been set. - -### SetUpdatedAt - -`func (o *Transaction) SetUpdatedAt(v time.Time)` - -SetUpdatedAt sets UpdatedAt field to given value. - - -### GetCurrency - -`func (o *Transaction) GetCurrency() string` - -GetCurrency returns the Currency field if non-nil, zero value otherwise. - -### GetCurrencyOk - -`func (o *Transaction) GetCurrencyOk() (*string, bool)` - -GetCurrencyOk returns a tuple with the Currency field if it's non-nil, zero value otherwise -and a boolean to check if the value has been set. - -### SetCurrency - -`func (o *Transaction) SetCurrency(v string)` - -SetCurrency sets Currency field to given value. - - -### GetScheme - -`func (o *Transaction) GetScheme() string` - -GetScheme returns the Scheme field if non-nil, zero value otherwise. - -### GetSchemeOk - -`func (o *Transaction) GetSchemeOk() (*string, bool)` - -GetSchemeOk returns a tuple with the Scheme field if it's non-nil, zero value otherwise -and a boolean to check if the value has been set. - -### SetScheme - -`func (o *Transaction) SetScheme(v string)` - -SetScheme sets Scheme field to given value. - -### HasScheme - -`func (o *Transaction) HasScheme() bool` - -HasScheme returns a boolean if a field has been set. - -### GetType - -`func (o *Transaction) GetType() TransactionType` - -GetType returns the Type field if non-nil, zero value otherwise. - -### GetTypeOk - -`func (o *Transaction) GetTypeOk() (*TransactionType, bool)` - -GetTypeOk returns a tuple with the Type field if it's non-nil, zero value otherwise -and a boolean to check if the value has been set. - -### SetType - -`func (o *Transaction) SetType(v TransactionType)` - -SetType sets Type field to given value. - - -### GetStatus - -`func (o *Transaction) GetStatus() TransactionStatus` - -GetStatus returns the Status field if non-nil, zero value otherwise. - -### GetStatusOk - -`func (o *Transaction) GetStatusOk() (*TransactionStatus, bool)` - -GetStatusOk returns a tuple with the Status field if it's non-nil, zero value otherwise -and a boolean to check if the value has been set. - -### SetStatus - -`func (o *Transaction) SetStatus(v TransactionStatus)` - -SetStatus sets Status field to given value. - - -### GetAmount - -`func (o *Transaction) GetAmount() string` - -GetAmount returns the Amount field if non-nil, zero value otherwise. - -### GetAmountOk - -`func (o *Transaction) GetAmountOk() (*string, bool)` - -GetAmountOk returns a tuple with the Amount field if it's non-nil, zero value otherwise -and a boolean to check if the value has been set. - -### SetAmount - -`func (o *Transaction) SetAmount(v string)` - -SetAmount sets Amount field to given value. - - -### GetSourceAccountID - -`func (o *Transaction) GetSourceAccountID() string` - -GetSourceAccountID returns the SourceAccountID field if non-nil, zero value otherwise. - -### GetSourceAccountIDOk - -`func (o *Transaction) GetSourceAccountIDOk() (*string, bool)` - -GetSourceAccountIDOk returns a tuple with the SourceAccountID field if it's non-nil, zero value otherwise -and a boolean to check if the value has been set. - -### SetSourceAccountID - -`func (o *Transaction) SetSourceAccountID(v string)` - -SetSourceAccountID sets SourceAccountID field to given value. - -### HasSourceAccountID - -`func (o *Transaction) HasSourceAccountID() bool` - -HasSourceAccountID returns a boolean if a field has been set. - -### GetDestinationAccountID - -`func (o *Transaction) GetDestinationAccountID() string` - -GetDestinationAccountID returns the DestinationAccountID field if non-nil, zero value otherwise. - -### GetDestinationAccountIDOk - -`func (o *Transaction) GetDestinationAccountIDOk() (*string, bool)` - -GetDestinationAccountIDOk returns a tuple with the DestinationAccountID field if it's non-nil, zero value otherwise -and a boolean to check if the value has been set. - -### SetDestinationAccountID - -`func (o *Transaction) SetDestinationAccountID(v string)` - -SetDestinationAccountID sets DestinationAccountID field to given value. - -### HasDestinationAccountID - -`func (o *Transaction) HasDestinationAccountID() bool` - -HasDestinationAccountID returns a boolean if a field has been set. - -### GetMetadata - -`func (o *Transaction) GetMetadata() map[string]string` - -GetMetadata returns the Metadata field if non-nil, zero value otherwise. - -### GetMetadataOk - -`func (o *Transaction) GetMetadataOk() (*map[string]string, bool)` - -GetMetadataOk returns a tuple with the Metadata field if it's non-nil, zero value otherwise -and a boolean to check if the value has been set. - -### SetMetadata - -`func (o *Transaction) SetMetadata(v map[string]string)` - -SetMetadata sets Metadata field to given value. - -### HasMetadata - -`func (o *Transaction) HasMetadata() bool` - -HasMetadata returns a boolean if a field has been set. - -### SetMetadataNil - -`func (o *Transaction) SetMetadataNil(b bool)` - - SetMetadataNil sets the value for Metadata to be an explicit nil - -### UnsetMetadata -`func (o *Transaction) UnsetMetadata()` - -UnsetMetadata ensures that no value is present for Metadata, not even an explicit nil - -[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/docs/TransactionStatus.md b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/docs/TransactionStatus.md deleted file mode 100644 index 6a25fbcad9..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/docs/TransactionStatus.md +++ /dev/null @@ -1,15 +0,0 @@ -# TransactionStatus - -## Enum - - -* `PENDING` (value: `"PENDING"`) - -* `SUCCEEDED` (value: `"SUCCEEDED"`) - -* `FAILED` (value: `"FAILED"`) - - -[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/docs/TransactionType.md b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/docs/TransactionType.md deleted file mode 100644 index a1df99c8f8..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/docs/TransactionType.md +++ /dev/null @@ -1,15 +0,0 @@ -# TransactionType - -## Enum - - -* `PAYIN` (value: `"PAYIN"`) - -* `PAYOUT` (value: `"PAYOUT"`) - -* `TRANSFER` (value: `"TRANSFER"`) - - -[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/git_push.sh b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/git_push.sh deleted file mode 100644 index aa98ffa9cb..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/git_push.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/sh -# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ -# -# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com" - -git_user_id=$1 -git_repo_id=$2 -release_note=$3 -git_host=$4 - -if [ "$git_host" = "" ]; then - git_host="github.com" - echo "[INFO] No command line input provided. Set \$git_host to $git_host" -fi - -if [ "$git_user_id" = "" ]; then - git_user_id="formancehq" - echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" -fi - -if [ "$git_repo_id" = "" ]; then - git_repo_id="payments" - echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" -fi - -if [ "$release_note" = "" ]; then - release_note="Minor update" - echo "[INFO] No command line input provided. Set \$release_note to $release_note" -fi - -# Initialize the local directory as a Git repository -git init - -# Adds the files in the local repository and stages them for commit. -git add . - -# Commits the tracked changes and prepares them to be pushed to a remote repository. -git commit -m "$release_note" - -# Sets the new remote -git_remote=$(git remote) -if [ "$git_remote" = "" ]; then # git remote not defined - - if [ "$GIT_TOKEN" = "" ]; then - echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." - git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git - else - git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git - fi - -fi - -git pull origin master - -# Pushes (Forces) the changes in the local repository up to the remote repository -echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" -git push origin master 2>&1 | grep -v 'To https' diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/go.mod b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/go.mod deleted file mode 100644 index 8ffaa15379..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/formancehq/payments/genericclient - -go 1.18 diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/model_account.go b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/model_account.go deleted file mode 100644 index 9cf515289b..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/model_account.go +++ /dev/null @@ -1,209 +0,0 @@ -/* -GENERIC connector API - -No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - -API version: v0.1 -*/ - -// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. - -package genericclient - -import ( - "encoding/json" - "time" -) - -// checks if the Account type satisfies the MappedNullable interface at compile time -var _ MappedNullable = &Account{} - -// Account struct for Account -type Account struct { - Id string `json:"id"` - AccountName string `json:"accountName"` - CreatedAt time.Time `json:"createdAt"` - Metadata map[string]string `json:"metadata,omitempty"` -} - -// NewAccount instantiates a new Account object -// This constructor will assign default values to properties that have it defined, -// and makes sure properties required by API are set, but the set of arguments -// will change when the set of required properties is changed -func NewAccount(id string, accountName string, createdAt time.Time) *Account { - this := Account{} - this.Id = id - this.AccountName = accountName - this.CreatedAt = createdAt - return &this -} - -// NewAccountWithDefaults instantiates a new Account object -// This constructor will only assign default values to properties that have it defined, -// but it doesn't guarantee that properties required by API are set -func NewAccountWithDefaults() *Account { - this := Account{} - return &this -} - -// GetId returns the Id field value -func (o *Account) GetId() string { - if o == nil { - var ret string - return ret - } - - return o.Id -} - -// GetIdOk returns a tuple with the Id field value -// and a boolean to check if the value has been set. -func (o *Account) GetIdOk() (*string, bool) { - if o == nil { - return nil, false - } - return &o.Id, true -} - -// SetId sets field value -func (o *Account) SetId(v string) { - o.Id = v -} - -// GetAccountName returns the AccountName field value -func (o *Account) GetAccountName() string { - if o == nil { - var ret string - return ret - } - - return o.AccountName -} - -// GetAccountNameOk returns a tuple with the AccountName field value -// and a boolean to check if the value has been set. -func (o *Account) GetAccountNameOk() (*string, bool) { - if o == nil { - return nil, false - } - return &o.AccountName, true -} - -// SetAccountName sets field value -func (o *Account) SetAccountName(v string) { - o.AccountName = v -} - -// GetCreatedAt returns the CreatedAt field value -func (o *Account) GetCreatedAt() time.Time { - if o == nil { - var ret time.Time - return ret - } - - return o.CreatedAt -} - -// GetCreatedAtOk returns a tuple with the CreatedAt field value -// and a boolean to check if the value has been set. -func (o *Account) GetCreatedAtOk() (*time.Time, bool) { - if o == nil { - return nil, false - } - return &o.CreatedAt, true -} - -// SetCreatedAt sets field value -func (o *Account) SetCreatedAt(v time.Time) { - o.CreatedAt = v -} - -// GetMetadata returns the Metadata field value if set, zero value otherwise (both if not set or set to explicit null). -func (o *Account) GetMetadata() map[string]string { - if o == nil { - var ret map[string]string - return ret - } - return o.Metadata -} - -// GetMetadataOk returns a tuple with the Metadata field value if set, nil otherwise -// and a boolean to check if the value has been set. -// NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *Account) GetMetadataOk() (*map[string]string, bool) { - if o == nil || IsNil(o.Metadata) { - return nil, false - } - return &o.Metadata, true -} - -// HasMetadata returns a boolean if a field has been set. -func (o *Account) HasMetadata() bool { - if o != nil && IsNil(o.Metadata) { - return true - } - - return false -} - -// SetMetadata gets a reference to the given map[string]string and assigns it to the Metadata field. -func (o *Account) SetMetadata(v map[string]string) { - o.Metadata = v -} - -func (o Account) MarshalJSON() ([]byte, error) { - toSerialize,err := o.ToMap() - if err != nil { - return []byte{}, err - } - return json.Marshal(toSerialize) -} - -func (o Account) ToMap() (map[string]interface{}, error) { - toSerialize := map[string]interface{}{} - toSerialize["id"] = o.Id - toSerialize["accountName"] = o.AccountName - toSerialize["createdAt"] = o.CreatedAt - if o.Metadata != nil { - toSerialize["metadata"] = o.Metadata - } - return toSerialize, nil -} - -type NullableAccount struct { - value *Account - isSet bool -} - -func (v NullableAccount) Get() *Account { - return v.value -} - -func (v *NullableAccount) Set(val *Account) { - v.value = val - v.isSet = true -} - -func (v NullableAccount) IsSet() bool { - return v.isSet -} - -func (v *NullableAccount) Unset() { - v.value = nil - v.isSet = false -} - -func NewNullableAccount(val *Account) *NullableAccount { - return &NullableAccount{value: val, isSet: true} -} - -func (v NullableAccount) MarshalJSON() ([]byte, error) { - return json.Marshal(v.value) -} - -func (v *NullableAccount) UnmarshalJSON(src []byte) error { - v.isSet = true - return json.Unmarshal(src, &v.value) -} - - diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/model_balance.go b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/model_balance.go deleted file mode 100644 index b19651a845..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/model_balance.go +++ /dev/null @@ -1,144 +0,0 @@ -/* -GENERIC connector API - -No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - -API version: v0.1 -*/ - -// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. - -package genericclient - -import ( - "encoding/json" -) - -// checks if the Balance type satisfies the MappedNullable interface at compile time -var _ MappedNullable = &Balance{} - -// Balance struct for Balance -type Balance struct { - Amount string `json:"amount"` - Currency string `json:"currency"` -} - -// NewBalance instantiates a new Balance object -// This constructor will assign default values to properties that have it defined, -// and makes sure properties required by API are set, but the set of arguments -// will change when the set of required properties is changed -func NewBalance(amount string, currency string) *Balance { - this := Balance{} - this.Amount = amount - this.Currency = currency - return &this -} - -// NewBalanceWithDefaults instantiates a new Balance object -// This constructor will only assign default values to properties that have it defined, -// but it doesn't guarantee that properties required by API are set -func NewBalanceWithDefaults() *Balance { - this := Balance{} - return &this -} - -// GetAmount returns the Amount field value -func (o *Balance) GetAmount() string { - if o == nil { - var ret string - return ret - } - - return o.Amount -} - -// GetAmountOk returns a tuple with the Amount field value -// and a boolean to check if the value has been set. -func (o *Balance) GetAmountOk() (*string, bool) { - if o == nil { - return nil, false - } - return &o.Amount, true -} - -// SetAmount sets field value -func (o *Balance) SetAmount(v string) { - o.Amount = v -} - -// GetCurrency returns the Currency field value -func (o *Balance) GetCurrency() string { - if o == nil { - var ret string - return ret - } - - return o.Currency -} - -// GetCurrencyOk returns a tuple with the Currency field value -// and a boolean to check if the value has been set. -func (o *Balance) GetCurrencyOk() (*string, bool) { - if o == nil { - return nil, false - } - return &o.Currency, true -} - -// SetCurrency sets field value -func (o *Balance) SetCurrency(v string) { - o.Currency = v -} - -func (o Balance) MarshalJSON() ([]byte, error) { - toSerialize,err := o.ToMap() - if err != nil { - return []byte{}, err - } - return json.Marshal(toSerialize) -} - -func (o Balance) ToMap() (map[string]interface{}, error) { - toSerialize := map[string]interface{}{} - toSerialize["amount"] = o.Amount - toSerialize["currency"] = o.Currency - return toSerialize, nil -} - -type NullableBalance struct { - value *Balance - isSet bool -} - -func (v NullableBalance) Get() *Balance { - return v.value -} - -func (v *NullableBalance) Set(val *Balance) { - v.value = val - v.isSet = true -} - -func (v NullableBalance) IsSet() bool { - return v.isSet -} - -func (v *NullableBalance) Unset() { - v.value = nil - v.isSet = false -} - -func NewNullableBalance(val *Balance) *NullableBalance { - return &NullableBalance{value: val, isSet: true} -} - -func (v NullableBalance) MarshalJSON() ([]byte, error) { - return json.Marshal(v.value) -} - -func (v *NullableBalance) UnmarshalJSON(src []byte) error { - v.isSet = true - return json.Unmarshal(src, &v.value) -} - - diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/model_balances.go b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/model_balances.go deleted file mode 100644 index e106b17c14..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/model_balances.go +++ /dev/null @@ -1,199 +0,0 @@ -/* -GENERIC connector API - -No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - -API version: v0.1 -*/ - -// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. - -package genericclient - -import ( - "encoding/json" - "time" -) - -// checks if the Balances type satisfies the MappedNullable interface at compile time -var _ MappedNullable = &Balances{} - -// Balances struct for Balances -type Balances struct { - Id string `json:"id"` - AccountID string `json:"accountID"` - At time.Time `json:"at"` - Balances []Balance `json:"balances"` -} - -// NewBalances instantiates a new Balances object -// This constructor will assign default values to properties that have it defined, -// and makes sure properties required by API are set, but the set of arguments -// will change when the set of required properties is changed -func NewBalances(id string, accountID string, at time.Time, balances []Balance) *Balances { - this := Balances{} - this.Id = id - this.AccountID = accountID - this.At = at - this.Balances = balances - return &this -} - -// NewBalancesWithDefaults instantiates a new Balances object -// This constructor will only assign default values to properties that have it defined, -// but it doesn't guarantee that properties required by API are set -func NewBalancesWithDefaults() *Balances { - this := Balances{} - return &this -} - -// GetId returns the Id field value -func (o *Balances) GetId() string { - if o == nil { - var ret string - return ret - } - - return o.Id -} - -// GetIdOk returns a tuple with the Id field value -// and a boolean to check if the value has been set. -func (o *Balances) GetIdOk() (*string, bool) { - if o == nil { - return nil, false - } - return &o.Id, true -} - -// SetId sets field value -func (o *Balances) SetId(v string) { - o.Id = v -} - -// GetAccountID returns the AccountID field value -func (o *Balances) GetAccountID() string { - if o == nil { - var ret string - return ret - } - - return o.AccountID -} - -// GetAccountIDOk returns a tuple with the AccountID field value -// and a boolean to check if the value has been set. -func (o *Balances) GetAccountIDOk() (*string, bool) { - if o == nil { - return nil, false - } - return &o.AccountID, true -} - -// SetAccountID sets field value -func (o *Balances) SetAccountID(v string) { - o.AccountID = v -} - -// GetAt returns the At field value -func (o *Balances) GetAt() time.Time { - if o == nil { - var ret time.Time - return ret - } - - return o.At -} - -// GetAtOk returns a tuple with the At field value -// and a boolean to check if the value has been set. -func (o *Balances) GetAtOk() (*time.Time, bool) { - if o == nil { - return nil, false - } - return &o.At, true -} - -// SetAt sets field value -func (o *Balances) SetAt(v time.Time) { - o.At = v -} - -// GetBalances returns the Balances field value -func (o *Balances) GetBalances() []Balance { - if o == nil { - var ret []Balance - return ret - } - - return o.Balances -} - -// GetBalancesOk returns a tuple with the Balances field value -// and a boolean to check if the value has been set. -func (o *Balances) GetBalancesOk() ([]Balance, bool) { - if o == nil { - return nil, false - } - return o.Balances, true -} - -// SetBalances sets field value -func (o *Balances) SetBalances(v []Balance) { - o.Balances = v -} - -func (o Balances) MarshalJSON() ([]byte, error) { - toSerialize,err := o.ToMap() - if err != nil { - return []byte{}, err - } - return json.Marshal(toSerialize) -} - -func (o Balances) ToMap() (map[string]interface{}, error) { - toSerialize := map[string]interface{}{} - toSerialize["id"] = o.Id - toSerialize["accountID"] = o.AccountID - toSerialize["at"] = o.At - toSerialize["balances"] = o.Balances - return toSerialize, nil -} - -type NullableBalances struct { - value *Balances - isSet bool -} - -func (v NullableBalances) Get() *Balances { - return v.value -} - -func (v *NullableBalances) Set(val *Balances) { - v.value = val - v.isSet = true -} - -func (v NullableBalances) IsSet() bool { - return v.isSet -} - -func (v *NullableBalances) Unset() { - v.value = nil - v.isSet = false -} - -func NewNullableBalances(val *Balances) *NullableBalances { - return &NullableBalances{value: val, isSet: true} -} - -func (v NullableBalances) MarshalJSON() ([]byte, error) { - return json.Marshal(v.value) -} - -func (v *NullableBalances) UnmarshalJSON(src []byte) error { - v.isSet = true - return json.Unmarshal(src, &v.value) -} - - diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/model_beneficiary.go b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/model_beneficiary.go deleted file mode 100644 index 9677bd5af4..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/model_beneficiary.go +++ /dev/null @@ -1,209 +0,0 @@ -/* -GENERIC connector API - -No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - -API version: v0.1 -*/ - -// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. - -package genericclient - -import ( - "encoding/json" - "time" -) - -// checks if the Beneficiary type satisfies the MappedNullable interface at compile time -var _ MappedNullable = &Beneficiary{} - -// Beneficiary struct for Beneficiary -type Beneficiary struct { - Id string `json:"id"` - CreatedAt time.Time `json:"createdAt"` - OwnerName string `json:"ownerName"` - Metadata map[string]string `json:"metadata,omitempty"` -} - -// NewBeneficiary instantiates a new Beneficiary object -// This constructor will assign default values to properties that have it defined, -// and makes sure properties required by API are set, but the set of arguments -// will change when the set of required properties is changed -func NewBeneficiary(id string, createdAt time.Time, ownerName string) *Beneficiary { - this := Beneficiary{} - this.Id = id - this.CreatedAt = createdAt - this.OwnerName = ownerName - return &this -} - -// NewBeneficiaryWithDefaults instantiates a new Beneficiary object -// This constructor will only assign default values to properties that have it defined, -// but it doesn't guarantee that properties required by API are set -func NewBeneficiaryWithDefaults() *Beneficiary { - this := Beneficiary{} - return &this -} - -// GetId returns the Id field value -func (o *Beneficiary) GetId() string { - if o == nil { - var ret string - return ret - } - - return o.Id -} - -// GetIdOk returns a tuple with the Id field value -// and a boolean to check if the value has been set. -func (o *Beneficiary) GetIdOk() (*string, bool) { - if o == nil { - return nil, false - } - return &o.Id, true -} - -// SetId sets field value -func (o *Beneficiary) SetId(v string) { - o.Id = v -} - -// GetCreatedAt returns the CreatedAt field value -func (o *Beneficiary) GetCreatedAt() time.Time { - if o == nil { - var ret time.Time - return ret - } - - return o.CreatedAt -} - -// GetCreatedAtOk returns a tuple with the CreatedAt field value -// and a boolean to check if the value has been set. -func (o *Beneficiary) GetCreatedAtOk() (*time.Time, bool) { - if o == nil { - return nil, false - } - return &o.CreatedAt, true -} - -// SetCreatedAt sets field value -func (o *Beneficiary) SetCreatedAt(v time.Time) { - o.CreatedAt = v -} - -// GetOwnerName returns the OwnerName field value -func (o *Beneficiary) GetOwnerName() string { - if o == nil { - var ret string - return ret - } - - return o.OwnerName -} - -// GetOwnerNameOk returns a tuple with the OwnerName field value -// and a boolean to check if the value has been set. -func (o *Beneficiary) GetOwnerNameOk() (*string, bool) { - if o == nil { - return nil, false - } - return &o.OwnerName, true -} - -// SetOwnerName sets field value -func (o *Beneficiary) SetOwnerName(v string) { - o.OwnerName = v -} - -// GetMetadata returns the Metadata field value if set, zero value otherwise (both if not set or set to explicit null). -func (o *Beneficiary) GetMetadata() map[string]string { - if o == nil { - var ret map[string]string - return ret - } - return o.Metadata -} - -// GetMetadataOk returns a tuple with the Metadata field value if set, nil otherwise -// and a boolean to check if the value has been set. -// NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *Beneficiary) GetMetadataOk() (*map[string]string, bool) { - if o == nil || IsNil(o.Metadata) { - return nil, false - } - return &o.Metadata, true -} - -// HasMetadata returns a boolean if a field has been set. -func (o *Beneficiary) HasMetadata() bool { - if o != nil && IsNil(o.Metadata) { - return true - } - - return false -} - -// SetMetadata gets a reference to the given map[string]string and assigns it to the Metadata field. -func (o *Beneficiary) SetMetadata(v map[string]string) { - o.Metadata = v -} - -func (o Beneficiary) MarshalJSON() ([]byte, error) { - toSerialize,err := o.ToMap() - if err != nil { - return []byte{}, err - } - return json.Marshal(toSerialize) -} - -func (o Beneficiary) ToMap() (map[string]interface{}, error) { - toSerialize := map[string]interface{}{} - toSerialize["id"] = o.Id - toSerialize["createdAt"] = o.CreatedAt - toSerialize["ownerName"] = o.OwnerName - if o.Metadata != nil { - toSerialize["metadata"] = o.Metadata - } - return toSerialize, nil -} - -type NullableBeneficiary struct { - value *Beneficiary - isSet bool -} - -func (v NullableBeneficiary) Get() *Beneficiary { - return v.value -} - -func (v *NullableBeneficiary) Set(val *Beneficiary) { - v.value = val - v.isSet = true -} - -func (v NullableBeneficiary) IsSet() bool { - return v.isSet -} - -func (v *NullableBeneficiary) Unset() { - v.value = nil - v.isSet = false -} - -func NewNullableBeneficiary(val *Beneficiary) *NullableBeneficiary { - return &NullableBeneficiary{value: val, isSet: true} -} - -func (v NullableBeneficiary) MarshalJSON() ([]byte, error) { - return json.Marshal(v.value) -} - -func (v *NullableBeneficiary) UnmarshalJSON(src []byte) error { - v.isSet = true - return json.Unmarshal(src, &v.value) -} - - diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/model_error.go b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/model_error.go deleted file mode 100644 index 085c21f12e..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/model_error.go +++ /dev/null @@ -1,144 +0,0 @@ -/* -GENERIC connector API - -No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - -API version: v0.1 -*/ - -// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. - -package genericclient - -import ( - "encoding/json" -) - -// checks if the Error type satisfies the MappedNullable interface at compile time -var _ MappedNullable = &Error{} - -// Error struct for Error -type Error struct { - Title string `json:"Title"` - Detail string `json:"Detail"` -} - -// NewError instantiates a new Error object -// This constructor will assign default values to properties that have it defined, -// and makes sure properties required by API are set, but the set of arguments -// will change when the set of required properties is changed -func NewError(title string, detail string) *Error { - this := Error{} - this.Title = title - this.Detail = detail - return &this -} - -// NewErrorWithDefaults instantiates a new Error object -// This constructor will only assign default values to properties that have it defined, -// but it doesn't guarantee that properties required by API are set -func NewErrorWithDefaults() *Error { - this := Error{} - return &this -} - -// GetTitle returns the Title field value -func (o *Error) GetTitle() string { - if o == nil { - var ret string - return ret - } - - return o.Title -} - -// GetTitleOk returns a tuple with the Title field value -// and a boolean to check if the value has been set. -func (o *Error) GetTitleOk() (*string, bool) { - if o == nil { - return nil, false - } - return &o.Title, true -} - -// SetTitle sets field value -func (o *Error) SetTitle(v string) { - o.Title = v -} - -// GetDetail returns the Detail field value -func (o *Error) GetDetail() string { - if o == nil { - var ret string - return ret - } - - return o.Detail -} - -// GetDetailOk returns a tuple with the Detail field value -// and a boolean to check if the value has been set. -func (o *Error) GetDetailOk() (*string, bool) { - if o == nil { - return nil, false - } - return &o.Detail, true -} - -// SetDetail sets field value -func (o *Error) SetDetail(v string) { - o.Detail = v -} - -func (o Error) MarshalJSON() ([]byte, error) { - toSerialize,err := o.ToMap() - if err != nil { - return []byte{}, err - } - return json.Marshal(toSerialize) -} - -func (o Error) ToMap() (map[string]interface{}, error) { - toSerialize := map[string]interface{}{} - toSerialize["Title"] = o.Title - toSerialize["Detail"] = o.Detail - return toSerialize, nil -} - -type NullableError struct { - value *Error - isSet bool -} - -func (v NullableError) Get() *Error { - return v.value -} - -func (v *NullableError) Set(val *Error) { - v.value = val - v.isSet = true -} - -func (v NullableError) IsSet() bool { - return v.isSet -} - -func (v *NullableError) Unset() { - v.value = nil - v.isSet = false -} - -func NewNullableError(val *Error) *NullableError { - return &NullableError{value: val, isSet: true} -} - -func (v NullableError) MarshalJSON() ([]byte, error) { - return json.Marshal(v.value) -} - -func (v *NullableError) UnmarshalJSON(src []byte) error { - v.isSet = true - return json.Unmarshal(src, &v.value) -} - - diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/model_transaction.go b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/model_transaction.go deleted file mode 100644 index 25b75a670b..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/model_transaction.go +++ /dev/null @@ -1,461 +0,0 @@ -/* -GENERIC connector API - -No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - -API version: v0.1 -*/ - -// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. - -package genericclient - -import ( - "encoding/json" - "time" -) - -// checks if the Transaction type satisfies the MappedNullable interface at compile time -var _ MappedNullable = &Transaction{} - -// Transaction struct for Transaction -type Transaction struct { - Id string `json:"id"` - RelatedTransactionID *string `json:"relatedTransactionID,omitempty"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - Currency string `json:"currency"` - Scheme *string `json:"scheme,omitempty"` - Type TransactionType `json:"type"` - Status TransactionStatus `json:"status"` - Amount string `json:"amount"` - SourceAccountID *string `json:"sourceAccountID,omitempty"` - DestinationAccountID *string `json:"destinationAccountID,omitempty"` - Metadata map[string]string `json:"metadata,omitempty"` -} - -// NewTransaction instantiates a new Transaction object -// This constructor will assign default values to properties that have it defined, -// and makes sure properties required by API are set, but the set of arguments -// will change when the set of required properties is changed -func NewTransaction(id string, createdAt time.Time, updatedAt time.Time, currency string, type_ TransactionType, status TransactionStatus, amount string) *Transaction { - this := Transaction{} - this.Id = id - this.CreatedAt = createdAt - this.UpdatedAt = updatedAt - this.Currency = currency - this.Type = type_ - this.Status = status - this.Amount = amount - return &this -} - -// NewTransactionWithDefaults instantiates a new Transaction object -// This constructor will only assign default values to properties that have it defined, -// but it doesn't guarantee that properties required by API are set -func NewTransactionWithDefaults() *Transaction { - this := Transaction{} - return &this -} - -// GetId returns the Id field value -func (o *Transaction) GetId() string { - if o == nil { - var ret string - return ret - } - - return o.Id -} - -// GetIdOk returns a tuple with the Id field value -// and a boolean to check if the value has been set. -func (o *Transaction) GetIdOk() (*string, bool) { - if o == nil { - return nil, false - } - return &o.Id, true -} - -// SetId sets field value -func (o *Transaction) SetId(v string) { - o.Id = v -} - -// GetRelatedTransactionID returns the RelatedTransactionID field value if set, zero value otherwise. -func (o *Transaction) GetRelatedTransactionID() string { - if o == nil || IsNil(o.RelatedTransactionID) { - var ret string - return ret - } - return *o.RelatedTransactionID -} - -// GetRelatedTransactionIDOk returns a tuple with the RelatedTransactionID field value if set, nil otherwise -// and a boolean to check if the value has been set. -func (o *Transaction) GetRelatedTransactionIDOk() (*string, bool) { - if o == nil || IsNil(o.RelatedTransactionID) { - return nil, false - } - return o.RelatedTransactionID, true -} - -// HasRelatedTransactionID returns a boolean if a field has been set. -func (o *Transaction) HasRelatedTransactionID() bool { - if o != nil && !IsNil(o.RelatedTransactionID) { - return true - } - - return false -} - -// SetRelatedTransactionID gets a reference to the given string and assigns it to the RelatedTransactionID field. -func (o *Transaction) SetRelatedTransactionID(v string) { - o.RelatedTransactionID = &v -} - -// GetCreatedAt returns the CreatedAt field value -func (o *Transaction) GetCreatedAt() time.Time { - if o == nil { - var ret time.Time - return ret - } - - return o.CreatedAt -} - -// GetCreatedAtOk returns a tuple with the CreatedAt field value -// and a boolean to check if the value has been set. -func (o *Transaction) GetCreatedAtOk() (*time.Time, bool) { - if o == nil { - return nil, false - } - return &o.CreatedAt, true -} - -// SetCreatedAt sets field value -func (o *Transaction) SetCreatedAt(v time.Time) { - o.CreatedAt = v -} - -// GetUpdatedAt returns the UpdatedAt field value -func (o *Transaction) GetUpdatedAt() time.Time { - if o == nil { - var ret time.Time - return ret - } - - return o.UpdatedAt -} - -// GetUpdatedAtOk returns a tuple with the UpdatedAt field value -// and a boolean to check if the value has been set. -func (o *Transaction) GetUpdatedAtOk() (*time.Time, bool) { - if o == nil { - return nil, false - } - return &o.UpdatedAt, true -} - -// SetUpdatedAt sets field value -func (o *Transaction) SetUpdatedAt(v time.Time) { - o.UpdatedAt = v -} - -// GetCurrency returns the Currency field value -func (o *Transaction) GetCurrency() string { - if o == nil { - var ret string - return ret - } - - return o.Currency -} - -// GetCurrencyOk returns a tuple with the Currency field value -// and a boolean to check if the value has been set. -func (o *Transaction) GetCurrencyOk() (*string, bool) { - if o == nil { - return nil, false - } - return &o.Currency, true -} - -// SetCurrency sets field value -func (o *Transaction) SetCurrency(v string) { - o.Currency = v -} - -// GetScheme returns the Scheme field value if set, zero value otherwise. -func (o *Transaction) GetScheme() string { - if o == nil || IsNil(o.Scheme) { - var ret string - return ret - } - return *o.Scheme -} - -// GetSchemeOk returns a tuple with the Scheme field value if set, nil otherwise -// and a boolean to check if the value has been set. -func (o *Transaction) GetSchemeOk() (*string, bool) { - if o == nil || IsNil(o.Scheme) { - return nil, false - } - return o.Scheme, true -} - -// HasScheme returns a boolean if a field has been set. -func (o *Transaction) HasScheme() bool { - if o != nil && !IsNil(o.Scheme) { - return true - } - - return false -} - -// SetScheme gets a reference to the given string and assigns it to the Scheme field. -func (o *Transaction) SetScheme(v string) { - o.Scheme = &v -} - -// GetType returns the Type field value -func (o *Transaction) GetType() TransactionType { - if o == nil { - var ret TransactionType - return ret - } - - return o.Type -} - -// GetTypeOk returns a tuple with the Type field value -// and a boolean to check if the value has been set. -func (o *Transaction) GetTypeOk() (*TransactionType, bool) { - if o == nil { - return nil, false - } - return &o.Type, true -} - -// SetType sets field value -func (o *Transaction) SetType(v TransactionType) { - o.Type = v -} - -// GetStatus returns the Status field value -func (o *Transaction) GetStatus() TransactionStatus { - if o == nil { - var ret TransactionStatus - return ret - } - - return o.Status -} - -// GetStatusOk returns a tuple with the Status field value -// and a boolean to check if the value has been set. -func (o *Transaction) GetStatusOk() (*TransactionStatus, bool) { - if o == nil { - return nil, false - } - return &o.Status, true -} - -// SetStatus sets field value -func (o *Transaction) SetStatus(v TransactionStatus) { - o.Status = v -} - -// GetAmount returns the Amount field value -func (o *Transaction) GetAmount() string { - if o == nil { - var ret string - return ret - } - - return o.Amount -} - -// GetAmountOk returns a tuple with the Amount field value -// and a boolean to check if the value has been set. -func (o *Transaction) GetAmountOk() (*string, bool) { - if o == nil { - return nil, false - } - return &o.Amount, true -} - -// SetAmount sets field value -func (o *Transaction) SetAmount(v string) { - o.Amount = v -} - -// GetSourceAccountID returns the SourceAccountID field value if set, zero value otherwise. -func (o *Transaction) GetSourceAccountID() string { - if o == nil || IsNil(o.SourceAccountID) { - var ret string - return ret - } - return *o.SourceAccountID -} - -// GetSourceAccountIDOk returns a tuple with the SourceAccountID field value if set, nil otherwise -// and a boolean to check if the value has been set. -func (o *Transaction) GetSourceAccountIDOk() (*string, bool) { - if o == nil || IsNil(o.SourceAccountID) { - return nil, false - } - return o.SourceAccountID, true -} - -// HasSourceAccountID returns a boolean if a field has been set. -func (o *Transaction) HasSourceAccountID() bool { - if o != nil && !IsNil(o.SourceAccountID) { - return true - } - - return false -} - -// SetSourceAccountID gets a reference to the given string and assigns it to the SourceAccountID field. -func (o *Transaction) SetSourceAccountID(v string) { - o.SourceAccountID = &v -} - -// GetDestinationAccountID returns the DestinationAccountID field value if set, zero value otherwise. -func (o *Transaction) GetDestinationAccountID() string { - if o == nil || IsNil(o.DestinationAccountID) { - var ret string - return ret - } - return *o.DestinationAccountID -} - -// GetDestinationAccountIDOk returns a tuple with the DestinationAccountID field value if set, nil otherwise -// and a boolean to check if the value has been set. -func (o *Transaction) GetDestinationAccountIDOk() (*string, bool) { - if o == nil || IsNil(o.DestinationAccountID) { - return nil, false - } - return o.DestinationAccountID, true -} - -// HasDestinationAccountID returns a boolean if a field has been set. -func (o *Transaction) HasDestinationAccountID() bool { - if o != nil && !IsNil(o.DestinationAccountID) { - return true - } - - return false -} - -// SetDestinationAccountID gets a reference to the given string and assigns it to the DestinationAccountID field. -func (o *Transaction) SetDestinationAccountID(v string) { - o.DestinationAccountID = &v -} - -// GetMetadata returns the Metadata field value if set, zero value otherwise (both if not set or set to explicit null). -func (o *Transaction) GetMetadata() map[string]string { - if o == nil { - var ret map[string]string - return ret - } - return o.Metadata -} - -// GetMetadataOk returns a tuple with the Metadata field value if set, nil otherwise -// and a boolean to check if the value has been set. -// NOTE: If the value is an explicit nil, `nil, true` will be returned -func (o *Transaction) GetMetadataOk() (*map[string]string, bool) { - if o == nil || IsNil(o.Metadata) { - return nil, false - } - return &o.Metadata, true -} - -// HasMetadata returns a boolean if a field has been set. -func (o *Transaction) HasMetadata() bool { - if o != nil && IsNil(o.Metadata) { - return true - } - - return false -} - -// SetMetadata gets a reference to the given map[string]string and assigns it to the Metadata field. -func (o *Transaction) SetMetadata(v map[string]string) { - o.Metadata = v -} - -func (o Transaction) MarshalJSON() ([]byte, error) { - toSerialize,err := o.ToMap() - if err != nil { - return []byte{}, err - } - return json.Marshal(toSerialize) -} - -func (o Transaction) ToMap() (map[string]interface{}, error) { - toSerialize := map[string]interface{}{} - toSerialize["id"] = o.Id - if !IsNil(o.RelatedTransactionID) { - toSerialize["relatedTransactionID"] = o.RelatedTransactionID - } - toSerialize["createdAt"] = o.CreatedAt - toSerialize["updatedAt"] = o.UpdatedAt - toSerialize["currency"] = o.Currency - if !IsNil(o.Scheme) { - toSerialize["scheme"] = o.Scheme - } - toSerialize["type"] = o.Type - toSerialize["status"] = o.Status - toSerialize["amount"] = o.Amount - if !IsNil(o.SourceAccountID) { - toSerialize["sourceAccountID"] = o.SourceAccountID - } - if !IsNil(o.DestinationAccountID) { - toSerialize["destinationAccountID"] = o.DestinationAccountID - } - if o.Metadata != nil { - toSerialize["metadata"] = o.Metadata - } - return toSerialize, nil -} - -type NullableTransaction struct { - value *Transaction - isSet bool -} - -func (v NullableTransaction) Get() *Transaction { - return v.value -} - -func (v *NullableTransaction) Set(val *Transaction) { - v.value = val - v.isSet = true -} - -func (v NullableTransaction) IsSet() bool { - return v.isSet -} - -func (v *NullableTransaction) Unset() { - v.value = nil - v.isSet = false -} - -func NewNullableTransaction(val *Transaction) *NullableTransaction { - return &NullableTransaction{value: val, isSet: true} -} - -func (v NullableTransaction) MarshalJSON() ([]byte, error) { - return json.Marshal(v.value) -} - -func (v *NullableTransaction) UnmarshalJSON(src []byte) error { - v.isSet = true - return json.Unmarshal(src, &v.value) -} - - diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/model_transaction_status.go b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/model_transaction_status.go deleted file mode 100644 index 135e86e7a0..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/model_transaction_status.go +++ /dev/null @@ -1,113 +0,0 @@ -/* -GENERIC connector API - -No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - -API version: v0.1 -*/ - -// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. - -package genericclient - -import ( - "encoding/json" - "fmt" -) - -// TransactionStatus the model 'TransactionStatus' -type TransactionStatus string - -// List of TransactionStatus -const ( - PENDING TransactionStatus = "PENDING" - SUCCEEDED TransactionStatus = "SUCCEEDED" - FAILED TransactionStatus = "FAILED" -) - -// All allowed values of TransactionStatus enum -var AllowedTransactionStatusEnumValues = []TransactionStatus{ - "PENDING", - "SUCCEEDED", - "FAILED", -} - -func (v *TransactionStatus) UnmarshalJSON(src []byte) error { - var value string - err := json.Unmarshal(src, &value) - if err != nil { - return err - } - enumTypeValue := TransactionStatus(value) - for _, existing := range AllowedTransactionStatusEnumValues { - if existing == enumTypeValue { - *v = enumTypeValue - return nil - } - } - - return fmt.Errorf("%+v is not a valid TransactionStatus", value) -} - -// NewTransactionStatusFromValue returns a pointer to a valid TransactionStatus -// for the value passed as argument, or an error if the value passed is not allowed by the enum -func NewTransactionStatusFromValue(v string) (*TransactionStatus, error) { - ev := TransactionStatus(v) - if ev.IsValid() { - return &ev, nil - } else { - return nil, fmt.Errorf("invalid value '%v' for TransactionStatus: valid values are %v", v, AllowedTransactionStatusEnumValues) - } -} - -// IsValid return true if the value is valid for the enum, false otherwise -func (v TransactionStatus) IsValid() bool { - for _, existing := range AllowedTransactionStatusEnumValues { - if existing == v { - return true - } - } - return false -} - -// Ptr returns reference to TransactionStatus value -func (v TransactionStatus) Ptr() *TransactionStatus { - return &v -} - -type NullableTransactionStatus struct { - value *TransactionStatus - isSet bool -} - -func (v NullableTransactionStatus) Get() *TransactionStatus { - return v.value -} - -func (v *NullableTransactionStatus) Set(val *TransactionStatus) { - v.value = val - v.isSet = true -} - -func (v NullableTransactionStatus) IsSet() bool { - return v.isSet -} - -func (v *NullableTransactionStatus) Unset() { - v.value = nil - v.isSet = false -} - -func NewNullableTransactionStatus(val *TransactionStatus) *NullableTransactionStatus { - return &NullableTransactionStatus{value: val, isSet: true} -} - -func (v NullableTransactionStatus) MarshalJSON() ([]byte, error) { - return json.Marshal(v.value) -} - -func (v *NullableTransactionStatus) UnmarshalJSON(src []byte) error { - v.isSet = true - return json.Unmarshal(src, &v.value) -} - diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/model_transaction_type.go b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/model_transaction_type.go deleted file mode 100644 index 0b6ee10aa5..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/model_transaction_type.go +++ /dev/null @@ -1,113 +0,0 @@ -/* -GENERIC connector API - -No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - -API version: v0.1 -*/ - -// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. - -package genericclient - -import ( - "encoding/json" - "fmt" -) - -// TransactionType the model 'TransactionType' -type TransactionType string - -// List of TransactionType -const ( - PAYIN TransactionType = "PAYIN" - PAYOUT TransactionType = "PAYOUT" - TRANSFER TransactionType = "TRANSFER" -) - -// All allowed values of TransactionType enum -var AllowedTransactionTypeEnumValues = []TransactionType{ - "PAYIN", - "PAYOUT", - "TRANSFER", -} - -func (v *TransactionType) UnmarshalJSON(src []byte) error { - var value string - err := json.Unmarshal(src, &value) - if err != nil { - return err - } - enumTypeValue := TransactionType(value) - for _, existing := range AllowedTransactionTypeEnumValues { - if existing == enumTypeValue { - *v = enumTypeValue - return nil - } - } - - return fmt.Errorf("%+v is not a valid TransactionType", value) -} - -// NewTransactionTypeFromValue returns a pointer to a valid TransactionType -// for the value passed as argument, or an error if the value passed is not allowed by the enum -func NewTransactionTypeFromValue(v string) (*TransactionType, error) { - ev := TransactionType(v) - if ev.IsValid() { - return &ev, nil - } else { - return nil, fmt.Errorf("invalid value '%v' for TransactionType: valid values are %v", v, AllowedTransactionTypeEnumValues) - } -} - -// IsValid return true if the value is valid for the enum, false otherwise -func (v TransactionType) IsValid() bool { - for _, existing := range AllowedTransactionTypeEnumValues { - if existing == v { - return true - } - } - return false -} - -// Ptr returns reference to TransactionType value -func (v TransactionType) Ptr() *TransactionType { - return &v -} - -type NullableTransactionType struct { - value *TransactionType - isSet bool -} - -func (v NullableTransactionType) Get() *TransactionType { - return v.value -} - -func (v *NullableTransactionType) Set(val *TransactionType) { - v.value = val - v.isSet = true -} - -func (v NullableTransactionType) IsSet() bool { - return v.isSet -} - -func (v *NullableTransactionType) Unset() { - v.value = nil - v.isSet = false -} - -func NewNullableTransactionType(val *TransactionType) *NullableTransactionType { - return &NullableTransactionType{value: val, isSet: true} -} - -func (v NullableTransactionType) MarshalJSON() ([]byte, error) { - return json.Marshal(v.value) -} - -func (v *NullableTransactionType) UnmarshalJSON(src []byte) error { - v.isSet = true - return json.Unmarshal(src, &v.value) -} - diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/response.go b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/response.go deleted file mode 100644 index 404b486b0b..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/response.go +++ /dev/null @@ -1,47 +0,0 @@ -/* -GENERIC connector API - -No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - -API version: v0.1 -*/ - -// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. - -package genericclient - -import ( - "net/http" -) - -// APIResponse stores the API response returned by the server. -type APIResponse struct { - *http.Response `json:"-"` - Message string `json:"message,omitempty"` - // Operation is the name of the OpenAPI operation. - Operation string `json:"operation,omitempty"` - // RequestURL is the request URL. This value is always available, even if the - // embedded *http.Response is nil. - RequestURL string `json:"url,omitempty"` - // Method is the HTTP method used for the request. This value is always - // available, even if the embedded *http.Response is nil. - Method string `json:"method,omitempty"` - // Payload holds the contents of the response body (which may be nil or empty). - // This is provided here as the raw response.Body() reader will have already - // been drained. - Payload []byte `json:"-"` -} - -// NewAPIResponse returns a new APIResponse object. -func NewAPIResponse(r *http.Response) *APIResponse { - - response := &APIResponse{Response: r} - return response -} - -// NewAPIResponseWithError returns a new APIResponse object with the provided error message. -func NewAPIResponseWithError(errorMessage string) *APIResponse { - - response := &APIResponse{Message: errorMessage} - return response -} diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/utils.go b/components/payments/cmd/connectors/internal/connectors/generic/client/generated/utils.go deleted file mode 100644 index eb4d077e94..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/utils.go +++ /dev/null @@ -1,347 +0,0 @@ -/* -GENERIC connector API - -No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) - -API version: v0.1 -*/ - -// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. - -package genericclient - -import ( - "encoding/json" - "reflect" - "time" -) - -// PtrBool is a helper routine that returns a pointer to given boolean value. -func PtrBool(v bool) *bool { return &v } - -// PtrInt is a helper routine that returns a pointer to given integer value. -func PtrInt(v int) *int { return &v } - -// PtrInt32 is a helper routine that returns a pointer to given integer value. -func PtrInt32(v int32) *int32 { return &v } - -// PtrInt64 is a helper routine that returns a pointer to given integer value. -func PtrInt64(v int64) *int64 { return &v } - -// PtrFloat32 is a helper routine that returns a pointer to given float value. -func PtrFloat32(v float32) *float32 { return &v } - -// PtrFloat64 is a helper routine that returns a pointer to given float value. -func PtrFloat64(v float64) *float64 { return &v } - -// PtrString is a helper routine that returns a pointer to given string value. -func PtrString(v string) *string { return &v } - -// PtrTime is helper routine that returns a pointer to given Time value. -func PtrTime(v time.Time) *time.Time { return &v } - -type NullableBool struct { - value *bool - isSet bool -} - -func (v NullableBool) Get() *bool { - return v.value -} - -func (v *NullableBool) Set(val *bool) { - v.value = val - v.isSet = true -} - -func (v NullableBool) IsSet() bool { - return v.isSet -} - -func (v *NullableBool) Unset() { - v.value = nil - v.isSet = false -} - -func NewNullableBool(val *bool) *NullableBool { - return &NullableBool{value: val, isSet: true} -} - -func (v NullableBool) MarshalJSON() ([]byte, error) { - return json.Marshal(v.value) -} - -func (v *NullableBool) UnmarshalJSON(src []byte) error { - v.isSet = true - return json.Unmarshal(src, &v.value) -} - -type NullableInt struct { - value *int - isSet bool -} - -func (v NullableInt) Get() *int { - return v.value -} - -func (v *NullableInt) Set(val *int) { - v.value = val - v.isSet = true -} - -func (v NullableInt) IsSet() bool { - return v.isSet -} - -func (v *NullableInt) Unset() { - v.value = nil - v.isSet = false -} - -func NewNullableInt(val *int) *NullableInt { - return &NullableInt{value: val, isSet: true} -} - -func (v NullableInt) MarshalJSON() ([]byte, error) { - return json.Marshal(v.value) -} - -func (v *NullableInt) UnmarshalJSON(src []byte) error { - v.isSet = true - return json.Unmarshal(src, &v.value) -} - -type NullableInt32 struct { - value *int32 - isSet bool -} - -func (v NullableInt32) Get() *int32 { - return v.value -} - -func (v *NullableInt32) Set(val *int32) { - v.value = val - v.isSet = true -} - -func (v NullableInt32) IsSet() bool { - return v.isSet -} - -func (v *NullableInt32) Unset() { - v.value = nil - v.isSet = false -} - -func NewNullableInt32(val *int32) *NullableInt32 { - return &NullableInt32{value: val, isSet: true} -} - -func (v NullableInt32) MarshalJSON() ([]byte, error) { - return json.Marshal(v.value) -} - -func (v *NullableInt32) UnmarshalJSON(src []byte) error { - v.isSet = true - return json.Unmarshal(src, &v.value) -} - -type NullableInt64 struct { - value *int64 - isSet bool -} - -func (v NullableInt64) Get() *int64 { - return v.value -} - -func (v *NullableInt64) Set(val *int64) { - v.value = val - v.isSet = true -} - -func (v NullableInt64) IsSet() bool { - return v.isSet -} - -func (v *NullableInt64) Unset() { - v.value = nil - v.isSet = false -} - -func NewNullableInt64(val *int64) *NullableInt64 { - return &NullableInt64{value: val, isSet: true} -} - -func (v NullableInt64) MarshalJSON() ([]byte, error) { - return json.Marshal(v.value) -} - -func (v *NullableInt64) UnmarshalJSON(src []byte) error { - v.isSet = true - return json.Unmarshal(src, &v.value) -} - -type NullableFloat32 struct { - value *float32 - isSet bool -} - -func (v NullableFloat32) Get() *float32 { - return v.value -} - -func (v *NullableFloat32) Set(val *float32) { - v.value = val - v.isSet = true -} - -func (v NullableFloat32) IsSet() bool { - return v.isSet -} - -func (v *NullableFloat32) Unset() { - v.value = nil - v.isSet = false -} - -func NewNullableFloat32(val *float32) *NullableFloat32 { - return &NullableFloat32{value: val, isSet: true} -} - -func (v NullableFloat32) MarshalJSON() ([]byte, error) { - return json.Marshal(v.value) -} - -func (v *NullableFloat32) UnmarshalJSON(src []byte) error { - v.isSet = true - return json.Unmarshal(src, &v.value) -} - -type NullableFloat64 struct { - value *float64 - isSet bool -} - -func (v NullableFloat64) Get() *float64 { - return v.value -} - -func (v *NullableFloat64) Set(val *float64) { - v.value = val - v.isSet = true -} - -func (v NullableFloat64) IsSet() bool { - return v.isSet -} - -func (v *NullableFloat64) Unset() { - v.value = nil - v.isSet = false -} - -func NewNullableFloat64(val *float64) *NullableFloat64 { - return &NullableFloat64{value: val, isSet: true} -} - -func (v NullableFloat64) MarshalJSON() ([]byte, error) { - return json.Marshal(v.value) -} - -func (v *NullableFloat64) UnmarshalJSON(src []byte) error { - v.isSet = true - return json.Unmarshal(src, &v.value) -} - -type NullableString struct { - value *string - isSet bool -} - -func (v NullableString) Get() *string { - return v.value -} - -func (v *NullableString) Set(val *string) { - v.value = val - v.isSet = true -} - -func (v NullableString) IsSet() bool { - return v.isSet -} - -func (v *NullableString) Unset() { - v.value = nil - v.isSet = false -} - -func NewNullableString(val *string) *NullableString { - return &NullableString{value: val, isSet: true} -} - -func (v NullableString) MarshalJSON() ([]byte, error) { - return json.Marshal(v.value) -} - -func (v *NullableString) UnmarshalJSON(src []byte) error { - v.isSet = true - return json.Unmarshal(src, &v.value) -} - -type NullableTime struct { - value *time.Time - isSet bool -} - -func (v NullableTime) Get() *time.Time { - return v.value -} - -func (v *NullableTime) Set(val *time.Time) { - v.value = val - v.isSet = true -} - -func (v NullableTime) IsSet() bool { - return v.isSet -} - -func (v *NullableTime) Unset() { - v.value = nil - v.isSet = false -} - -func NewNullableTime(val *time.Time) *NullableTime { - return &NullableTime{value: val, isSet: true} -} - -func (v NullableTime) MarshalJSON() ([]byte, error) { - return v.value.MarshalJSON() -} - -func (v *NullableTime) UnmarshalJSON(src []byte) error { - v.isSet = true - return json.Unmarshal(src, &v.value) -} - -// IsNil checks if an input is nil -func IsNil(i interface{}) bool { - if i == nil { - return true - } - switch reflect.TypeOf(i).Kind() { - case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.UnsafePointer, reflect.Interface, reflect.Slice: - return reflect.ValueOf(i).IsNil() - case reflect.Array: - return reflect.ValueOf(i).IsZero() - } - return false -} - -type MappedNullable interface { - ToMap() (map[string]interface{}, error) -} diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generic-openapi.yaml b/components/payments/cmd/connectors/internal/connectors/generic/client/generic-openapi.yaml deleted file mode 100644 index 4782cf9d38..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/generic-openapi.yaml +++ /dev/null @@ -1,298 +0,0 @@ -openapi: 3.0.3 -info: - title: GENERIC connector API - version: "v0.1" - -# ---------------------- PATHS ---------------------- -paths: - /accounts: - get: - summary: Get all accounts - operationId: getAccounts - parameters: - - $ref: '#/components/parameters/PageSize' - - $ref: '#/components/parameters/Page' - - $ref: '#/components/parameters/Sort' - - $ref: '#/components/parameters/CreatedAtFrom' - responses: - 200: - $ref: '#/components/responses/Accounts' - default: - $ref: '#/components/responses/ErrorResponse' - - /accounts/{accountId}/balances: - get: - summary: Get account balance - operationId: getAccountBalances - parameters: - - $ref: '#/components/parameters/AccountId' - responses: - 200: - $ref: '#/components/responses/Balances' - default: - $ref: '#/components/responses/ErrorResponse' - - /beneficiaries: - get: - summary: Get all beneficiaries - operationId: getBeneficiaries - parameters: - - $ref: '#/components/parameters/PageSize' - - $ref: '#/components/parameters/Page' - - $ref: '#/components/parameters/Sort' - - $ref: '#/components/parameters/CreatedAtFrom' - responses: - 200: - $ref: '#/components/responses/Beneficiaries' - default: - $ref: '#/components/responses/ErrorResponse' - - /transactions: - get: - summary: Get all transactions - operationId: getTransactions - parameters: - - $ref: '#/components/parameters/PageSize' - - $ref: '#/components/parameters/Page' - - $ref: '#/components/parameters/Sort' - - $ref: '#/components/parameters/UpdatedAtFrom' - responses: - 200: - $ref: '#/components/responses/Transactions' - default: - $ref: '#/components/responses/ErrorResponse' - -# ---------------------- COMPONENTS ---------------------- -components: - # ---------------------- PARAMETERS ---------------------- - parameters: - AccountId: - name: accountId - in: path - required: true - schema: - type: string - PageSize: - name: pageSize - in: query - description: Number of items per page - example: 100 - schema: - type: integer - format: int64 - minimum: 1 - default: 100 - Page: - name: page - in: query - description: Page number - example: 1 - schema: - type: integer - format: int64 - minimum: 1 - default: 1 - Sort: - name: sort - in: query - description: Sort order - example: createdAt:asc - schema: - type: string - CreatedAtFrom: - name: createdAtFrom - in: query - description: Filter by created at date - schema: - type: string - format: date-time - UpdatedAtFrom: - name: updatedAtFrom - in: query - description: Filter by updated at date - schema: - type: string - format: date-time - - # ---------------------- RESPONSES ---------------------- - responses: - NoContent: - description: No content - - ErrorResponse: - description: General error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - - Accounts: - description: List of accounts - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Account' - - Balances: - description: Account balances - content: - application/json: - schema: - $ref: '#/components/schemas/Balances' - - Beneficiaries: - description: List of beneficiaries - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Beneficiary' - - Transactions: - description: List of transactions - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Transaction' - - # ---------------------- SCHEMAS ---------------------- - schemas: - Error: - type: object - required: - - Title - - Detail - properties: - Title: - type: string - Detail: - type: string - - Account: - type: object - required: - - id - - accountName - - createdAt - properties: - id: - type: string - accountName: - type: string - createdAt: - type: string - format: date-time - metadata: - $ref: '#/components/schemas/Metadata' - - Balances: - type: object - required: - - id - - accountID - - at - - balances - properties: - id: - type: string - accountID: - type: string - at: - type: string - format: date-time - balances: - type: array - items: - $ref: '#/components/schemas/Balance' - - Balance: - type: object - required: - - amount - - currency - properties: - amount: - type: string - currency: - type: string - - Beneficiary: - type: object - required: - - id - - createdAt - - ownerName - properties: - id: - type: string - createdAt: - type: string - format: date-time - ownerName: - type: string - metadata: - $ref: '#/components/schemas/Metadata' - - - Transaction: - type: object - required: - - id - - createdAt - - updatedAt - - currency - - type - - status - - amount - properties: - id: - type: string - relatedTransactionID: - type: string - createdAt: - type: string - format: date-time - updatedAt: - type: string - format: date-time - currency: - type: string - scheme: - type: string - type: - $ref: '#/components/schemas/TransactionType' - status: - $ref: '#/components/schemas/TransactionStatus' - amount: - type: string - sourceAccountID: - type: string - destinationAccountID: - type: string - metadata: - $ref: '#/components/schemas/Metadata' - - Metadata: - type: object - additionalProperties: - type: string - nullable: true - - TransactionType: - type: string - enum: - - PAYIN - - PAYOUT - - TRANSFER - - TransactionStatus: - type: string - enum: - - PENDING - - SUCCEEDED - - FAILED \ No newline at end of file diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/transactions.go b/components/payments/cmd/connectors/internal/connectors/generic/client/transactions.go deleted file mode 100644 index 2e0ca3c6a4..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/client/transactions.go +++ /dev/null @@ -1,30 +0,0 @@ -package client - -import ( - "context" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/genericclient" -) - -func (c *Client) ListTransactions(ctx context.Context, page, pageSize int64, updatedAtFrom time.Time) ([]genericclient.Transaction, error) { - f := connectors.ClientMetrics(ctx, "generic", "list_transactions") - now := time.Now() - defer f(ctx, now) - - req := c.apiClient.DefaultApi.GetTransactions(ctx). - Page(page). - PageSize(pageSize) - - if !updatedAtFrom.IsZero() { - req = req.UpdatedAtFrom(updatedAtFrom) - } - - transactions, _, err := req.Execute() - if err != nil { - return nil, err - } - - return transactions, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/generic/config.go b/components/payments/cmd/connectors/internal/connectors/generic/config.go deleted file mode 100644 index a74b1125fb..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/config.go +++ /dev/null @@ -1,53 +0,0 @@ -package generic - -import ( - "encoding/json" - "fmt" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/configtemplate" -) - -const ( - pageSize = 100 - defaultPollingPeriod = 2 * time.Minute -) - -type Config struct { - Name string `json:"name" yaml:"name" bson:"name"` - APIKey string `json:"apiKey" yaml:"apiKey" bson:"apiKey"` - Endpoint string `json:"endpoint" yaml:"endpoint" bson:"endpoint"` - PollingPeriod connectors.Duration `json:"pollingPeriod" yaml:"pollingPeriod" bson:"pollingPeriod"` -} - -func (c Config) String() string { - return fmt.Sprintf("endpoint=%s", c.Endpoint) -} - -func (c Config) Validate() error { - if c.Name == "" { - return ErrMissingName - } - - return nil -} - -func (c Config) Marshal() ([]byte, error) { - return json.Marshal(c) -} - -func (c Config) ConnectorName() string { - return c.Name -} - -func (c Config) BuildTemplate() (string, configtemplate.Config) { - cfg := configtemplate.NewConfig() - - cfg.AddParameter("name", configtemplate.TypeString, name.String(), false) - cfg.AddParameter("endpoint", configtemplate.TypeString, "", false) - cfg.AddParameter("apiKey", configtemplate.TypeString, "", false) - cfg.AddParameter("pollingPeriod", configtemplate.TypeDurationNs, defaultPollingPeriod.String(), false) - - return name.String(), cfg -} diff --git a/components/payments/cmd/connectors/internal/connectors/generic/connector.go b/components/payments/cmd/connectors/internal/connectors/generic/connector.go deleted file mode 100644 index cfd5d0aabe..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/connector.go +++ /dev/null @@ -1,119 +0,0 @@ -package generic - -import ( - "context" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" -) - -const name = models.ConnectorProviderGeneric - -var ( - mainTaskDescriptor = TaskDescriptor{ - Name: "Main task to periodically fetch users and transactions", - Key: taskNameMain, - } -) - -type Connector struct { - logger logging.Logger - cfg Config -} - -func newConnector(logger logging.Logger, cfg Config) *Connector { - return &Connector{ - logger: logger.WithFields(map[string]any{ - "component": "connector", - }), - cfg: cfg, - } -} - -func (c *Connector) UpdateConfig(ctx task.ConnectorContext, config models.ConnectorConfigObject) error { - cfg, ok := config.(Config) - if !ok { - return connectors.ErrInvalidConfig - } - - c.cfg = cfg - - if c.cfg.Endpoint == "" { - // Nothing more to do - return nil - } - - restartTask := c.cfg.PollingPeriod.Duration != cfg.PollingPeriod.Duration - - if restartTask { - taskDescriptor, err := models.EncodeTaskDescriptor(mainTaskDescriptor) - if err != nil { - return err - } - - return ctx.Scheduler().Schedule(ctx.Context(), taskDescriptor, models.TaskSchedulerOptions{ - // We want to polling every c.cfg.PollingPeriod.Duration seconds the users - // and their transactions. - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: c.cfg.PollingPeriod.Duration, - // No need to restart this task, since the connector is not existing or - // was uninstalled previously, the task does not exists in the database - RestartOption: models.OPTIONS_STOP_AND_RESTART, - }) - } - - return nil -} - -func (c *Connector) Install(ctx task.ConnectorContext) error { - if c.cfg.Endpoint == "" { - // Nothing to do - return nil - } - - mainDescriptor, err := models.EncodeTaskDescriptor(mainTaskDescriptor) - if err != nil { - return err - } - - return ctx.Scheduler().Schedule(ctx.Context(), mainDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: c.cfg.PollingPeriod.Duration, - // No need to restart this task, since the connector is not existing or - // was uninstalled previously, the task does not exists in the database - RestartOption: models.OPTIONS_RESTART_NEVER, - }) -} - -func (c *Connector) Uninstall(ctx context.Context) error { - return nil -} - -func (c *Connector) Resolve(descriptor models.TaskDescriptor) task.Task { - taskDescriptor, err := models.DecodeTaskDescriptor[TaskDescriptor](descriptor) - if err != nil { - panic(err) - } - - return resolveTasks(c.logger, c.cfg)(taskDescriptor) -} - -func (c *Connector) SupportedCurrenciesAndDecimals() map[string]int { - return supportedCurrenciesWithDecimal -} - -func (c *Connector) InitiatePayment(ctx task.ConnectorContext, transfer *models.TransferInitiation) error { - return connectors.ErrNotImplemented -} - -func (c *Connector) ReversePayment(ctx task.ConnectorContext, transferReversal *models.TransferReversal) error { - return connectors.ErrNotImplemented -} - -func (c *Connector) CreateExternalBankAccount(ctx task.ConnectorContext, bankAccount *models.BankAccount) error { - return connectors.ErrNotImplemented -} - -var _ connectors.Connector = &Connector{} diff --git a/components/payments/cmd/connectors/internal/connectors/generic/currencies.go b/components/payments/cmd/connectors/internal/connectors/generic/currencies.go deleted file mode 100644 index 501980dc2b..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/currencies.go +++ /dev/null @@ -1,7 +0,0 @@ -package generic - -import "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - -var ( - supportedCurrenciesWithDecimal = currency.ISO4217Currencies -) diff --git a/components/payments/cmd/connectors/internal/connectors/generic/errors.go b/components/payments/cmd/connectors/internal/connectors/generic/errors.go deleted file mode 100644 index 707e9c68d9..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/errors.go +++ /dev/null @@ -1,14 +0,0 @@ -package generic - -import "errors" - -var ( - // ErrMissingTask is returned when the task is missing. - ErrMissingTask = errors.New("task is not implemented") - - // ErrMissingEndpoint is returned when the endpoint is missing. - ErrMissingEndpoint = errors.New("missing endpoint from config") - - // ErrMissingName is returned when the name is missing. - ErrMissingName = errors.New("missing name from config") -) diff --git a/components/payments/cmd/connectors/internal/connectors/generic/loader.go b/components/payments/cmd/connectors/internal/connectors/generic/loader.go deleted file mode 100644 index 29342d85a8..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/loader.go +++ /dev/null @@ -1,46 +0,0 @@ -package generic - -import ( - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/gorilla/mux" -) - -type Loader struct{} - -const allowedTasks = 50 - -func (l *Loader) AllowTasks() int { - return allowedTasks -} - -func (l *Loader) Name() models.ConnectorProvider { - return name -} - -func (l *Loader) Load(logger logging.Logger, config Config) connectors.Connector { - return newConnector(logger, config) -} - -func (l *Loader) ApplyDefaults(cfg Config) Config { - if cfg.PollingPeriod.Duration == 0 { - cfg.PollingPeriod.Duration = defaultPollingPeriod - } - - if cfg.Name == "" { - cfg.Name = name.String() - } - - return cfg -} - -func (l *Loader) Router(store *storage.Storage) *mux.Router { - return nil -} - -// NewLoader creates a new loader. -func NewLoader() *Loader { - return &Loader{} -} diff --git a/components/payments/cmd/connectors/internal/connectors/generic/task_fetch_accounts.go b/components/payments/cmd/connectors/internal/connectors/generic/task_fetch_accounts.go deleted file mode 100644 index 73bf267b42..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/task_fetch_accounts.go +++ /dev/null @@ -1,130 +0,0 @@ -package generic - -import ( - "context" - "encoding/json" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/generic/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" -) - -func taskFetchAccounts(client *client.Client, config *Config) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - resolver task.StateResolver, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "generic.taskFetchAccounts", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - err := ingestAccounts(ctx, connectorID, client, ingester, scheduler) - if err != nil { - otel.RecordError(span, err) - return err - } - - taskTransactions, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch transactions from client", - Key: taskNameFetchTransactions, - }) - if err != nil { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - err = scheduler.Schedule(ctx, taskTransactions, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - return nil - } -} - -func ingestAccounts( - ctx context.Context, - connectorID models.ConnectorID, - client *client.Client, - ingester ingestion.Ingester, - scheduler task.Scheduler, -) error { - - balancesTasks := make([]models.TaskDescriptor, 0) - for page := 1; ; page++ { - accounts, err := client.ListAccounts(ctx, int64(page), pageSize) - if err != nil { - return err - } - - if len(accounts) == 0 { - break - } - - accountsBatch := make([]*models.Account, 0, len(accounts)) - for _, account := range accounts { - raw, err := json.Marshal(account) - if err != nil { - return err - } - - accountsBatch = append(accountsBatch, &models.Account{ - ID: models.AccountID{ - Reference: account.Id, - ConnectorID: connectorID, - }, - ConnectorID: connectorID, - CreatedAt: account.CreatedAt, - Reference: account.Id, - AccountName: account.AccountName, - Type: models.AccountTypeInternal, - Metadata: account.Metadata, - RawData: raw, - }) - - balanceTask, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch balances from client", - Key: taskNameFetchBalances, - AccountID: account.Id, - }) - if err != nil { - return err - } - - balancesTasks = append(balancesTasks, balanceTask) - - } - - if err := ingester.IngestAccounts(ctx, ingestion.AccountBatch(accountsBatch)); err != nil { - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - for _, balanceTask := range balancesTasks { - if err := scheduler.Schedule(ctx, balanceTask, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }); err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return errors.Wrap(task.ErrRetryable, err.Error()) - } - } - } - - return nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/generic/task_fetch_balances.go b/components/payments/cmd/connectors/internal/connectors/generic/task_fetch_balances.go deleted file mode 100644 index 32ea2cf759..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/task_fetch_balances.go +++ /dev/null @@ -1,88 +0,0 @@ -package generic - -import ( - "context" - "fmt" - "math/big" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/generic/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/genericclient" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "go.opentelemetry.io/otel/attribute" -) - -func taskFetchBalances(client *client.Client, config *Config, accountID string) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - resolver task.StateResolver, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "generic.taskFetchBalances", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - balances, err := client.GetBalances(ctx, accountID) - if err != nil { - // retryable error already handled by the client - otel.RecordError(span, err) - return err - } - - if err := ingestBalancesBatch(ctx, connectorID, ingester, accountID, balances); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func ingestBalancesBatch( - ctx context.Context, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - accountID string, - balances *genericclient.Balances, -) error { - if balances == nil { - return nil - } - - balancesBatch := make([]*models.Balance, 0, len(balances.Balances)) - for _, balance := range balances.Balances { - var amount big.Int - _, ok := amount.SetString(balance.Amount, 10) - if !ok { - return fmt.Errorf("failed to parse amount: %s", balance.Amount) - } - - balancesBatch = append(balancesBatch, &models.Balance{ - AccountID: models.AccountID{ - Reference: accountID, - ConnectorID: connectorID, - }, - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, balance.Currency), - Balance: &amount, - CreatedAt: balances.At, - LastUpdatedAt: balances.At, - ConnectorID: connectorID, - }) - } - - if err := ingester.IngestBalances(ctx, ingestion.BalanceBatch(balancesBatch), false); err != nil { - return err - } - - return nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/generic/task_fetch_beneficiaries.go b/components/payments/cmd/connectors/internal/connectors/generic/task_fetch_beneficiaries.go deleted file mode 100644 index 7dadc85bfe..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/task_fetch_beneficiaries.go +++ /dev/null @@ -1,106 +0,0 @@ -package generic - -import ( - "context" - "encoding/json" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/generic/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" -) - -type fetchBeneficiariesState struct { - LastCreatedAt time.Time -} - -func taskFetchBeneficiaries(client *client.Client, config *Config) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - resolver task.StateResolver, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "generic.taskFetchBeneficiaries", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - state := task.MustResolveTo(ctx, resolver, fetchBeneficiariesState{}) - - newState, err := ingestBeneficiaries(ctx, connectorID, client, ingester, state) - if err != nil { - otel.RecordError(span, err) - return err - } - - if err := ingester.UpdateTaskState(ctx, newState); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func ingestBeneficiaries( - ctx context.Context, - connectorID models.ConnectorID, - client *client.Client, - ingester ingestion.Ingester, - state fetchBeneficiariesState, -) (fetchBeneficiariesState, error) { - newState := fetchBeneficiariesState{ - LastCreatedAt: state.LastCreatedAt, - } - - for page := 1; ; page++ { - beneficiaries, err := client.ListBeneficiaries(ctx, int64(page), pageSize, state.LastCreatedAt) - if err != nil { - return fetchBeneficiariesState{}, err - } - - if len(beneficiaries) == 0 { - break - } - - beneficiaryBatch := make([]*models.Account, 0, len(beneficiaries)) - for _, beneficiary := range beneficiaries { - raw, err := json.Marshal(beneficiary) - if err != nil { - return fetchBeneficiariesState{}, err - } - - beneficiaryBatch = append(beneficiaryBatch, &models.Account{ - ID: models.AccountID{ - Reference: beneficiary.Id, - ConnectorID: connectorID, - }, - ConnectorID: connectorID, - CreatedAt: beneficiary.CreatedAt, - Reference: beneficiary.Id, - AccountName: beneficiary.OwnerName, - Type: models.AccountTypeExternal, - Metadata: beneficiary.Metadata, - RawData: raw, - }) - - newState.LastCreatedAt = beneficiary.CreatedAt - } - - if err := ingester.IngestAccounts(ctx, ingestion.AccountBatch(beneficiaryBatch)); err != nil { - return fetchBeneficiariesState{}, errors.Wrap(task.ErrRetryable, err.Error()) - } - } - - return newState, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/generic/task_fetch_transactions.go b/components/payments/cmd/connectors/internal/connectors/generic/task_fetch_transactions.go deleted file mode 100644 index 2da4446633..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/task_fetch_transactions.go +++ /dev/null @@ -1,205 +0,0 @@ -package generic - -import ( - "context" - "encoding/json" - "fmt" - "math/big" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/generic/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/genericclient" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" -) - -type fetchTransactionsState struct { - LastUpdatedAt time.Time `json:"last_updated_at"` -} - -func taskFetchTransactions(client *client.Client, config *Config) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - resolver task.StateResolver, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "generic.taskFetchTransactions", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - state := task.MustResolveTo(ctx, resolver, fetchTransactionsState{}) - - newState, err := ingestTransactions(ctx, connectorID, client, ingester, scheduler, state) - if err != nil { - otel.RecordError(span, err) - return err - } - - if err := ingester.UpdateTaskState(ctx, newState); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func ingestTransactions( - ctx context.Context, - connectorID models.ConnectorID, - client *client.Client, - ingester ingestion.Ingester, - scheduler task.Scheduler, - state fetchTransactionsState, -) (fetchTransactionsState, error) { - newState := fetchTransactionsState{ - LastUpdatedAt: state.LastUpdatedAt, - } - - for page := 1; ; page++ { - transactions, err := client.ListTransactions(ctx, int64(page), pageSize, state.LastUpdatedAt) - if err != nil { - return fetchTransactionsState{}, err - } - - if len(transactions) == 0 { - break - } - - paymentBatch := make([]ingestion.PaymentBatchElement, 0, len(transactions)) - for _, transaction := range transactions { - elt, err := translate(ctx, connectorID, transaction) - if err != nil { - return fetchTransactionsState{}, err - } - - paymentBatch = append(paymentBatch, elt) - - newState.LastUpdatedAt = transaction.UpdatedAt - } - - if err := ingester.IngestPayments(ctx, ingestion.PaymentBatch(paymentBatch)); err != nil { - return fetchTransactionsState{}, errors.Wrap(task.ErrRetryable, err.Error()) - } - } - - return newState, nil -} - -func translate( - ctx context.Context, - connectorID models.ConnectorID, - transaction genericclient.Transaction, -) (ingestion.PaymentBatchElement, error) { - paymentType := matchPaymentType(transaction.Type) - paymentStatus := matchPaymentStatus(transaction.Status) - - var amount big.Int - _, ok := amount.SetString(transaction.Amount, 10) - if !ok { - return ingestion.PaymentBatchElement{}, fmt.Errorf("failed to parse amount: %s", transaction.Amount) - } - - raw, err := json.Marshal(transaction) - if err != nil { - return ingestion.PaymentBatchElement{}, err - } - - paymentID := models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: transaction.Id, - Type: paymentType, - }, - ConnectorID: connectorID, - } - elt := ingestion.PaymentBatchElement{ - Payment: &models.Payment{ - ID: paymentID, - ConnectorID: connectorID, - CreatedAt: transaction.CreatedAt, - Reference: transaction.Id, - Amount: &amount, - InitialAmount: &amount, - Type: paymentType, - Status: paymentStatus, - Scheme: models.PaymentSchemeOther, - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transaction.Currency), - RawData: raw, - }, - } - - if transaction.SourceAccountID != nil && *transaction.SourceAccountID != "" { - elt.Payment.SourceAccountID = &models.AccountID{ - Reference: *transaction.SourceAccountID, - ConnectorID: connectorID, - } - } - - if transaction.DestinationAccountID != nil && *transaction.DestinationAccountID != "" { - elt.Payment.DestinationAccountID = &models.AccountID{ - Reference: *transaction.DestinationAccountID, - ConnectorID: connectorID, - } - } - - for k, v := range transaction.Metadata { - elt.Payment.Metadata = append(elt.Payment.Metadata, &models.PaymentMetadata{ - PaymentID: paymentID, - CreatedAt: transaction.CreatedAt, - Key: k, - Value: v, - Changelog: []models.MetadataChangelog{ - { - CreatedAt: transaction.CreatedAt, - Value: v, - }, - }, - }) - - } - - return elt, nil -} - -func matchPaymentType( - transactionType genericclient.TransactionType, -) models.PaymentType { - switch transactionType { - case genericclient.PAYIN: - return models.PaymentTypePayIn - case genericclient.PAYOUT: - return models.PaymentTypePayOut - case genericclient.TRANSFER: - return models.PaymentTypeTransfer - default: - return models.PaymentTypeOther - } -} - -func matchPaymentStatus( - status genericclient.TransactionStatus, -) models.PaymentStatus { - switch status { - case genericclient.PENDING: - return models.PaymentStatusPending - case genericclient.FAILED: - return models.PaymentStatusFailed - case genericclient.SUCCEEDED: - return models.PaymentStatusSucceeded - default: - return models.PaymentStatusOther - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/generic/task_main.go b/components/payments/cmd/connectors/internal/connectors/generic/task_main.go deleted file mode 100644 index 3b721a464d..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/task_main.go +++ /dev/null @@ -1,68 +0,0 @@ -package generic - -import ( - "context" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" -) - -// taskMain is the main task of the connector. It launches the other tasks. -func taskMain() task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - scheduler task.Scheduler, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "generoc.taskMain", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - taskAccounts, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch accounts from client", - Key: taskNameFetchAccounts, - }) - if err != nil { - otel.RecordError(span, err) - return err - } - - err = scheduler.Schedule(ctx, taskAccounts, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - taskBeneficiaries, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch beneficiaries from client", - Key: taskNameFetchBeneficiaries, - }) - if err != nil { - otel.RecordError(span, err) - return err - } - - err = scheduler.Schedule(ctx, taskBeneficiaries, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - return nil - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/generic/task_resolve.go b/components/payments/cmd/connectors/internal/connectors/generic/task_resolve.go deleted file mode 100644 index 1616816757..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/generic/task_resolve.go +++ /dev/null @@ -1,52 +0,0 @@ -package generic - -import ( - "fmt" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/generic/client" - "github.com/formancehq/payments/cmd/connectors/internal/task" -) - -const ( - taskNameMain = "main" - taskNameFetchAccounts = "fetch-accounts" - taskNameFetchBalances = "fetch-balances" - taskNameFetchBeneficiaries = "fetch-beneficiaries" - taskNameFetchTransactions = "fetch-transactions" -) - -// TaskDescriptor is the definition of a task. -type TaskDescriptor struct { - Name string `json:"name" yaml:"name" bson:"name"` - Key string `json:"key" yaml:"key" bson:"key"` - AccountID string `json:"account_id" yaml:"account_id" bson:"account_id"` -} - -func resolveTasks(logger logging.Logger, config Config) func(taskDefinition TaskDescriptor) task.Task { - genericClient := client.NewClient( - config.APIKey, - config.Endpoint, - logger, - ) - - return func(taskDescriptor TaskDescriptor) task.Task { - switch taskDescriptor.Key { - case taskNameMain: - return taskMain() - case taskNameFetchAccounts: - return taskFetchAccounts(genericClient, &config) - case taskNameFetchBeneficiaries: - return taskFetchBeneficiaries(genericClient, &config) - case taskNameFetchBalances: - return taskFetchBalances(genericClient, &config, taskDescriptor.AccountID) - case taskNameFetchTransactions: - return taskFetchTransactions(genericClient, &config) - } - - // This should never happen. - return func() error { - return fmt.Errorf("key '%s': %w", taskDescriptor.Key, ErrMissingTask) - } - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/client/client.go b/components/payments/cmd/connectors/internal/connectors/mangopay/client/client.go deleted file mode 100644 index 8beec103c1..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/mangopay/client/client.go +++ /dev/null @@ -1,52 +0,0 @@ -package client - -import ( - "context" - "net/http" - "strings" - "time" - - "github.com/formancehq/go-libs/logging" - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" - "golang.org/x/oauth2/clientcredentials" -) - -// TODO(polo): Fetch Client wallets (FEES, ...) in the future -type Client struct { - httpClient *http.Client - - clientID string - endpoint string - - logger logging.Logger -} - -func newHTTPClient(clientID, apiKey, endpoint string) *http.Client { - config := clientcredentials.Config{ - ClientID: clientID, - ClientSecret: apiKey, - TokenURL: endpoint + "/v2.01/oauth/token", - } - - httpClient := config.Client(context.Background()) - - return &http.Client{ - Timeout: 10 * time.Second, - Transport: otelhttp.NewTransport(httpClient.Transport), - } -} - -func NewClient(clientID, apiKey, endpoint string, logger logging.Logger) (*Client, error) { - endpoint = strings.TrimSuffix(endpoint, "/") - - c := &Client{ - httpClient: newHTTPClient(clientID, apiKey, endpoint), - - clientID: clientID, - endpoint: endpoint, - - logger: logger, - } - - return c, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/client/dispute.go b/components/payments/cmd/connectors/internal/connectors/mangopay/client/dispute.go deleted file mode 100644 index 92a17b3ef8..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/mangopay/client/dispute.go +++ /dev/null @@ -1,66 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" -) - -type Dispute struct { - Id string `json:"Id"` - Tag string `json:"Tag"` - InitialTransactionId string `json:"InitialTransactionId"` - InitialTransactionType string `json:"InitialTransactionType"` - InitialTransactionNature string `json:"InitialTransactionNature"` - DisputeType string `json:"DisputeType"` - ContestDeadlineDate int64 `json:"ContestDeadlineDate"` - DisputedFunds Funds `json:"DisputedFunds"` - ContestedFunds Funds `json:"ContestedFunds"` - Status string `json:"Status"` - StatusMessage string `json:"StatusMessage"` - DisputeReason string `json:"DisputeReason"` - ResultCode string `json:"ResultCode"` - ResultMessage string `json:"ResultMessage"` - CreationDate int64 `json:"CreationDate"` - ClosedDate int64 `json:"ClosedDate"` -} - -func (c *Client) GetDispute(ctx context.Context, disputeID string) (*Dispute, error) { - f := connectors.ClientMetrics(ctx, "mangopay", "get_dispute") - now := time.Now() - defer f(ctx, now) - - endpoint := fmt.Sprintf("%s/v2.01/%s/disputes/%s", c.endpoint, c.clientID, disputeID) - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return nil, fmt.Errorf("failed to create get dispute request: %w", err) - } - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to get dispute: %w", err) - } - - defer func() { - err = resp.Body.Close() - if err != nil { - c.logger.Error(err) - } - }() - - if resp.StatusCode != http.StatusOK { - return nil, unmarshalErrorWithRetry(resp.StatusCode, resp.Body).Error() - } - - var dispute Dispute - if err := json.NewDecoder(resp.Body).Decode(&dispute); err != nil { - return nil, fmt.Errorf("failed to unmarshal dispute response body: %w", err) - } - - return &dispute, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/client/error.go b/components/payments/cmd/connectors/internal/connectors/mangopay/client/error.go deleted file mode 100644 index b57fa36fa4..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/mangopay/client/error.go +++ /dev/null @@ -1,79 +0,0 @@ -package client - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/pkg/errors" -) - -type mangopayError struct { - StatusCode int `json:"-"` - Message string `json:"Message"` - Type string `json:"Type"` - Errors map[string]string `json:"Errors"` - WithRetry bool `json:"-"` -} - -func (me *mangopayError) Error() error { - var errorMessage string - if len(me.Errors) > 0 { - for _, message := range me.Errors { - errorMessage = message - break - } - } - - var err error - if errorMessage == "" { - err = fmt.Errorf("unexpected status code: %d", me.StatusCode) - } else { - err = fmt.Errorf("%d: %s", me.StatusCode, errorMessage) - } - - if me.WithRetry { - return checkStatusCodeError(me.StatusCode, err) - } else { - return errors.Wrap(task.ErrNonRetryable, err.Error()) - } -} - -func unmarshalErrorWithRetry(statusCode int, body io.ReadCloser) *mangopayError { - var ce mangopayError - _ = json.NewDecoder(body).Decode(&ce) - - ce.StatusCode = statusCode - ce.WithRetry = true - - return &ce -} - -func unmarshalErrorWithoutRetry(statusCode int, body io.ReadCloser) *mangopayError { - var ce mangopayError - _ = json.NewDecoder(body).Decode(&ce) - - ce.StatusCode = statusCode - ce.WithRetry = false - - return &ce -} - -func checkStatusCodeError(statusCode int, err error) error { - switch statusCode { - case http.StatusTooEarly, http.StatusRequestTimeout: - return errors.Wrap(task.ErrRetryable, err.Error()) - case http.StatusTooManyRequests: - // Retry rate limit errors - // TODO(polo): add rate limit handling - return errors.Wrap(task.ErrRetryable, err.Error()) - case http.StatusInternalServerError, http.StatusBadGateway, - http.StatusServiceUnavailable, http.StatusGatewayTimeout: - // Retry internal errors - return errors.Wrap(task.ErrRetryable, err.Error()) - default: - return errors.Wrap(task.ErrNonRetryable, err.Error()) - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/client/transfer.go b/components/payments/cmd/connectors/internal/connectors/mangopay/client/transfer.go deleted file mode 100644 index 31172b186e..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/mangopay/client/transfer.go +++ /dev/null @@ -1,122 +0,0 @@ -package client - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" -) - -type Funds struct { - Currency string `json:"Currency"` - Amount json.Number `json:"Amount"` -} - -type TransferRequest struct { - AuthorID string `json:"AuthorId"` - CreditedUserID string `json:"CreditedUserId,omitempty"` - DebitedFunds Funds `json:"DebitedFunds"` - Fees Funds `json:"Fees"` - DebitedWalletID string `json:"DebitedWalletId"` - CreditedWalletID string `json:"CreditedWalletId"` -} - -type TransferResponse struct { - ID string `json:"Id"` - CreationDate int64 `json:"CreationDate"` - AuthorID string `json:"AuthorId"` - CreditedUserID string `json:"CreditedUserId"` - DebitedFunds Funds `json:"DebitedFunds"` - Fees Funds `json:"Fees"` - CreditedFunds Funds `json:"CreditedFunds"` - Status string `json:"Status"` - ResultCode string `json:"ResultCode"` - ResultMessage string `json:"ResultMessage"` - Type string `json:"Type"` - ExecutionDate int64 `json:"ExecutionDate"` - Nature string `json:"Nature"` - DebitedWalletID string `json:"DebitedWalletId"` - CreditedWalletID string `json:"CreditedWalletId"` -} - -func (c *Client) InitiateWalletTransfer(ctx context.Context, transferRequest *TransferRequest) (*TransferResponse, error) { - f := connectors.ClientMetrics(ctx, "mangopay", "initiate_transfer") - now := time.Now() - defer f(ctx, now) - - endpoint := fmt.Sprintf("%s/v2.01/%s/transfers", c.endpoint, c.clientID) - - body, err := json.Marshal(transferRequest) - if err != nil { - return nil, fmt.Errorf("failed to marshal transfer request: %w", err) - } - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(body)) - if err != nil { - return nil, fmt.Errorf("failed to create transfer request: %w", err) - } - req.Header.Set("Content-Type", "application/json") - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to get wallets: %w", err) - } - - defer func() { - err = resp.Body.Close() - if err != nil { - c.logger.Error(err) - } - }() - - if resp.StatusCode != http.StatusOK { - // Never retry transfer initiation - return nil, unmarshalErrorWithoutRetry(resp.StatusCode, resp.Body).Error() - } - - var transferResponse TransferResponse - if err := json.NewDecoder(resp.Body).Decode(&transferResponse); err != nil { - return nil, fmt.Errorf("failed to unmarshal wallets response body: %w", err) - } - - return &transferResponse, nil -} - -func (c *Client) GetWalletTransfer(ctx context.Context, transferID string) (*TransferResponse, error) { - f := connectors.ClientMetrics(ctx, "mangopay", "get_transfer") - now := time.Now() - defer f(ctx, now) - - endpoint := fmt.Sprintf("%s/v2.01/%s/transfers/%s", c.endpoint, c.clientID, transferID) - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody) - if err != nil { - return nil, fmt.Errorf("failed to create login request: %w", err) - } - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to get wallets: %w", err) - } - - defer func() { - err = resp.Body.Close() - if err != nil { - c.logger.Error(err) - } - }() - - if resp.StatusCode != http.StatusOK { - return nil, unmarshalErrorWithRetry(resp.StatusCode, resp.Body).Error() - } - - var transfer TransferResponse - if err := json.NewDecoder(resp.Body).Decode(&transfer); err != nil { - return nil, fmt.Errorf("failed to unmarshal wallets response body: %w", err) - } - - return &transfer, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/client/users.go b/components/payments/cmd/connectors/internal/connectors/mangopay/client/users.go deleted file mode 100644 index 7e782e83ed..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/mangopay/client/users.go +++ /dev/null @@ -1,82 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "strconv" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" -) - -type user struct { - ID string `json:"Id"` - CreationDate int64 `json:"CreationDate"` -} - -func (c *Client) GetAllUsers(ctx context.Context, lastPage int, pageSize int) ([]*user, int, error) { - var users []*user - var currentPage int - - for currentPage = lastPage; ; currentPage++ { - pagedUsers, err := c.getUsers(ctx, currentPage, pageSize) - if err != nil { - return nil, lastPage, err - } - - if len(pagedUsers) == 0 { - break - } - - users = append(users, pagedUsers...) - - if len(pagedUsers) < pageSize { - break - } - } - - return users, currentPage, nil -} - -func (c *Client) getUsers(ctx context.Context, page int, pageSize int) ([]*user, error) { - f := connectors.ClientMetrics(ctx, "mangopay", "list_users") - now := time.Now() - defer f(ctx, now) - - endpoint := fmt.Sprintf("%s/v2.01/%s/users", c.endpoint, c.clientID) - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody) - if err != nil { - return nil, fmt.Errorf("failed to create login request: %w", err) - } - - q := req.URL.Query() - q.Add("per_page", strconv.Itoa(pageSize)) - q.Add("page", fmt.Sprint(page)) - q.Add("Sort", "CreationDate:ASC") - req.URL.RawQuery = q.Encode() - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to get users: %w", err) - } - - defer func() { - err = resp.Body.Close() - if err != nil { - c.logger.Error(err) - } - }() - - if resp.StatusCode != http.StatusOK { - return nil, unmarshalErrorWithRetry(resp.StatusCode, resp.Body).Error() - } - - var users []*user - if err := json.NewDecoder(resp.Body).Decode(&users); err != nil { - return nil, fmt.Errorf("failed to unmarshal users response body: %w", err) - } - - return users, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/client/wallets.go b/components/payments/cmd/connectors/internal/connectors/mangopay/client/wallets.go deleted file mode 100644 index cbc0bb82d7..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/mangopay/client/wallets.go +++ /dev/null @@ -1,100 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "strconv" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" -) - -type Wallet struct { - ID string `json:"Id"` - Owners []string `json:"Owners"` - Description string `json:"Description"` - CreationDate int64 `json:"CreationDate"` - Currency string `json:"Currency"` - Balance struct { - Currency string `json:"Currency"` - Amount json.Number `json:"Amount"` - } `json:"Balance"` -} - -func (c *Client) GetWallets(ctx context.Context, userID string, page, pageSize int) ([]*Wallet, error) { - f := connectors.ClientMetrics(ctx, "mangopay", "list_wallets") - now := time.Now() - defer f(ctx, now) - - endpoint := fmt.Sprintf("%s/v2.01/%s/users/%s/wallets", c.endpoint, c.clientID, userID) - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody) - if err != nil { - return nil, fmt.Errorf("failed to create login request: %w", err) - } - - q := req.URL.Query() - q.Add("per_page", strconv.Itoa(pageSize)) - q.Add("page", fmt.Sprint(page)) - q.Add("Sort", "CreationDate:ASC") - req.URL.RawQuery = q.Encode() - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to get wallets: %w", err) - } - - defer func() { - err = resp.Body.Close() - if err != nil { - c.logger.Error(err) - } - }() - - if resp.StatusCode != http.StatusOK { - return nil, unmarshalErrorWithRetry(resp.StatusCode, resp.Body).Error() - } - - var wallets []*Wallet - if err := json.NewDecoder(resp.Body).Decode(&wallets); err != nil { - return nil, fmt.Errorf("failed to unmarshal wallets response body: %w", err) - } - - return wallets, nil -} - -func (c *Client) GetWallet(ctx context.Context, walletID string) (*Wallet, error) { - f := connectors.ClientMetrics(ctx, "mangopay", "get_wallets") - now := time.Now() - defer f(ctx, now) - - endpoint := fmt.Sprintf("%s/v2.01/%s/wallets/%s", c.endpoint, c.clientID, walletID) - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody) - if err != nil { - return nil, fmt.Errorf("failed to create wallet request: %w", err) - } - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to get wallet: %w", err) - } - - defer func() { - err = resp.Body.Close() - if err != nil { - c.logger.Error(err) - } - }() - - if resp.StatusCode != http.StatusOK { - return nil, unmarshalErrorWithRetry(resp.StatusCode, resp.Body).Error() - } - - var wallet Wallet - if err := json.NewDecoder(resp.Body).Decode(&wallet); err != nil { - return nil, fmt.Errorf("failed to unmarshal wallet response body: %w", err) - } - - return &wallet, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/config.go b/components/payments/cmd/connectors/internal/connectors/mangopay/config.go deleted file mode 100644 index b010120644..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/mangopay/config.go +++ /dev/null @@ -1,67 +0,0 @@ -package mangopay - -import ( - "encoding/json" - "fmt" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/configtemplate" -) - -const ( - pageSize = 100 - defaultPollingPeriod = 2 * time.Minute -) - -type Config struct { - Name string `json:"name" yaml:"name" bson:"name"` - ClientID string `json:"clientID" yaml:"clientID" bson:"clientID"` - APIKey string `json:"apiKey" yaml:"apiKey" bson:"apiKey"` - Endpoint string `json:"endpoint" yaml:"endpoint" bson:"endpoint"` - PollingPeriod connectors.Duration `json:"pollingPeriod" yaml:"pollingPeriod" bson:"pollingPeriod"` -} - -func (c Config) String() string { - return fmt.Sprintf("clientID=%s, apiKey=****", c.ClientID) -} - -func (c Config) Validate() error { - if c.ClientID == "" { - return ErrMissingClientID - } - - if c.APIKey == "" { - return ErrMissingAPIKey - } - - if c.Endpoint == "" { - return ErrMissingEndpoint - } - - if c.Name == "" { - return ErrMissingName - } - - return nil -} - -func (c Config) Marshal() ([]byte, error) { - return json.Marshal(c) -} - -func (c Config) ConnectorName() string { - return c.Name -} - -func (c Config) BuildTemplate() (string, configtemplate.Config) { - cfg := configtemplate.NewConfig() - - cfg.AddParameter("name", configtemplate.TypeString, name.String(), false) - cfg.AddParameter("clientID", configtemplate.TypeString, "", true) - cfg.AddParameter("apiKey", configtemplate.TypeString, "", true) - cfg.AddParameter("endpoint", configtemplate.TypeString, "", true) - cfg.AddParameter("pollingPeriod", configtemplate.TypeDurationNs, defaultPollingPeriod.String(), false) - - return name.String(), cfg -} diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/connector.go b/components/payments/cmd/connectors/internal/connectors/mangopay/connector.go deleted file mode 100644 index b509c43620..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/mangopay/connector.go +++ /dev/null @@ -1,166 +0,0 @@ -package mangopay - -import ( - "context" - "errors" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" -) - -const name = models.ConnectorProviderMangopay - -var ( - mainTaskDescriptor = TaskDescriptor{ - Name: "Main task to periodically fetch users and transactions", - Key: taskNameMain, - } -) - -type Connector struct { - logger logging.Logger - cfg Config - - taskMemoryState taskMemoryState -} - -func newConnector(logger logging.Logger, cfg Config) *Connector { - return &Connector{ - logger: logger.WithFields(map[string]any{ - "component": "connector", - }), - cfg: cfg, - } -} - -func (c *Connector) UpdateConfig(ctx task.ConnectorContext, config models.ConnectorConfigObject) error { - cfg, ok := config.(Config) - if !ok { - return connectors.ErrInvalidConfig - } - - restartTask := c.cfg.PollingPeriod.Duration != cfg.PollingPeriod.Duration - - c.cfg = cfg - - if restartTask { - taskDescriptor, err := models.EncodeTaskDescriptor(mainTaskDescriptor) - if err != nil { - return err - } - - return ctx.Scheduler().Schedule(ctx.Context(), taskDescriptor, models.TaskSchedulerOptions{ - // We want to polling every c.cfg.PollingPeriod.Duration seconds the users - // and their transactions. - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: c.cfg.PollingPeriod.Duration, - // No need to restart this task, since the connector is not existing or - // was uninstalled previously, the task does not exists in the database - RestartOption: models.OPTIONS_STOP_AND_RESTART, - }) - } - - return nil -} - -func (c *Connector) Install(ctx task.ConnectorContext) error { - // Create hooks on mangopay sync - createWebhookDescriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "First task to create webhooks", - Key: taskNameCreateWebhook, - }) - if err != nil { - return err - } - - if err := ctx.Scheduler().Schedule(ctx.Context(), createWebhookDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW_SYNC, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }); err != nil { - return err - } - - mainDescriptor, err := models.EncodeTaskDescriptor(mainTaskDescriptor) - if err != nil { - return err - } - - return ctx.Scheduler().Schedule(ctx.Context(), mainDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: c.cfg.PollingPeriod.Duration, - // No need to restart this task, since the connector is not existing or - // was uninstalled previously, the task does not exists in the database - RestartOption: models.OPTIONS_RESTART_NEVER, - }) -} - -func (c *Connector) Uninstall(ctx context.Context) error { - return nil -} - -func (c *Connector) Resolve(descriptor models.TaskDescriptor) task.Task { - taskDescriptor, err := models.DecodeTaskDescriptor[TaskDescriptor](descriptor) - if err != nil { - panic(err) - } - - return resolveTasks(c.logger, c.cfg, &c.taskMemoryState)(taskDescriptor) -} - -func (c *Connector) SupportedCurrenciesAndDecimals() map[string]int { - return supportedCurrenciesWithDecimal -} - -func (c *Connector) InitiatePayment(ctx task.ConnectorContext, transfer *models.TransferInitiation) error { - taskDescriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Initiate payment", - Key: taskNameInitiatePayment, - TransferID: transfer.ID.String(), - }) - if err != nil { - return err - } - - scheduleOption := models.OPTIONS_RUN_NOW_SYNC - scheduledAt := transfer.ScheduledAt - if !scheduledAt.IsZero() { - scheduleOption = models.OPTIONS_RUN_SCHEDULED_AT - } - - err = ctx.Scheduler().Schedule(ctx.Context(), taskDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: scheduleOption, - ScheduleAt: scheduledAt, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return err - } - - return nil -} - -func (c *Connector) ReversePayment(ctx task.ConnectorContext, transferReversal *models.TransferReversal) error { - return connectors.ErrNotImplemented -} - -func (c *Connector) CreateExternalBankAccount(ctx task.ConnectorContext, bankAccount *models.BankAccount) error { - descriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Create external bank account", - Key: taskNameCreateExternalBankAccount, - BankAccountID: bankAccount.ID, - }) - if err != nil { - return err - } - if err := ctx.Scheduler().Schedule(ctx.Context(), descriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW_SYNC, - }); err != nil { - return err - } - - return nil -} - -var _ connectors.Connector = &Connector{} diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/errors.go b/components/payments/cmd/connectors/internal/connectors/mangopay/errors.go deleted file mode 100644 index 9328b7c6d3..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/mangopay/errors.go +++ /dev/null @@ -1,20 +0,0 @@ -package mangopay - -import "errors" - -var ( - // ErrMissingTask is returned when the task is missing. - ErrMissingTask = errors.New("task is not implemented") - - // ErrMissingClientID is returned when the clientID is missing. - ErrMissingClientID = errors.New("missing clientID from config") - - // ErrMissingAPIKey is returned when the apiKey is missing. - ErrMissingAPIKey = errors.New("missing apiKey from config") - - // ErrMissingEndpoint is returned when the endpoint is missing. - ErrMissingEndpoint = errors.New("missing endpoint from config") - - // ErrMissingName is returned when the name is missing. - ErrMissingName = errors.New("missing name from config") -) diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/loader.go b/components/payments/cmd/connectors/internal/connectors/mangopay/loader.go deleted file mode 100644 index 67ee43baf0..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/mangopay/loader.go +++ /dev/null @@ -1,52 +0,0 @@ -package mangopay - -import ( - "net/http" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/gorilla/mux" -) - -type Loader struct{} - -const allowedTasks = 50 - -func (l *Loader) AllowTasks() int { - return allowedTasks -} - -func (l *Loader) Name() models.ConnectorProvider { - return name -} - -func (l *Loader) Load(logger logging.Logger, config Config) connectors.Connector { - return newConnector(logger, config) -} - -func (l *Loader) ApplyDefaults(cfg Config) Config { - if cfg.PollingPeriod.Duration == 0 { - cfg.PollingPeriod.Duration = defaultPollingPeriod - } - - if cfg.Name == "" { - cfg.Name = name.String() - } - - return cfg -} - -func (l *Loader) Router(store *storage.Storage) *mux.Router { - r := mux.NewRouter() - - r.Path("/").Methods(http.MethodPost).Handler(handleWebhooks(store)) - - return r -} - -// NewLoader creates a new loader. -func NewLoader() *Loader { - return &Loader{} -} diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/task_create_external_bank_account.go b/components/payments/cmd/connectors/internal/connectors/mangopay/task_create_external_bank_account.go deleted file mode 100644 index e49106f850..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/mangopay/task_create_external_bank_account.go +++ /dev/null @@ -1,188 +0,0 @@ -package mangopay - -import ( - "context" - "encoding/json" - "fmt" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/mangopay/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/google/uuid" - "go.opentelemetry.io/otel/attribute" -) - -func taskCreateExternalBankAccount(mangopayClient *client.Client, bankAccountID uuid.UUID) task.Task { - return func( - ctx context.Context, - connectorID models.ConnectorID, - taskID models.TaskID, - storageReader storage.Reader, - ingester ingestion.Ingester, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "mangopay.taskCreateExternalBankAccount", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("bankAccountID", bankAccountID.String()), - ) - defer span.End() - - bankAccount, err := storageReader.GetBankAccount(ctx, bankAccountID, true) - if err != nil { - otel.RecordError(span, err) - return err - } - - if err := createExternalBankAccount(ctx, connectorID, mangopayClient, bankAccount, ingester); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func createExternalBankAccount( - ctx context.Context, - connectorID models.ConnectorID, - mangopayClient *client.Client, - bankAccount *models.BankAccount, - ingester ingestion.Ingester, -) error { - userID := models.ExtractNamespacedMetadata(bankAccount.Metadata, client.MangopayUserIDMetadataKey) - if userID == "" { - return fmt.Errorf("missing userID in bank account metadata") - } - - ownerAddress := client.OwnerAddress{ - AddressLine1: models.ExtractNamespacedMetadata(bankAccount.Metadata, models.BankAccountOwnerAddressLine1MetadataKey), - AddressLine2: models.ExtractNamespacedMetadata(bankAccount.Metadata, models.BankAccountOwnerAddressLine2MetadataKey), - City: models.ExtractNamespacedMetadata(bankAccount.Metadata, models.BankAccountOwnerCityMetadataKey), - Region: models.ExtractNamespacedMetadata(bankAccount.Metadata, models.BankAccountOwnerRegionMetadataKey), - PostalCode: models.ExtractNamespacedMetadata(bankAccount.Metadata, models.BankAccountOwnerPostalCodeMetadataKey), - Country: bankAccount.Country, - } - - var mangopayBankAccount *client.BankAccount - if bankAccount.IBAN != "" { - req := &client.CreateIBANBankAccountRequest{ - OwnerName: bankAccount.Name, - OwnerAddress: &ownerAddress, - IBAN: bankAccount.IBAN, - BIC: bankAccount.SwiftBicCode, - Tag: models.ExtractNamespacedMetadata(bankAccount.Metadata, client.MangopayTagMetadataKey), - } - - var err error - mangopayBankAccount, err = mangopayClient.CreateIBANBankAccount(ctx, userID, req) - if err != nil { - return err - } - } else { - switch bankAccount.Country { - case "US": - req := &client.CreateUSBankAccountRequest{ - OwnerName: bankAccount.Name, - OwnerAddress: &ownerAddress, - AccountNumber: bankAccount.AccountNumber, - ABA: models.ExtractNamespacedMetadata(bankAccount.Metadata, client.MangopayABAMetadataKey), - DepositAccountType: models.ExtractNamespacedMetadata(bankAccount.Metadata, client.MangopayDepositAccountTypeMetadataKey), - Tag: models.ExtractNamespacedMetadata(bankAccount.Metadata, client.MangopayTagMetadataKey), - } - - var err error - mangopayBankAccount, err = mangopayClient.CreateUSBankAccount(ctx, userID, req) - if err != nil { - return err - } - - case "CA": - req := &client.CreateCABankAccountRequest{ - OwnerName: bankAccount.Name, - OwnerAddress: &ownerAddress, - AccountNumber: bankAccount.AccountNumber, - InstitutionNumber: models.ExtractNamespacedMetadata(bankAccount.Metadata, client.MangopayInstitutionNumberMetadataKey), - BranchCode: models.ExtractNamespacedMetadata(bankAccount.Metadata, client.MangopayBranchCodeMetadataKey), - BankName: models.ExtractNamespacedMetadata(bankAccount.Metadata, client.MangopayBankNameMetadataKey), - Tag: models.ExtractNamespacedMetadata(bankAccount.Metadata, client.MangopayTagMetadataKey), - } - - var err error - mangopayBankAccount, err = mangopayClient.CreateCABankAccount(ctx, userID, req) - if err != nil { - return err - } - - case "GB": - req := &client.CreateGBBankAccountRequest{ - OwnerName: bankAccount.Name, - OwnerAddress: &ownerAddress, - AccountNumber: bankAccount.AccountNumber, - SortCode: models.ExtractNamespacedMetadata(bankAccount.Metadata, client.MangopaySortCodeMetadataKey), - Tag: models.ExtractNamespacedMetadata(bankAccount.Metadata, client.MangopayTagMetadataKey), - } - - var err error - mangopayBankAccount, err = mangopayClient.CreateGBBankAccount(ctx, userID, req) - if err != nil { - return err - } - - default: - req := &client.CreateOtherBankAccountRequest{ - OwnerName: bankAccount.Name, - OwnerAddress: &ownerAddress, - AccountNumber: bankAccount.AccountNumber, - BIC: bankAccount.SwiftBicCode, - Country: bankAccount.Country, - Tag: models.ExtractNamespacedMetadata(bankAccount.Metadata, client.MangopayTagMetadataKey), - } - - var err error - mangopayBankAccount, err = mangopayClient.CreateOtherBankAccount(ctx, userID, req) - if err != nil { - return err - } - } - } - - if mangopayBankAccount != nil { - buf, err := json.Marshal(mangopayBankAccount) - if err != nil { - return err - } - - externalAccount := &models.Account{ - ID: models.AccountID{ - Reference: mangopayBankAccount.ID, - ConnectorID: connectorID, - }, - CreatedAt: time.Unix(mangopayBankAccount.CreationDate, 0), - Reference: mangopayBankAccount.ID, - ConnectorID: connectorID, - AccountName: mangopayBankAccount.OwnerName, - Type: models.AccountTypeExternal, - Metadata: map[string]string{ - "user_id": userID, - }, - RawData: buf, - } - - if err := ingester.IngestAccounts(ctx, ingestion.AccountBatch{externalAccount}); err != nil { - return err - } - - if err := ingester.LinkBankAccountWithAccount(ctx, bankAccount, &externalAccount.ID); err != nil { - return err - } - } - - return nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/task_fetch_bank_accounts.go b/components/payments/cmd/connectors/internal/connectors/mangopay/task_fetch_bank_accounts.go deleted file mode 100644 index 1a46301b4a..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/mangopay/task_fetch_bank_accounts.go +++ /dev/null @@ -1,138 +0,0 @@ -package mangopay - -import ( - "context" - "encoding/json" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/mangopay/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" -) - -type fetchBankAccountsState struct { - LastPage int `json:"last_page"` - LastCreationDate time.Time `json:"last_creation_date"` -} - -func taskFetchBankAccounts(client *client.Client, config *Config, userID string) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - scheduler task.Scheduler, - ingester ingestion.Ingester, - resolver task.StateResolver, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "mangopay.taskFetchBankAccounts", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("userID", userID), - ) - defer span.End() - - state := task.MustResolveTo(ctx, resolver, fetchBankAccountsState{}) - if state.LastPage == 0 { - // If last page is 0, it means we haven't fetched any wallets yet. - // Mangopay pages starts at 1. - state.LastPage = 1 - } - - newState, err := ingestBankAccounts(ctx, client, userID, connectorID, scheduler, ingester, state) - if err != nil { - otel.RecordError(span, err) - // Retry is already handled by the function - return err - } - - if err := ingester.UpdateTaskState(ctx, newState); err != nil { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - return nil - } -} - -func ingestBankAccounts( - ctx context.Context, - client *client.Client, - userID string, - connectorID models.ConnectorID, - scheduler task.Scheduler, - ingester ingestion.Ingester, - state fetchBankAccountsState, -) (fetchBankAccountsState, error) { - var currentPage int - - newState := fetchBankAccountsState{ - LastCreationDate: state.LastCreationDate, - } - - for currentPage = state.LastPage; ; currentPage++ { - pagedBankAccounts, err := client.GetBankAccounts(ctx, userID, currentPage, pageSize) - if err != nil { - // The client is already deciding if the error is retryable or not. - return fetchBankAccountsState{}, err - } - - if len(pagedBankAccounts) == 0 { - break - } - - var accountBatch ingestion.AccountBatch - for _, bankAccount := range pagedBankAccounts { - creationDate := time.Unix(bankAccount.CreationDate, 0) - switch creationDate.Compare(state.LastCreationDate) { - case -1, 0: - // creationDate <= state.LastCreationDate, nothing to do, - // we already processed this bank account. - continue - default: - } - newState.LastCreationDate = creationDate - - buf, err := json.Marshal(bankAccount) - if err != nil { - return fetchBankAccountsState{}, errors.Wrap(task.ErrRetryable, err.Error()) - } - - accountBatch = append(accountBatch, &models.Account{ - ID: models.AccountID{ - Reference: bankAccount.ID, - ConnectorID: connectorID, - }, - CreatedAt: creationDate, - Reference: bankAccount.ID, - ConnectorID: connectorID, - AccountName: bankAccount.OwnerName, - Type: models.AccountTypeExternal, - Metadata: map[string]string{ - "user_id": userID, - }, - RawData: buf, - }) - - newState.LastCreationDate = creationDate - } - - if err := ingester.IngestAccounts(ctx, accountBatch); err != nil { - return fetchBankAccountsState{}, errors.Wrap(task.ErrRetryable, err.Error()) - } - - if len(pagedBankAccounts) < pageSize { - break - } - } - - newState.LastPage = currentPage - - return newState, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/task_fetch_transactions.go b/components/payments/cmd/connectors/internal/connectors/mangopay/task_fetch_transactions.go deleted file mode 100644 index 6b4b23df23..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/mangopay/task_fetch_transactions.go +++ /dev/null @@ -1,216 +0,0 @@ -package mangopay - -import ( - "context" - "encoding/json" - "fmt" - "math/big" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/mangopay/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" -) - -type fetchTransactionsState struct { - // Mangopay only allows us to sort/filter by creation date. - // So in order to have every last updates of transactions, we need to - // keep track of the first transaction with created status in order to - // refetch all transactions created after this one. - // Example: - // - SUCCEEDED - // - FAILED - // - CREATED -> We want to keep track of the creation date of this transaction since we want its updates - // - SUCCEEDED - // - CREATED - // - SUCCEEDED - FirstCreatedTransactionCreationDate time.Time `json:"first_created_transaction_creation_date"` -} - -func taskFetchTransactions(client *client.Client, config *Config, walletsID string) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - resolver task.StateResolver, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "mangopay.taskFetchTransactions", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("walletsID", walletsID), - ) - defer span.End() - - state := task.MustResolveTo(ctx, resolver, fetchTransactionsState{}) - - newState, err := fetchTransactions(ctx, client, walletsID, connectorID, ingester, state) - if err != nil { - otel.RecordError(span, err) - // Retry is already handled by the function - return err - } - - if err := ingester.UpdateTaskState(ctx, newState); err != nil { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - return nil - } -} - -func fetchTransactions( - ctx context.Context, - client *client.Client, - walletsID string, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - state fetchTransactionsState, -) (fetchTransactionsState, error) { - newState := fetchTransactionsState{} - - var firstCreatedCreationDate time.Time - var lastCreationDate time.Time - for page := 1; ; page++ { - pagedPayments, err := client.GetTransactions(ctx, walletsID, page, pageSize, state.FirstCreatedTransactionCreationDate) - if err != nil { - // Client is already deciding if the error is retryable or not. - return fetchTransactionsState{}, err - } - - if len(pagedPayments) == 0 { - break - } - - batch := ingestion.PaymentBatch{} - for _, payment := range pagedPayments { - batchElement, err := processPayment(ctx, connectorID, payment) - if err != nil { - return fetchTransactionsState{}, errors.Wrap(task.ErrRetryable, err.Error()) - } - - if batchElement.Payment != nil { - // State update - if firstCreatedCreationDate.IsZero() && - batchElement.Payment.Status == models.PaymentStatusPending { - firstCreatedCreationDate = batchElement.Payment.CreatedAt - } - - lastCreationDate = batchElement.Payment.CreatedAt - } - - batch = append(batch, batchElement) - } - - err = ingester.IngestPayments(ctx, batch) - if err != nil { - return fetchTransactionsState{}, errors.Wrap(task.ErrRetryable, err.Error()) - } - - if len(pagedPayments) < pageSize { - break - } - } - - newState.FirstCreatedTransactionCreationDate = firstCreatedCreationDate - if newState.FirstCreatedTransactionCreationDate.IsZero() { - // No new created payments, let's set the last creation date to the last - // transaction we fetched. - newState.FirstCreatedTransactionCreationDate = lastCreationDate - } - - return newState, nil -} - -func processPayment( - ctx context.Context, - connectorID models.ConnectorID, - payment *client.Payment, -) (ingestion.PaymentBatchElement, error) { - rawData, err := json.Marshal(payment) - if err != nil { - return ingestion.PaymentBatchElement{}, fmt.Errorf("failed to marshal transaction: %w", err) - } - - paymentType := matchPaymentType(payment.Type) - paymentStatus := matchPaymentStatus(payment.Status) - - var amount big.Int - _, ok := amount.SetString(payment.DebitedFunds.Amount.String(), 10) - if !ok { - return ingestion.PaymentBatchElement{}, fmt.Errorf("failed to parse amount %s", payment.DebitedFunds.Amount.String()) - } - - batchElement := ingestion.PaymentBatchElement{ - Payment: &models.Payment{ - ID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: payment.Id, - Type: paymentType, - }, - ConnectorID: connectorID, - }, - CreatedAt: time.Unix(payment.CreationDate, 0), - Reference: payment.Id, - Amount: &amount, - InitialAmount: &amount, - ConnectorID: connectorID, - Type: paymentType, - Status: paymentStatus, - Scheme: models.PaymentSchemeOther, - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, payment.DebitedFunds.Currency), - RawData: rawData, - }, - } - - if payment.DebitedWalletID != "" { - batchElement.Payment.SourceAccountID = &models.AccountID{ - Reference: payment.DebitedWalletID, - ConnectorID: connectorID, - } - } - - if payment.CreditedWalletID != "" { - batchElement.Payment.DestinationAccountID = &models.AccountID{ - Reference: payment.CreditedWalletID, - ConnectorID: connectorID, - } - } - - return batchElement, nil -} - -func matchPaymentType(paymentType string) models.PaymentType { - switch paymentType { - case "PAYIN": - return models.PaymentTypePayIn - case "PAYOUT": - return models.PaymentTypePayOut - case "TRANSFER": - return models.PaymentTypeTransfer - } - - return models.PaymentTypeOther -} - -func matchPaymentStatus(paymentStatus string) models.PaymentStatus { - switch paymentStatus { - case "CREATED": - return models.PaymentStatusPending - case "SUCCEEDED": - return models.PaymentStatusSucceeded - case "FAILED": - return models.PaymentStatusFailed - } - - return models.PaymentStatusOther -} diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/task_fetch_users.go b/components/payments/cmd/connectors/internal/connectors/mangopay/task_fetch_users.go deleted file mode 100644 index 9eb2669144..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/mangopay/task_fetch_users.go +++ /dev/null @@ -1,133 +0,0 @@ -package mangopay - -import ( - "context" - "errors" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/mangopay/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "go.opentelemetry.io/otel/attribute" -) - -type fetchUsersState struct { - LastPage int `json:"last_page"` - LastCreationDate time.Time `json:"last_creation_date"` - - FetchCount int `json:"-"` -} - -func taskFetchUsers(client *client.Client, config *Config) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - scheduler task.Scheduler, - ingester ingestion.Ingester, - resolver task.StateResolver, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "mangopay.taskFetchUsers", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - state := task.MustResolveTo(ctx, resolver, fetchUsersState{}) - if state.LastPage == 0 { - // If last page is 0, it means we haven't fetched any users yet. - // Mangopay pages starts at 1. - state.LastPage = 1 - } - - newState, err := ingestUsers(ctx, client, config, connectorID, scheduler, state) - if err != nil { - otel.RecordError(span, err) - return err - } - - if err := ingester.UpdateTaskState(ctx, newState); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func ingestUsers( - ctx context.Context, - client *client.Client, - config *Config, - connectorID models.ConnectorID, - scheduler task.Scheduler, - state fetchUsersState, -) (fetchUsersState, error) { - users, lastPage, err := client.GetAllUsers(ctx, state.LastPage, pageSize) - if err != nil { - return fetchUsersState{}, err - } - - newState := fetchUsersState{ - LastPage: lastPage, - LastCreationDate: state.LastCreationDate, - } - - for _, user := range users { - userCreationDate := time.Unix(user.CreationDate, 0) - switch userCreationDate.Compare(state.LastCreationDate) { - case -1, 0: - // creationDate <= state.LastCreationDate, nothing to do, - // we already processed this user. - continue - default: - } - - walletsTask, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch wallets from client by user", - Key: taskNameFetchWallets, - UserID: user.ID, - }) - if err != nil { - return fetchUsersState{}, err - } - - err = scheduler.Schedule(ctx, walletsTask, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: config.PollingPeriod.Duration, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return fetchUsersState{}, err - } - - // Bank accounts are never fetched using webhooks, so we need to keep - // polling them. - bankAccountsTask, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch bank accounts from client by user", - Key: taskNameFetchBankAccounts, - UserID: user.ID, - }) - if err != nil { - return fetchUsersState{}, err - } - - err = scheduler.Schedule(ctx, bankAccountsTask, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: config.PollingPeriod.Duration, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return fetchUsersState{}, err - } - - newState.LastCreationDate = userCreationDate - } - - return newState, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/task_fetch_wallets.go b/components/payments/cmd/connectors/internal/connectors/mangopay/task_fetch_wallets.go deleted file mode 100644 index 96bd97e8ba..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/mangopay/task_fetch_wallets.go +++ /dev/null @@ -1,226 +0,0 @@ -package mangopay - -import ( - "context" - "encoding/json" - "fmt" - "math/big" - "sync" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/mangopay/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" -) - -// taskFetchWallets in run inside a periodic task to fetch wallets from the client. -func taskFetchWallets( - client *client.Client, - config *Config, - taskMemoryState *taskMemoryState, - userID string, -) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - scheduler task.Scheduler, - ingester ingestion.Ingester, - resolver task.StateResolver, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "mangopay.taskFetchWallets", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("userID", userID), - ) - defer span.End() - - err := ingestWallets(ctx, client, config, taskMemoryState, userID, connectorID, scheduler, ingester) - if err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func ingestWallets( - ctx context.Context, - client *client.Client, - config *Config, - taskMemoryState *taskMemoryState, - userID string, - connectorID models.ConnectorID, - scheduler task.Scheduler, - ingester ingestion.Ingester, -) error { - for currentPage := 1; ; currentPage++ { - pagedWallets, err := client.GetWallets(ctx, userID, currentPage, pageSize) - if err != nil { - // The client is already deciding if the error is retryable or not. - // Just return it. - return err - } - - if len(pagedWallets) == 0 { - break - } - - if err = handleWallets( - ctx, - config, - taskMemoryState, - userID, - connectorID, - ingester, - scheduler, - pagedWallets, - ); err != nil { - // Since we're just ingesting data, we can safely retry the task in - // case of error - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - if len(pagedWallets) < pageSize { - break - } - } - - return nil -} - -func handleWallets( - ctx context.Context, - config *Config, - taskMemoryState *taskMemoryState, - userID string, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - pagedWallets []*client.Wallet, -) error { - var accountBatch ingestion.AccountBatch - var balanceBatch ingestion.BalanceBatch - var transactionTasks []models.TaskDescriptor - var err error - for _, wallet := range pagedWallets { - transactionTasks, err = appendTransactionTask( - taskMemoryState, - transactionTasks, - userID, - wallet, - ) - if err != nil { - return err - } - - buf, err := json.Marshal(wallet) - if err != nil { - return err - } - - accountBatch = append(accountBatch, &models.Account{ - ID: models.AccountID{ - Reference: wallet.ID, - ConnectorID: connectorID, - }, - CreatedAt: time.Unix(wallet.CreationDate, 0), - Reference: wallet.ID, - ConnectorID: connectorID, - DefaultAsset: currency.FormatAsset(supportedCurrenciesWithDecimal, wallet.Currency), - AccountName: wallet.Description, - // Wallets are internal accounts on our side, since we - // can have their balances. - Type: models.AccountTypeInternal, - Metadata: map[string]string{ - "user_id": userID, - }, - RawData: buf, - }) - - var amount big.Int - _, ok := amount.SetString(wallet.Balance.Amount.String(), 10) - if !ok { - return fmt.Errorf("failed to parse amount: %s", wallet.Balance.Amount.String()) - } - - now := time.Now() - balanceBatch = append(balanceBatch, &models.Balance{ - AccountID: models.AccountID{ - Reference: wallet.ID, - ConnectorID: connectorID, - }, - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, wallet.Balance.Currency), - Balance: &amount, - CreatedAt: now, - LastUpdatedAt: now, - ConnectorID: connectorID, - }) - } - - if err := ingester.IngestAccounts(ctx, accountBatch); err != nil { - return err - } - - if err := ingester.IngestBalances(ctx, balanceBatch, false); err != nil { - return err - } - - for _, transactionTask := range transactionTasks { - err := scheduler.Schedule(ctx, transactionTask, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return err - } - } - - return nil -} - -func appendTransactionTask( - taskMemoryState *taskMemoryState, - transactionTasks []models.TaskDescriptor, - userID string, - wallet *client.Wallet, -) ([]models.TaskDescriptor, error) { - if taskMemoryState.fetchTransactionsOnce == nil { - taskMemoryState.fetchTransactionsOnce = make(map[string]*sync.Once) - } - - key := userID + wallet.ID - _, ok := taskMemoryState.fetchTransactionsOnce[key] - if !ok { - taskMemoryState.fetchTransactionsOnce[key] = &sync.Once{} - } - - once := taskMemoryState.fetchTransactionsOnce[key] - - var err error - once.Do(func() { - var transactionTask models.TaskDescriptor - transactionTask, err = models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch transactions from client by user and wallets", - Key: taskNameFetchTransactions, - UserID: userID, - WalletID: wallet.ID, - }) - if err != nil { - return - } - - transactionTasks = append(transactionTasks, transactionTask) - }) - - return transactionTasks, err -} diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/task_main.go b/components/payments/cmd/connectors/internal/connectors/mangopay/task_main.go deleted file mode 100644 index 47cd876529..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/mangopay/task_main.go +++ /dev/null @@ -1,50 +0,0 @@ -package mangopay - -import ( - "context" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" -) - -// taskMain is the main task of the connector. It launches the other tasks. -func taskMain() task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - scheduler task.Scheduler, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "mangopay.taskMain", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - taskUsers, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch users from client", - Key: taskNameFetchUsers, - }) - if err != nil { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - err = scheduler.Schedule(ctx, taskUsers, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - return nil - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/task_payments.go b/components/payments/cmd/connectors/internal/connectors/mangopay/task_payments.go deleted file mode 100644 index ee8ee08c5d..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/mangopay/task_payments.go +++ /dev/null @@ -1,348 +0,0 @@ -package mangopay - -import ( - "context" - "encoding/json" - "errors" - "regexp" - "time" - - "github.com/formancehq/go-libs/contextutil" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/mangopay/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "go.opentelemetry.io/otel/attribute" -) - -var ( - bankWireRefPatternRegexp = regexp.MustCompile("[a-zA-Z0-9 ]*") -) - -func taskInitiatePayment(mangopayClient *client.Client, transferID string) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - storageReader storage.Reader, - ) error { - transferInitiationID := models.MustTransferInitiationIDFromString(transferID) - - ctx, span := connectors.StartSpan( - ctx, - "mangopay.taskInitiatePayment", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("transferID", transferID), - attribute.String("reference", transferInitiationID.Reference), - ) - defer span.End() - - transfer, err := getTransfer(ctx, storageReader, transferInitiationID, true) - if err != nil { - otel.RecordError(span, err) - return err - } - - if err := initiatePayment(ctx, mangopayClient, transfer, connectorID, ingester, scheduler, storageReader); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func initiatePayment( - ctx context.Context, - mangopayClient *client.Client, - transfer *models.TransferInitiation, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - storageReader storage.Reader, -) error { - var err error - var paymentID *models.PaymentID - defer func() { - if err != nil { - ctx, cancel := contextutil.Detached(ctx) - defer cancel() - _ = ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusFailed, err.Error(), time.Now()) - } - }() - - err = ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusProcessing, "", time.Now()) - if err != nil { - return err - } - - if transfer.SourceAccount == nil { - err = errors.New("no source account provided") - return err - } - - if transfer.SourceAccount.Type == models.AccountTypeExternal { - err = errors.New("payin not implemented: source account must be an internal account") - return err - } - - userID, ok := transfer.SourceAccount.Metadata["user_id"] - if !ok || userID == "" { - err = errors.New("missing user_id in source account metadata") - return err - } - - // No need to modify the amount since it's already in the correct format - // and precision (checked before during API call) - var curr string - curr, _, err = currency.GetCurrencyAndPrecisionFromAsset(supportedCurrenciesWithDecimal, transfer.Asset) - if err != nil { - return err - } - - bankWireRef := "" - if len(transfer.Description) <= 12 && bankWireRefPatternRegexp.MatchString(transfer.Description) { - bankWireRef = transfer.Description - } - - var connectorPaymentID string - var paymentType models.PaymentType - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) - defer cancel() - switch transfer.DestinationAccount.Type { - case models.AccountTypeInternal: - // Transfer between internal accounts - var resp *client.TransferResponse - resp, err = mangopayClient.InitiateWalletTransfer(ctx, &client.TransferRequest{ - AuthorID: userID, - DebitedFunds: client.Funds{ - Currency: curr, - Amount: json.Number(transfer.Amount.String()), - }, - Fees: client.Funds{ - Currency: curr, - Amount: "0", - }, - DebitedWalletID: transfer.SourceAccountID.Reference, - CreditedWalletID: transfer.DestinationAccountID.Reference, - }) - if err != nil { - return err - } - - connectorPaymentID = resp.ID - paymentType = models.PaymentTypeTransfer - case models.AccountTypeExternal: - // Payout to an external account - var resp *client.PayoutResponse - resp, err = mangopayClient.InitiatePayout(ctx, &client.PayoutRequest{ - AuthorID: userID, - DebitedFunds: client.Funds{ - Currency: curr, - Amount: json.Number(transfer.Amount.String()), - }, - Fees: client.Funds{ - Currency: curr, - Amount: json.Number("0"), - }, - DebitedWalletID: transfer.SourceAccountID.Reference, - BankAccountID: transfer.DestinationAccountID.Reference, - BankWireRef: bankWireRef, - }) - if err != nil { - return err - } - - connectorPaymentID = resp.ID - paymentType = models.PaymentTypePayOut - } - - paymentID = &models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: connectorPaymentID, - Type: paymentType, - }, - ConnectorID: connectorID, - } - err = ingester.AddTransferInitiationPaymentID(ctx, transfer, paymentID, time.Now()) - if err != nil { - return err - } - - taskDescriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Update transfer initiation status", - Key: taskNameUpdatePaymentStatus, - TransferID: transfer.ID.String(), - PaymentID: paymentID.String(), - Attempt: 1, - }) - if err != nil { - return err - } - - ctx, _ = contextutil.DetachedWithTimeout(ctx, 10*time.Second) - err = scheduler.Schedule(ctx, taskDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return err - } - - return nil -} - -func taskUpdatePaymentStatus( - mangopayClient *client.Client, - transferID string, - pID string, - attempt int, -) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - storageReader storage.Reader, - ) error { - paymentID := models.MustPaymentIDFromString(pID) - transferInitiationID := models.MustTransferInitiationIDFromString(transferID) - - ctx, span := connectors.StartSpan( - ctx, - "mangopay.taskUpdatePaymentStatus", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("transferID", transferID), - attribute.String("paymentID", paymentID.String()), - attribute.String("reference", transferInitiationID.Reference), - ) - defer span.End() - - transfer, err := getTransfer(ctx, storageReader, transferInitiationID, false) - if err != nil { - otel.RecordError(span, err) - return err - } - - if err := udpatePaymentStatus(ctx, mangopayClient, transfer, paymentID, connectorID, attempt, ingester, scheduler, storageReader); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func udpatePaymentStatus( - ctx context.Context, - mangopayClient *client.Client, - transfer *models.TransferInitiation, - paymentID *models.PaymentID, - connectorID models.ConnectorID, - attempt int, - ingester ingestion.Ingester, - scheduler task.Scheduler, - storageReader storage.Reader, -) error { - var err error - var status string - var resultMessage string - switch transfer.Type { - case models.TransferInitiationTypeTransfer: - var resp *client.TransferResponse - resp, err = mangopayClient.GetWalletTransfer(ctx, paymentID.Reference) - if err != nil { - return err - } - - status = resp.Status - resultMessage = resp.ResultMessage - case models.TransferInitiationTypePayout: - var resp *client.PayoutResponse - resp, err = mangopayClient.GetPayout(ctx, paymentID.Reference) - if err != nil { - return err - } - - status = resp.Status - resultMessage = resp.ResultMessage - } - - switch status { - case "CREATED": - taskDescriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Update transfer initiation status", - Key: taskNameUpdatePaymentStatus, - TransferID: transfer.ID.String(), - PaymentID: paymentID.String(), - Attempt: attempt + 1, - }) - if err != nil { - return err - } - - err = scheduler.Schedule(ctx, taskDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_IN_DURATION, - Duration: 2 * time.Minute, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return err - } - case "SUCCEEDED": - err = ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusProcessed, "", time.Now()) - if err != nil { - return err - } - - return nil - case "FAILED": - err = ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusFailed, resultMessage, time.Now()) - if err != nil { - return err - } - - return nil - } - - return nil -} - -func getTransfer( - ctx context.Context, - reader storage.Reader, - transferID models.TransferInitiationID, - expand bool, -) (*models.TransferInitiation, error) { - transfer, err := reader.ReadTransferInitiation(ctx, transferID) - if err != nil { - return nil, err - } - - if expand { - if transfer.SourceAccountID != nil { - sourceAccount, err := reader.GetAccount(ctx, transfer.SourceAccountID.String()) - if err != nil { - return nil, err - } - transfer.SourceAccount = sourceAccount - } - - destinationAccount, err := reader.GetAccount(ctx, transfer.DestinationAccountID.String()) - if err != nil { - return nil, err - } - transfer.DestinationAccount = destinationAccount - } - - return transfer, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/task_resolve.go b/components/payments/cmd/connectors/internal/connectors/mangopay/task_resolve.go deleted file mode 100644 index 86ecca0f92..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/mangopay/task_resolve.go +++ /dev/null @@ -1,94 +0,0 @@ -package mangopay - -import ( - "fmt" - "sync" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/mangopay/client" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/google/uuid" -) - -const ( - taskNameMain = "main" - taskNameFetchUsers = "fetch-users" - taskNameFetchTransactions = "fetch-transactions" - taskNameFetchWallets = "fetch-wallets" - taskNameFetchBankAccounts = "fetch-bank-accounts" - taskNameInitiatePayment = "initiate-payment" - taskNameUpdatePaymentStatus = "update-payment-status" - taskNameCreateExternalBankAccount = "create-external-bank-account" - taskNameCreateWebhook = "create-webhook" - taskNameHandleWebhook = "handle-webhook" -) - -// TaskDescriptor is the definition of a task. -type TaskDescriptor struct { - Name string `json:"name" yaml:"name" bson:"name"` - Key string `json:"key" yaml:"key" bson:"key"` - UserID string `json:"userID" yaml:"userID" bson:"userID"` - WalletID string `json:"walletID" yaml:"walletID" bson:"walletID"` - TransferID string `json:"transferID" yaml:"transferID" bson:"transferID"` - PaymentID string `json:"paymentID" yaml:"paymentID" bson:"paymentID"` - Attempt int `json:"attempt" yaml:"attempt" bson:"attempt"` - BankAccountID uuid.UUID `json:"bankAccountID,omitempty" yaml:"bankAccountID" bson:"bankAccountID"` - WebhookID uuid.UUID `json:"webhookId,omitempty" yaml:"webhookId" bson:"webhookId"` - PollingPeriod connectors.Duration `json:"pollingPeriod" yaml:"pollingPeriod" bson:"pollingPeriod"` -} - -// internal state not pushed in the database -type taskMemoryState struct { - // We want to fetch the transactions once per service start. - fetchTransactionsOnce map[string]*sync.Once -} - -// clientID, apiKey, endpoint string, logger logging -func resolveTasks(logger logging.Logger, config Config, taskMemoryState *taskMemoryState) func(taskDefinition TaskDescriptor) task.Task { - mangopayClient, err := client.NewClient( - config.ClientID, - config.APIKey, - config.Endpoint, - logger, - ) - if err != nil { - logger.Error(err) - - return func(taskDescriptor TaskDescriptor) task.Task { - return func() error { - return fmt.Errorf("cannot build mangopay client: %w", err) - } - } - } - - return func(taskDescriptor TaskDescriptor) task.Task { - switch taskDescriptor.Key { - case taskNameMain: - return taskMain() - case taskNameFetchUsers: - return taskFetchUsers(mangopayClient, &config) - case taskNameFetchBankAccounts: - return taskFetchBankAccounts(mangopayClient, &config, taskDescriptor.UserID) - case taskNameFetchTransactions: - return taskFetchTransactions(mangopayClient, &config, taskDescriptor.WalletID) - case taskNameFetchWallets: - return taskFetchWallets(mangopayClient, &config, taskMemoryState, taskDescriptor.UserID) - case taskNameCreateWebhook: - return taskCreateWebhooks(mangopayClient) - case taskNameHandleWebhook: - return taskHandleWebhooks(mangopayClient, taskDescriptor.WebhookID) - case taskNameInitiatePayment: - return taskInitiatePayment(mangopayClient, taskDescriptor.TransferID) - case taskNameUpdatePaymentStatus: - return taskUpdatePaymentStatus(mangopayClient, taskDescriptor.TransferID, taskDescriptor.PaymentID, taskDescriptor.Attempt) - case taskNameCreateExternalBankAccount: - return taskCreateExternalBankAccount(mangopayClient, taskDescriptor.BankAccountID) - } - - // This should never happen. - return func() error { - return fmt.Errorf("key '%s': %w", taskDescriptor.Key, ErrMissingTask) - } - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/task_webhooks.go b/components/payments/cmd/connectors/internal/connectors/mangopay/task_webhooks.go deleted file mode 100644 index a20d9eaa07..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/mangopay/task_webhooks.go +++ /dev/null @@ -1,607 +0,0 @@ -package mangopay - -import ( - "context" - "encoding/json" - "fmt" - "math/big" - "net/http" - "os" - "time" - - "github.com/formancehq/go-libs/api" - "github.com/formancehq/go-libs/contextutil" - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/mangopay/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/google/uuid" - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -func handleWebhooks(store *storage.Storage) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - connectorContext := task.ConnectorContextFromContext(r.Context()) - webhookID := connectors.WebhookIDFromContext(r.Context()) - span := trace.SpanFromContext(r.Context()) - - // Mangopay does not send us the event inside the body, but using - // URL query. - eventType := r.URL.Query().Get("EventType") - resourceID := r.URL.Query().Get("RessourceId") - - hook := client.Webhook{ - ResourceID: resourceID, - EventType: client.EventType(eventType), - } - - body, err := json.Marshal(hook) - if err != nil { - otel.RecordError(span, err) - api.InternalServerError(w, r, err) - return - } - - if err := store.UpdateWebhookRequestBody(r.Context(), webhookID, body); err != nil { - otel.RecordError(span, err) - api.InternalServerError(w, r, err) - return - } - - detachedCtx, _ := contextutil.DetachedWithTimeout(r.Context(), 30*time.Second) - taskDescriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "handle webhook", - Key: taskNameHandleWebhook, - WebhookID: webhookID, - }) - if err != nil { - otel.RecordError(span, err) - api.InternalServerError(w, r, err) - return - } - - err = connectorContext.Scheduler().Schedule(detachedCtx, taskDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - otel.RecordError(span, err) - api.InternalServerError(w, r, err) - return - } - } -} - -func taskCreateWebhooks(c *client.Client) task.Task { - return func( - ctx context.Context, - logger logging.Logger, - taskID models.TaskID, - connectorID models.ConnectorID, - storageReader storage.Reader, - ingester ingestion.Ingester, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "mangopay.taskCreateWebhooks", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - stackPublicURL := os.Getenv("STACK_PUBLIC_URL") - if stackPublicURL == "" { - err := errors.New("STACK_PUBLIC_URL is not set") - otel.RecordError(span, err) - return err - } - - webhookURL := fmt.Sprintf("%s/api/payments/connectors/webhooks/mangopay/%s/", stackPublicURL, &connectorID) - logger.Infof("creating webhook for mangopay with url %s", webhookURL) - - alreadyExistingHooks, err := c.ListAllHooks(ctx) - if err != nil { - otel.RecordError(span, err) - return err - } - - activeHooks := make(map[client.EventType]*client.Hook) - for _, hook := range alreadyExistingHooks { - // Mangopay allows only one active hook per event type. - if hook.Validity == "VALID" { - activeHooks[hook.EventType] = hook - } - } - - for _, eventType := range client.AllEventTypes { - if v, ok := activeHooks[eventType]; ok { - // Already created, continue - - if v.URL != webhookURL { - // If the URL is different, update it - err := c.UpdateHook(ctx, v.ID, webhookURL) - if err != nil { - otel.RecordError(span, err) - return err - } - - logger.Infof("updated webhook for mangopay with event type %s", eventType) - } - - continue - } - - // Otherwise, create it - err := c.CreateHook(ctx, eventType, webhookURL) - if err != nil { - otel.RecordError(span, err) - return err - } - - logger.Infof("created webhook for mangopay with event type %s", eventType) - } - - return nil - } -} - -func taskHandleWebhooks(c *client.Client, webhookID uuid.UUID) task.Task { - return func( - ctx context.Context, - logger logging.Logger, - taskID models.TaskID, - connectorID models.ConnectorID, - storageReader storage.Reader, - ingester ingestion.Ingester, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "mangopay.taskHandleWebhooks", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("webhookID", webhookID.String()), - ) - defer span.End() - - w, err := storageReader.GetWebhook(ctx, webhookID) - if err != nil { - otel.RecordError(span, err) - return err - } - - webhook, err := c.UnmarshalWebhooks((string(w.RequestBody))) - if err != nil { - otel.RecordError(span, err) - return err - } - - switch webhook.EventType { - case client.EventTypeTransferNormalCreated, - client.EventTypeTransferNormalFailed, - client.EventTypeTransferNormalSucceeded: - logger.WithField("webhook", webhook).Info("handling transfer webhook") - return handleTransfer( - ctx, - c, - connectorID, - webhook, - ingester, - ) - - case client.EventTypePayoutNormalCreated, - client.EventTypePayoutNormalFailed, - client.EventTypePayoutNormalSucceeded, - client.EventTypePayoutInstantFailed, - client.EventTypePayoutInstantSucceeded: - logger.WithField("webhook", webhook).Info("handling payout webhook") - return handlePayout( - ctx, - c, - connectorID, - webhook, - ingester, - ) - - case client.EventTypePayinNormalCreated, - client.EventTypePayinNormalSucceeded, - client.EventTypePayinNormalFailed: - logger.WithField("webhook", webhook).Info("handling payin webhook") - return handlePayIn( - ctx, - c, - connectorID, - webhook, - ingester, - ) - - case client.EventTypeTransferRefundFailed, - client.EventTypeTransferRefundSucceeded, - client.EventTypePayOutRefundFailed, - client.EventTypePayOutRefundSucceeded, - client.EventTypePayinRefundFailed, - client.EventTypePayinRefundSucceeded: - logger.WithField("webhook", webhook).Info("handling refunds webhook") - return handleRefunds( - ctx, - c, - connectorID, - webhook, - ingester, - storageReader, - ) - - case client.EventTypeTransferRefundCreated, - client.EventTypePayOutRefundCreated, - client.EventTypePayinRefundCreated: - // NOTE: we don't handle these events, as we are only interested in - // the refund successed or failures. - - default: - // ignore unknown events - logger.Errorf("unknown event type: %s", webhook.EventType) - return nil - } - - return nil - } -} - -func handleTransfer( - ctx context.Context, - c *client.Client, - connectorID models.ConnectorID, - webhook *client.Webhook, - ingester ingestion.Ingester, -) error { - transfer, err := c.GetWalletTransfer(ctx, webhook.ResourceID) - if err != nil { - return err - } - - if err := fetchWallet(ctx, c, connectorID, transfer.CreditedWalletID, ingester); err != nil { - return err - } - - if err := fetchWallet(ctx, c, connectorID, transfer.DebitedWalletID, ingester); err != nil { - return err - } - - var amount big.Int - _, ok := amount.SetString(transfer.DebitedFunds.Amount.String(), 10) - if !ok { - return fmt.Errorf("failed to parse amount %s", transfer.DebitedFunds.Amount.String()) - } - paymentStatus := matchPaymentStatus(transfer.Status) - raw, err := json.Marshal(transfer) - if err != nil { - return fmt.Errorf("failed to marshal transfer: %w", err) - } - - payment := &models.Payment{ - ID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: transfer.ID, - Type: models.PaymentTypeTransfer, - }, - ConnectorID: connectorID, - }, - CreatedAt: time.Unix(transfer.CreationDate, 0), - Reference: transfer.ID, - Amount: &amount, - InitialAmount: &amount, - ConnectorID: connectorID, - Type: models.PaymentTypeTransfer, - Status: paymentStatus, - Scheme: models.PaymentSchemeOther, - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transfer.DebitedFunds.Currency), - RawData: raw, - } - - if transfer.DebitedWalletID != "" { - payment.SourceAccountID = &models.AccountID{ - Reference: transfer.DebitedWalletID, - ConnectorID: connectorID, - } - } - - if transfer.CreditedWalletID != "" { - payment.DestinationAccountID = &models.AccountID{ - Reference: transfer.CreditedWalletID, - ConnectorID: connectorID, - } - } - - err = ingester.IngestPayments(ctx, []ingestion.PaymentBatchElement{{ - Payment: payment, - }}) - if err != nil { - return err - } - - return nil -} - -func handlePayIn( - ctx context.Context, - c *client.Client, - connectorID models.ConnectorID, - webhook *client.Webhook, - ingester ingestion.Ingester, -) error { - payin, err := c.GetPayin(ctx, webhook.ResourceID) - if err != nil { - return err - } - - // In case of a payin, there is no debited wallet id, so we can only - // fetch the credited wallet. - if err := fetchWallet(ctx, c, connectorID, payin.CreditedWalletID, ingester); err != nil { - return err - } - - var amount big.Int - _, ok := amount.SetString(payin.DebitedFunds.Amount.String(), 10) - if !ok { - return fmt.Errorf("failed to parse amount %s", payin.DebitedFunds.Amount.String()) - } - paymentStatus := matchPaymentStatus(payin.Status) - raw, err := json.Marshal(payin) - if err != nil { - return fmt.Errorf("failed to marshal transfer: %w", err) - } - - payment := &models.Payment{ - ID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: payin.ID, - Type: models.PaymentTypePayIn, - }, - ConnectorID: connectorID, - }, - CreatedAt: time.Unix(payin.CreationDate, 0), - Reference: payin.ID, - Amount: &amount, - InitialAmount: &amount, - ConnectorID: connectorID, - Type: models.PaymentTypePayIn, - Status: paymentStatus, - Scheme: models.PaymentSchemeOther, - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, payin.DebitedFunds.Currency), - RawData: raw, - } - - if payin.CreditedWalletID != "" { - payment.DestinationAccountID = &models.AccountID{ - Reference: payin.CreditedWalletID, - ConnectorID: connectorID, - } - } - - err = ingester.IngestPayments(ctx, []ingestion.PaymentBatchElement{{ - Payment: payment, - }}) - if err != nil { - return err - } - - return nil -} - -func handlePayout( - ctx context.Context, - c *client.Client, - connectorID models.ConnectorID, - webhook *client.Webhook, - ingester ingestion.Ingester, -) error { - payout, err := c.GetPayout(ctx, webhook.ResourceID) - if err != nil { - return err - } - - // In case of a payout, there is no credited wallet id, so we can only - // fetch the debited wallet. - if err := fetchWallet(ctx, c, connectorID, payout.DebitedWalletID, ingester); err != nil { - return err - } - - var amount big.Int - _, ok := amount.SetString(payout.DebitedFunds.Amount.String(), 10) - if !ok { - return fmt.Errorf("failed to parse amount %s", payout.DebitedFunds.Amount.String()) - } - paymentStatus := matchPaymentStatus(payout.Status) - raw, err := json.Marshal(payout) - if err != nil { - return fmt.Errorf("failed to marshal transfer: %w", err) - } - - payment := &models.Payment{ - ID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: payout.ID, - Type: models.PaymentTypePayOut, - }, - ConnectorID: connectorID, - }, - CreatedAt: time.Unix(payout.CreationDate, 0), - Reference: payout.ID, - Amount: &amount, - InitialAmount: &amount, - ConnectorID: connectorID, - Type: models.PaymentTypePayOut, - Status: paymentStatus, - Scheme: models.PaymentSchemeOther, - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, payout.DebitedFunds.Currency), - RawData: raw, - } - - if payout.DebitedWalletID != "" { - payment.DestinationAccountID = &models.AccountID{ - Reference: payout.DebitedWalletID, - ConnectorID: connectorID, - } - } - - err = ingester.IngestPayments(ctx, []ingestion.PaymentBatchElement{{ - Payment: payment, - }}) - if err != nil { - return err - } - - return nil -} - -func handleRefunds( - ctx context.Context, - c *client.Client, - connectorID models.ConnectorID, - webhook *client.Webhook, - ingester ingestion.Ingester, - storageReader storage.Reader, -) error { - refund, err := c.GetRefund(ctx, webhook.ResourceID) - if err != nil { - return err - } - - var amountRefunded big.Int - _, ok := amountRefunded.SetString(refund.DebitedFunds.Amount.String(), 10) - if !ok { - return fmt.Errorf("failed to parse amount %s", refund.DebitedFunds.Amount.String()) - } - paymentType := matchPaymentType(refund.InitialTransactionType) - - var payment *models.Payment - if webhook.EventType == client.EventTypePayOutRefundSucceeded { - payment, err = storageReader.GetPayment(ctx, models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: refund.InitialTransactionID, - Type: paymentType, - }, - ConnectorID: connectorID, - }.String()) - if err != nil { - return err - } - - payment.Amount = payment.Amount.Sub(payment.Amount, &amountRefunded) - } - - paymentStatus := models.PaymentStatusRefundedFailure - if webhook.EventType == client.EventTypePayOutRefundSucceeded { - paymentStatus = models.PaymentStatusRefunded - } - - raw, err := json.Marshal(refund) - if err != nil { - return fmt.Errorf("failed to marshal refund: %w", err) - } - - adjustment := &models.PaymentAdjustment{ - PaymentID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: refund.InitialTransactionID, - Type: paymentType, - }, - ConnectorID: connectorID, - }, - CreatedAt: time.Unix(refund.CreationDate, 0), - Reference: refund.ID, - Amount: &amountRefunded, - Status: paymentStatus, - RawData: raw, - } - - if err := ingester.IngestPayments(ctx, []ingestion.PaymentBatchElement{{ - Payment: payment, - Adjustment: adjustment, - }}); err != nil { - return err - } - - return nil -} - -func fetchWallet( - ctx context.Context, - c *client.Client, - connectorID models.ConnectorID, - walletID string, - ingester ingestion.Ingester, -) error { - if walletID == "" { - return nil - } - - wallet, err := c.GetWallet(ctx, walletID) - if err != nil { - return err - } - - raw, err := json.Marshal(wallet) - if err != nil { - return err - } - - userID := "" - if len(wallet.Owners) > 0 { - userID = wallet.Owners[0] - } - - account := &models.Account{ - ID: models.AccountID{ - Reference: wallet.ID, - ConnectorID: connectorID, - }, - CreatedAt: time.Unix(wallet.CreationDate, 0), - Reference: wallet.ID, - ConnectorID: connectorID, - DefaultAsset: currency.FormatAsset(supportedCurrenciesWithDecimal, wallet.Currency), - AccountName: wallet.Description, - // Wallets are internal accounts on our side, since we - // can have their balances. - Type: models.AccountTypeInternal, - Metadata: map[string]string{ - "user_id": userID, - }, - RawData: raw, - } - - var amount big.Int - _, ok := amount.SetString(wallet.Balance.Amount.String(), 10) - if !ok { - return fmt.Errorf("failed to parse amount: %s", wallet.Balance.Amount.String()) - } - - now := time.Now() - balance := &models.Balance{ - AccountID: models.AccountID{ - Reference: wallet.ID, - ConnectorID: connectorID, - }, - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, wallet.Balance.Currency), - Balance: &amount, - CreatedAt: now, - LastUpdatedAt: now, - ConnectorID: connectorID, - } - - if err := ingester.IngestAccounts(ctx, []*models.Account{account}); err != nil { - return err - } - - if err := ingester.IngestBalances(ctx, []*models.Balance{balance}, false); err != nil { - return err - } - - return nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/modulr/client/accounts.go b/components/payments/cmd/connectors/internal/connectors/modulr/client/accounts.go deleted file mode 100644 index 5e98921332..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/modulr/client/accounts.go +++ /dev/null @@ -1,64 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "strconv" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" -) - -//nolint:tagliatelle // allow for clients -type Account struct { - ID string `json:"id"` - Name string `json:"name"` - Status string `json:"status"` - Balance string `json:"balance"` - Currency string `json:"currency"` - CustomerID string `json:"customerId"` - Identifiers []struct { - AccountNumber string `json:"accountNumber"` - SortCode string `json:"sortCode"` - Type string `json:"type"` - } `json:"identifiers"` - DirectDebit bool `json:"directDebit"` - CreatedDate string `json:"createdDate"` -} - -func (m *Client) GetAccounts(ctx context.Context, page, pageSize int) (*responseWrapper[[]*Account], error) { - f := connectors.ClientMetrics(ctx, "modulr", "list_accounts") - now := time.Now() - defer f(ctx, now) - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, m.buildEndpoint("accounts"), http.NoBody) - if err != nil { - return nil, fmt.Errorf("failed to create accounts request: %w", err) - } - - q := req.URL.Query() - q.Add("page", strconv.Itoa(page)) - q.Add("size", strconv.Itoa(pageSize)) - q.Add("sortField", "createdDate") - q.Add("sortOrder", "asc") - req.URL.RawQuery = q.Encode() - - resp, err := m.httpClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, unmarshalErrorWithRetry(resp.StatusCode, resp.Body).Error() - } - - var res responseWrapper[[]*Account] - if err = json.NewDecoder(resp.Body).Decode(&res); err != nil { - return nil, err - } - - return &res, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/modulr/client/beneficiaries.go b/components/payments/cmd/connectors/internal/connectors/modulr/client/beneficiaries.go deleted file mode 100644 index da3d6933dc..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/modulr/client/beneficiaries.go +++ /dev/null @@ -1,55 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "strconv" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" -) - -type Beneficiary struct { - ID string `json:"id"` - Name string `json:"name"` - Created string `json:"created"` -} - -func (m *Client) GetBeneficiaries(ctx context.Context, page, pageSize int, modifiedSince string) (*responseWrapper[[]*Beneficiary], error) { - f := connectors.ClientMetrics(ctx, "modulr", "list_beneficiaries") - now := time.Now() - defer f(ctx, now) - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, m.buildEndpoint("beneficiaries"), http.NoBody) - if err != nil { - return nil, fmt.Errorf("failed to create accounts request: %w", err) - } - - q := req.URL.Query() - q.Add("page", strconv.Itoa(page)) - q.Add("size", strconv.Itoa(pageSize)) - if modifiedSince != "" { - q.Add("modifiedSince", modifiedSince) - } - req.URL.RawQuery = q.Encode() - - resp, err := m.httpClient.Do(req) - if err != nil { - return nil, err - } - - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, unmarshalErrorWithRetry(resp.StatusCode, resp.Body).Error() - } - - var res responseWrapper[[]*Beneficiary] - if err = json.NewDecoder(resp.Body).Decode(&res); err != nil { - return nil, err - } - - return &res, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/modulr/client/error.go b/components/payments/cmd/connectors/internal/connectors/modulr/client/error.go deleted file mode 100644 index 1b4e117343..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/modulr/client/error.go +++ /dev/null @@ -1,83 +0,0 @@ -package client - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/pkg/errors" -) - -type modulrError struct { - StatusCode int `json:"-"` - Field string `json:"field"` - Code string `json:"code"` - Message string `json:"message"` - ErrorCode string `json:"errorCode"` - SourceService string `json:"sourceService"` - WithRetry bool `json:"-"` -} - -func (me *modulrError) Error() error { - var err error - if me.Message == "" { - err = fmt.Errorf("unexpected status code: %d", me.StatusCode) - } else { - err = fmt.Errorf("%d: %s", me.StatusCode, me.Message) - } - - if me.WithRetry { - return checkStatusCodeError(me.StatusCode, err) - } - - return errors.Wrap(task.ErrNonRetryable, err.Error()) -} - -func unmarshalError(statusCode int, body io.ReadCloser, withRetry bool) *modulrError { - var ces []modulrError - _ = json.NewDecoder(body).Decode(&ces) - - if len(ces) == 0 { - return &modulrError{ - StatusCode: statusCode, - WithRetry: withRetry, - } - } - - return &modulrError{ - StatusCode: statusCode, - Field: ces[0].Field, - Code: ces[0].Code, - Message: ces[0].Message, - ErrorCode: ces[0].ErrorCode, - SourceService: ces[0].SourceService, - WithRetry: withRetry, - } -} - -func unmarshalErrorWithRetry(statusCode int, body io.ReadCloser) *modulrError { - return unmarshalError(statusCode, body, true) -} - -func unmarshalErrorWithoutRetry(statusCode int, body io.ReadCloser) *modulrError { - return unmarshalError(statusCode, body, false) -} - -func checkStatusCodeError(statusCode int, err error) error { - switch statusCode { - case http.StatusTooEarly, http.StatusRequestTimeout: - return errors.Wrap(task.ErrRetryable, err.Error()) - case http.StatusTooManyRequests: - // Retry rate limit errors - // TODO(polo): add rate limit handling - return errors.Wrap(task.ErrRetryable, err.Error()) - case http.StatusInternalServerError, http.StatusBadGateway, - http.StatusServiceUnavailable, http.StatusGatewayTimeout: - // Retry internal errors - return errors.Wrap(task.ErrRetryable, err.Error()) - default: - return errors.Wrap(task.ErrNonRetryable, err.Error()) - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/modulr/client/payout.go b/components/payments/cmd/connectors/internal/connectors/modulr/client/payout.go deleted file mode 100644 index 04ab644174..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/modulr/client/payout.go +++ /dev/null @@ -1,83 +0,0 @@ -package client - -import ( - "bytes" - "context" - "encoding/json" - "net/http" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" -) - -type PayoutRequest struct { - SourceAccountID string `json:"sourceAccountId"` - Destination struct { - Type string `json:"type"` - ID string `json:"id"` - } `json:"destination"` - Currency string `json:"currency"` - Amount json.Number `json:"amount"` - Reference string `json:"reference"` - ExternalReference string `json:"externalReference"` -} - -type PayoutResponse struct { - ID string `json:"id"` - Status string `json:"status"` - CreatedDate string `json:"createdDate"` - ExternalReference string `json:"externalReference"` - ApprovalStatus string `json:"approvalStatus"` - Message string `json:"message"` -} - -func (c *Client) InitiatePayout(ctx context.Context, payoutRequest *PayoutRequest) (*PayoutResponse, error) { - f := connectors.ClientMetrics(ctx, "modulr", "initiate_payout") - now := time.Now() - defer f(ctx, now) - - body, err := json.Marshal(payoutRequest) - if err != nil { - return nil, err - } - - resp, err := c.httpClient.Post(c.buildEndpoint("payments"), "application/json", bytes.NewBuffer(body)) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusCreated { - return nil, unmarshalErrorWithoutRetry(resp.StatusCode, resp.Body).Error() - } - - var res PayoutResponse - if err = json.NewDecoder(resp.Body).Decode(&res); err != nil { - return nil, err - } - - return &res, nil -} - -func (c *Client) GetPayout(ctx context.Context, payoutID string) (*PayoutResponse, error) { - f := connectors.ClientMetrics(ctx, "modulr", "get_payout") - now := time.Now() - defer f(ctx, now) - - resp, err := c.httpClient.Get(c.buildEndpoint("payments?id=%s", payoutID)) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, unmarshalErrorWithRetry(resp.StatusCode, resp.Body).Error() - } - - var res PayoutResponse - if err = json.NewDecoder(resp.Body).Decode(&res); err != nil { - return nil, err - } - - return &res, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/modulr/config.go b/components/payments/cmd/connectors/internal/connectors/modulr/config.go deleted file mode 100644 index 343e8b733a..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/modulr/config.go +++ /dev/null @@ -1,69 +0,0 @@ -package modulr - -import ( - "encoding/json" - "fmt" - "strconv" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/configtemplate" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/modulr/client" -) - -const ( - defaultPollingPeriod = 2 * time.Minute - defaultPageSize = 100 -) - -type Config struct { - Name string `json:"name" bson:"name"` - APIKey string `json:"apiKey" bson:"apiKey"` - APISecret string `json:"apiSecret" bson:"apiSecret"` - Endpoint string `json:"endpoint" bson:"endpoint"` - PollingPeriod connectors.Duration `json:"pollingPeriod" yaml:"pollingPeriod" bson:"pollingPeriod"` - PageSize int `json:"pageSize" yaml:"pageSize" bson:"pageSize"` -} - -// String obfuscates sensitive fields and returns a string representation of the config. -// This is used for logging. -func (c Config) String() string { - return fmt.Sprintf("endpoint=%s, apiSecret=***, apiKey=****", c.Endpoint) -} - -func (c Config) Validate() error { - if c.APIKey == "" { - return ErrMissingAPIKey - } - - if c.APISecret == "" { - return ErrMissingAPISecret - } - - if c.Name == "" { - return ErrMissingName - } - - return nil -} - -func (c Config) Marshal() ([]byte, error) { - return json.Marshal(c) -} - -func (c Config) ConnectorName() string { - return c.Name -} - -func (c Config) BuildTemplate() (string, configtemplate.Config) { - cfg := configtemplate.NewConfig() - - cfg.AddParameter("name", configtemplate.TypeString, name.String(), false) - cfg.AddParameter("apiKey", configtemplate.TypeString, "", true) - cfg.AddParameter("apiSecret", configtemplate.TypeString, "", true) - cfg.AddParameter("endpoint", configtemplate.TypeString, client.SandboxAPIEndpoint, false) - cfg.AddParameter("pollingPeriod", configtemplate.TypeDurationNs, defaultPollingPeriod.String(), false) - cfg.AddParameter("pageSize", configtemplate.TypeDurationUnsignedInteger, strconv.Itoa(defaultPageSize), false) - - return name.String(), cfg -} diff --git a/components/payments/cmd/connectors/internal/connectors/modulr/connector.go b/components/payments/cmd/connectors/internal/connectors/modulr/connector.go deleted file mode 100644 index 23f43232e6..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/modulr/connector.go +++ /dev/null @@ -1,136 +0,0 @@ -package modulr - -import ( - "context" - - "github.com/formancehq/payments/internal/models" - "github.com/pkg/errors" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/task" -) - -const name = models.ConnectorProviderModulr - -var ( - mainTaskDescriptor = TaskDescriptor{ - Name: "Main task to periodically fetch accounts and transactions", - Key: taskNameMain, - } -) - -type Connector struct { - logger logging.Logger - cfg Config -} - -func newConnector(logger logging.Logger, cfg Config) *Connector { - return &Connector{ - logger: logger.WithFields(map[string]any{ - "component": "connector", - }), - cfg: cfg, - } -} - -func (c *Connector) UpdateConfig(ctx task.ConnectorContext, config models.ConnectorConfigObject) error { - cfg, ok := config.(Config) - if !ok { - return connectors.ErrInvalidConfig - } - - restartTask := c.cfg.PollingPeriod.Duration != cfg.PollingPeriod.Duration - - c.cfg = cfg - - if restartTask { - taskDescriptor, err := models.EncodeTaskDescriptor(mainTaskDescriptor) - if err != nil { - return err - } - - return ctx.Scheduler().Schedule(ctx.Context(), taskDescriptor, models.TaskSchedulerOptions{ - // We want to polling every c.cfg.PollingPeriod.Duration seconds the users - // and their transactions. - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: c.cfg.PollingPeriod.Duration, - // No need to restart this task, since the connector is not existing or - // was uninstalled previously, the task does not exists in the database - RestartOption: models.OPTIONS_STOP_AND_RESTART, - }) - } - - return nil -} - -func (c *Connector) Install(ctx task.ConnectorContext) error { - taskDescriptor, err := models.EncodeTaskDescriptor(mainTaskDescriptor) - if err != nil { - return err - } - - return ctx.Scheduler().Schedule(ctx.Context(), taskDescriptor, models.TaskSchedulerOptions{ - // We want to polling every c.cfg.PollingPeriod.Duration seconds the users - // and their transactions. - ScheduleOption: models.OPTIONS_RUN_NOW, - // No need to restart this task, since the connector is not existing or - // was uninstalled previously, the task does not exists in the database - RestartOption: models.OPTIONS_RESTART_NEVER, - }) -} - -func (c *Connector) Uninstall(ctx context.Context) error { - return nil -} - -func (c *Connector) Resolve(descriptor models.TaskDescriptor) task.Task { - taskDescriptor, err := models.DecodeTaskDescriptor[TaskDescriptor](descriptor) - if err != nil { - return nil - } - - return resolveTasks(c.logger, c.cfg)(taskDescriptor) -} - -func (c *Connector) SupportedCurrenciesAndDecimals() map[string]int { - return supportedCurrenciesWithDecimal -} - -func (c *Connector) InitiatePayment(ctx task.ConnectorContext, transfer *models.TransferInitiation) error { - taskDescriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Initiate payment", - Key: taskNameInitiatePayment, - TransferID: transfer.ID.String(), - }) - if err != nil { - return err - } - - scheduleOption := models.OPTIONS_RUN_NOW_SYNC - scheduledAt := transfer.ScheduledAt - if !scheduledAt.IsZero() { - scheduleOption = models.OPTIONS_RUN_SCHEDULED_AT - } - - err = ctx.Scheduler().Schedule(ctx.Context(), taskDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: scheduleOption, - ScheduleAt: scheduledAt, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return err - } - - return nil -} - -func (c *Connector) ReversePayment(ctx task.ConnectorContext, transferReversal *models.TransferReversal) error { - return connectors.ErrNotImplemented -} - -func (c *Connector) CreateExternalBankAccount(ctx task.ConnectorContext, bankAccount *models.BankAccount) error { - return connectors.ErrNotImplemented -} - -var _ connectors.Connector = &Connector{} diff --git a/components/payments/cmd/connectors/internal/connectors/modulr/errors.go b/components/payments/cmd/connectors/internal/connectors/modulr/errors.go deleted file mode 100644 index f756d3c1f5..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/modulr/errors.go +++ /dev/null @@ -1,17 +0,0 @@ -package modulr - -import "github.com/pkg/errors" - -var ( - // ErrMissingTask is returned when the task is missing. - ErrMissingTask = errors.New("task is not implemented") - - // ErrMissingAPIKey is returned when the api key is missing from config. - ErrMissingAPIKey = errors.New("missing apiKey from config") - - // ErrMissingAPISecret is returned when the api secret is missing from config. - ErrMissingAPISecret = errors.New("missing apiSecret from config") - - // ErrMissingName is returned when the name is missing from config. - ErrMissingName = errors.New("missing name from config") -) diff --git a/components/payments/cmd/connectors/internal/connectors/modulr/loader.go b/components/payments/cmd/connectors/internal/connectors/modulr/loader.go deleted file mode 100644 index c8b7a88c74..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/modulr/loader.go +++ /dev/null @@ -1,51 +0,0 @@ -package modulr - -import ( - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/gorilla/mux" -) - -type Loader struct{} - -const allowedTasks = 50 - -func (l *Loader) AllowTasks() int { - return allowedTasks -} - -func (l *Loader) Name() models.ConnectorProvider { - return name -} - -func (l *Loader) Load(logger logging.Logger, config Config) connectors.Connector { - return newConnector(logger, config) -} - -func (l *Loader) ApplyDefaults(cfg Config) Config { - if cfg.PollingPeriod.Duration == 0 { - cfg.PollingPeriod.Duration = defaultPollingPeriod - } - - if cfg.Name == "" { - cfg.Name = name.String() - } - - if cfg.PageSize == 0 { - cfg.PageSize = defaultPageSize - } - - return cfg -} - -func (l *Loader) Router(_ *storage.Storage) *mux.Router { - // Webhooks are not implemented yet - return nil -} - -// NewLoader creates a new loader. -func NewLoader() *Loader { - return &Loader{} -} diff --git a/components/payments/cmd/connectors/internal/connectors/modulr/task_fetch_accounts.go b/components/payments/cmd/connectors/internal/connectors/modulr/task_fetch_accounts.go deleted file mode 100644 index b1e8fcffb9..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/modulr/task_fetch_accounts.go +++ /dev/null @@ -1,179 +0,0 @@ -package modulr - -import ( - "context" - "encoding/json" - "fmt" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/modulr/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" -) - -func taskFetchAccounts(config Config, client *client.Client) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "modulr.taskFetchAccounts", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - err := fetchAccounts( - ctx, - config, - client, - connectorID, - ingester, - scheduler, - ) - if err != nil { - otel.RecordError(span, err) - // Retry errors are handled by the function - return err - } - - return nil - } -} - -func fetchAccounts( - ctx context.Context, - config Config, - client *client.Client, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, -) error { - for page := 0; ; page++ { - pagedAccounts, err := client.GetAccounts( - ctx, - page, - config.PageSize, - ) - if err != nil { - // Retry errors are handled by the client - return err - } - - if len(pagedAccounts.Content) == 0 { - break - } - - if err := ingestAccountsBatch(ctx, connectorID, ingester, pagedAccounts.Content); err != nil { - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - for _, account := range pagedAccounts.Content { - transactionsTask, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch transactions from client by account", - Key: taskNameFetchTransactions, - AccountID: account.ID, - }) - if err != nil { - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - err = scheduler.Schedule(ctx, transactionsTask, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return errors.Wrap(task.ErrRetryable, err.Error()) - } - } - - if len(pagedAccounts.Content) < config.PageSize { - break - } - - if page+1 >= pagedAccounts.TotalPages { - // Modulr paging starts at 0, so the last page is TotalPages - 1. - break - } - } - - return nil -} - -func ingestAccountsBatch( - ctx context.Context, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - accounts []*client.Account, -) error { - accountsBatch := ingestion.AccountBatch{} - balancesBatch := ingestion.BalanceBatch{} - - for _, account := range accounts { - raw, err := json.Marshal(account) - if err != nil { - return err - } - - openingDate, err := time.Parse(timeTemplate, account.CreatedDate) - if err != nil { - return err - } - - accountsBatch = append(accountsBatch, &models.Account{ - ID: models.AccountID{ - Reference: account.ID, - ConnectorID: connectorID, - }, - CreatedAt: openingDate, - Reference: account.ID, - ConnectorID: connectorID, - DefaultAsset: currency.FormatAsset(supportedCurrenciesWithDecimal, account.Currency), - AccountName: account.Name, - Type: models.AccountTypeInternal, - RawData: raw, - }) - - // No need to check if the currency is supported for accounts and - // balances. - precision := supportedCurrenciesWithDecimal[account.Currency] - - amount, err := currency.GetAmountWithPrecisionFromString(account.Balance, precision) - if err != nil { - return fmt.Errorf("failed to parse amount %s: %w", account.Balance, err) - } - - now := time.Now() - balancesBatch = append(balancesBatch, &models.Balance{ - AccountID: models.AccountID{ - Reference: account.ID, - ConnectorID: connectorID, - }, - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, account.Currency), - Balance: amount, - CreatedAt: now, - LastUpdatedAt: now, - ConnectorID: connectorID, - }) - } - - if err := ingester.IngestAccounts(ctx, accountsBatch); err != nil { - return err - } - - if err := ingester.IngestBalances(ctx, balancesBatch, false); err != nil { - return err - } - - return nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/modulr/task_fetch_beneficiaries.go b/components/payments/cmd/connectors/internal/connectors/modulr/task_fetch_beneficiaries.go deleted file mode 100644 index 6bcc7d953d..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/modulr/task_fetch_beneficiaries.go +++ /dev/null @@ -1,198 +0,0 @@ -package modulr - -import ( - "context" - "encoding/json" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/modulr/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" -) - -type fetchBeneficiariesState struct { - LastCreated time.Time `json:"last_created"` -} - -func (s *fetchBeneficiariesState) UpdateLatest(latest *client.Beneficiary) error { - createdTime, err := time.Parse("2006-01-02T15:04:05.999-0700", latest.Created) - if err != nil { - return err - } - if createdTime.After(s.LastCreated) { - s.LastCreated = createdTime - } - return nil -} - -func (s *fetchBeneficiariesState) FindLatest(beneficiaries []*client.Beneficiary) error { - for _, beneficiary := range beneficiaries { - if err := s.UpdateLatest(beneficiary); err != nil { - return err - } - } - return nil -} - -func (s *fetchBeneficiariesState) IsNew(beneficiary *client.Beneficiary) (bool, error) { - createdTime, err := time.Parse("2006-01-02T15:04:05.999-0700", beneficiary.Created) - if err != nil { - return false, err - } - return createdTime.After(s.LastCreated), nil -} - -func (s *fetchBeneficiariesState) FilterNew(beneficiaries []*client.Beneficiary) ([]*client.Beneficiary, error) { - // beneficiaries are not assumed to be sorted by creation date. - result := make([]*client.Beneficiary, 0, len(beneficiaries)) - for _, beneficiary := range beneficiaries { - isNew, err := s.IsNew(beneficiary) - if err != nil { - return nil, err - } - if !isNew { - continue - } - result = append(result, beneficiary) - } - return result, nil -} - -func (s *fetchBeneficiariesState) GetFilterValue() string { - if s.LastCreated.IsZero() { - return "" - } - return s.LastCreated.Format("2006-01-02T15:04:05-0700") -} - -func taskFetchBeneficiaries(config Config, client *client.Client) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - resolver task.StateResolver, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "modulr.taskFetchBeneficiaries", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - state, err := fetchBeneficiaries( - ctx, - config, - client, - connectorID, - ingester, - scheduler, - task.MustResolveTo(ctx, resolver, fetchBeneficiariesState{}), - ) - if err != nil { - otel.RecordError(span, err) - return err - } - - if err := ingester.UpdateTaskState(ctx, state); err != nil { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - return nil - } -} - -func fetchBeneficiaries( - ctx context.Context, - config Config, - client *client.Client, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - state fetchBeneficiariesState, -) (fetchBeneficiariesState, error) { - newState := state - for page := 0; ; page++ { - pagedBeneficiaries, err := client.GetBeneficiaries( - ctx, - page, - config.PageSize, - state.GetFilterValue(), - ) - if err != nil { - // Retry errors are handled by the client - return newState, err - } - if len(pagedBeneficiaries.Content) == 0 { - break - } - beneficiaries, err := state.FilterNew(pagedBeneficiaries.Content) - if err != nil { - return newState, errors.Wrap(task.ErrRetryable, err.Error()) - } - if err := newState.FindLatest(beneficiaries); err != nil { - return newState, errors.Wrap(task.ErrRetryable, err.Error()) - } - if err := ingestBeneficiariesAccountsBatch(ctx, connectorID, ingester, beneficiaries); err != nil { - return newState, errors.Wrap(task.ErrRetryable, err.Error()) - } - - if len(pagedBeneficiaries.Content) < config.PageSize { - break - } - - if page+1 >= pagedBeneficiaries.TotalPages { - // Modulr paging starts at 0, so the last page is TotalPages - 1. - break - } - } - - return newState, nil -} - -func ingestBeneficiariesAccountsBatch( - ctx context.Context, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - beneficiaries []*client.Beneficiary, -) error { - accountsBatch := ingestion.AccountBatch{} - for _, beneficiary := range beneficiaries { - raw, err := json.Marshal(beneficiary) - if err != nil { - return err - } - - openingDate, err := time.Parse(timeTemplate, beneficiary.Created) - if err != nil { - return err - } - - accountsBatch = append(accountsBatch, &models.Account{ - ID: models.AccountID{ - Reference: beneficiary.ID, - ConnectorID: connectorID, - }, - CreatedAt: openingDate, - Reference: beneficiary.ID, - ConnectorID: connectorID, - AccountName: beneficiary.Name, - Type: models.AccountTypeExternal, - RawData: raw, - }) - } - - if err := ingester.IngestAccounts(ctx, accountsBatch); err != nil { - return err - } - - return nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/modulr/task_fetch_transactions.go b/components/payments/cmd/connectors/internal/connectors/modulr/task_fetch_transactions.go deleted file mode 100644 index 3d6b1d9349..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/modulr/task_fetch_transactions.go +++ /dev/null @@ -1,275 +0,0 @@ -package modulr - -import ( - "context" - "encoding/json" - "fmt" - "strings" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/modulr/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" -) - -type fetchTransactionsState struct { - LastTransactionTime time.Time `json:"last_transaction_time"` -} - -func (s *fetchTransactionsState) UpdateLatest(latest *client.Transaction) error { - transactionTime, err := time.Parse("2006-01-02T15:04:05.999-0700", latest.TransactionDate) - if err != nil { - return err - } - if transactionTime.After(s.LastTransactionTime) { - s.LastTransactionTime = transactionTime - } - return nil -} - -func (s *fetchTransactionsState) FindLatest(transactions []*client.Transaction) error { - for _, transaction := range transactions { - if err := s.UpdateLatest(transaction); err != nil { - return err - } - } - return nil -} - -func (s *fetchTransactionsState) IsNew(transaction *client.Transaction) (bool, error) { - transactionTime, err := time.Parse("2006-01-02T15:04:05.999-0700", transaction.TransactionDate) - if err != nil { - return false, err - } - return transactionTime.After(s.LastTransactionTime), nil -} - -func (s *fetchTransactionsState) FilterNew(transactions []*client.Transaction) ([]*client.Transaction, error) { - result := make([]*client.Transaction, 0, len(transactions)) - for _, transaction := range transactions { - isNew, err := s.IsNew(transaction) - if err != nil { - return nil, err - } - if !isNew { - continue - } - result = append(result, transaction) - } - return result, nil -} - -func (s *fetchTransactionsState) GetFilterValue() string { - if s.LastTransactionTime.IsZero() { - return "" - } - return s.LastTransactionTime.Format("2006-01-02T15:04:05-0700") -} - -func taskFetchTransactions(config Config, client *client.Client, accountID string) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - resolver task.StateResolver, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "modulr.taskFetchTransactions", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("accountID", accountID), - ) - defer span.End() - - state, err := fetchTransactions( - ctx, - config, - client, - accountID, - connectorID, - ingester, - task.MustResolveTo(ctx, resolver, fetchTransactionsState{}), - ) - if err != nil { - otel.RecordError(span, err) - // Retry errors are handled by the function - return err - } - - if err := ingester.UpdateTaskState(ctx, state); err != nil { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - return nil - } -} - -func fetchTransactions( - ctx context.Context, - config Config, - client *client.Client, - accountID string, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - state fetchTransactionsState, -) (fetchTransactionsState, error) { - newState := state - for page := 0; ; page++ { - pagedTransactions, err := client.GetTransactions( - ctx, - accountID, - page, - config.PageSize, - state.GetFilterValue(), - ) - if err != nil { - // Retry errors are handled by the client - return newState, err - } - - if len(pagedTransactions.Content) == 0 { - break - } - - transactions, err := state.FilterNew(pagedTransactions.Content) - if err != nil { - return newState, errors.Wrap(task.ErrRetryable, err.Error()) - } - - batch, err := toBatch(connectorID, accountID, transactions) - if err != nil { - return newState, errors.Wrap(task.ErrRetryable, err.Error()) - } - - if err := ingester.IngestPayments(ctx, batch); err != nil { - return newState, errors.Wrap(task.ErrRetryable, err.Error()) - } - - if err := newState.FindLatest(transactions); err != nil { - return newState, errors.Wrap(task.ErrRetryable, err.Error()) - } - - if len(pagedTransactions.Content) < config.PageSize { - break - } - - if page+1 >= pagedTransactions.TotalPages { - // Modulr paging starts at 0, so the last page is TotalPages - 1. - break - } - } - - return newState, nil -} - -func toBatch( - connectorID models.ConnectorID, - accountID string, - transactions []*client.Transaction, -) (ingestion.PaymentBatch, error) { - batch := ingestion.PaymentBatch{} - - for _, transaction := range transactions { - - rawData, err := json.Marshal(transaction) - if err != nil { - return nil, fmt.Errorf("failed to marshal transaction: %w", err) - } - - paymentType := matchTransactionType(transaction.Type) - - precision, ok := supportedCurrenciesWithDecimal[transaction.Account.Currency] - if !ok { - continue - } - - amount, err := currency.GetAmountWithPrecisionFromString(transaction.Amount.String(), precision) - if err != nil { - return nil, fmt.Errorf("failed to parse amount %s: %w", transaction.Amount, err) - } - - createdAt, err := time.Parse(timeTemplate, transaction.PostedDate) - if err != nil { - return nil, fmt.Errorf("failed to parse posted date %s: %w", transaction.PostedDate, err) - } - - batchElement := ingestion.PaymentBatchElement{ - Payment: &models.Payment{ - ID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: transaction.ID, - Type: paymentType, - }, - ConnectorID: connectorID, - }, - CreatedAt: createdAt, - Reference: transaction.ID, - ConnectorID: connectorID, - Type: paymentType, - Status: models.PaymentStatusSucceeded, - Scheme: models.PaymentSchemeOther, - Amount: amount, - InitialAmount: amount, - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transaction.Account.Currency), - RawData: rawData, - }, - } - - switch paymentType { - case models.PaymentTypePayIn: - batchElement.Payment.DestinationAccountID = &models.AccountID{ - Reference: accountID, - ConnectorID: connectorID, - } - case models.PaymentTypePayOut: - batchElement.Payment.SourceAccountID = &models.AccountID{ - Reference: accountID, - ConnectorID: connectorID, - } - default: - if transaction.Credit { - batchElement.Payment.DestinationAccountID = &models.AccountID{ - Reference: accountID, - ConnectorID: connectorID, - } - } else { - batchElement.Payment.SourceAccountID = &models.AccountID{ - Reference: accountID, - ConnectorID: connectorID, - } - } - } - - batch = append(batch, batchElement) - } - - return batch, nil -} - -func matchTransactionType(transactionType string) models.PaymentType { - if transactionType == "PI_REV" || - transactionType == "PO_REV" || - transactionType == "ADHOC" || - transactionType == "INT_INTERC" { - return models.PaymentTypeOther - } - - if strings.HasPrefix(transactionType, "PI_") { - return models.PaymentTypePayIn - } - - if strings.HasPrefix(transactionType, "PO_") { - return models.PaymentTypePayOut - } - - return models.PaymentTypeOther -} diff --git a/components/payments/cmd/connectors/internal/connectors/modulr/task_main.go b/components/payments/cmd/connectors/internal/connectors/modulr/task_main.go deleted file mode 100644 index 87bf2d121c..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/modulr/task_main.go +++ /dev/null @@ -1,70 +0,0 @@ -package modulr - -import ( - "context" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" -) - -// taskMain is the main task of the connector. It launches the other tasks. -func taskMain(config Config) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - scheduler task.Scheduler, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "modulr.taskMain", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - taskAccounts, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch accounts from client", - Key: taskNameFetchAccounts, - }) - if err != nil { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - err = scheduler.Schedule(ctx, taskAccounts, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: config.PollingPeriod.Duration, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - taskBeneficiaries, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch beneficiaries from client", - Key: taskNameFetchBeneficiaries, - }) - if err != nil { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - err = scheduler.Schedule(ctx, taskBeneficiaries, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: config.PollingPeriod.Duration, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - return nil - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/modulr/task_payments.go b/components/payments/cmd/connectors/internal/connectors/modulr/task_payments.go deleted file mode 100644 index c856868278..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/modulr/task_payments.go +++ /dev/null @@ -1,341 +0,0 @@ -package modulr - -import ( - "context" - "encoding/json" - "regexp" - "time" - - "github.com/formancehq/go-libs/contextutil" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/modulr/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" -) - -var ( - ReferencePatternRegexp = regexp.MustCompile("[a-zA-Z0-9 ]*") -) - -func taskInitiatePayment(modulrClient *client.Client, transferID string) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - storageReader storage.Reader, - ) error { - transferInitiationID := models.MustTransferInitiationIDFromString(transferID) - - ctx, span := connectors.StartSpan( - ctx, - "modulr.taskInitiatePayment", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("transferID", transferID), - attribute.String("reference", transferInitiationID.Reference), - ) - defer span.End() - - transfer, err := getTransfer(ctx, storageReader, transferInitiationID, true) - if err != nil { - otel.RecordError(span, err) - return err - } - - if err := initiatePayment(ctx, modulrClient, transfer, connectorID, ingester, scheduler, storageReader); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func initiatePayment( - ctx context.Context, - modulrClient *client.Client, - transfer *models.TransferInitiation, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - storageReader storage.Reader, -) error { - var err error - var paymentID *models.PaymentID - defer func() { - if err != nil { - ctx, cancel := contextutil.Detached(ctx) - defer cancel() - _ = ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusFailed, err.Error(), time.Now()) - } - }() - - err = ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusProcessing, "", time.Now()) - if err != nil { - return err - } - - if transfer.SourceAccount == nil { - err = errors.New("no source account provided") - return err - } - - if transfer.SourceAccount.Type == models.AccountTypeExternal { - err = errors.New("payin not implemented: source account must be an internal account") - return err - } - - var curr string - var precision int - curr, precision, err = currency.GetCurrencyAndPrecisionFromAsset(supportedCurrenciesWithDecimal, transfer.Asset) - if err != nil { - return err - } - - amount, err := currency.GetStringAmountFromBigIntWithPrecision(transfer.Amount, precision) - if err != nil { - return err - } - - description := "" - if len(transfer.Description) <= 18 && ReferencePatternRegexp.MatchString(transfer.Description) { - description = transfer.Description - } - - var connectorPaymentID string - var paymentType models.PaymentType - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) - defer cancel() - switch transfer.DestinationAccount.Type { - case models.AccountTypeInternal: - // Transfer between internal accounts - var resp *client.TransferResponse - resp, err = modulrClient.InitiateTransfer(ctx, &client.TransferRequest{ - SourceAccountID: transfer.SourceAccountID.Reference, - Destination: client.Destination{ - Type: string(client.DestinationTypeAccount), - ID: transfer.DestinationAccountID.Reference, - }, - Currency: curr, - Amount: json.Number(amount), - Reference: description, - ExternalReference: description, - PaymentDate: time.Now().Add(24 * time.Hour).Format("2006-01-02"), - }) - if err != nil { - return err - } - - connectorPaymentID = resp.ID - paymentType = models.PaymentTypeTransfer - case models.AccountTypeExternal: - // Payout to an external account - var resp *client.PayoutResponse - resp, err = modulrClient.InitiatePayout(ctx, &client.PayoutRequest{ - SourceAccountID: transfer.SourceAccountID.Reference, - Destination: client.Destination{ - Type: string(client.DestinationTypeBeneficiary), - ID: transfer.DestinationAccountID.Reference, - }, - Currency: curr, - Amount: json.Number(amount), - Reference: description, - ExternalReference: description, - }) - if err != nil { - return err - } - - connectorPaymentID = resp.ID - paymentType = models.PaymentTypePayOut - } - - paymentID = &models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: connectorPaymentID, - Type: paymentType, - }, - ConnectorID: connectorID, - } - err = ingester.AddTransferInitiationPaymentID(ctx, transfer, paymentID, time.Now()) - if err != nil { - return err - } - - taskDescriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Update transfer initiation status", - Key: taskNameUpdatePaymentStatus, - TransferID: transfer.ID.String(), - PaymentID: paymentID.String(), - Attempt: 1, - }) - if err != nil { - return err - } - - ctx, _ = contextutil.DetachedWithTimeout(ctx, 10*time.Second) - err = scheduler.Schedule(ctx, taskDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return err - } - - return nil -} - -func taskUpdatePaymentStatus( - modulrClient *client.Client, - transferID string, - pID string, - attempt int, -) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - storageReader storage.Reader, - ) error { - paymentID := models.MustPaymentIDFromString(pID) - transferInitiationID := models.MustTransferInitiationIDFromString(transferID) - - ctx, span := connectors.StartSpan( - ctx, - "modulr.taskUpdatePaymentStatus", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("transferID", transferID), - attribute.String("paymentID", pID), - attribute.String("reference", transferInitiationID.Reference), - ) - defer span.End() - - transfer, err := getTransfer(ctx, storageReader, transferInitiationID, false) - if err != nil { - otel.RecordError(span, err) - return err - } - - if err := updatePaymentStatus(ctx, modulrClient, transfer, paymentID, attempt, ingester, scheduler, storageReader); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func updatePaymentStatus( - ctx context.Context, - modulrClient *client.Client, - transfer *models.TransferInitiation, - paymentID *models.PaymentID, - attempt int, - ingester ingestion.Ingester, - scheduler task.Scheduler, - storageReader storage.Reader, -) error { - var err error - var status string - var resultMessage string - switch transfer.Type { - case models.TransferInitiationTypeTransfer: - var resp *client.TransferResponse - resp, err = modulrClient.GetTransfer(ctx, paymentID.Reference) - if err != nil { - return err - } - - status = resp.Status - resultMessage = resp.Message - case models.TransferInitiationTypePayout: - var resp *client.PayoutResponse - resp, err = modulrClient.GetPayout(ctx, paymentID.Reference) - if err != nil { - return err - } - - status = resp.Status - resultMessage = resp.Message - } - - switch status { - case "SUBMITTED", "PENDING_FOR_DATE", "PENDING_FOR_FUNDS", "VALIDATED", "SCREENING_REQ": - taskDescriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Update transfer initiation status", - Key: taskNameUpdatePaymentStatus, - TransferID: transfer.ID.String(), - PaymentID: paymentID.String(), - Attempt: attempt + 1, - }) - if err != nil { - return err - } - - err = scheduler.Schedule(ctx, taskDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_IN_DURATION, - Duration: 2 * time.Minute, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return err - } - case "EXT_PROC", "PROCESSED", "RECONCILED": - err = ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusProcessed, "", time.Now()) - if err != nil { - return err - } - - return nil - default: - err = ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusFailed, resultMessage, time.Now()) - if err != nil { - return err - } - - return nil - } - - return nil -} - -func getTransfer( - ctx context.Context, - reader storage.Reader, - transferID models.TransferInitiationID, - expand bool, -) (*models.TransferInitiation, error) { - transfer, err := reader.ReadTransferInitiation(ctx, transferID) - if err != nil { - return nil, err - } - - if expand { - if transfer.SourceAccountID != nil { - sourceAccount, err := reader.GetAccount(ctx, transfer.SourceAccountID.String()) - if err != nil { - return nil, err - } - transfer.SourceAccount = sourceAccount - } - - destinationAccount, err := reader.GetAccount(ctx, transfer.DestinationAccountID.String()) - if err != nil { - return nil, err - } - transfer.DestinationAccount = destinationAccount - } - - return transfer, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/modulr/task_resolve.go b/components/payments/cmd/connectors/internal/connectors/modulr/task_resolve.go deleted file mode 100644 index 74b4631423..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/modulr/task_resolve.go +++ /dev/null @@ -1,66 +0,0 @@ -package modulr - -import ( - "fmt" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors/modulr/client" - "github.com/formancehq/payments/cmd/connectors/internal/task" - - "github.com/formancehq/go-libs/logging" -) - -const ( - taskNameMain = "main" - taskNameFetchTransactions = "fetch-transactions" - taskNameFetchAccounts = "fetch-accounts" - taskNameFetchBeneficiaries = "fetch-beneficiaries" - taskNameInitiatePayment = "initiate-payment" - taskNameUpdatePaymentStatus = "update-payment-status" -) - -const ( - timeTemplate = "2006-01-02T15:04:05-0700" -) - -// TaskDescriptor is the definition of a task. -type TaskDescriptor struct { - Name string `json:"name" yaml:"name" bson:"name"` - Key string `json:"key" yaml:"key" bson:"key"` - AccountID string `json:"accountID" yaml:"accountID" bson:"accountID"` - TransferID string `json:"transferID" yaml:"transferID" bson:"transferID"` - PaymentID string `json:"paymentID" yaml:"paymentID" bson:"paymentID"` - Attempt int `json:"attempt" yaml:"attempt" bson:"attempt"` -} - -func resolveTasks(logger logging.Logger, config Config) func(taskDefinition TaskDescriptor) task.Task { - modulrClient, err := client.NewClient(config.APIKey, config.APISecret, config.Endpoint) - if err != nil { - return func(taskDefinition TaskDescriptor) task.Task { - return func() error { - return fmt.Errorf("key '%s': %w", taskDefinition.Key, ErrMissingTask) - } - } - } - - return func(taskDefinition TaskDescriptor) task.Task { - switch taskDefinition.Key { - case taskNameMain: - return taskMain(config) - case taskNameFetchAccounts: - return taskFetchAccounts(config, modulrClient) - case taskNameFetchBeneficiaries: - return taskFetchBeneficiaries(config, modulrClient) - case taskNameInitiatePayment: - return taskInitiatePayment(modulrClient, taskDefinition.TransferID) - case taskNameUpdatePaymentStatus: - return taskUpdatePaymentStatus(modulrClient, taskDefinition.TransferID, taskDefinition.PaymentID, taskDefinition.Attempt) - case taskNameFetchTransactions: - return taskFetchTransactions(config, modulrClient, taskDefinition.AccountID) - } - - // This should never happen. - return func() error { - return fmt.Errorf("key '%s': %w", taskDefinition.Key, ErrMissingTask) - } - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/moneycorp/client/client.go b/components/payments/cmd/connectors/internal/connectors/moneycorp/client/client.go deleted file mode 100644 index 0e9f9094c4..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/moneycorp/client/client.go +++ /dev/null @@ -1,45 +0,0 @@ -package client - -import ( - "net/http" - "strings" - "time" - - "github.com/formancehq/go-libs/logging" - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" -) - -type Client struct { - httpClient *http.Client - - endpoint string - - logger logging.Logger -} - -func newHTTPClient(clientID, apiKey, endpoint string, logger logging.Logger) *http.Client { - return &http.Client{ - Timeout: 10 * time.Second, - Transport: &apiTransport{ - logger: logger, - clientID: clientID, - apiKey: apiKey, - endpoint: endpoint, - underlying: otelhttp.NewTransport(http.DefaultTransport), - }, - } -} - -func NewClient(clientID, apiKey, endpoint string, logger logging.Logger) (*Client, error) { - endpoint = strings.TrimSuffix(endpoint, "/") - - c := &Client{ - httpClient: newHTTPClient(clientID, apiKey, endpoint, logger), - - endpoint: endpoint, - - logger: logger, - } - - return c, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/moneycorp/client/error.go b/components/payments/cmd/connectors/internal/connectors/moneycorp/client/error.go deleted file mode 100644 index eb6f3a463e..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/moneycorp/client/error.go +++ /dev/null @@ -1,83 +0,0 @@ -package client - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/pkg/errors" -) - -type moneycorpErrors struct { - Errors []*moneycorpError `json:"errors"` -} - -type moneycorpError struct { - StatusCode int `json:"-"` - Code string `json:"code"` - Title string `json:"title"` - Detail string `json:"detail"` - WithRetry bool `json:"-"` -} - -func (me *moneycorpError) Error() error { - var err error - if me.Detail == "" { - err = fmt.Errorf("unexpected status code: %d", me.StatusCode) - } else { - err = fmt.Errorf("%d: %s", me.StatusCode, me.Detail) - } - - if me.WithRetry { - return checkStatusCodeError(me.StatusCode, err) - } - - return errors.Wrap(task.ErrNonRetryable, err.Error()) -} - -func unmarshalError(statusCode int, body io.ReadCloser, withRetry bool) *moneycorpError { - var ces moneycorpErrors - _ = json.NewDecoder(body).Decode(&ces) - - if len(ces.Errors) == 0 { - return &moneycorpError{ - StatusCode: statusCode, - WithRetry: withRetry, - } - } - - return &moneycorpError{ - StatusCode: statusCode, - Code: ces.Errors[0].Code, - Title: ces.Errors[0].Title, - Detail: ces.Errors[0].Detail, - WithRetry: withRetry, - } -} - -func unmarshalErrorWithoutRetry(statusCode int, body io.ReadCloser) *moneycorpError { - return unmarshalError(statusCode, body, false) -} - -func unmarshalErrorWithRetry(statusCode int, body io.ReadCloser) *moneycorpError { - return unmarshalError(statusCode, body, true) -} - -func checkStatusCodeError(statusCode int, err error) error { - switch statusCode { - case http.StatusTooEarly, http.StatusRequestTimeout: - return errors.Wrap(task.ErrRetryable, err.Error()) - case http.StatusTooManyRequests: - // Retry rate limit errors - // TODO(polo): add rate limit handling - return errors.Wrap(task.ErrRetryable, err.Error()) - case http.StatusInternalServerError, http.StatusBadGateway, - http.StatusServiceUnavailable, http.StatusGatewayTimeout: - // Retry internal errors - return errors.Wrap(task.ErrRetryable, err.Error()) - default: - return errors.Wrap(task.ErrNonRetryable, err.Error()) - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/moneycorp/client/payout.go b/components/payments/cmd/connectors/internal/connectors/moneycorp/client/payout.go deleted file mode 100644 index adb31ea342..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/moneycorp/client/payout.go +++ /dev/null @@ -1,134 +0,0 @@ -package client - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" -) - -type payoutRequest struct { - Payout struct { - Attributes *PayoutRequest `json:"attributes"` - } `json:"data"` -} - -type PayoutRequest struct { - SourceAccountID string `json:"-"` - IdempotencyKey string `json:"-"` - RecipientID string `json:"recipientId"` - PaymentDate string `json:"paymentDate"` - PaymentAmount json.Number `json:"paymentAmount"` - PaymentCurrency string `json:"paymentCurrency"` - PaymentMethgod string `json:"paymentMethod"` - PaymentReference string `json:"paymentReference"` - ClientReference string `json:"clientReference"` - PaymentPurpose string `json:"paymentPurpose"` -} - -type payoutResponse struct { - Payout *PayoutResponse `json:"data"` -} - -type PayoutResponse struct { - ID string `json:"id"` - Attributes struct { - AccountID string `json:"accountId"` - PaymentAmount json.Number `json:"paymentAmount"` - PaymentCurrency string `json:"paymentCurrency"` - PaymentApproved bool `json:"paymentApproved"` - PaymentStatus string `json:"paymentStatus"` - PaymentMethod string `json:"paymentMethod"` - PaymentDate string `json:"paymentDate"` - PaymentValueDate string `json:"paymentValueDate"` - RecipientDetails struct { - RecipientID int32 `json:"recipientId"` - } `json:"recipientDetails"` - PaymentReference string `json:"paymentReference"` - ClientReference string `json:"clientReference"` - CreatedAt string `json:"createdAt"` - CreatedBy string `json:"createdBy"` - UpdatedAt string `json:"updatedAt"` - PaymentPurpose string `json:"paymentPurpose"` - } `json:"attributes"` -} - -func (c *Client) InitiatePayout(ctx context.Context, pr *PayoutRequest) (*PayoutResponse, error) { - f := connectors.ClientMetrics(ctx, "moneycorp", "initiate_payout") - now := time.Now() - defer f(ctx, now) - - endpoint := fmt.Sprintf("%s/accounts/%s/payments", c.endpoint, pr.SourceAccountID) - - reqBody := &payoutRequest{} - reqBody.Payout.Attributes = pr - body, err := json.Marshal(reqBody) - if err != nil { - return nil, fmt.Errorf("failed to marshal payout request: %w", err) - } - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(body)) - if err != nil { - return nil, fmt.Errorf("failed to create login request: %w", err) - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Idempotency-Key", pr.IdempotencyKey) - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusCreated { - // Never retry payout initiation - return nil, unmarshalErrorWithoutRetry(resp.StatusCode, resp.Body).Error() - } - - var res payoutResponse - if err = json.NewDecoder(resp.Body).Decode(&res); err != nil { - return nil, err - } - - return res.Payout, nil -} - -func (c *Client) GetPayout(ctx context.Context, accountID string, payoutID string) (*PayoutResponse, error) { - f := connectors.ClientMetrics(ctx, "moneycorp", "get_payout") - now := time.Now() - defer f(ctx, now) - - endpoint := fmt.Sprintf("%s/accounts/%s/payments/%s", c.endpoint, accountID, payoutID) - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody) - if err != nil { - return nil, fmt.Errorf("failed to create get payout request request: %w", err) - } - req.Header.Set("Content-Type", "application/json") - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to get account balances: %w", err) - } - - defer func() { - err = resp.Body.Close() - if err != nil { - c.logger.Error(err) - } - }() - - if resp.StatusCode != http.StatusOK { - return nil, unmarshalErrorWithRetry(resp.StatusCode, resp.Body).Error() - } - - var payoutResponse payoutResponse - if err := json.NewDecoder(resp.Body).Decode(&payoutResponse); err != nil { - return nil, fmt.Errorf("failed to unmarshal wallets response body: %w", err) - } - - return payoutResponse.Payout, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/moneycorp/client/transfer.go b/components/payments/cmd/connectors/internal/connectors/moneycorp/client/transfer.go deleted file mode 100644 index 2badd511f9..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/moneycorp/client/transfer.go +++ /dev/null @@ -1,133 +0,0 @@ -package client - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" -) - -type transferRequest struct { - Transfer struct { - Attributes *TransferRequest `json:"attributes"` - } `json:"data"` -} - -type TransferRequest struct { - SourceAccountID string `json:"-"` - IdempotencyKey string `json:"-"` - ReceivingAccountID string `json:"receivingAccountId"` - TransferAmount json.Number `json:"transferAmount"` - TransferCurrency string `json:"transferCurrency"` - TransferReference string `json:"transferReference,omitempty"` - ClientReference string `json:"clientReference,omitempty"` -} - -type transferResponse struct { - Transfer *TransferResponse `json:"data"` -} - -type TransferResponse struct { - ID string `json:"id"` - Attributes struct { - SendingAccountID int64 `json:"sendingAccountId"` - SendingAccountName string `json:"sendingAccountName"` - ReceivingAccountID int64 `json:"receivingAccountId"` - ReceivingAccountName string `json:"receivingAccountName"` - CreatedAt string `json:"createdAt"` - CreatedBy string `json:"createdBy"` - UpdatedAt string `json:"updatedAt"` - TransferReference string `json:"transferReference"` - ClientReference string `json:"clientReference"` - TransferDate string `json:"transferDate"` - TransferAmount json.Number `json:"transferAmount"` - TransferCurrency string `json:"transferCurrency"` - TransferStatus string `json:"transferStatus"` - } `json:"attributes"` -} - -func (c *Client) InitiateTransfer(ctx context.Context, tr *TransferRequest) (*TransferResponse, error) { - f := connectors.ClientMetrics(ctx, "moneycorp", "initiate_transfer") - now := time.Now() - defer f(ctx, now) - - endpoint := fmt.Sprintf("%s/accounts/%s/transfers", c.endpoint, tr.SourceAccountID) - - reqBody := &transferRequest{} - reqBody.Transfer.Attributes = tr - body, err := json.Marshal(reqBody) - if err != nil { - return nil, fmt.Errorf("failed to marshal transfer request: %w", err) - } - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(body)) - if err != nil { - return nil, fmt.Errorf("failed to create transfer request: %w", err) - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Idempotency-Key", tr.IdempotencyKey) - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to get account balances: %w", err) - } - - defer func() { - err = resp.Body.Close() - if err != nil { - c.logger.Error(err) - } - }() - - if resp.StatusCode != http.StatusCreated { - // Never retry transfer initiation - return nil, unmarshalErrorWithoutRetry(resp.StatusCode, resp.Body).Error() - } - - var transferResponse transferResponse - if err := json.NewDecoder(resp.Body).Decode(&transferResponse); err != nil { - return nil, fmt.Errorf("failed to unmarshal wallets response body: %w", err) - } - - return transferResponse.Transfer, nil -} - -func (c *Client) GetTransfer(ctx context.Context, accountID string, transferID string) (*TransferResponse, error) { - f := connectors.ClientMetrics(ctx, "moneycorp", "get_transfer") - now := time.Now() - defer f(ctx, now) - - endpoint := fmt.Sprintf("%s/accounts/%s/transfers/%s", c.endpoint, accountID, transferID) - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody) - if err != nil { - return nil, fmt.Errorf("failed to create get transfer request: %w", err) - } - req.Header.Set("Content-Type", "application/json") - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to get account balances: %w", err) - } - - defer func() { - err = resp.Body.Close() - if err != nil { - c.logger.Error(err) - } - }() - - if resp.StatusCode != http.StatusOK { - return nil, unmarshalErrorWithRetry(resp.StatusCode, resp.Body).Error() - } - - var transferResponse transferResponse - if err := json.NewDecoder(resp.Body).Decode(&transferResponse); err != nil { - return nil, fmt.Errorf("failed to unmarshal wallets response body: %w", err) - } - - return transferResponse.Transfer, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/moneycorp/config.go b/components/payments/cmd/connectors/internal/connectors/moneycorp/config.go deleted file mode 100644 index a59c08fa26..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/moneycorp/config.go +++ /dev/null @@ -1,67 +0,0 @@ -package moneycorp - -import ( - "encoding/json" - "fmt" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/configtemplate" -) - -const ( - pageSize = 100 - defaultPollingPeriod = 2 * time.Minute -) - -type Config struct { - Name string `json:"name" yaml:"name" bson:"name"` - ClientID string `json:"clientID" yaml:"clientID" bson:"clientID"` - APIKey string `json:"apiKey" yaml:"apiKey" bson:"apiKey"` - Endpoint string `json:"endpoint" yaml:"endpoint" bson:"endpoint"` - PollingPeriod connectors.Duration `json:"pollingPeriod" yaml:"pollingPeriod" bson:"pollingPeriod"` -} - -func (c Config) String() string { - return fmt.Sprintf("clientID=%s, apiKey=****", c.ClientID) -} - -func (c Config) Validate() error { - if c.ClientID == "" { - return ErrMissingClientID - } - - if c.APIKey == "" { - return ErrMissingAPIKey - } - - if c.Endpoint == "" { - return ErrMissingEndpoint - } - - if c.Name == "" { - return ErrMissingName - } - - return nil -} - -func (c Config) Marshal() ([]byte, error) { - return json.Marshal(c) -} - -func (c Config) ConnectorName() string { - return c.Name -} - -func (c Config) BuildTemplate() (string, configtemplate.Config) { - cfg := configtemplate.NewConfig() - - cfg.AddParameter("name", configtemplate.TypeString, name.String(), false) - cfg.AddParameter("clientID", configtemplate.TypeString, "", true) - cfg.AddParameter("apiKey", configtemplate.TypeString, "", true) - cfg.AddParameter("endpoint", configtemplate.TypeString, "", true) - cfg.AddParameter("pollingPeriod", configtemplate.TypeDurationNs, defaultPollingPeriod.String(), false) - - return name.String(), cfg -} diff --git a/components/payments/cmd/connectors/internal/connectors/moneycorp/connector.go b/components/payments/cmd/connectors/internal/connectors/moneycorp/connector.go deleted file mode 100644 index 1d2654771b..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/moneycorp/connector.go +++ /dev/null @@ -1,136 +0,0 @@ -package moneycorp - -import ( - "context" - "errors" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" -) - -const name = models.ConnectorProviderMoneycorp - -var ( - mainTaskDescriptor = TaskDescriptor{ - Name: "Main task to periodically fetch accounts and transactions", - Key: taskNameMain, - } -) - -type Connector struct { - logger logging.Logger - cfg Config -} - -func newConnector(logger logging.Logger, cfg Config) *Connector { - return &Connector{ - logger: logger.WithFields(map[string]any{ - "component": "connector", - }), - cfg: cfg, - } -} - -func (c *Connector) UpdateConfig(ctx task.ConnectorContext, config models.ConnectorConfigObject) error { - cfg, ok := config.(Config) - if !ok { - return connectors.ErrInvalidConfig - } - - restartTask := c.cfg.PollingPeriod.Duration != cfg.PollingPeriod.Duration - - c.cfg = cfg - - if restartTask { - taskDescriptor, err := models.EncodeTaskDescriptor(mainTaskDescriptor) - if err != nil { - return err - } - - return ctx.Scheduler().Schedule(ctx.Context(), taskDescriptor, models.TaskSchedulerOptions{ - // We want to polling every c.cfg.PollingPeriod.Duration seconds the users - // and their transactions. - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: c.cfg.PollingPeriod.Duration, - // No need to restart this task, since the connector is not existing or - // was uninstalled previously, the task does not exists in the database - RestartOption: models.OPTIONS_STOP_AND_RESTART, - }) - } - - return nil -} - -func (c *Connector) Install(ctx task.ConnectorContext) error { - taskDescriptor, err := models.EncodeTaskDescriptor(mainTaskDescriptor) - if err != nil { - return err - } - - return ctx.Scheduler().Schedule(ctx.Context(), taskDescriptor, models.TaskSchedulerOptions{ - // We want to polling every c.cfg.PollingPeriod.Duration seconds the users - // and their transactions. - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: c.cfg.PollingPeriod.Duration, - // No need to restart this task, since the connector is not existing or - // was uninstalled previously, the task does not exists in the database - RestartOption: models.OPTIONS_RESTART_NEVER, - }) -} - -func (c *Connector) Uninstall(ctx context.Context) error { - return nil -} - -func (c *Connector) Resolve(descriptor models.TaskDescriptor) task.Task { - taskDescriptor, err := models.DecodeTaskDescriptor[TaskDescriptor](descriptor) - if err != nil { - panic(err) - } - - return resolveTasks(c.logger, c.cfg)(taskDescriptor) -} - -func (c *Connector) SupportedCurrenciesAndDecimals() map[string]int { - return supportedCurrenciesWithDecimal -} - -func (c *Connector) InitiatePayment(ctx task.ConnectorContext, transfer *models.TransferInitiation) error { - taskDescriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Initiate payment", - Key: taskNameInitiatePayment, - TransferID: transfer.ID.String(), - }) - if err != nil { - return err - } - - scheduleOption := models.OPTIONS_RUN_NOW_SYNC - scheduledAt := transfer.ScheduledAt - if !scheduledAt.IsZero() { - scheduleOption = models.OPTIONS_RUN_SCHEDULED_AT - } - - err = ctx.Scheduler().Schedule(ctx.Context(), taskDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: scheduleOption, - ScheduleAt: scheduledAt, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return err - } - - return nil -} - -func (c *Connector) ReversePayment(ctx task.ConnectorContext, transferReversal *models.TransferReversal) error { - return connectors.ErrNotImplemented -} - -func (c *Connector) CreateExternalBankAccount(ctx task.ConnectorContext, bankAccount *models.BankAccount) error { - return connectors.ErrNotImplemented -} - -var _ connectors.Connector = &Connector{} diff --git a/components/payments/cmd/connectors/internal/connectors/moneycorp/errors.go b/components/payments/cmd/connectors/internal/connectors/moneycorp/errors.go deleted file mode 100644 index 8e034ec98d..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/moneycorp/errors.go +++ /dev/null @@ -1,20 +0,0 @@ -package moneycorp - -import "errors" - -var ( - // ErrMissingTask is returned when the task is missing. - ErrMissingTask = errors.New("task is not implemented") - - // ErrMissingClientID is returned when the clientID is missing. - ErrMissingClientID = errors.New("missing clientID from config") - - // ErrMissingAPIKey is returned when the apiKey is missing. - ErrMissingAPIKey = errors.New("missing apiKey from config") - - // ErrMissingEndpoint is returned when the endpoint is missing. - ErrMissingEndpoint = errors.New("missing endpoint from config") - - // ErrMissingName is returned when the name is missing. - ErrMissingName = errors.New("missing name from config") -) diff --git a/components/payments/cmd/connectors/internal/connectors/moneycorp/loader.go b/components/payments/cmd/connectors/internal/connectors/moneycorp/loader.go deleted file mode 100644 index c44a03c58c..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/moneycorp/loader.go +++ /dev/null @@ -1,47 +0,0 @@ -package moneycorp - -import ( - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/gorilla/mux" -) - -type Loader struct{} - -const allowedTasks = 50 - -func (l *Loader) AllowTasks() int { - return allowedTasks -} - -func (l *Loader) Name() models.ConnectorProvider { - return name -} - -func (l *Loader) Load(logger logging.Logger, config Config) connectors.Connector { - return newConnector(logger, config) -} - -func (l *Loader) ApplyDefaults(cfg Config) Config { - if cfg.PollingPeriod.Duration == 0 { - cfg.PollingPeriod.Duration = defaultPollingPeriod - } - - if cfg.Name == "" { - cfg.Name = name.String() - } - - return cfg -} - -func (l *Loader) Router(_ *storage.Storage) *mux.Router { - // Webhooks are not implemented yet - return nil -} - -// NewLoader creates a new loader. -func NewLoader() *Loader { - return &Loader{} -} diff --git a/components/payments/cmd/connectors/internal/connectors/moneycorp/task_fetch_accounts.go b/components/payments/cmd/connectors/internal/connectors/moneycorp/task_fetch_accounts.go deleted file mode 100644 index 51bc589c5e..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/moneycorp/task_fetch_accounts.go +++ /dev/null @@ -1,186 +0,0 @@ -package moneycorp - -import ( - "context" - "encoding/json" - "errors" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/moneycorp/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "go.opentelemetry.io/otel/attribute" -) - -type fetchAccountsState struct { - LastPage int `json:"last_page"` - // Moneycorp does not send the creation date for accounts, but we can still - // sort by ID created (which is incremental when creating accounts). - LastIDCreated string `json:"last_id_created"` -} - -func taskFetchAccounts(client *client.Client, config *Config) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - resolver task.StateResolver, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "moneycorp.taskFetchAccounts", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - state := task.MustResolveTo(ctx, resolver, fetchAccountsState{}) - - newState, err := fetchAccounts(ctx, config, client, connectorID, ingester, scheduler, state) - if err != nil { - otel.RecordError(span, err) - return err - } - - if err := ingester.UpdateTaskState(ctx, newState); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func fetchAccounts( - ctx context.Context, - config *Config, - client *client.Client, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - state fetchAccountsState, -) (fetchAccountsState, error) { - newState := fetchAccountsState{ - LastPage: state.LastPage, - LastIDCreated: state.LastIDCreated, - } - - for page := state.LastPage; ; page++ { - newState.LastPage = page - - pagedAccounts, err := client.GetAccounts(ctx, page, pageSize) - if err != nil { - return fetchAccountsState{}, err - } - - if len(pagedAccounts) == 0 { - break - } - - batch := ingestion.AccountBatch{} - transactionTasks := []models.TaskDescriptor{} - balanceTasks := []models.TaskDescriptor{} - recipientTasks := []models.TaskDescriptor{} - for _, account := range pagedAccounts { - if account.ID <= state.LastIDCreated { - continue - } - - raw, err := json.Marshal(account) - if err != nil { - return fetchAccountsState{}, err - } - - batch = append(batch, &models.Account{ - ID: models.AccountID{ - Reference: account.ID, - ConnectorID: connectorID, - }, - // Moneycorp does not send the opening date of the account - CreatedAt: time.Now().UTC(), - Reference: account.ID, - ConnectorID: connectorID, - AccountName: account.Attributes.AccountName, - Type: models.AccountTypeInternal, - RawData: raw, - }) - - transactionTask, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch transactions from client by account", - Key: taskNameFetchTransactions, - AccountID: account.ID, - }) - if err != nil { - return fetchAccountsState{}, err - } - transactionTasks = append(transactionTasks, transactionTask) - - balanceTask, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch balances from client by account", - Key: taskNameFetchBalances, - AccountID: account.ID, - }) - if err != nil { - return fetchAccountsState{}, err - } - balanceTasks = append(balanceTasks, balanceTask) - - recipientTask, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch recipients from client", - Key: taskNameFetchRecipients, - AccountID: account.ID, - }) - if err != nil { - return fetchAccountsState{}, err - } - recipientTasks = append(recipientTasks, recipientTask) - - newState.LastIDCreated = account.ID - } - - if err := ingester.IngestAccounts(ctx, batch); err != nil { - return fetchAccountsState{}, err - } - - for _, transactionTask := range transactionTasks { - if err := scheduler.Schedule(ctx, transactionTask, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: config.PollingPeriod.Duration, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }); err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return fetchAccountsState{}, err - } - } - - for _, balanceTask := range balanceTasks { - if err := scheduler.Schedule(ctx, balanceTask, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: config.PollingPeriod.Duration, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }); err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return fetchAccountsState{}, err - } - } - - for _, recipientTask := range recipientTasks { - if err := scheduler.Schedule(ctx, recipientTask, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: config.PollingPeriod.Duration, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }); err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return fetchAccountsState{}, err - } - } - - if len(pagedAccounts) < pageSize { - break - } - } - - return newState, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/moneycorp/task_fetch_balances.go b/components/payments/cmd/connectors/internal/connectors/moneycorp/task_fetch_balances.go deleted file mode 100644 index 2f96463d33..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/moneycorp/task_fetch_balances.go +++ /dev/null @@ -1,97 +0,0 @@ -package moneycorp - -import ( - "context" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/moneycorp/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" -) - -func taskFetchBalances(client *client.Client, accountID string) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "moneycorp.taskFetchBalances", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("accountID", accountID), - ) - defer span.End() - - if err := fetchBalances(ctx, client, accountID, connectorID, ingester); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func fetchBalances( - ctx context.Context, - client *client.Client, - accountID string, - connectorID models.ConnectorID, - ingester ingestion.Ingester, -) error { - balances, err := client.GetAccountBalances(ctx, accountID) - if err != nil { - // retryable error already handled by the client - return err - } - - if err := ingestBalancesBatch(ctx, connectorID, ingester, accountID, balances); err != nil { - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - return nil -} - -func ingestBalancesBatch( - ctx context.Context, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - accountID string, - balances []*client.Balance, -) error { - batch := ingestion.BalanceBatch{} - for _, balance := range balances { - precision, err := currency.GetPrecision(supportedCurrenciesWithDecimal, balance.Attributes.CurrencyCode) - if err != nil { - return err - } - - amount, err := currency.GetAmountWithPrecisionFromString(balance.Attributes.AvailableBalance.String(), precision) - if err != nil { - return err - } - - now := time.Now() - batch = append(batch, &models.Balance{ - AccountID: models.AccountID{ - Reference: accountID, - ConnectorID: connectorID, - }, - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, balance.Attributes.CurrencyCode), - Balance: amount, - CreatedAt: now, - LastUpdatedAt: now, - ConnectorID: connectorID, - }) - } - - return ingester.IngestBalances(ctx, batch, false) -} diff --git a/components/payments/cmd/connectors/internal/connectors/moneycorp/task_fetch_recipients.go b/components/payments/cmd/connectors/internal/connectors/moneycorp/task_fetch_recipients.go deleted file mode 100644 index 5e9823eaad..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/moneycorp/task_fetch_recipients.go +++ /dev/null @@ -1,135 +0,0 @@ -package moneycorp - -import ( - "context" - "encoding/json" - "fmt" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/moneycorp/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" -) - -type fetchRecipientsState struct { - LastPage int `json:"last_page"` - // Moneycorp does not allow us to sort by , but we can still - // sort by ID created (which is incremental when creating accounts). - LastCreatedAt time.Time `json:"last_created_at"` -} - -func taskFetchRecipients(client *client.Client, accountID string) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - resolver task.StateResolver, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "moneycorp.taskFetchRecipients", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("accountID", accountID), - ) - defer span.End() - - state := task.MustResolveTo(ctx, resolver, fetchRecipientsState{}) - - newState, err := fetchRecipients(ctx, client, accountID, connectorID, ingester, scheduler, state) - if err != nil { - otel.RecordError(span, err) - return err - } - - if err := ingester.UpdateTaskState(ctx, newState); err != nil { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - return nil - } -} - -func fetchRecipients( - ctx context.Context, - client *client.Client, - accountID string, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - state fetchRecipientsState, -) (fetchRecipientsState, error) { - newState := fetchRecipientsState{ - LastPage: state.LastPage, - LastCreatedAt: state.LastCreatedAt, - } - - for page := 0; ; page++ { - newState.LastPage = page - - pagedRecipients, err := client.GetRecipients(ctx, accountID, page, pageSize) - if err != nil { - // Retryable errors already handled by the client - return fetchRecipientsState{}, err - } - - if len(pagedRecipients) == 0 { - break - } - - batch := ingestion.AccountBatch{} - for _, recipient := range pagedRecipients { - createdAt, err := time.Parse("2006-01-02T15:04:05.999999999", recipient.Attributes.CreatedAt) - if err != nil { - return fetchRecipientsState{}, errors.Wrap(task.ErrNonRetryable, fmt.Sprintf("failed to parse transaction date: %v", err)) - } - - switch createdAt.Compare(state.LastCreatedAt) { - case -1, 0: - continue - default: - } - - raw, err := json.Marshal(recipient) - if err != nil { - return fetchRecipientsState{}, errors.Wrap(task.ErrRetryable, err.Error()) - } - - batch = append(batch, &models.Account{ - ID: models.AccountID{ - Reference: recipient.ID, - ConnectorID: connectorID, - }, - // Moneycorp does not send the opening date of the account - CreatedAt: createdAt, - Reference: recipient.ID, - ConnectorID: connectorID, - DefaultAsset: currency.FormatAsset(supportedCurrenciesWithDecimal, recipient.Attributes.BankAccountCurrency), - AccountName: recipient.Attributes.BankAccountName, - Type: models.AccountTypeExternal, - RawData: raw, - }) - - newState.LastCreatedAt = createdAt - } - - if err := ingester.IngestAccounts(ctx, batch); err != nil { - return fetchRecipientsState{}, errors.Wrap(task.ErrRetryable, err.Error()) - } - - if len(pagedRecipients) < pageSize { - break - } - } - - return newState, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/moneycorp/task_fetch_transactions.go b/components/payments/cmd/connectors/internal/connectors/moneycorp/task_fetch_transactions.go deleted file mode 100644 index 536952923f..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/moneycorp/task_fetch_transactions.go +++ /dev/null @@ -1,214 +0,0 @@ -package moneycorp - -import ( - "context" - "encoding/json" - "fmt" - "strconv" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/moneycorp/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" -) - -type fetchTransactionsState struct { - LastCreatedAt time.Time `json:"last_created_at"` -} - -func taskFetchTransactions(client *client.Client, accountID string) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - resolver task.StateResolver, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "moneycorp.taskFetchTransactions", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("accountID", accountID), - ) - defer span.End() - - state := task.MustResolveTo(ctx, resolver, fetchTransactionsState{}) - - newState, err := fetchTransactions(ctx, client, accountID, connectorID, ingester, state) - if err != nil { - otel.RecordError(span, err) - return err - } - - if err := ingester.UpdateTaskState(ctx, newState); err != nil { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - return nil - } -} - -func fetchTransactions( - ctx context.Context, - client *client.Client, - accountID string, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - state fetchTransactionsState, -) (fetchTransactionsState, error) { - newState := fetchTransactionsState{ - LastCreatedAt: state.LastCreatedAt, - } - - for page := 0; ; page++ { - pagedTransactions, err := client.GetTransactions(ctx, accountID, page, pageSize, state.LastCreatedAt) - if err != nil { - // retryable error already handled by the client - return fetchTransactionsState{}, err - } - - if len(pagedTransactions) == 0 { - break - } - - batch := ingestion.PaymentBatch{} - for _, transaction := range pagedTransactions { - createdAt, err := time.Parse("2006-01-02T15:04:05.999999999", transaction.Attributes.CreatedAt) - if err != nil { - return fetchTransactionsState{}, errors.Wrap(task.ErrNonRetryable, fmt.Sprintf("failed to parse transaction date: %v", err)) - } - - switch createdAt.Compare(state.LastCreatedAt) { - case -1, 0: - continue - default: - } - - newState.LastCreatedAt = createdAt - - batchElement, err := toPaymentBatch(connectorID, transaction) - if err != nil { - return fetchTransactionsState{}, errors.Wrap(task.ErrRetryable, err.Error()) - } - - if batchElement == nil { - continue - } - - batch = append(batch, *batchElement) - } - - if err := ingester.IngestPayments(ctx, batch); err != nil { - return fetchTransactionsState{}, errors.Wrap(task.ErrRetryable, err.Error()) - } - - if len(pagedTransactions) < pageSize { - break - } - } - - return fetchTransactionsState{}, nil -} - -func toPaymentBatch( - connectorID models.ConnectorID, - transaction *client.Transaction, -) (*ingestion.PaymentBatchElement, error) { - rawData, err := json.Marshal(transaction) - if err != nil { - return nil, fmt.Errorf("failed to marshal transaction: %w", err) - } - - paymentType, shouldBeRecorded := matchPaymentType(transaction.Attributes.Type, transaction.Attributes.Direction) - if !shouldBeRecorded { - return nil, nil - } - - createdAt, err := time.Parse("2006-01-02T15:04:05.999999999", transaction.Attributes.CreatedAt) - if err != nil { - return nil, fmt.Errorf("failed to parse transaction date: %w", err) - } - - c, err := currency.GetPrecision(supportedCurrenciesWithDecimal, transaction.Attributes.Currency) - if err != nil { - return nil, err - } - - amount, err := currency.GetAmountWithPrecisionFromString(transaction.Attributes.Amount.String(), c) - if err != nil { - return nil, err - } - - batchElement := ingestion.PaymentBatchElement{ - Payment: &models.Payment{ - ID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: transaction.ID, - Type: paymentType, - }, - ConnectorID: connectorID, - }, - CreatedAt: createdAt, - Reference: transaction.ID, - ConnectorID: connectorID, - Amount: amount, - InitialAmount: amount, - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transaction.Attributes.Currency), - Type: paymentType, - Status: models.PaymentStatusSucceeded, - Scheme: models.PaymentSchemeOther, - RawData: rawData, - }, - } - - switch paymentType { - case models.PaymentTypePayIn: - batchElement.Payment.DestinationAccountID = &models.AccountID{ - Reference: strconv.Itoa(int(transaction.Attributes.AccountID)), - ConnectorID: connectorID, - } - case models.PaymentTypePayOut: - batchElement.Payment.SourceAccountID = &models.AccountID{ - Reference: strconv.Itoa(int(transaction.Attributes.AccountID)), - ConnectorID: connectorID, - } - default: - if transaction.Attributes.Direction == "Debit" { - batchElement.Payment.SourceAccountID = &models.AccountID{ - Reference: strconv.Itoa(int(transaction.Attributes.AccountID)), - ConnectorID: connectorID, - } - } else { - batchElement.Payment.DestinationAccountID = &models.AccountID{ - Reference: strconv.Itoa(int(transaction.Attributes.AccountID)), - ConnectorID: connectorID, - } - } - } - - return &batchElement, nil -} - -func matchPaymentType(transactionType string, transactionDirection string) (models.PaymentType, bool) { - switch transactionType { - case "Transfer": - return models.PaymentTypeTransfer, true - case "Payment", "Exchange", "Charge", "Refund": - switch transactionDirection { - case "Debit": - return models.PaymentTypePayOut, true - case "Credit": - return models.PaymentTypePayIn, true - } - } - - return models.PaymentTypeOther, false -} diff --git a/components/payments/cmd/connectors/internal/connectors/moneycorp/task_main.go b/components/payments/cmd/connectors/internal/connectors/moneycorp/task_main.go deleted file mode 100644 index 4391b0da7a..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/moneycorp/task_main.go +++ /dev/null @@ -1,50 +0,0 @@ -package moneycorp - -import ( - "context" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" -) - -// taskMain is the main task of the connector. It launches the other tasks. -func taskMain() task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - scheduler task.Scheduler, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "moneycorp.taskMain", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - taskAccounts, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch accounts from client", - Key: taskNameFetchAccounts, - }) - if err != nil { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - err = scheduler.Schedule(ctx, taskAccounts, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - return nil - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/moneycorp/task_payments.go b/components/payments/cmd/connectors/internal/connectors/moneycorp/task_payments.go deleted file mode 100644 index e2ecc98149..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/moneycorp/task_payments.go +++ /dev/null @@ -1,326 +0,0 @@ -package moneycorp - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "time" - - "github.com/formancehq/go-libs/contextutil" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/moneycorp/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "go.opentelemetry.io/otel/attribute" -) - -func taskInitiatePayment(moneycorpClient *client.Client, transferID string) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - storageReader storage.Reader, - ) error { - transferInitiationID := models.MustTransferInitiationIDFromString(transferID) - - ctx, span := connectors.StartSpan( - ctx, - "moneycorp.taskInitiatePayment", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("transferID", transferID), - attribute.String("reference", transferInitiationID.Reference), - ) - defer span.End() - - transfer, err := getTransfer(ctx, storageReader, transferInitiationID, true) - if err != nil { - otel.RecordError(span, err) - return err - } - - if err := initiatePayment(ctx, moneycorpClient, transfer, connectorID, ingester, scheduler, storageReader); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func initiatePayment( - ctx context.Context, - moneycorpClient *client.Client, - transfer *models.TransferInitiation, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - storageReader storage.Reader, -) error { - var err error - var paymentID *models.PaymentID - defer func() { - if err != nil { - ctx, cancel := contextutil.Detached(ctx) - defer cancel() - _ = ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusFailed, err.Error(), time.Now()) - } - }() - - err = ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusProcessing, "", time.Now()) - if err != nil { - return err - } - - if transfer.SourceAccount == nil { - err = errors.New("no source account provided") - } - - if transfer.SourceAccount.Type == models.AccountTypeExternal { - err = errors.New("payin not implemented: source account must be an internal account") - return err - } - - var curr string - var precision int - curr, precision, err = currency.GetCurrencyAndPrecisionFromAsset(supportedCurrenciesWithDecimal, transfer.Asset) - if err != nil { - return err - } - - amount, err := currency.GetStringAmountFromBigIntWithPrecision(transfer.Amount, precision) - if err != nil { - return err - } - - var connectorPaymentID string - var paymentType models.PaymentType - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) - defer cancel() - switch transfer.DestinationAccount.Type { - case models.AccountTypeInternal: - // Transfer between internal accounts - var resp *client.TransferResponse - resp, err = moneycorpClient.InitiateTransfer(ctx, &client.TransferRequest{ - SourceAccountID: transfer.SourceAccountID.Reference, - IdempotencyKey: fmt.Sprintf("%s_%d", transfer.ID.Reference, len(transfer.RelatedAdjustments)), - ReceivingAccountID: transfer.DestinationAccountID.Reference, - TransferAmount: json.Number(amount), - TransferCurrency: curr, - TransferReference: transfer.Description, - ClientReference: transfer.Description, - }) - if err != nil { - return err - } - - connectorPaymentID = resp.ID - paymentType = models.PaymentTypeTransfer - case models.AccountTypeExternal: - // Payout to an external account - var resp *client.PayoutResponse - resp, err = moneycorpClient.InitiatePayout(ctx, &client.PayoutRequest{ - SourceAccountID: transfer.SourceAccountID.Reference, - IdempotencyKey: fmt.Sprintf("%s_%d", transfer.ID.Reference, len(transfer.RelatedAdjustments)), - RecipientID: transfer.DestinationAccountID.Reference, - PaymentAmount: json.Number(amount), - PaymentCurrency: curr, - PaymentMethgod: "Standard", - PaymentReference: transfer.Description, - ClientReference: transfer.Description, - }) - if err != nil { - return err - } - - connectorPaymentID = resp.ID - paymentType = models.PaymentTypePayOut - } - - paymentID = &models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: connectorPaymentID, - Type: paymentType, - }, - ConnectorID: connectorID, - } - err = ingester.AddTransferInitiationPaymentID(ctx, transfer, paymentID, time.Now()) - if err != nil { - return err - } - - taskDescriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Update transfer initiation status", - Key: taskNameUpdatePaymentStatus, - TransferID: transfer.ID.String(), - PaymentID: paymentID.String(), - Attempt: 1, - }) - if err != nil { - return err - } - - ctx, _ = contextutil.DetachedWithTimeout(ctx, 10*time.Second) - err = scheduler.Schedule(ctx, taskDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return err - } - - return nil -} - -func taskUpdatePaymentStatus( - moneycorpClient *client.Client, - transferID string, - pID string, - attempt int, -) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - storageReader storage.Reader, - ) error { - paymentID := models.MustPaymentIDFromString(pID) - transferInitiationID := models.MustTransferInitiationIDFromString(transferID) - - ctx, span := connectors.StartSpan( - ctx, - "moneycorp.taskUpdatePaymentStatus", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("transferID", transferID), - attribute.String("paymentID", pID), - attribute.Int("attempt", attempt), - attribute.String("reference", transferInitiationID.Reference), - ) - defer span.End() - - transfer, err := getTransfer(ctx, storageReader, transferInitiationID, true) - if err != nil { - otel.RecordError(span, err) - return err - } - - if err := updatePaymentStatus(ctx, moneycorpClient, transfer, paymentID, attempt, ingester, scheduler, storageReader); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func updatePaymentStatus( - ctx context.Context, - moneycorpClient *client.Client, - transfer *models.TransferInitiation, - paymentID *models.PaymentID, - attempt int, - ingester ingestion.Ingester, - scheduler task.Scheduler, - storageReader storage.Reader, -) error { - var err error - var status string - var resultMessage string - switch transfer.Type { - case models.TransferInitiationTypeTransfer: - var resp *client.TransferResponse - resp, err = moneycorpClient.GetTransfer(ctx, transfer.SourceAccount.Reference, paymentID.Reference) - if err != nil { - return err - } - - status = resp.Attributes.TransferStatus - case models.TransferInitiationTypePayout: - var resp *client.PayoutResponse - resp, err = moneycorpClient.GetPayout(ctx, transfer.SourceAccount.Reference, paymentID.Reference) - if err != nil { - return err - } - - status = resp.Attributes.PaymentStatus - } - - switch status { - case "Awaiting Dispatch": - taskDescriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Update transfer initiation status", - Key: taskNameUpdatePaymentStatus, - TransferID: transfer.ID.String(), - PaymentID: paymentID.String(), - Attempt: attempt + 1, - }) - if err != nil { - return err - } - - err = scheduler.Schedule(ctx, taskDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_IN_DURATION, - Duration: 2 * time.Minute, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return err - } - case "Cleared", "Sent": - err = ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusProcessed, "", time.Now()) - if err != nil { - return err - } - - return nil - case "Unauthorised", "Failed", "Cancelled": - err = ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusFailed, resultMessage, time.Now()) - if err != nil { - return err - } - - return nil - } - - return nil -} - -func getTransfer( - ctx context.Context, - reader storage.Reader, - transferID models.TransferInitiationID, - expand bool, -) (*models.TransferInitiation, error) { - transfer, err := reader.ReadTransferInitiation(ctx, transferID) - if err != nil { - return nil, err - } - - if expand { - if transfer.SourceAccountID != nil { - sourceAccount, err := reader.GetAccount(ctx, transfer.SourceAccountID.String()) - if err != nil { - return nil, err - } - transfer.SourceAccount = sourceAccount - } - - destinationAccount, err := reader.GetAccount(ctx, transfer.DestinationAccountID.String()) - if err != nil { - return nil, err - } - transfer.DestinationAccount = destinationAccount - } - - return transfer, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/moneycorp/task_resolve.go b/components/payments/cmd/connectors/internal/connectors/moneycorp/task_resolve.go deleted file mode 100644 index 4be23abd3c..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/moneycorp/task_resolve.go +++ /dev/null @@ -1,72 +0,0 @@ -package moneycorp - -import ( - "fmt" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/moneycorp/client" - "github.com/formancehq/payments/cmd/connectors/internal/task" -) - -const ( - taskNameMain = "main" - taskNameFetchAccounts = "fetch-accounts" - taskNameFetchRecipients = "fetch-recipients" - taskNameFetchTransactions = "fetch-transactions" - taskNameFetchBalances = "fetch-balances" - taskNameInitiatePayment = "initiate-payment" - taskNameUpdatePaymentStatus = "update-payment-status" -) - -// TaskDescriptor is the definition of a task. -type TaskDescriptor struct { - Name string `json:"name" yaml:"name" bson:"name"` - Key string `json:"key" yaml:"key" bson:"key"` - AccountID string `json:"accountID" yaml:"accountID" bson:"accountID"` - TransferID string `json:"transferID" yaml:"transferID" bson:"transferID"` - PaymentID string `json:"paymentID" yaml:"paymentID" bson:"paymentID"` - Attempt int `json:"attempt" yaml:"attempt" bson:"attempt"` -} - -// clientID, apiKey, endpoint string, logger logging -func resolveTasks(logger logging.Logger, config Config) func(taskDefinition TaskDescriptor) task.Task { - moneycorpClient, err := client.NewClient( - config.ClientID, - config.APIKey, - config.Endpoint, - logger, - ) - if err != nil { - logger.Error(err) - - return func(taskDescriptor TaskDescriptor) task.Task { - return func() error { - return fmt.Errorf("cannot build moneycorp client: %w", err) - } - } - } - - return func(taskDescriptor TaskDescriptor) task.Task { - switch taskDescriptor.Key { - case taskNameMain: - return taskMain() - case taskNameFetchAccounts: - return taskFetchAccounts(moneycorpClient, &config) - case taskNameFetchRecipients: - return taskFetchRecipients(moneycorpClient, taskDescriptor.AccountID) - case taskNameFetchTransactions: - return taskFetchTransactions(moneycorpClient, taskDescriptor.AccountID) - case taskNameFetchBalances: - return taskFetchBalances(moneycorpClient, taskDescriptor.AccountID) - case taskNameInitiatePayment: - return taskInitiatePayment(moneycorpClient, taskDescriptor.TransferID) - case taskNameUpdatePaymentStatus: - return taskUpdatePaymentStatus(moneycorpClient, taskDescriptor.TransferID, taskDescriptor.PaymentID, taskDescriptor.Attempt) - } - - // This should never happen. - return func() error { - return fmt.Errorf("key '%s': %w", taskDescriptor.Key, ErrMissingTask) - } - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/client/accounts.go b/components/payments/cmd/connectors/internal/connectors/stripe/client/accounts.go deleted file mode 100644 index f35bfa8a86..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/client/accounts.go +++ /dev/null @@ -1,84 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/pkg/errors" - "github.com/stripe/stripe-go/v72" -) - -const ( - accountsEndpoint = "https://api.stripe.com/v1/accounts" -) - -//nolint:tagliatelle // allow different styled tags in client -type AccountsListResponse struct { - HasMore bool `json:"has_more"` - Data []*stripe.Account `json:"data"` -} - -func (d *DefaultClient) Accounts(ctx context.Context, - options ...ClientOption, -) ([]*stripe.Account, bool, error) { - f := connectors.ClientMetrics(ctx, "stripe", "list_accounts") - now := time.Now() - defer f(ctx, now) - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, accountsEndpoint, nil) - if err != nil { - return nil, false, errors.Wrap(err, "creating http request") - } - - for _, opt := range options { - opt.Apply(req) - } - - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - req.SetBasicAuth(d.apiKey, "") // gfyrag: really weird authentication right? - - var httpResponse *http.Response - - httpResponse, err = d.httpClient.Do(req) - if err != nil { - return nil, false, errors.Wrap(err, "doing request") - } - defer httpResponse.Body.Close() - - if httpResponse.StatusCode != http.StatusOK { - return nil, false, fmt.Errorf("unexpected status code: %d", httpResponse.StatusCode) - } - - type listResponse struct { - AccountsListResponse - Data []json.RawMessage `json:"data"` - } - - rsp := &listResponse{} - - err = json.NewDecoder(httpResponse.Body).Decode(rsp) - if err != nil { - return nil, false, errors.Wrap(err, "decoding response") - } - - accounts := make([]*stripe.Account, 0) - - if len(rsp.Data) > 0 { - for _, data := range rsp.Data { - account := &stripe.Account{} - - err = json.Unmarshal(data, &account) - if err != nil { - return nil, false, err - } - - accounts = append(accounts, account) - } - } - - return accounts, rsp.HasMore, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/client/balance_transactions.go b/components/payments/cmd/connectors/internal/connectors/stripe/client/balance_transactions.go deleted file mode 100644 index 9c53d77399..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/client/balance_transactions.go +++ /dev/null @@ -1,88 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/pkg/errors" - "github.com/stripe/stripe-go/v72" -) - -const ( - balanceTransactionsEndpoint = "https://api.stripe.com/v1/balance_transactions" -) - -//nolint:tagliatelle // allow different styled tags in client -type TransactionsListResponse struct { - HasMore bool `json:"has_more"` - Data []*stripe.BalanceTransaction `json:"data"` -} - -func (d *DefaultClient) BalanceTransactions(ctx context.Context, - options ...ClientOption, -) ([]*stripe.BalanceTransaction, bool, error) { - f := connectors.ClientMetrics(ctx, "stripe", "list_balance_transactions") - now := time.Now() - defer f(ctx, now) - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, balanceTransactionsEndpoint, nil) - if err != nil { - return nil, false, errors.Wrap(err, "creating http request") - } - - for _, opt := range options { - opt.Apply(req) - } - - if d.stripeAccount != "" { - req.Header.Set("Stripe-Account", d.stripeAccount) - } - - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - req.SetBasicAuth(d.apiKey, "") // gfyrag: really weird authentication right? - - var httpResponse *http.Response - - httpResponse, err = d.httpClient.Do(req) - if err != nil { - return nil, false, errors.Wrap(err, "doing request") - } - defer httpResponse.Body.Close() - - if httpResponse.StatusCode != http.StatusOK { - return nil, false, fmt.Errorf("unexpected status code: %d", httpResponse.StatusCode) - } - - type listResponse struct { - TransactionsListResponse - Data []json.RawMessage `json:"data"` - } - - rsp := &listResponse{} - - err = json.NewDecoder(httpResponse.Body).Decode(rsp) - if err != nil { - return nil, false, errors.Wrap(err, "decoding response") - } - - asBalanceTransactions := make([]*stripe.BalanceTransaction, 0) - - if len(rsp.Data) > 0 { - for _, data := range rsp.Data { - asBalanceTransaction := &stripe.BalanceTransaction{} - - err = json.Unmarshal(data, &asBalanceTransaction) - if err != nil { - return nil, false, err - } - - asBalanceTransactions = append(asBalanceTransactions, asBalanceTransaction) - } - } - - return asBalanceTransactions, rsp.HasMore, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/client/balances.go b/components/payments/cmd/connectors/internal/connectors/stripe/client/balances.go deleted file mode 100644 index 651768f630..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/client/balances.go +++ /dev/null @@ -1,67 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/pkg/errors" - "github.com/stripe/stripe-go/v72" -) - -const ( - balanceEndpoint = "https://api.stripe.com/v1/balance" -) - -type BalanceResponse struct { - *stripe.Balance -} - -func (d *DefaultClient) Balance(ctx context.Context, options ...ClientOption) (*stripe.Balance, error) { - f := connectors.ClientMetrics(ctx, "stripe", "get_balance") - now := time.Now() - defer f(ctx, now) - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, balanceEndpoint, nil) - if err != nil { - return nil, errors.Wrap(err, "creating http request") - } - - for _, opt := range options { - opt.Apply(req) - } - - if d.stripeAccount != "" { - req.Header.Set("Stripe-Account", d.stripeAccount) - } - - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - req.SetBasicAuth(d.apiKey, "") // gfyrag: really weird authentication right? - - var httpResponse *http.Response - httpResponse, err = d.httpClient.Do(req) - if err != nil { - return nil, errors.Wrap(err, "doing request") - } - defer httpResponse.Body.Close() - - if httpResponse.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected status code: %d", httpResponse.StatusCode) - } - - type balanceResponse struct { - BalanceResponse - } - - rsp := &balanceResponse{} - - err = json.NewDecoder(httpResponse.Body).Decode(rsp) - if err != nil { - return nil, errors.Wrap(err, "decoding response") - } - - return rsp.Balance, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/client/client.go b/components/payments/cmd/connectors/internal/connectors/stripe/client/client.go deleted file mode 100644 index cd7dcfd7bf..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/client/client.go +++ /dev/null @@ -1,72 +0,0 @@ -package client - -import ( - "context" - "net/http" - "time" - - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" - - "github.com/stripe/stripe-go/v72" -) - -type ClientOption interface { - Apply(req *http.Request) -} -type ClientOptionFn func(req *http.Request) - -func (fn ClientOptionFn) Apply(req *http.Request) { - fn(req) -} - -func QueryParam(key, value string) ClientOptionFn { - return func(req *http.Request) { - q := req.URL.Query() - q.Set(key, value) - req.URL.RawQuery = q.Encode() - } -} - -type Client interface { - Accounts(ctx context.Context, options ...ClientOption) ([]*stripe.Account, bool, error) - ExternalAccounts(ctx context.Context, options ...ClientOption) ([]*stripe.ExternalAccount, bool, error) - BalanceTransactions(ctx context.Context, options ...ClientOption) ([]*stripe.BalanceTransaction, bool, error) - Balance(ctx context.Context, options ...ClientOption) (*stripe.Balance, error) - CreateTransfer(ctx context.Context, CreateTransferRequest *CreateTransferRequest, options ...ClientOption) (*stripe.Transfer, error) - ReverseTransfer(ctx context.Context, createTransferReversalRequest *CreateTransferReversalRequest, options ...ClientOption) (*stripe.Reversal, error) - CreatePayout(ctx context.Context, createPayoutRequest *CreatePayoutRequest, options ...ClientOption) (*stripe.Payout, error) - GetPayout(ctx context.Context, payoutID string, options ...ClientOption) (*stripe.Payout, error) - ForAccount(account string) Client -} - -type DefaultClient struct { - httpClient *http.Client - apiKey string - stripeAccount string -} - -func NewDefaultClient(apiKey string) *DefaultClient { - return &DefaultClient{ - httpClient: newHTTPClient(), - apiKey: apiKey, - } -} - -func (d *DefaultClient) ForAccount(account string) Client { - cp := *d - cp.stripeAccount = account - - return &cp -} - -func newHTTPClient() *http.Client { - return &http.Client{ - Transport: otelhttp.NewTransport(http.DefaultTransport), - } -} - -var _ Client = &DefaultClient{} - -func DatePtr(t time.Time) *time.Time { - return &t -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/client/client_test.go b/components/payments/cmd/connectors/internal/connectors/stripe/client/client_test.go deleted file mode 100644 index e0c74412b8..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/client/client_test.go +++ /dev/null @@ -1,277 +0,0 @@ -package client - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "reflect" - "sync" - "testing" - "time" - - "github.com/stripe/stripe-go/v72" -) - -type httpMockExpectation interface { - handle(t *testing.T, r *http.Request) (*http.Response, error) -} - -type httpMock struct { - t *testing.T - expectations []httpMockExpectation - mu sync.Mutex -} - -func (mock *httpMock) RoundTrip(request *http.Request) (*http.Response, error) { - mock.mu.Lock() - defer mock.mu.Unlock() - - if len(mock.expectations) == 0 { - return nil, fmt.Errorf("no more expectations") - } - - expectations := mock.expectations[0] - if len(mock.expectations) == 1 { - mock.expectations = make([]httpMockExpectation, 0) - } else { - mock.expectations = mock.expectations[1:] - } - - return expectations.handle(mock.t, request) -} - -var _ http.RoundTripper = &httpMock{} - -type HTTPExpect[REQUEST any, RESPONSE any] struct { - statusCode int - path string - method string - requestBody *REQUEST - responseBody *RESPONSE - queryParams map[string]any -} - -func (e *HTTPExpect[REQUEST, RESPONSE]) handle(t *testing.T, request *http.Request) (*http.Response, error) { - t.Helper() - - if e.path != request.URL.Path { - return nil, fmt.Errorf("expected url was '%s', got, '%s'", e.path, request.URL.Path) - } - - if e.method != request.Method { - return nil, fmt.Errorf("expected method was '%s', got, '%s'", e.method, request.Method) - } - - if e.requestBody != nil { - body := new(REQUEST) - - err := json.NewDecoder(request.Body).Decode(body) - if err != nil { - panic(err) - } - - if !reflect.DeepEqual(*e.responseBody, *body) { - return nil, fmt.Errorf("mismatch body") - } - } - - for key, value := range e.queryParams { - qpvalue := "" - - switch value.(type) { - case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: - qpvalue = fmt.Sprintf("%d", value) - default: - qpvalue = fmt.Sprintf("%s", value) - } - - if rvalue := request.URL.Query().Get(key); rvalue != qpvalue { - return nil, fmt.Errorf("expected query param '%s' with value '%s', got '%s'", key, qpvalue, rvalue) - } - } - - data := make([]byte, 0) - - if e.responseBody != nil { - var err error - - data, err = json.Marshal(e.responseBody) - if err != nil { - panic(err) - } - } - - return &http.Response{ - StatusCode: e.statusCode, - Body: io.NopCloser(bytes.NewReader(data)), - ContentLength: int64(len(data)), - Request: request, - }, nil -} - -func (e *HTTPExpect[REQUEST, RESPONSE]) Path(p string) *HTTPExpect[REQUEST, RESPONSE] { - e.path = p - - return e -} - -func (e *HTTPExpect[REQUEST, RESPONSE]) Method(p string) *HTTPExpect[REQUEST, RESPONSE] { - e.method = p - - return e -} - -func (e *HTTPExpect[REQUEST, RESPONSE]) Body(body *REQUEST) *HTTPExpect[REQUEST, RESPONSE] { - e.requestBody = body - - return e -} - -func (e *HTTPExpect[REQUEST, RESPONSE]) QueryParam(key string, value any) *HTTPExpect[REQUEST, RESPONSE] { - e.queryParams[key] = value - - return e -} - -func (e *HTTPExpect[REQUEST, RESPONSE]) RespondsWith(statusCode int, - body *RESPONSE, -) *HTTPExpect[REQUEST, RESPONSE] { - e.statusCode = statusCode - e.responseBody = body - - return e -} - -func Expect[REQUEST, RESPONSE any](mock *httpMock) *HTTPExpect[REQUEST, RESPONSE] { - expectations := &HTTPExpect[REQUEST, RESPONSE]{ - queryParams: map[string]any{}, - } - - mock.mu.Lock() - defer mock.mu.Unlock() - - mock.expectations = append(mock.expectations, expectations) - - return expectations -} - -type StripeBalanceTransactionListExpect struct { - *HTTPExpect[struct{}, MockedListResponse] -} - -func (e *StripeBalanceTransactionListExpect) Path(p string) *StripeBalanceTransactionListExpect { - e.HTTPExpect.Path(p) - - return e -} - -func (e *StripeBalanceTransactionListExpect) Method(p string) *StripeBalanceTransactionListExpect { - e.HTTPExpect.Method(p) - - return e -} - -func (e *StripeBalanceTransactionListExpect) QueryParam(key string, - value any, -) *StripeBalanceTransactionListExpect { - e.HTTPExpect.QueryParam(key, value) - - return e -} - -func (e *StripeBalanceTransactionListExpect) RespondsWith(statusCode int, hasMore bool, - body ...*stripe.BalanceTransaction, -) *StripeBalanceTransactionListExpect { - e.HTTPExpect.RespondsWith(statusCode, &MockedListResponse{ - HasMore: hasMore, - Data: body, - }) - - return e -} - -func (e *StripeBalanceTransactionListExpect) StartingAfter(v string) *StripeBalanceTransactionListExpect { - e.QueryParam("starting_after", v) - - return e -} - -func (e *StripeBalanceTransactionListExpect) CreatedLte(v time.Time) *StripeBalanceTransactionListExpect { - e.QueryParam("created[lte]", v.Unix()) - - return e -} - -func (e *StripeBalanceTransactionListExpect) Limit(v int) *StripeBalanceTransactionListExpect { - e.QueryParam("limit", v) - - return e -} - -func ExpectBalanceTransactionList(mock *httpMock) *StripeBalanceTransactionListExpect { - e := Expect[struct{}, MockedListResponse](mock) - e.Path("/v1/balance_transactions").Method(http.MethodGet) - - return &StripeBalanceTransactionListExpect{ - HTTPExpect: e, - } -} - -type BalanceTransactionSource stripe.BalanceTransactionSource - -func (t *BalanceTransactionSource) MarshalJSON() ([]byte, error) { - type Aux BalanceTransactionSource - - return json.Marshal(struct { - Aux - Charge *stripe.Charge `json:"charge"` - Payout *stripe.Payout `json:"payout"` - Refund *stripe.Refund `json:"refund"` - Transfer *stripe.Transfer `json:"transfer"` - }{ - Aux: Aux(*t), - Charge: t.Charge, - Payout: t.Payout, - Refund: t.Refund, - Transfer: t.Transfer, - }) -} - -type BalanceTransaction stripe.BalanceTransaction - -func (t *BalanceTransaction) MarshalJSON() ([]byte, error) { - type Aux BalanceTransaction - - return json.Marshal(struct { - Aux - Source *BalanceTransactionSource `json:"source"` - }{ - Aux: Aux(*t), - Source: (*BalanceTransactionSource)(t.Source), - }) -} - -//nolint:tagliatelle // allow snake_case in client -type MockedListResponse struct { - HasMore bool `json:"has_more"` - Data []*stripe.BalanceTransaction `json:"data"` -} - -func (t *MockedListResponse) MarshalJSON() ([]byte, error) { - type Aux MockedListResponse - - txs := make([]*BalanceTransaction, 0) - for _, tx := range t.Data { - txs = append(txs, (*BalanceTransaction)(tx)) - } - - return json.Marshal(struct { - Aux - Data []*BalanceTransaction `json:"data"` - }{ - Aux: Aux(*t), - Data: txs, - }) -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/client/external_accounts.go b/components/payments/cmd/connectors/internal/connectors/stripe/client/external_accounts.go deleted file mode 100644 index cd5ff3bb57..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/client/external_accounts.go +++ /dev/null @@ -1,81 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/pkg/errors" - "github.com/stripe/stripe-go/v72" -) - -const ( - externalAccountsEndpoint = "https://api.stripe.com/v1/accounts/%s/external_accounts" -) - -func (d *DefaultClient) ExternalAccounts(ctx context.Context, options ...ClientOption) ([]*stripe.ExternalAccount, bool, error) { - f := connectors.ClientMetrics(ctx, "stripe", "list_external_accounts") - now := time.Now() - defer f(ctx, now) - - if d.stripeAccount == "" { - return nil, false, errors.New("stripe account is required") - } - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf(externalAccountsEndpoint, d.stripeAccount), nil) - if err != nil { - return nil, false, errors.Wrap(err, "creating http request") - } - - for _, opt := range options { - opt.Apply(req) - } - - req.Header.Set("Stripe-Account", d.stripeAccount) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - req.SetBasicAuth(d.apiKey, "") // gfyrag: really weird authentication right? - - var httpResponse *http.Response - - httpResponse, err = d.httpClient.Do(req) - if err != nil { - return nil, false, errors.Wrap(err, "doing request") - } - defer httpResponse.Body.Close() - - if httpResponse.StatusCode != http.StatusOK { - return nil, false, fmt.Errorf("unexpected status code: %d", httpResponse.StatusCode) - } - - type listResponse struct { - TransactionsListResponse - Data []json.RawMessage `json:"data"` - } - - rsp := &listResponse{} - - err = json.NewDecoder(httpResponse.Body).Decode(rsp) - if err != nil { - return nil, false, errors.Wrap(err, "decoding response") - } - - externalAccounts := make([]*stripe.ExternalAccount, 0) - - if len(rsp.Data) > 0 { - for _, data := range rsp.Data { - account := &stripe.ExternalAccount{} - - err = json.Unmarshal(data, &account) - if err != nil { - return nil, false, err - } - - externalAccounts = append(externalAccounts, account) - } - } - - return externalAccounts, rsp.HasMore, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/client/payouts.go b/components/payments/cmd/connectors/internal/connectors/stripe/client/payouts.go deleted file mode 100644 index 611f6634ee..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/client/payouts.go +++ /dev/null @@ -1,71 +0,0 @@ -package client - -import ( - "context" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/pkg/errors" - "github.com/stripe/stripe-go/v72" - "github.com/stripe/stripe-go/v72/payout" -) - -type CreatePayoutRequest struct { - IdempotencyKey string - Amount int64 - Currency string - Destination string - Description string -} - -func (d *DefaultClient) CreatePayout(ctx context.Context, createPayoutRequest *CreatePayoutRequest, options ...ClientOption) (*stripe.Payout, error) { - f := connectors.ClientMetrics(ctx, "stripe", "initiate_payout") - now := time.Now() - defer f(ctx, now) - - stripe.Key = d.apiKey - - params := &stripe.PayoutParams{ - Params: stripe.Params{ - Context: ctx, - }, - Amount: stripe.Int64(createPayoutRequest.Amount), - Currency: stripe.String(createPayoutRequest.Currency), - Destination: stripe.String(createPayoutRequest.Destination), - Method: stripe.String("standard"), - } - - if d.stripeAccount != "" { - params.SetStripeAccount(d.stripeAccount) - } - - if createPayoutRequest.IdempotencyKey != "" { - params.IdempotencyKey = stripe.String(createPayoutRequest.IdempotencyKey) - } - - if createPayoutRequest.Description != "" { - params.Description = stripe.String(createPayoutRequest.Description) - } - - payoutResponse, err := payout.New(params) - if err != nil { - return nil, errors.Wrap(err, "creating transfer") - } - - return payoutResponse, nil -} - -func (d *DefaultClient) GetPayout(ctx context.Context, payoutID string, options ...ClientOption) (*stripe.Payout, error) { - f := connectors.ClientMetrics(ctx, "stripe", "get_payout") - now := time.Now() - defer f(ctx, now) - - stripe.Key = d.apiKey - - payoutResponse, err := payout.Get(payoutID, nil) - if err != nil { - return nil, errors.Wrap(err, "getting payout") - } - - return payoutResponse, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/client/transfer_reversal.go b/components/payments/cmd/connectors/internal/connectors/stripe/client/transfer_reversal.go deleted file mode 100644 index 192281f3d2..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/client/transfer_reversal.go +++ /dev/null @@ -1,46 +0,0 @@ -package client - -import ( - "context" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/stripe/stripe-go/v72" - "github.com/stripe/stripe-go/v72/reversal" -) - -type CreateTransferReversalRequest struct { - TransferID string - Amount int64 - Description string - Metadata map[string]string -} - -func (d *DefaultClient) ReverseTransfer(ctx context.Context, createTransferReversalRequest *CreateTransferReversalRequest, options ...ClientOption) (*stripe.Reversal, error) { - f := connectors.ClientMetrics(ctx, "stripe", "reverse_transfer") - now := time.Now() - defer f(ctx, now) - - stripe.Key = d.apiKey - - params := &stripe.ReversalParams{ - Params: stripe.Params{ - Context: ctx, - Metadata: createTransferReversalRequest.Metadata, - }, - Transfer: stripe.String(createTransferReversalRequest.TransferID), - Amount: stripe.Int64(createTransferReversalRequest.Amount), - Description: stripe.String(createTransferReversalRequest.Description), - } - - if d.stripeAccount != "" { - params.SetStripeAccount(d.stripeAccount) - } - - reversalResponse, err := reversal.New(params) - if err != nil { - return nil, err - } - - return reversalResponse, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/client/transfers.go b/components/payments/cmd/connectors/internal/connectors/stripe/client/transfers.go deleted file mode 100644 index 3813e99c78..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/client/transfers.go +++ /dev/null @@ -1,51 +0,0 @@ -package client - -import ( - "context" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/pkg/errors" - "github.com/stripe/stripe-go/v72" - "github.com/stripe/stripe-go/v72/transfer" -) - -type CreateTransferRequest struct { - IdempotencyKey string - Amount int64 - Currency string - Destination string - Description string -} - -func (d *DefaultClient) CreateTransfer(ctx context.Context, createTransferRequest *CreateTransferRequest, options ...ClientOption) (*stripe.Transfer, error) { - f := connectors.ClientMetrics(ctx, "stripe", "initiate_transfer") - now := time.Now() - defer f(ctx, now) - - stripe.Key = d.apiKey - - params := &stripe.TransferParams{ - Params: stripe.Params{ - Context: ctx, - }, - Amount: stripe.Int64(createTransferRequest.Amount), - Currency: stripe.String(createTransferRequest.Currency), - Destination: stripe.String(createTransferRequest.Destination), - } - - if d.stripeAccount != "" { - params.SetStripeAccount(d.stripeAccount) - } - - if createTransferRequest.IdempotencyKey != "" { - params.IdempotencyKey = stripe.String(createTransferRequest.IdempotencyKey) - } - - transferResponse, err := transfer.New(params) - if err != nil { - return nil, errors.Wrap(err, "creating transfer") - } - - return transferResponse, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/config.go b/components/payments/cmd/connectors/internal/connectors/stripe/config.go deleted file mode 100644 index fdcf01e17f..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/config.go +++ /dev/null @@ -1,66 +0,0 @@ -package stripe - -import ( - "encoding/json" - "errors" - "fmt" - "strconv" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors/configtemplate" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" -) - -const ( - defaultPageSize = 10 - defaultPollingPeriod = 2 * time.Minute -) - -type Config struct { - Name string `json:"name" yaml:"name" bson:"name"` - PollingPeriod connectors.Duration `json:"pollingPeriod" yaml:"pollingPeriod" bson:"pollingPeriod"` - APIKey string `json:"apiKey" yaml:"apiKey" bson:"apiKey"` - TimelineConfig `bson:",inline"` -} - -// String obfuscates sensitive fields and returns a string representation of the config. -// This is used for logging. -func (c Config) String() string { - return fmt.Sprintf("pollingPeriod=%s, pageSize=%d, apiKey=****", c.PollingPeriod, c.PageSize) -} - -func (c Config) Validate() error { - if c.APIKey == "" { - return errors.New("missing api key") - } - - if c.Name == "" { - return errors.New("missing name") - } - - return nil -} - -func (c Config) ConnectorName() string { - return c.Name -} - -func (c Config) Marshal() ([]byte, error) { - return json.Marshal(c) -} - -type TimelineConfig struct { - PageSize uint64 `json:"pageSize" yaml:"pageSize" bson:"pageSize"` -} - -func (c Config) BuildTemplate() (string, configtemplate.Config) { - cfg := configtemplate.NewConfig() - - cfg.AddParameter("name", configtemplate.TypeString, name.String(), false) - cfg.AddParameter("apiKey", configtemplate.TypeString, "", true) - cfg.AddParameter("pollingPeriod", configtemplate.TypeDurationNs, defaultPollingPeriod.String(), false) - cfg.AddParameter("pageSize", configtemplate.TypeDurationUnsignedInteger, strconv.Itoa(defaultPageSize), false) - - return name.String(), cfg -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/connector.go b/components/payments/cmd/connectors/internal/connectors/stripe/connector.go deleted file mode 100644 index 39c2666bba..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/connector.go +++ /dev/null @@ -1,153 +0,0 @@ -package stripe - -import ( - "context" - "errors" - - "github.com/formancehq/go-libs/contextutil" - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" -) - -const name = models.ConnectorProviderStripe - -var ( - mainTaskDescriptor = TaskDescriptor{ - Name: "Main task to periodically fetch transactions", - Main: true, - } -) - -type Connector struct { - logger logging.Logger - cfg Config -} - -func newConnector(logger logging.Logger, cfg Config) *Connector { - return &Connector{ - logger: logger.WithFields(map[string]any{ - "component": "connector", - }), - cfg: cfg, - } -} - -func (c *Connector) UpdateConfig(ctx task.ConnectorContext, config models.ConnectorConfigObject) error { - cfg, ok := config.(Config) - if !ok { - return connectors.ErrInvalidConfig - } - - restartTask := c.cfg.PollingPeriod.Duration != cfg.PollingPeriod.Duration - - c.cfg = cfg - - if restartTask { - descriptor, err := models.EncodeTaskDescriptor(mainTaskDescriptor) - if err != nil { - return err - } - - return ctx.Scheduler().Schedule(ctx.Context(), descriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: c.cfg.PollingPeriod.Duration, - // No need to restart this task, since the connector is not existing or - // was uninstalled previously, the task does not exists in the database - RestartOption: models.OPTIONS_STOP_AND_RESTART, - }) - } - - return nil -} - -func (c *Connector) Install(ctx task.ConnectorContext) error { - descriptor, err := models.EncodeTaskDescriptor(mainTaskDescriptor) - if err != nil { - return err - } - - return ctx.Scheduler().Schedule(ctx.Context(), descriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: c.cfg.PollingPeriod.Duration, - // No need to restart this task, since the connector is not existing or - // was uninstalled previously, the task does not exists in the database - RestartOption: models.OPTIONS_RESTART_NEVER, - }) -} - -func (c *Connector) Uninstall(ctx context.Context) error { - return nil -} - -func (c *Connector) Resolve(descriptor models.TaskDescriptor) task.Task { - taskDescriptor, err := models.DecodeTaskDescriptor[TaskDescriptor](descriptor) - if err != nil { - panic(err) - } - - return c.resolveTasks()(taskDescriptor) -} - -func (c *Connector) SupportedCurrenciesAndDecimals() map[string]int { - return supportedCurrenciesWithDecimal -} - -func (c *Connector) InitiatePayment(ctx task.ConnectorContext, transfer *models.TransferInitiation) error { - taskDescriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Initiate payment", - Key: taskNameInitiatePayment, - TransferID: transfer.ID.String(), - }) - if err != nil { - return err - } - - scheduleOption := models.OPTIONS_RUN_NOW_SYNC - scheduledAt := transfer.ScheduledAt - if !scheduledAt.IsZero() { - scheduleOption = models.OPTIONS_RUN_SCHEDULED_AT - } - - err = ctx.Scheduler().Schedule(ctx.Context(), taskDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: scheduleOption, - ScheduleAt: scheduledAt, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return err - } - - return nil -} - -func (c *Connector) ReversePayment(ctx task.ConnectorContext, transferReversal *models.TransferReversal) error { - // Detach the context since we're launching an async task and we're mostly - // coming from a HTTP request. - detachedCtx, _ := contextutil.Detached(ctx.Context()) - taskDescriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Reverse payment", - Key: taskNameReversePayment, - TransferReversalID: transferReversal.ID.String(), - }) - if err != nil { - return err - } - - err = ctx.Scheduler().Schedule(detachedCtx, taskDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW_SYNC, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return err - } - - return nil -} - -func (c *Connector) CreateExternalBankAccount(ctx task.ConnectorContext, bankAccount *models.BankAccount) error { - return connectors.ErrNotImplemented -} - -var _ connectors.Connector = &Connector{} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/currencies.go b/components/payments/cmd/connectors/internal/connectors/stripe/currencies.go deleted file mode 100644 index 4e6518ec52..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/currencies.go +++ /dev/null @@ -1,162 +0,0 @@ -package stripe - -import "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - -var ( - // c.f. https://stripe.com/docs/currencies#zero-decimal - supportedCurrenciesWithDecimal = map[string]int{ - "USD": currency.ISO4217Currencies["USD"], // United States dollar - "AED": currency.ISO4217Currencies["AED"], // United Arab Emirates dirham - "AFN": currency.ISO4217Currencies["AFN"], // Afghan afghani - "ALL": currency.ISO4217Currencies["ALL"], // Albanian lek - "AMD": currency.ISO4217Currencies["AMD"], // Armenian dram - "ANG": currency.ISO4217Currencies["ANG"], // Netherlands Antillean guilder - "AOA": currency.ISO4217Currencies["AOA"], // Angolan kwanza - "ARS": currency.ISO4217Currencies["ARS"], // Argentine peso - "AUD": currency.ISO4217Currencies["AUD"], // Australian dollar - "AWG": currency.ISO4217Currencies["AWG"], // Aruban florin - "AZN": currency.ISO4217Currencies["AZN"], // Azerbaijani manat - "BAM": currency.ISO4217Currencies["BAM"], // Bosnia and Herzegovina convertible mark - "BBD": currency.ISO4217Currencies["BBD"], // Barbados dollar - "BDT": currency.ISO4217Currencies["BDT"], // Bangladeshi taka - "BGN": currency.ISO4217Currencies["BGN"], // Bulgarian lev - "BIF": currency.ISO4217Currencies["BIF"], // Burundian franc - "BMD": currency.ISO4217Currencies["BMD"], // Bermudian dollar - "BND": currency.ISO4217Currencies["BND"], // Brunei dollar - "BOB": currency.ISO4217Currencies["BOB"], // Bolivian boliviano - "BRL": currency.ISO4217Currencies["BRL"], // Brazilian real - "BSD": currency.ISO4217Currencies["BSD"], // Bahamian dollar - "BWP": currency.ISO4217Currencies["BWP"], // Botswana pula - "BYN": currency.ISO4217Currencies["BYN"], // Belarusian ruble - "BZD": currency.ISO4217Currencies["BZD"], // Belize dollar - "CAD": currency.ISO4217Currencies["CAD"], // Canadian dollar - "CDF": currency.ISO4217Currencies["CDF"], // Congolese franc - "CHF": currency.ISO4217Currencies["CHF"], // Swiss franc - "CLP": currency.ISO4217Currencies["CLP"], // Chilean peso - "CNY": currency.ISO4217Currencies["CNY"], // Chinese yuan - "COP": currency.ISO4217Currencies["COP"], // Colombian peso - "CRC": currency.ISO4217Currencies["CRC"], // Costa Rican colon - "CVE": currency.ISO4217Currencies["CVE"], // Cape Verdean escudo - "CZK": currency.ISO4217Currencies["CZK"], // Czech koruna - "DJF": currency.ISO4217Currencies["DJF"], // Djiboutian franc - "DKK": currency.ISO4217Currencies["DKK"], // Danish krone - "DOP": currency.ISO4217Currencies["DOP"], // Dominican peso - "DZD": currency.ISO4217Currencies["DZD"], // Algerian dinar - "EGP": currency.ISO4217Currencies["EGP"], // Egyptian pound - "ETB": currency.ISO4217Currencies["ETB"], // Ethiopian birr - "EUR": currency.ISO4217Currencies["EUR"], // Euro - "FJD": currency.ISO4217Currencies["FJD"], // Fiji dollar - "FKP": currency.ISO4217Currencies["FKP"], // Falkland Islands pound - "GBP": currency.ISO4217Currencies["GBP"], // Pound sterling - "GEL": currency.ISO4217Currencies["GEL"], // Georgian lari - "GIP": currency.ISO4217Currencies["GIP"], // Gibraltar pound - "GMD": currency.ISO4217Currencies["GMD"], // Gambian dalasi - "GNF": currency.ISO4217Currencies["GNF"], // Guinean franc - "GTQ": currency.ISO4217Currencies["GTQ"], // Guatemalan quetzal - "GYD": currency.ISO4217Currencies["GYD"], // Guyanese dollar - "HKD": currency.ISO4217Currencies["HKD"], // Hong Kong dollar - "HNL": currency.ISO4217Currencies["HNL"], // Honduran lempira - "HTG": currency.ISO4217Currencies["HTG"], // Haitian gourde - "IDR": currency.ISO4217Currencies["IDR"], // Indonesian rupiah - "ILS": currency.ISO4217Currencies["ILS"], // Israeli new shekel - "INR": currency.ISO4217Currencies["INR"], // Indian rupee - "JMD": currency.ISO4217Currencies["JMD"], // Jamaican dollar - "JPY": currency.ISO4217Currencies["JPY"], // Japanese yen - "KES": currency.ISO4217Currencies["KES"], // Kenyan shilling - "KGS": currency.ISO4217Currencies["KGS"], // Kyrgyzstani som - "KHR": currency.ISO4217Currencies["KHR"], // Cambodian riel - "KMF": currency.ISO4217Currencies["KMF"], // Comoro franc - "KRW": currency.ISO4217Currencies["KRW"], // South Korean won - "KYD": currency.ISO4217Currencies["KYD"], // Cayman Islands dollar - "KZT": currency.ISO4217Currencies["KZT"], // Kazakhstani tenge - "LAK": currency.ISO4217Currencies["LAK"], // Lao kip - "LBP": currency.ISO4217Currencies["LBP"], // Lebanese pound - "LKR": currency.ISO4217Currencies["LKR"], // Sri Lankan rupee - "LRD": currency.ISO4217Currencies["LRD"], // Liberian dollar - "LSL": currency.ISO4217Currencies["LSL"], // Lesotho loti - "MAD": currency.ISO4217Currencies["MAD"], // Moroccan dirham - "MDL": currency.ISO4217Currencies["MDL"], // Moldovan leu - "MKD": currency.ISO4217Currencies["MKD"], // Macedonian denar - "MMK": currency.ISO4217Currencies["MMK"], // Burmese kyat - "MNT": currency.ISO4217Currencies["MNT"], // Mongolian tögrög - "MOP": currency.ISO4217Currencies["MOP"], // Macanese pataca - "MUR": currency.ISO4217Currencies["MUR"], // Mauritian rupee - "MVR": currency.ISO4217Currencies["MVR"], // Maldivian rufiyaa - "MWK": currency.ISO4217Currencies["MWK"], // Malawian kwacha - "MXN": currency.ISO4217Currencies["MXN"], // Mexican peso - "MYR": currency.ISO4217Currencies["MYR"], // Malaysian ringgit - "MZN": currency.ISO4217Currencies["MZN"], // Mozambican metical - "NAD": currency.ISO4217Currencies["NAD"], // Namibian dollar - "NGN": currency.ISO4217Currencies["NGN"], // Nigerian naira - "NIO": currency.ISO4217Currencies["NIO"], // Nicaraguan córdoba - "NOK": currency.ISO4217Currencies["NOK"], // Norwegian krone - "NPR": currency.ISO4217Currencies["NPR"], // Nepalese rupee - "NZD": currency.ISO4217Currencies["NZD"], // New Zealand dollar - "PAB": currency.ISO4217Currencies["PAB"], // Panamanian balboa - "PEN": currency.ISO4217Currencies["PEN"], // Peruvian sol - "PGK": currency.ISO4217Currencies["PGK"], // Papua New Guinean kina - "PHP": currency.ISO4217Currencies["PHP"], // Philippine peso - "PKR": currency.ISO4217Currencies["PKR"], // Pakistani rupee - "PLN": currency.ISO4217Currencies["PLN"], // Polish złoty - "PYG": currency.ISO4217Currencies["PYG"], // Paraguayan guaraní - "QAR": currency.ISO4217Currencies["QAR"], // Qatari riyal - "RON": currency.ISO4217Currencies["RON"], // Romanian leu - "RSD": currency.ISO4217Currencies["RSD"], // Serbian dinar - "RUB": currency.ISO4217Currencies["RUB"], // Russian ruble - "RWF": currency.ISO4217Currencies["RWF"], // Rwandan franc - "SAR": currency.ISO4217Currencies["SAR"], // Saudi riyal - "SBD": currency.ISO4217Currencies["SBD"], // Solomon Islands dollar - "SCR": currency.ISO4217Currencies["SCR"], // Seychelles rupee - "SEK": currency.ISO4217Currencies["SEK"], // Swedish krona/kronor - "SGD": currency.ISO4217Currencies["SGD"], // Singapore dollar - "SHP": currency.ISO4217Currencies["SHP"], // Saint Helena pound - "SOS": currency.ISO4217Currencies["SOS"], // Somali shilling - "SRD": currency.ISO4217Currencies["SRD"], // Surinamese dollar - "SZL": currency.ISO4217Currencies["SZL"], // Swazi lilangeni - "THB": currency.ISO4217Currencies["THB"], // Thai baht - "TJS": currency.ISO4217Currencies["TJS"], // Tajikistani somoni - "TOP": currency.ISO4217Currencies["TOP"], // Tongan paʻanga - "TRY": currency.ISO4217Currencies["TRY"], // Turkish lira - "TTD": currency.ISO4217Currencies["TTD"], // Trinidad and Tobago dollar - "TZS": currency.ISO4217Currencies["TZS"], // Tanzanian shilling - "UAH": currency.ISO4217Currencies["UAH"], // Ukrainian hryvnia - "UYU": currency.ISO4217Currencies["UYU"], // Uruguayan peso - "UZS": currency.ISO4217Currencies["UZS"], // Uzbekistan som - "VND": currency.ISO4217Currencies["VND"], // Vietnamese đồng - "VUV": currency.ISO4217Currencies["VUV"], // Vanuatu vatu - "WST": currency.ISO4217Currencies["WST"], // Samoan tala - "XAF": currency.ISO4217Currencies["XAF"], // Central African CFA franc - "XCD": currency.ISO4217Currencies["XCD"], // East Caribbean dollar - "XOF": currency.ISO4217Currencies["XOF"], // West African CFA franc - "XPF": currency.ISO4217Currencies["XPF"], // CFP franc - "YER": currency.ISO4217Currencies["YER"], // Yemeni rial - "ZAR": currency.ISO4217Currencies["ZAR"], // South African rand - "ZMW": currency.ISO4217Currencies["ZMW"], // Zambian kwacha - - // Unsupported currencies - // The following currencies are not in the ISO 4217 standard, - //so let's not handle them for now. - // "SLE": 2 // Sierra Leonean leone - // "STD": 2 // São Tomé and Príncipe dobra - - // The following currencies have not the same decimals in Stripe compared - // to ISO 4217 standard, so let's not handle them for now. - // "MGA": 2, // Malagasy ariary - - // The following currencies are 3 decimals currencies, but in order - // to use them with Stripe, it requires the last digit to be 0. - // Let's not handle them for now. - // "BHD": 3, // Bahraini dinar - // "JOD": 3, // Jordanian dinar - // "KWD": 3, // Kuwaiti dinar - // "OMR": 3, // Omani rial - // "TND": 3, // Tunisian dinar - - // The following currencies are apecial cases in stripe API (cf link above) - // let's not handle them for now. - // "ISK": 0, // Icelandic króna - // "HUF": 2, // Hungarian forint - // "UGX": 0, // Ugandan shilling - // "TWD": 2 // New Taiwan dollar - } -) diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/ingester.go b/components/payments/cmd/connectors/internal/connectors/stripe/ingester.go deleted file mode 100644 index ebbead8734..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/ingester.go +++ /dev/null @@ -1,43 +0,0 @@ -package stripe - -import ( - "context" - - "github.com/stripe/stripe-go/v72" -) - -type ingestTransaction func(ctx context.Context, batch []*stripe.BalanceTransaction, commitState TimelineState, tail bool) error -type ingestAccounts func(ctx context.Context, batch []*stripe.Account, commitState TimelineState, tail bool) error -type ingestExternalAccounts func(ctx context.Context, batch []*stripe.ExternalAccount, commitState TimelineState, tail bool) error - -type Ingester interface { - IngestTransactions(ctx context.Context, batch []*stripe.BalanceTransaction, commitState TimelineState, tail bool) error - IngestAccounts(ctx context.Context, batch []*stripe.Account, commitState TimelineState, tail bool) error - IngestExternalAccounts(ctx context.Context, batch []*stripe.ExternalAccount, commitState TimelineState, tail bool) error -} - -type ingester struct { - it ingestTransaction - ia ingestAccounts - iea ingestExternalAccounts -} - -func NewIngester(it ingestTransaction, ia ingestAccounts, iea ingestExternalAccounts) Ingester { - return &ingester{ - it: it, - ia: ia, - iea: iea, - } -} - -func (i *ingester) IngestTransactions(ctx context.Context, batch []*stripe.BalanceTransaction, commitState TimelineState, tail bool) error { - return i.it(ctx, batch, commitState, tail) -} - -func (i *ingester) IngestAccounts(ctx context.Context, batch []*stripe.Account, commitState TimelineState, tail bool) error { - return i.ia(ctx, batch, commitState, tail) -} - -func (i *ingester) IngestExternalAccounts(ctx context.Context, batch []*stripe.ExternalAccount, commitState TimelineState, tail bool) error { - return i.iea(ctx, batch, commitState, tail) -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/loader.go b/components/payments/cmd/connectors/internal/connectors/stripe/loader.go deleted file mode 100644 index 6cb25a59d2..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/loader.go +++ /dev/null @@ -1,51 +0,0 @@ -package stripe - -import ( - "github.com/formancehq/payments/internal/models" - "github.com/gorilla/mux" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/storage" -) - -type Loader struct{} - -const allowedTasks = 50 - -func (l *Loader) AllowTasks() int { - return allowedTasks -} - -func (l *Loader) Name() models.ConnectorProvider { - return name -} - -func (l *Loader) Load(logger logging.Logger, config Config) connectors.Connector { - return newConnector(logger, config) -} - -func (l *Loader) ApplyDefaults(cfg Config) Config { - if cfg.PageSize == 0 { - cfg.PageSize = defaultPageSize - } - - if cfg.PollingPeriod.Duration == 0 { - cfg.PollingPeriod = connectors.Duration{Duration: defaultPollingPeriod} - } - - if cfg.Name == "" { - cfg.Name = name.String() - } - - return cfg -} - -func (l *Loader) Router(_ *storage.Storage) *mux.Router { - // Webhooks are not implemented yet - return nil -} - -func NewLoader() *Loader { - return &Loader{} -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/state.go b/components/payments/cmd/connectors/internal/connectors/stripe/state.go deleted file mode 100644 index 9ae3edbc62..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/state.go +++ /dev/null @@ -1,11 +0,0 @@ -package stripe - -import "time" - -type TimelineState struct { - OldestID string `bson:"oldestID,omitempty" json:"oldestID"` - OldestDate *time.Time `bson:"oldestDate,omitempty" json:"oldestDate"` - MoreRecentID string `bson:"moreRecentID,omitempty" json:"moreRecentID"` - MoreRecentDate *time.Time `bson:"moreRecentDate,omitempty" json:"moreRecentDate"` - NoMoreHistory bool `bson:"noMoreHistory" json:"noMoreHistory"` -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/task_fetch_accounts.go b/components/payments/cmd/connectors/internal/connectors/stripe/task_fetch_accounts.go deleted file mode 100644 index 80fa20b90b..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/task_fetch_accounts.go +++ /dev/null @@ -1,212 +0,0 @@ -package stripe - -import ( - "context" - "encoding/json" - "time" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/stripe/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/pkg/errors" - "github.com/stripe/stripe-go/v72" - "go.opentelemetry.io/otel/attribute" -) - -const ( - rootAccountReference = "root" -) - -func fetchAccountsTask(config TimelineConfig, client *client.DefaultClient) task.Task { - return func( - ctx context.Context, - logger logging.Logger, - taskID models.TaskID, - connectorID models.ConnectorID, - resolver task.StateResolver, - scheduler task.Scheduler, - ingester ingestion.Ingester, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "stripe.fetchAccountsTask", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - // Register root account. - if err := registerRootAccount(ctx, connectorID, ingester, scheduler); err != nil { - otel.RecordError(span, err) - return err - } - - tt := NewTimelineTrigger( - logger, - NewIngester( - func(ctx context.Context, batch []*stripe.BalanceTransaction, commitState TimelineState, tail bool) error { - return nil - - }, - func(ctx context.Context, batch []*stripe.Account, commitState TimelineState, tail bool) error { - if err := ingestAccountsBatch(ctx, connectorID, ingester, batch); err != nil { - return err - } - - for _, account := range batch { - transactionsTask, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch transactions for a specific connected account", - Key: taskNameFetchPaymentsForAccounts, - Account: account.ID, - }) - if err != nil { - return errors.Wrap(err, "failed to transform task descriptor") - } - - err = scheduler.Schedule(ctx, transactionsTask, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return errors.Wrap(err, "scheduling connected account") - } - - balanceTask, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch balance for a specific connected account", - Key: taskNameFetchBalances, - Account: account.ID, - }) - if err != nil { - return errors.Wrap(err, "failed to transform task descriptor") - } - - err = scheduler.Schedule(ctx, balanceTask, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return errors.Wrap(err, "scheduling connected account") - } - - externalAccountsTask, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch external account for a specific connected account", - Key: taskNameFetchExternalAccounts, - Account: account.ID, - }) - if err != nil { - return errors.Wrap(err, "failed to transform task descriptor") - } - - err = scheduler.Schedule(ctx, externalAccountsTask, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return errors.Wrap(err, "scheduling connected account") - } - } - - return nil - }, - func(ctx context.Context, batch []*stripe.ExternalAccount, commitState TimelineState, tail bool) error { - return nil - - }, - ), - NewTimeline(client, - config, task.MustResolveTo(ctx, resolver, TimelineState{})), - TimelineTriggerTypeAccounts, - ) - - if err := tt.Fetch(ctx); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func registerRootAccount( - ctx context.Context, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, -) error { - if err := ingester.IngestAccounts(ctx, ingestion.AccountBatch{ - { - ID: models.AccountID{ - Reference: rootAccountReference, - ConnectorID: connectorID, - }, - ConnectorID: connectorID, - CreatedAt: time.Now().UTC(), - Reference: rootAccountReference, - Type: models.AccountTypeInternal, - }, - }); err != nil { - return err - } - balanceTask, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch balance for the root account", - Key: taskNameFetchBalances, - Account: rootAccountReference, - }) - if err != nil { - return errors.Wrap(err, "failed to transform task descriptor") - } - err = scheduler.Schedule(ctx, balanceTask, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return errors.Wrap(err, "scheduling connected account") - } - - return nil -} - -func ingestAccountsBatch( - ctx context.Context, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - accounts []*stripe.Account, -) error { - batch := ingestion.AccountBatch{} - for _, account := range accounts { - raw, err := json.Marshal(account) - if err != nil { - return err - } - - metadata := make(map[string]string) - for k, v := range account.Metadata { - metadata[k] = v - } - - batch = append(batch, &models.Account{ - ID: models.AccountID{ - Reference: account.ID, - ConnectorID: connectorID, - }, - CreatedAt: time.Unix(account.Created, 0).UTC(), - Reference: account.ID, - ConnectorID: connectorID, - DefaultAsset: currency.FormatAsset(supportedCurrenciesWithDecimal, string(account.DefaultCurrency)), - Type: models.AccountTypeInternal, - RawData: raw, - Metadata: metadata, - }) - } - - if err := ingester.IngestAccounts(ctx, batch); err != nil { - return err - } - - return nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/task_fetch_balances.go b/components/payments/cmd/connectors/internal/connectors/stripe/task_fetch_balances.go deleted file mode 100644 index c54230af82..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/task_fetch_balances.go +++ /dev/null @@ -1,72 +0,0 @@ -package stripe - -import ( - "context" - "math/big" - "time" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/stripe/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "go.opentelemetry.io/otel/attribute" -) - -func balanceTask(account string, client *client.DefaultClient) task.Task { - return func( - ctx context.Context, - logger logging.Logger, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - resolver task.StateResolver, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "stripe.balanceTask", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("account", account), - ) - defer span.End() - - stripeAccount := account - if account == rootAccountReference { - // special case for root account - stripeAccount = "" - } - - balances, err := client.ForAccount(stripeAccount).Balance(ctx) - if err != nil { - otel.RecordError(span, err) - return err - } - - batch := ingestion.BalanceBatch{} - for _, balance := range balances.Available { - timestamp := time.Now() - batch = append(batch, &models.Balance{ - AccountID: models.AccountID{ - Reference: account, - ConnectorID: connectorID, - }, - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, string(balance.Currency)), - Balance: big.NewInt(balance.Value), - CreatedAt: timestamp, - LastUpdatedAt: timestamp, - ConnectorID: connectorID, - }) - } - - if err := ingester.IngestBalances(ctx, batch, false); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/task_fetch_external_accounts.go b/components/payments/cmd/connectors/internal/connectors/stripe/task_fetch_external_accounts.go deleted file mode 100644 index 2810cd3e33..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/task_fetch_external_accounts.go +++ /dev/null @@ -1,102 +0,0 @@ -package stripe - -import ( - "context" - "encoding/json" - "time" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/stripe/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/stripe/stripe-go/v72" - "go.opentelemetry.io/otel/attribute" -) - -func fetchExternalAccountsTask(config TimelineConfig, account string, client *client.DefaultClient) task.Task { - return func( - ctx context.Context, - logger logging.Logger, - taskID models.TaskID, - connectorID models.ConnectorID, - resolver task.StateResolver, - scheduler task.Scheduler, - ingester ingestion.Ingester, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "stripe.fetchExternalAccountsTask", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("account", account), - ) - defer span.End() - - tt := NewTimelineTrigger( - logger, - NewIngester( - func(ctx context.Context, batch []*stripe.BalanceTransaction, commitState TimelineState, tail bool) error { - return nil - - }, - func(ctx context.Context, batch []*stripe.Account, commitState TimelineState, tail bool) error { - return nil - }, - func(ctx context.Context, batch []*stripe.ExternalAccount, commitState TimelineState, tail bool) error { - if err := ingestExternalAccountsBatch(ctx, connectorID, ingester, batch); err != nil { - return err - } - return nil - }, - ), - NewTimeline(client.ForAccount(account), - config, task.MustResolveTo(ctx, resolver, TimelineState{})), - TimelineTriggerTypeExternalAccounts, - ) - - if err := tt.Fetch(ctx); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func ingestExternalAccountsBatch( - ctx context.Context, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - accounts []*stripe.ExternalAccount, -) error { - batch := ingestion.AccountBatch{} - for _, account := range accounts { - raw, err := json.Marshal(account) - if err != nil { - return err - } - - batch = append(batch, &models.Account{ - ID: models.AccountID{ - Reference: account.ID, - ConnectorID: connectorID, - }, - CreatedAt: time.Unix(account.BankAccount.Account.Created, 0).UTC(), - Reference: account.ID, - ConnectorID: connectorID, - DefaultAsset: currency.FormatAsset(supportedCurrenciesWithDecimal, string(account.BankAccount.Account.DefaultCurrency)), - Type: models.AccountTypeExternal, - RawData: raw, - }) - } - - if err := ingester.IngestAccounts(ctx, batch); err != nil { - return err - } - - return nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/task_fetch_payments.go b/components/payments/cmd/connectors/internal/connectors/stripe/task_fetch_payments.go deleted file mode 100644 index c144dc734d..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/task_fetch_payments.go +++ /dev/null @@ -1,65 +0,0 @@ -package stripe - -import ( - "context" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/stripe/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/stripe/stripe-go/v72" - "go.opentelemetry.io/otel/attribute" -) - -func fetchPaymentsTask(config TimelineConfig, client *client.DefaultClient) task.Task { - return func( - ctx context.Context, - logger logging.Logger, - taskID models.TaskID, - connectorID models.ConnectorID, - resolver task.StateResolver, - scheduler task.Scheduler, - ingester ingestion.Ingester, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "stripe.fetchPaymentsTask", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("account", rootAccountReference), - ) - defer span.End() - - tt := NewTimelineTrigger( - logger, - NewIngester( - func(ctx context.Context, batch []*stripe.BalanceTransaction, commitState TimelineState, tail bool) error { - if err := ingestBatch(ctx, connectorID, rootAccountReference, logger, ingester, batch, commitState, tail); err != nil { - return err - } - - return nil - }, - func(ctx context.Context, batch []*stripe.Account, commitState TimelineState, tail bool) error { - return nil - }, - func(ctx context.Context, batch []*stripe.ExternalAccount, commitState TimelineState, tail bool) error { - return nil - }, - ), - NewTimeline(client, - config, task.MustResolveTo(ctx, resolver, TimelineState{})), - TimelineTriggerTypeTransactions, - ) - - if err := tt.Fetch(ctx); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/task_fetch_payments_for_connected_account.go b/components/payments/cmd/connectors/internal/connectors/stripe/task_fetch_payments_for_connected_account.go deleted file mode 100644 index fe55592619..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/task_fetch_payments_for_connected_account.go +++ /dev/null @@ -1,109 +0,0 @@ -package stripe - -import ( - "context" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/stripe/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/stripe/stripe-go/v72" - "go.opentelemetry.io/otel/attribute" -) - -func ingestBatch( - ctx context.Context, - connectorID models.ConnectorID, - account string, - logger logging.Logger, - ingester ingestion.Ingester, - bts []*stripe.BalanceTransaction, - commitState TimelineState, - tail bool, -) error { - batch := ingestion.PaymentBatch{} - - for i := range bts { - batchElement, handled := createBatchElement(connectorID, bts[i], account, !tail) - - if !handled { - logger.Debugf("Balance transaction type not handled: %s", bts[i].Type) - - continue - } - - if batchElement.Adjustment == nil && batchElement.Payment == nil { - continue - } - - batch = append(batch, batchElement) - } - - logger.WithFields(map[string]interface{}{ - "state": commitState, - }).Debugf("updating state") - - err := ingester.IngestPayments(ctx, batch) - if err != nil { - return err - } - - err = ingester.UpdateTaskState(ctx, commitState) - if err != nil { - return err - } - - return nil -} - -func connectedAccountTask(config TimelineConfig, account string, client *client.DefaultClient) task.Task { - return func( - ctx context.Context, - logger logging.Logger, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - resolver task.StateResolver, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "stripe.connectedAccountTask", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("account", account), - ) - defer span.End() - - trigger := NewTimelineTrigger( - logger, - NewIngester( - func(ctx context.Context, batch []*stripe.BalanceTransaction, commitState TimelineState, tail bool) error { - if err := ingestBatch(ctx, connectorID, account, logger, ingester, batch, commitState, tail); err != nil { - return err - } - - return nil - }, - func(ctx context.Context, batch []*stripe.Account, commitState TimelineState, tail bool) error { - return nil - }, - func(ctx context.Context, batch []*stripe.ExternalAccount, commitState TimelineState, tail bool) error { - return nil - }, - ), - NewTimeline(client. - ForAccount(account), config, task.MustResolveTo(ctx, resolver, TimelineState{})), - TimelineTriggerTypeTransactions, - ) - - if err := trigger.Fetch(ctx); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/task_main.go b/components/payments/cmd/connectors/internal/connectors/stripe/task_main.go deleted file mode 100644 index 5cf7de4632..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/task_main.go +++ /dev/null @@ -1,68 +0,0 @@ -package stripe - -import ( - "context" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" -) - -// Launch accounts and payments tasks -func (c *Connector) mainTask() task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - scheduler task.Scheduler, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "stripe.mainTask", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - taskAccounts, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch accounts from client", - Key: taskNameFetchAccounts, - }) - if err != nil { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - err = scheduler.Schedule(ctx, taskAccounts, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - taskPayments, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch payments from client", - Key: taskNameFetchPayments, - }) - if err != nil { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - err = scheduler.Schedule(ctx, taskPayments, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - return nil - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/task_payments.go b/components/payments/cmd/connectors/internal/connectors/stripe/task_payments.go deleted file mode 100644 index d0e445de09..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/task_payments.go +++ /dev/null @@ -1,309 +0,0 @@ -package stripe - -import ( - "context" - "errors" - "fmt" - "time" - - "github.com/formancehq/go-libs/contextutil" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/stripe/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/stripe/stripe-go/v72" - "go.opentelemetry.io/otel/attribute" -) - -const ( - transferIDKey string = "transfer_id" -) - -func initiatePaymentTask(transferID string, stripeClient *client.DefaultClient) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - storageReader storage.Reader, - ) error { - transferInitiationID := models.MustTransferInitiationIDFromString(transferID) - - ctx, span := connectors.StartSpan( - ctx, - "stripe.initiatePaymentTask", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("transferID", transferID), - attribute.String("reference", transferInitiationID.Reference), - ) - defer span.End() - - transfer, err := getTransfer(ctx, storageReader, transferInitiationID, true) - if err != nil { - otel.RecordError(span, err) - return err - } - - if err := initiatePayment(ctx, stripeClient, transfer, connectorID, ingester, scheduler, storageReader); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func initiatePayment( - ctx context.Context, - stripeClient *client.DefaultClient, - transfer *models.TransferInitiation, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - storageReader storage.Reader, -) error { - var err error - var paymentID *models.PaymentID - defer func() { - if err != nil { - ctx, cancel := contextutil.Detached(ctx) - defer cancel() - _ = ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusFailed, err.Error(), time.Now()) - } - }() - - err = ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusProcessing, "", time.Now()) - if err != nil { - return err - } - - if transfer.SourceAccount != nil { - if transfer.SourceAccount.Type == models.AccountTypeExternal { - err = errors.New("payin not implemented: source account must be an internal account") - return err - } - } - - var curr string - curr, _, err = currency.GetCurrencyAndPrecisionFromAsset(supportedCurrenciesWithDecimal, transfer.Asset) - if err != nil { - return err - } - - c := client.Client(stripeClient) - // If source account is nil, or equal to root (which is a special - // account we create for stripe for the balance platform), we don't need - // to set the stripe account. - if transfer.SourceAccount != nil && transfer.SourceAccount.Reference != rootAccountReference { - c = c.ForAccount(transfer.SourceAccountID.Reference) - } - - var connectorPaymentID string - var paymentType models.PaymentType - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) - defer cancel() - switch transfer.DestinationAccount.Type { - case models.AccountTypeInternal: - // Transfer between internal accounts - var resp *stripe.Transfer - resp, err = c.CreateTransfer(ctx, &client.CreateTransferRequest{ - IdempotencyKey: fmt.Sprintf("%s_%d", transfer.ID.Reference, len(transfer.RelatedAdjustments)), - Amount: transfer.Amount.Int64(), - Currency: curr, - Destination: transfer.DestinationAccountID.Reference, - Description: transfer.Description, - }) - if err != nil { - return err - } - - if transfer.Metadata == nil { - transfer.Metadata = make(map[string]string) - } - transfer.Metadata[transferIDKey] = resp.ID - connectorPaymentID = resp.BalanceTransaction.ID - paymentType = models.PaymentTypeTransfer - case models.AccountTypeExternal: - // Payout to an external account - var resp *stripe.Payout - resp, err = c.CreatePayout(ctx, &client.CreatePayoutRequest{ - IdempotencyKey: fmt.Sprintf("%s_%d", transfer.ID.Reference, len(transfer.RelatedAdjustments)), - Amount: transfer.Amount.Int64(), - Currency: curr, - Destination: transfer.DestinationAccountID.Reference, - Description: transfer.Description, - }) - if err != nil { - return err - } - - connectorPaymentID = resp.BalanceTransaction.ID - paymentType = models.PaymentTypePayOut - } - - paymentID = &models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: connectorPaymentID, - Type: paymentType, - }, - ConnectorID: connectorID, - } - - err = ingester.AddTransferInitiationPaymentID(ctx, transfer, paymentID, time.Now()) - if err != nil { - return err - } - - taskDescriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Update transfer initiation status", - Key: taskNameUpdatePaymentStatus, - TransferID: transfer.ID.String(), - PaymentID: paymentID.String(), - Attempt: 1, - }) - if err != nil { - return err - } - - ctx, _ = contextutil.DetachedWithTimeout(ctx, 10*time.Second) - err = scheduler.Schedule(ctx, taskDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return err - } - - return nil -} - -func updatePaymentStatusTask( - transferID string, - pID string, - attempt int, - stripeClient *client.DefaultClient, -) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - storageReader storage.Reader, - ) error { - paymentID := models.MustPaymentIDFromString(pID) - transferInitiationID := models.MustTransferInitiationIDFromString(transferID) - - ctx, span := connectors.StartSpan( - ctx, - "stripe.updatePaymentStatusTask", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("transferID", transferID), - attribute.String("paymentID", pID), - attribute.Int("attempt", attempt), - attribute.String("reference", transferInitiationID.Reference), - ) - defer span.End() - - transfer, err := getTransfer(ctx, storageReader, transferInitiationID, false) - if err != nil { - otel.RecordError(span, err) - return err - } - - if err := updatePaymentStatus(ctx, stripeClient, transfer, paymentID, attempt, ingester, scheduler, storageReader); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func updatePaymentStatus( - ctx context.Context, - stripeClient *client.DefaultClient, - transfer *models.TransferInitiation, - paymentID *models.PaymentID, - attempt int, - ingester ingestion.Ingester, - scheduler task.Scheduler, - storageReader storage.Reader, -) error { - var err error - var status stripe.PayoutFailureCode - var resultMessage string - switch transfer.Type { - case models.TransferInitiationTypeTransfer: - // Nothing to do - err = ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusProcessed, "", time.Now()) - if err != nil { - return err - } - - return nil - - case models.TransferInitiationTypePayout: - var resp *stripe.Payout - resp, err = stripeClient.GetPayout(ctx, paymentID.Reference) - if err != nil { - return err - } - - status = resp.FailureCode - resultMessage = resp.FailureMessage - } - - if status == "" { - err = ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusProcessed, "", time.Now()) - if err != nil { - return err - } - - return nil - } - - err = ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusFailed, resultMessage, time.Now()) - if err != nil { - return err - } - - return nil -} - -func getTransfer( - ctx context.Context, - reader storage.Reader, - transferID models.TransferInitiationID, - expand bool, -) (*models.TransferInitiation, error) { - transfer, err := reader.ReadTransferInitiation(ctx, transferID) - if err != nil { - return nil, err - } - - if expand { - if transfer.SourceAccountID != nil { - sourceAccount, err := reader.GetAccount(ctx, transfer.SourceAccountID.String()) - if err != nil { - return nil, err - } - transfer.SourceAccount = sourceAccount - } - - destinationAccount, err := reader.GetAccount(ctx, transfer.DestinationAccountID.String()) - if err != nil { - return nil, err - } - transfer.DestinationAccount = destinationAccount - } - - return transfer, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/task_resolve.go b/components/payments/cmd/connectors/internal/connectors/stripe/task_resolve.go deleted file mode 100644 index dbdc8814e4..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/task_resolve.go +++ /dev/null @@ -1,62 +0,0 @@ -package stripe - -import ( - "github.com/formancehq/payments/cmd/connectors/internal/connectors/stripe/client" - "github.com/formancehq/payments/cmd/connectors/internal/task" -) - -const ( - taskNameFetchAccounts = "fetch_accounts" - taskNameFetchPaymentsForAccounts = "fetch_transactions" - taskNameFetchPayments = "fetch_payments" - taskNameFetchBalances = "fetch_balance" - taskNameFetchExternalAccounts = "fetch_external_accounts" - taskNameInitiatePayment = "initiate-payment" - taskNameReversePayment = "reverse-payment" - taskNameUpdatePaymentStatus = "update-payment-status" -) - -// TaskDescriptor is the definition of a task. -type TaskDescriptor struct { - Name string `json:"name" yaml:"name" bson:"name"` - Key string `json:"key" yaml:"key" bson:"key"` - Main bool `json:"main,omitempty" yaml:"main" bson:"main"` - Account string `json:"account,omitempty" yaml:"account" bson:"account"` - TransferID string `json:"transferID,omitempty" yaml:"transferID" bson:"transferID"` - TransferReversalID string `json:"transferReversalID,omitempty" yaml:"transferReversalID" bson:"transferReversalID"` - PaymentID string `json:"paymentID,omitempty" yaml:"paymentID" bson:"paymentID"` - Attempt int `json:"attempt,omitempty" yaml:"attempt" bson:"attempt"` -} - -// clientID, apiKey, endpoint string, logger logging -func (c *Connector) resolveTasks() func(taskDefinition TaskDescriptor) task.Task { - client := client.NewDefaultClient(c.cfg.APIKey) - - return func(taskDescriptor TaskDescriptor) task.Task { - if taskDescriptor.Main { - return c.mainTask() - } - - switch taskDescriptor.Key { - case taskNameFetchPayments: - return fetchPaymentsTask(c.cfg.TimelineConfig, client) - case taskNameFetchAccounts: - return fetchAccountsTask(c.cfg.TimelineConfig, client) - case taskNameFetchExternalAccounts: - return fetchExternalAccountsTask(c.cfg.TimelineConfig, taskDescriptor.Account, client) - case taskNameFetchPaymentsForAccounts: - return connectedAccountTask(c.cfg.TimelineConfig, taskDescriptor.Account, client) - case taskNameFetchBalances: - return balanceTask(taskDescriptor.Account, client) - case taskNameInitiatePayment: - return initiatePaymentTask(taskDescriptor.TransferID, client) - case taskNameReversePayment: - return reversePaymentTask(taskDescriptor.TransferReversalID, client) - case taskNameUpdatePaymentStatus: - return updatePaymentStatusTask(taskDescriptor.TransferID, taskDescriptor.PaymentID, taskDescriptor.Attempt, client) - default: - // For compatibility with old tasks - return connectedAccountTask(c.cfg.TimelineConfig, taskDescriptor.Account, client) - } - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/task_reverse_payment.go b/components/payments/cmd/connectors/internal/connectors/stripe/task_reverse_payment.go deleted file mode 100644 index ad927b51d8..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/task_reverse_payment.go +++ /dev/null @@ -1,143 +0,0 @@ -package stripe - -import ( - "context" - "errors" - "time" - - "github.com/formancehq/go-libs/contextutil" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/stripe/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "go.opentelemetry.io/otel/attribute" -) - -func reversePaymentTask(transferReversalID string, stripeClient *client.DefaultClient) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - storageReader storage.Reader, - ) error { - reversalID := models.MustTransferReversalIDFromString(transferReversalID) - - ctx, span := connectors.StartSpan( - ctx, - "stripe.reversePaymentTask", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("transferReversalID", transferReversalID), - attribute.String("reference", reversalID.Reference), - ) - defer span.End() - - transferReversal, err := getTransferReversal(ctx, storageReader, reversalID) - if err != nil { - otel.RecordError(span, err) - return err - } - - transfer, err := getTransfer(ctx, storageReader, transferReversal.TransferInitiationID, true) - if err != nil { - otel.RecordError(span, err) - return err - } - - if err := reversePayment(ctx, stripeClient, transfer, transferReversal, connectorID, ingester, scheduler, storageReader); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func reversePayment( - ctx context.Context, - stripeClient *client.DefaultClient, - transfer *models.TransferInitiation, - transferReversal *models.TransferReversal, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - storageReader storage.Reader, -) error { - var err error - defer func() { - if err != nil { - ctx, cancel := contextutil.Detached(ctx) - defer cancel() - - transferReversal.Status = models.TransferReversalStatusFailed - transferReversal.Error = err.Error() - transferReversal.UpdatedAt = time.Now().UTC() - - _ = ingester.UpdateTransferReversalStatus(ctx, transfer, transferReversal) - } - }() - - c := client.Client(stripeClient) - // If source account is nil, or equal to root (which is a special - // account we create for stripe for the balance platform), we don't need - // to set the stripe account. - if transfer.SourceAccount != nil && transfer.SourceAccount.Reference != rootAccountReference { - c = c.ForAccount(transfer.SourceAccountID.Reference) - } - - transferID, err := getTransferIDFromMetadata(transfer) - if err != nil { - return err - } - - _, err = c.ReverseTransfer(ctx, &client.CreateTransferReversalRequest{ - TransferID: transferID, - Amount: transferReversal.Amount.Int64(), - Description: transferReversal.Description, - Metadata: transferReversal.Metadata, - }) - if err != nil { - return err - } - - transferReversal.Status = models.TransferReversalStatusProcessed - transferReversal.UpdatedAt = time.Now().UTC() - if err = ingester.UpdateTransferReversalStatus(ctx, transfer, transferReversal); err != nil { - return err - } - - return nil -} - -func getTransferReversal( - ctx context.Context, - reader storage.Reader, - transferReversalID models.TransferReversalID, -) (*models.TransferReversal, error) { - transferReversal, err := reader.GetTransferReversal(ctx, transferReversalID) - if err != nil { - return nil, err - } - - return transferReversal, nil -} - -func getTransferIDFromMetadata( - transfer *models.TransferInitiation, -) (string, error) { - if transfer.Metadata == nil { - return "", errors.New("metadata not found") - } - - transferID, ok := transfer.Metadata[transferIDKey] - if !ok { - return "", errors.New("transfer id not found in metadata") - } - - return transferID, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/timeline.go b/components/payments/cmd/connectors/internal/connectors/stripe/timeline.go deleted file mode 100644 index b27a76cca0..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/timeline.go +++ /dev/null @@ -1,54 +0,0 @@ -package stripe - -import ( - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors/stripe/client" -) - -type Timeline struct { - state TimelineState - firstIDAfterStartingAt string - startingAt time.Time - config TimelineConfig - client client.Client -} - -func NewTimeline(client client.Client, cfg TimelineConfig, state TimelineState, options ...TimelineOption) *Timeline { - defaultOptions := make([]TimelineOption, 0) - - c := &Timeline{ - config: cfg, - state: state, - client: client, - } - - options = append(defaultOptions, append([]TimelineOption{ - WithStartingAt(time.Now()), - }, options...)...) - - for _, opt := range options { - opt.apply(c) - } - - return c -} - -type TimelineOption interface { - apply(c *Timeline) -} -type TimelineOptionFn func(c *Timeline) - -func (fn TimelineOptionFn) apply(c *Timeline) { - fn(c) -} - -func WithStartingAt(v time.Time) TimelineOptionFn { - return func(c *Timeline) { - c.startingAt = v - } -} - -func (tl *Timeline) State() TimelineState { - return tl.state -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/timeline_connected_account.go b/components/payments/cmd/connectors/internal/connectors/stripe/timeline_connected_account.go deleted file mode 100644 index 3d53f23599..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/timeline_connected_account.go +++ /dev/null @@ -1,142 +0,0 @@ -package stripe - -import ( - "context" - "fmt" - "net/url" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors/stripe/client" - "github.com/stripe/stripe-go/v72" -) - -func (tl *Timeline) doAccountsRequest(ctx context.Context, queryParams url.Values, - to *[]*stripe.Account, -) (bool, error) { - options := make([]client.ClientOption, 0) - options = append(options, client.QueryParam("limit", fmt.Sprintf("%d", tl.config.PageSize))) - - for k, v := range queryParams { - options = append(options, client.QueryParam(k, v[0])) - } - - txs, hasMore, err := tl.client.Accounts(ctx, options...) - if err != nil { - return false, err - } - - *to = txs - - return hasMore, nil -} - -func (tl *Timeline) initAccounts(ctx context.Context) error { - ret := make([]*stripe.Account, 0) - params := url.Values{} - params.Set("limit", "1") - params.Set("created[lt]", fmt.Sprintf("%d", tl.startingAt.Unix())) - - _, err := tl.doAccountsRequest(ctx, params, &ret) - if err != nil { - return err - } - - if len(ret) > 0 { - tl.firstIDAfterStartingAt = ret[0].ID - } - - return nil -} - -func (tl *Timeline) AccountsTail(ctx context.Context, to *[]*stripe.Account) (bool, TimelineState, func(), error) { - queryParams := url.Values{} - - switch { - case tl.state.OldestID != "": - queryParams.Set("starting_after", tl.state.OldestID) - default: - queryParams.Set("created[lte]", fmt.Sprintf("%d", tl.startingAt.Unix())) - } - - hasMore, err := tl.doAccountsRequest(ctx, queryParams, to) - if err != nil { - return false, TimelineState{}, nil, err - } - - futureState := tl.state - - if len(*to) > 0 { - lastItem := (*to)[len(*to)-1] - futureState.OldestID = lastItem.ID - oldestDate := time.Unix(lastItem.Created, 0) - futureState.OldestDate = &oldestDate - - if futureState.MoreRecentID == "" { - firstItem := (*to)[0] - futureState.MoreRecentID = firstItem.ID - moreRecentDate := time.Unix(firstItem.Created, 0) - futureState.MoreRecentDate = &moreRecentDate - } - } - - futureState.NoMoreHistory = !hasMore - - return hasMore, futureState, func() { - tl.state = futureState - }, nil -} - -func (tl *Timeline) AccountsHead(ctx context.Context, to *[]*stripe.Account) (bool, TimelineState, func(), error) { - if tl.firstIDAfterStartingAt == "" && tl.state.MoreRecentID == "" { - err := tl.initAccounts(ctx) - if err != nil { - return false, TimelineState{}, nil, err - } - - if tl.firstIDAfterStartingAt == "" { - return false, TimelineState{ - NoMoreHistory: true, - }, func() {}, nil - } - } - - queryParams := url.Values{} - - switch { - case tl.state.MoreRecentID != "": - queryParams.Set("ending_before", tl.state.MoreRecentID) - case tl.firstIDAfterStartingAt != "": - queryParams.Set("ending_before", tl.firstIDAfterStartingAt) - } - - hasMore, err := tl.doAccountsRequest(ctx, queryParams, to) - if err != nil { - return false, TimelineState{}, nil, err - } - - futureState := tl.state - - if len(*to) > 0 { - firstItem := (*to)[0] - futureState.MoreRecentID = firstItem.ID - moreRecentDate := time.Unix(firstItem.Created, 0) - futureState.MoreRecentDate = &moreRecentDate - - if futureState.OldestID == "" { - lastItem := (*to)[len(*to)-1] - futureState.OldestID = lastItem.ID - oldestDate := time.Unix(lastItem.Created, 0) - futureState.OldestDate = &oldestDate - } - } - - futureState.NoMoreHistory = !hasMore - - for i, j := 0, len(*to)-1; i < j; i, j = i+1, j-1 { - (*to)[i], (*to)[j] = (*to)[j], (*to)[i] - } - - return hasMore, futureState, func() { - tl.state = futureState - }, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/timeline_external_accounts.go b/components/payments/cmd/connectors/internal/connectors/stripe/timeline_external_accounts.go deleted file mode 100644 index 251ad9e7c3..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/timeline_external_accounts.go +++ /dev/null @@ -1,144 +0,0 @@ -package stripe - -import ( - "context" - "fmt" - "net/url" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors/stripe/client" - "github.com/stripe/stripe-go/v72" -) - -//nolint:tagliatelle // allow different styled tags in client -type ExternalAccountsListResponse struct { - HasMore bool `json:"has_more"` - Data []*stripe.ExternalAccount `json:"data"` -} - -func (tl *Timeline) doExternalAccountsRequest(ctx context.Context, queryParams url.Values, - to *[]*stripe.ExternalAccount, -) (bool, error) { - options := make([]client.ClientOption, 0) - options = append(options, client.QueryParam("limit", fmt.Sprintf("%d", tl.config.PageSize))) - - for k, v := range queryParams { - options = append(options, client.QueryParam(k, v[0])) - } - - txs, hasMore, err := tl.client.ExternalAccounts(ctx, options...) - if err != nil { - return false, err - } - - *to = txs - - return hasMore, nil -} - -func (tl *Timeline) initExternalAccounts(ctx context.Context) error { - ret := make([]*stripe.ExternalAccount, 0) - - _, err := tl.doExternalAccountsRequest(ctx, url.Values{}, &ret) - if err != nil { - return err - } - - if len(ret) > 0 { - tl.firstIDAfterStartingAt = ret[0].ID - } - - return nil -} - -func (tl *Timeline) ExternalAccountsTail(ctx context.Context, to *[]*stripe.ExternalAccount) (bool, TimelineState, func(), error) { - queryParams := url.Values{} - - switch { - case tl.state.OldestID != "": - queryParams.Set("starting_after", tl.state.OldestID) - default: - } - - hasMore, err := tl.doExternalAccountsRequest(ctx, queryParams, to) - if err != nil { - return false, TimelineState{}, nil, err - } - - futureState := tl.state - - if len(*to) > 0 { - lastItem := (*to)[len(*to)-1] - futureState.OldestID = lastItem.ID - oldestDate := time.Unix(lastItem.BankAccount.Account.Created, 0) - futureState.OldestDate = &oldestDate - - if futureState.MoreRecentID == "" { - firstItem := (*to)[0] - futureState.MoreRecentID = firstItem.ID - moreRecentDate := time.Unix(firstItem.BankAccount.Account.Created, 0) - futureState.MoreRecentDate = &moreRecentDate - } - } - - futureState.NoMoreHistory = !hasMore - - return hasMore, futureState, func() { - tl.state = futureState - }, nil -} - -func (tl *Timeline) ExternalAccountsHead(ctx context.Context, to *[]*stripe.ExternalAccount) (bool, TimelineState, func(), error) { - if tl.firstIDAfterStartingAt == "" && tl.state.MoreRecentID == "" { - err := tl.initExternalAccounts(ctx) - if err != nil { - return false, TimelineState{}, nil, err - } - - if tl.firstIDAfterStartingAt == "" { - return false, TimelineState{ - NoMoreHistory: true, - }, func() {}, nil - } - } - - queryParams := url.Values{} - - switch { - case tl.state.MoreRecentID != "": - queryParams.Set("ending_before", tl.state.MoreRecentID) - case tl.firstIDAfterStartingAt != "": - queryParams.Set("ending_before", tl.firstIDAfterStartingAt) - } - - hasMore, err := tl.doExternalAccountsRequest(ctx, queryParams, to) - if err != nil { - return false, TimelineState{}, nil, err - } - - futureState := tl.state - - if len(*to) > 0 { - firstItem := (*to)[0] - futureState.MoreRecentID = firstItem.ID - moreRecentDate := time.Unix(firstItem.BankAccount.Account.Created, 0) - futureState.MoreRecentDate = &moreRecentDate - - if futureState.OldestID == "" { - lastItem := (*to)[len(*to)-1] - futureState.OldestID = lastItem.ID - oldestDate := time.Unix(lastItem.BankAccount.Account.Created, 0) - futureState.OldestDate = &oldestDate - } - } - - futureState.NoMoreHistory = !hasMore - - for i, j := 0, len(*to)-1; i < j; i, j = i+1, j-1 { - (*to)[i], (*to)[j] = (*to)[j], (*to)[i] - } - - return hasMore, futureState, func() { - tl.state = futureState - }, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/timeline_test.go b/components/payments/cmd/connectors/internal/connectors/stripe/timeline_test.go deleted file mode 100644 index 59503f4234..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/timeline_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package stripe - -import ( - "context" - "testing" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors/stripe/client" - "github.com/stretchr/testify/require" - "github.com/stripe/stripe-go/v72" -) - -func TestTimeline(t *testing.T) { - t.Parallel() - - mock := NewClientMock(t, true) - ref := time.Now() - timeline := NewTimeline(mock, TimelineConfig{ - PageSize: 2, - }, TimelineState{}, WithStartingAt(ref)) - - tx1 := &stripe.BalanceTransaction{ - ID: "tx1", - Created: ref.Add(-time.Minute).Unix(), - } - - tx2 := &stripe.BalanceTransaction{ - ID: "tx2", - Created: ref.Add(-2 * time.Minute).Unix(), - } - - mock.Expect(). - Limit(2). - CreatedLte(ref). - RespondsWith(true, tx1, tx2) - - ret := make([]*stripe.BalanceTransaction, 0) - hasMore, state, commit, err := timeline.TransactionsTail(context.TODO(), &ret) - require.NoError(t, err) - require.True(t, hasMore) - require.Equal(t, TimelineState{ - OldestID: "tx2", - OldestDate: client.DatePtr(time.Unix(tx2.Created, 0)), - MoreRecentID: "tx1", - MoreRecentDate: client.DatePtr(time.Unix(tx1.Created, 0)), - NoMoreHistory: false, - }, state) - - commit() - - tx3 := &stripe.BalanceTransaction{ - ID: "tx3", - Created: ref.Add(-3 * time.Minute).Unix(), - } - - mock.Expect().Limit(2).StartingAfter(tx2.ID).RespondsWith(false, tx3) - - hasMore, state, _, err = timeline.TransactionsTail(context.TODO(), &ret) - require.NoError(t, err) - require.False(t, hasMore) - require.Equal(t, TimelineState{ - OldestID: "tx3", - OldestDate: client.DatePtr(time.Unix(tx3.Created, 0)), - MoreRecentID: "tx1", - MoreRecentDate: client.DatePtr(time.Unix(tx1.Created, 0)), - NoMoreHistory: true, - }, state) -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/timeline_transactions.go b/components/payments/cmd/connectors/internal/connectors/stripe/timeline_transactions.go deleted file mode 100644 index 5df01e8178..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/timeline_transactions.go +++ /dev/null @@ -1,145 +0,0 @@ -package stripe - -import ( - "context" - "fmt" - "net/url" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors/stripe/client" - "github.com/stripe/stripe-go/v72" -) - -func (tl *Timeline) doTransactionsRequest(ctx context.Context, queryParams url.Values, - to *[]*stripe.BalanceTransaction, -) (bool, error) { - options := make([]client.ClientOption, 0) - options = append(options, client.QueryParam("limit", fmt.Sprintf("%d", tl.config.PageSize))) - options = append(options, client.QueryParam("expand[]", "data.source")) - options = append(options, client.QueryParam("expand[]", "data.source.charge")) - options = append(options, client.QueryParam("expand[]", "data.source.payment_intent")) - - for k, v := range queryParams { - options = append(options, client.QueryParam(k, v[0])) - } - - txs, hasMore, err := tl.client.BalanceTransactions(ctx, options...) - if err != nil { - return false, err - } - - *to = txs - - return hasMore, nil -} - -func (tl *Timeline) initTransactions(ctx context.Context) error { - ret := make([]*stripe.BalanceTransaction, 0) - params := url.Values{} - params.Set("limit", "1") - params.Set("created[lt]", fmt.Sprintf("%d", tl.startingAt.Unix())) - - _, err := tl.doTransactionsRequest(ctx, params, &ret) - if err != nil { - return err - } - - if len(ret) > 0 { - tl.firstIDAfterStartingAt = ret[0].ID - } - - return nil -} - -func (tl *Timeline) TransactionsTail(ctx context.Context, to *[]*stripe.BalanceTransaction) (bool, TimelineState, func(), error) { - queryParams := url.Values{} - - switch { - case tl.state.OldestID != "": - queryParams.Set("starting_after", tl.state.OldestID) - default: - queryParams.Set("created[lte]", fmt.Sprintf("%d", tl.startingAt.Unix())) - } - - hasMore, err := tl.doTransactionsRequest(ctx, queryParams, to) - if err != nil { - return false, TimelineState{}, nil, err - } - - futureState := tl.state - - if len(*to) > 0 { - lastItem := (*to)[len(*to)-1] - futureState.OldestID = lastItem.ID - oldestDate := time.Unix(lastItem.Created, 0) - futureState.OldestDate = &oldestDate - - if futureState.MoreRecentID == "" { - firstItem := (*to)[0] - futureState.MoreRecentID = firstItem.ID - moreRecentDate := time.Unix(firstItem.Created, 0) - futureState.MoreRecentDate = &moreRecentDate - } - } - - futureState.NoMoreHistory = !hasMore - - return hasMore, futureState, func() { - tl.state = futureState - }, nil -} - -func (tl *Timeline) TransactionsHead(ctx context.Context, to *[]*stripe.BalanceTransaction) (bool, TimelineState, func(), error) { - if tl.firstIDAfterStartingAt == "" && tl.state.MoreRecentID == "" { - err := tl.initTransactions(ctx) - if err != nil { - return false, TimelineState{}, nil, err - } - - if tl.firstIDAfterStartingAt == "" { - return false, TimelineState{ - NoMoreHistory: true, - }, func() {}, nil - } - } - - queryParams := url.Values{} - - switch { - case tl.state.MoreRecentID != "": - queryParams.Set("ending_before", tl.state.MoreRecentID) - case tl.firstIDAfterStartingAt != "": - queryParams.Set("ending_before", tl.firstIDAfterStartingAt) - } - - hasMore, err := tl.doTransactionsRequest(ctx, queryParams, to) - if err != nil { - return false, TimelineState{}, nil, err - } - - futureState := tl.state - - if len(*to) > 0 { - firstItem := (*to)[0] - futureState.MoreRecentID = firstItem.ID - moreRecentDate := time.Unix(firstItem.Created, 0) - futureState.MoreRecentDate = &moreRecentDate - - if futureState.OldestID == "" { - lastItem := (*to)[len(*to)-1] - futureState.OldestID = lastItem.ID - oldestDate := time.Unix(lastItem.Created, 0) - futureState.OldestDate = &oldestDate - } - } - - futureState.NoMoreHistory = !hasMore - - for i, j := 0, len(*to)-1; i < j; i, j = i+1, j-1 { - (*to)[i], (*to)[j] = (*to)[j], (*to)[i] - } - - return hasMore, futureState, func() { - tl.state = futureState - }, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/timeline_trigger.go b/components/payments/cmd/connectors/internal/connectors/stripe/timeline_trigger.go deleted file mode 100644 index ab5e072d94..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/timeline_trigger.go +++ /dev/null @@ -1,184 +0,0 @@ -package stripe - -import ( - "context" - - "github.com/formancehq/go-libs/logging" - "github.com/pkg/errors" - "github.com/stripe/stripe-go/v72" - "golang.org/x/sync/semaphore" -) - -type TimelineTriggerType string - -const ( - TimelineTriggerTypeTransactions TimelineTriggerType = "transactions" - TimelineTriggerTypeAccounts TimelineTriggerType = "accounts" - TimelineTriggerTypeExternalAccounts TimelineTriggerType = "external_accounts" -) - -func NewTimelineTrigger( - logger logging.Logger, - ingester Ingester, - timeline *Timeline, - timelineType TimelineTriggerType, -) *TimelineTrigger { - return &TimelineTrigger{ - logger: logger.WithFields(map[string]interface{}{ - "component": "timeline-trigger", - }), - ingester: ingester, - timeline: timeline, - timelineType: timelineType, - sem: semaphore.NewWeighted(1), - } -} - -type TimelineTrigger struct { - logger logging.Logger - ingester Ingester - timeline *Timeline - timelineType TimelineTriggerType - sem *semaphore.Weighted - cancel func() -} - -func (t *TimelineTrigger) Fetch(ctx context.Context) error { - if t.sem.TryAcquire(1) { - defer t.sem.Release(1) - - ctx, t.cancel = context.WithCancel(ctx) - if !t.timeline.State().NoMoreHistory { - if err := t.fetch(ctx, true); err != nil { - return err - } - } - - select { - case <-ctx.Done(): - return ctx.Err() - default: - if err := t.fetch(ctx, false); err != nil { - return err - } - } - } - - return nil -} - -func (t *TimelineTrigger) Cancel(ctx context.Context) { - if t.cancel != nil { - t.cancel() - - err := t.sem.Acquire(ctx, 1) - if err != nil { - panic(err) - } - - t.sem.Release(1) - } -} - -func (t *TimelineTrigger) fetch(ctx context.Context, tail bool) error { - for { - select { - case <-ctx.Done(): - return ctx.Err() - default: - hasMore, err := t.triggerPage(ctx, tail) - if err != nil { - return errors.Wrap(err, "error triggering tail page") - } - - if !hasMore { - return nil - } - } - } -} - -func (t *TimelineTrigger) triggerPage(ctx context.Context, tail bool) (bool, error) { - logger := t.logger.WithFields(map[string]interface{}{ - "tail": tail, - }) - - logger.Debugf("Trigger page") - - var hasMore bool - switch t.timelineType { - case TimelineTriggerTypeTransactions: - ret := make([]*stripe.BalanceTransaction, 0) - method := t.timeline.TransactionsHead - if tail { - method = t.timeline.TransactionsTail - } - - more, futureState, commitFn, err := method(ctx, &ret) - if err != nil { - return false, errors.Wrap(err, "fetching timeline") - } - hasMore = more - - logger.Debug("Ingest transactions batch") - - if len(ret) > 0 { - err = t.ingester.IngestTransactions(ctx, ret, futureState, tail) - if err != nil { - return false, errors.Wrap(err, "ingesting batch") - } - } - - commitFn() - - case TimelineTriggerTypeAccounts: - ret := make([]*stripe.Account, 0) - method := t.timeline.AccountsHead - if tail { - method = t.timeline.AccountsTail - } - - more, futureState, commitFn, err := method(ctx, &ret) - if err != nil { - return false, errors.Wrap(err, "fetching timeline") - } - hasMore = more - - logger.Debug("Ingest accounts batch") - - if len(ret) > 0 { - err = t.ingester.IngestAccounts(ctx, ret, futureState, tail) - if err != nil { - return false, errors.Wrap(err, "ingesting batch") - } - } - - commitFn() - - case TimelineTriggerTypeExternalAccounts: - ret := make([]*stripe.ExternalAccount, 0) - method := t.timeline.ExternalAccountsHead - if tail { - method = t.timeline.ExternalAccountsTail - } - - more, futureState, commitFn, err := method(ctx, &ret) - if err != nil { - return false, errors.Wrap(err, "fetching timeline") - } - hasMore = more - - logger.Debug("Ingest transactions batch") - - if len(ret) > 0 { - err = t.ingester.IngestExternalAccounts(ctx, ret, futureState, tail) - if err != nil { - return false, errors.Wrap(err, "ingesting batch") - } - } - - commitFn() - } - - return hasMore, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/timeline_trigger_test.go b/components/payments/cmd/connectors/internal/connectors/stripe/timeline_trigger_test.go deleted file mode 100644 index 21fa54ac2d..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/timeline_trigger_test.go +++ /dev/null @@ -1,122 +0,0 @@ -package stripe - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/formancehq/go-libs/logging" - "github.com/stretchr/testify/require" - "github.com/stripe/stripe-go/v72" -) - -func TestTimelineTrigger(t *testing.T) { - t.Parallel() - - const txCount = 12 - - mock := NewClientMock(t, true) - ref := time.Now().Add(-time.Minute * time.Duration(txCount) / 2) - timeline := NewTimeline(mock, TimelineConfig{ - PageSize: 2, - }, TimelineState{}, WithStartingAt(ref)) - - ingestedTx := make([]*stripe.BalanceTransaction, 0) - trigger := NewTimelineTrigger( - logging.FromContext(context.TODO()), - NewIngester( - func(ctx context.Context, batch []*stripe.BalanceTransaction, commitState TimelineState, tail bool) error { - ingestedTx = append(ingestedTx, batch...) - return nil - }, - func(ctx context.Context, batch []*stripe.Account, commitState TimelineState, tail bool) error { - return nil - }, - func(ctx context.Context, batch []*stripe.ExternalAccount, commitState TimelineState, tail bool) error { - return nil - }, - ), - timeline, - TimelineTriggerTypeTransactions, - ) - - allTxs := make([]*stripe.BalanceTransaction, txCount) - for i := 0; i < txCount/2; i++ { - allTxs[txCount/2+i] = &stripe.BalanceTransaction{ - ID: fmt.Sprintf("%d", txCount/2+i), - Created: ref.Add(-time.Duration(i) * time.Minute).Unix(), - } - allTxs[txCount/2-i-1] = &stripe.BalanceTransaction{ - ID: fmt.Sprintf("%d", txCount/2-i-1), - Created: ref.Add(time.Duration(i) * time.Minute).Unix(), - } - } - - for i := 0; i < txCount/2; i += 2 { - mock.Expect().Limit(2).RespondsWith(i < txCount/2-2, allTxs[txCount/2+i], allTxs[txCount/2+i+1]) - } - - for i := 0; i < txCount/2; i += 2 { - mock.Expect().Limit(2).RespondsWith(i < txCount/2-2, allTxs[txCount/2-i-2], allTxs[txCount/2-i-1]) - } - - ctx, cancel := context.WithDeadline(context.TODO(), time.Now().Add(time.Second)) - defer cancel() - - require.NoError(t, trigger.Fetch(ctx)) - require.Len(t, ingestedTx, txCount) -} - -func TestCancelTimelineTrigger(t *testing.T) { - t.Parallel() - - const txCount = 12 - - mock := NewClientMock(t, false) - ref := time.Now().Add(-time.Minute * time.Duration(txCount) / 2) - timeline := NewTimeline(mock, TimelineConfig{ - PageSize: 1, - }, TimelineState{}, WithStartingAt(ref)) - - waiting := make(chan struct{}) - trigger := NewTimelineTrigger( - logging.FromContext(context.TODO()), - NewIngester( - func(ctx context.Context, batch []*stripe.BalanceTransaction, commitState TimelineState, tail bool) error { - close(waiting) // Instruct the test the trigger is in fetching state - <-ctx.Done() - - return nil - }, - func(ctx context.Context, batch []*stripe.Account, commitState TimelineState, tail bool) error { - return nil - }, - func(ctx context.Context, batch []*stripe.ExternalAccount, commitState TimelineState, tail bool) error { - return nil - }, - ), - timeline, - TimelineTriggerTypeTransactions, - ) - - allTxs := make([]*stripe.BalanceTransaction, txCount) - for i := 0; i < txCount; i++ { - allTxs[i] = &stripe.BalanceTransaction{ - ID: fmt.Sprintf("%d", i), - } - mock.Expect().Limit(1).RespondsWith(i < txCount-1, allTxs[i]) - } - - go func() { - // TODO: Handle error - _ = trigger.Fetch(context.TODO()) - }() - select { - case <-time.After(time.Second): - t.Fatalf("timeout") - case <-waiting: - trigger.Cancel(context.TODO()) - require.NotEmpty(t, mock.expectations) - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/translate.go b/components/payments/cmd/connectors/internal/connectors/stripe/translate.go deleted file mode 100644 index 057035bab8..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/translate.go +++ /dev/null @@ -1,901 +0,0 @@ -package stripe - -import ( - "encoding/json" - "log" - "math/big" - "runtime/debug" - "strings" - "time" - - "github.com/davecgh/go-spew/spew" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/internal/models" - "github.com/stripe/stripe-go/v72" -) - -func createBatchElement( - connectorID models.ConnectorID, - balanceTransaction *stripe.BalanceTransaction, - account string, - forward bool, -) (ingestion.PaymentBatchElement, bool) { - var payment *models.Payment - var adjustment *models.PaymentAdjustment - - defer func() { - // DEBUG - if e := recover(); e != nil { - log.Println("Error translating transaction") - debug.PrintStack() - spew.Dump(balanceTransaction) - panic(e) - } - }() - - if balanceTransaction.Source == nil { - return ingestion.PaymentBatchElement{}, false - } - - rawData, err := json.Marshal(balanceTransaction) - if err != nil { - return ingestion.PaymentBatchElement{}, false - } - - switch balanceTransaction.Type { - case stripe.BalanceTransactionTypeCharge: - transactionCurrency := strings.ToUpper(string(balanceTransaction.Source.Charge.Currency)) - _, ok := supportedCurrenciesWithDecimal[transactionCurrency] - if !ok { - return ingestion.PaymentBatchElement{}, false - } - - paymentID := models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: balanceTransaction.ID, - Type: models.PaymentTypePayIn, - }, - ConnectorID: connectorID, - } - createdAt := time.Unix(balanceTransaction.Created, 0) - - var metadata []*models.PaymentMetadata - if balanceTransaction.Source.Charge.PaymentIntent != nil { - metadata = computeMetadata(paymentID, createdAt, balanceTransaction.Source.Charge.Metadata, balanceTransaction.Source.Charge.PaymentIntent.Metadata) - } else { - metadata = computeMetadata(paymentID, createdAt, balanceTransaction.Source.Charge.Metadata) - } - - payment = &models.Payment{ - ID: paymentID, - Reference: balanceTransaction.ID, - ConnectorID: connectorID, - Type: models.PaymentTypePayIn, - Status: models.PaymentStatusSucceeded, - Amount: big.NewInt(balanceTransaction.Source.Charge.Amount - balanceTransaction.Source.Charge.AmountRefunded), - InitialAmount: big.NewInt(balanceTransaction.Source.Charge.Amount), - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transactionCurrency), - RawData: rawData, - Scheme: models.PaymentScheme(balanceTransaction.Source.Charge.PaymentMethodDetails.Card.Brand), - CreatedAt: createdAt, - Metadata: metadata, - } - - if account != "" { - payment.DestinationAccountID = &models.AccountID{ - Reference: account, - ConnectorID: connectorID, - } - } - - case stripe.BalanceTransactionTypeRefund: - // Refund a charge - transactionCurrency := strings.ToUpper(string(balanceTransaction.Source.Refund.Charge.Currency)) - _, ok := supportedCurrenciesWithDecimal[transactionCurrency] - if !ok { - return ingestion.PaymentBatchElement{}, false - } - - paymentID := models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: balanceTransaction.Source.Refund.Charge.BalanceTransaction.ID, - Type: models.PaymentTypePayIn, - }, - ConnectorID: connectorID, - } - createdAt := time.Unix(balanceTransaction.Source.Refund.Charge.Created, 0) - // Created when a credit card charge refund is initiated. - // If you authorize and capture separately and the capture amount is - // less than the initial authorization, you see a balance transaction - // of type charge for the full authorization amount and another balance - // transaction of type refund for the uncaptured portion. - // cf https://stripe.com/docs/reports/balance-transaction-types - - var metadata []*models.PaymentMetadata - if balanceTransaction.Source.Refund.PaymentIntent != nil { - metadata = computeMetadata(paymentID, createdAt, balanceTransaction.Source.Refund.Metadata, balanceTransaction.Source.Refund.PaymentIntent.Metadata) - } else { - metadata = computeMetadata(paymentID, createdAt, balanceTransaction.Source.Refund.Metadata) - } - - payment = &models.Payment{ - ID: paymentID, - Reference: balanceTransaction.Source.Refund.Charge.BalanceTransaction.ID, - ConnectorID: connectorID, - Type: models.PaymentTypePayIn, - Status: models.PaymentStatusSucceeded, - Amount: big.NewInt(balanceTransaction.Source.Refund.Charge.Amount - balanceTransaction.Source.Refund.Charge.AmountRefunded), - InitialAmount: big.NewInt(balanceTransaction.Source.Refund.Charge.Amount), - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transactionCurrency), - RawData: rawData, - Scheme: models.PaymentScheme(balanceTransaction.Source.Refund.Charge.PaymentMethodDetails.Card.Brand), - CreatedAt: createdAt, - Metadata: metadata, - } - - if account != "" { - payment.DestinationAccountID = &models.AccountID{ - Reference: account, - ConnectorID: connectorID, - } - } - - adjustment = &models.PaymentAdjustment{ - PaymentID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: balanceTransaction.Source.Refund.Charge.BalanceTransaction.ID, - Type: models.PaymentTypePayIn, - }, - ConnectorID: connectorID, - }, - CreatedAt: time.Unix(balanceTransaction.Created, 0), - Reference: balanceTransaction.ID, - Amount: big.NewInt(balanceTransaction.Source.Refund.Amount), - Status: models.PaymentStatusRefunded, - RawData: rawData, - } - - case stripe.BalanceTransactionTypeRefundFailure: - // Refund a charge - transactionCurrency := strings.ToUpper(string(balanceTransaction.Source.Refund.Charge.Currency)) - _, ok := supportedCurrenciesWithDecimal[transactionCurrency] - if !ok { - return ingestion.PaymentBatchElement{}, false - } - - paymentID := models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: balanceTransaction.Source.Refund.Charge.BalanceTransaction.ID, - Type: models.PaymentTypePayIn, - }, - ConnectorID: connectorID, - } - createdAt := time.Unix(balanceTransaction.Source.Refund.Charge.Created, 0) - // Created when a credit card charge refund is initiated. - // If you authorize and capture separately and the capture amount is - // less than the initial authorization, you see a balance transaction - // of type charge for the full authorization amount and another balance - // transaction of type refund for the uncaptured portion. - // cf https://stripe.com/docs/reports/balance-transaction-types - - var metadata []*models.PaymentMetadata - if balanceTransaction.Source.Refund.PaymentIntent != nil { - metadata = computeMetadata(paymentID, createdAt, balanceTransaction.Source.Refund.Metadata, balanceTransaction.Source.Refund.PaymentIntent.Metadata) - } else { - metadata = computeMetadata(paymentID, createdAt, balanceTransaction.Source.Refund.Metadata) - } - - payment = &models.Payment{ - ID: paymentID, - Reference: balanceTransaction.Source.Refund.Charge.BalanceTransaction.ID, - ConnectorID: connectorID, - Type: models.PaymentTypePayIn, - Status: models.PaymentStatusSucceeded, - Amount: big.NewInt(balanceTransaction.Source.Refund.Charge.Amount - balanceTransaction.Source.Refund.Charge.AmountRefunded), - InitialAmount: big.NewInt(balanceTransaction.Source.Refund.Charge.Amount), - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transactionCurrency), - RawData: rawData, - Scheme: models.PaymentScheme(balanceTransaction.Source.Refund.Charge.PaymentMethodDetails.Card.Brand), - CreatedAt: createdAt, - Metadata: metadata, - } - - if account != "" { - payment.DestinationAccountID = &models.AccountID{ - Reference: account, - ConnectorID: connectorID, - } - } - - adjustment = &models.PaymentAdjustment{ - PaymentID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: balanceTransaction.Source.Refund.Charge.BalanceTransaction.ID, - Type: models.PaymentTypePayIn, - }, - ConnectorID: connectorID, - }, - CreatedAt: time.Unix(balanceTransaction.Created, 0), - Reference: balanceTransaction.ID, - Amount: big.NewInt(balanceTransaction.Source.Refund.Amount), - Status: models.PaymentStatusRefundedFailure, - RawData: rawData, - } - - case stripe.BalanceTransactionTypePayment: - transactionCurrency := strings.ToUpper(string(balanceTransaction.Source.Charge.Currency)) - _, ok := supportedCurrenciesWithDecimal[transactionCurrency] - if !ok { - return ingestion.PaymentBatchElement{}, false - } - - paymentID := models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: balanceTransaction.ID, - Type: models.PaymentTypePayIn, - }, - ConnectorID: connectorID, - } - createdAt := time.Unix(balanceTransaction.Created, 0) - - var metadata []*models.PaymentMetadata - if balanceTransaction.Source.Charge.PaymentIntent != nil { - metadata = computeMetadata(paymentID, createdAt, balanceTransaction.Source.Charge.Metadata, balanceTransaction.Source.Charge.PaymentIntent.Metadata) - } else { - metadata = computeMetadata(paymentID, createdAt, balanceTransaction.Source.Charge.Metadata) - } - - payment = &models.Payment{ - ID: paymentID, - Reference: balanceTransaction.ID, - ConnectorID: connectorID, - Type: models.PaymentTypePayIn, - Status: models.PaymentStatusSucceeded, - Amount: big.NewInt(balanceTransaction.Source.Charge.Amount - balanceTransaction.Source.Charge.AmountRefunded), - InitialAmount: big.NewInt(balanceTransaction.Source.Charge.Amount), - RawData: rawData, - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transactionCurrency), - Scheme: models.PaymentSchemeOther, - CreatedAt: createdAt, - Metadata: metadata, - } - - if account != "" { - payment.DestinationAccountID = &models.AccountID{ - Reference: account, - ConnectorID: connectorID, - } - } - - case stripe.BalanceTransactionTypePaymentRefund: - // Refund a charge - transactionCurrency := strings.ToUpper(string(balanceTransaction.Source.Refund.Charge.Currency)) - _, ok := supportedCurrenciesWithDecimal[transactionCurrency] - if !ok { - return ingestion.PaymentBatchElement{}, false - } - - paymentID := models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: balanceTransaction.Source.Refund.Charge.BalanceTransaction.ID, - Type: models.PaymentTypePayIn, - }, - ConnectorID: connectorID, - } - createdAt := time.Unix(balanceTransaction.Source.Refund.Charge.Created, 0) - // Created when a credit card charge refund is initiated. - // If you authorize and capture separately and the capture amount is - // less than the initial authorization, you see a balance transaction - // of type charge for the full authorization amount and another balance - // transaction of type refund for the uncaptured portion. - // cf https://stripe.com/docs/reports/balance-transaction-types - - var metadata []*models.PaymentMetadata - if balanceTransaction.Source.Refund.Charge.PaymentIntent != nil { - metadata = computeMetadata(paymentID, createdAt, balanceTransaction.Source.Refund.Charge.Metadata, balanceTransaction.Source.Refund.Charge.PaymentIntent.Metadata) - } else { - metadata = computeMetadata(paymentID, createdAt, balanceTransaction.Source.Refund.Charge.Metadata) - } - - payment = &models.Payment{ - ID: paymentID, - Reference: balanceTransaction.Source.Refund.Charge.BalanceTransaction.ID, - ConnectorID: connectorID, - Type: models.PaymentTypePayIn, - Status: models.PaymentStatusSucceeded, - Amount: big.NewInt(balanceTransaction.Source.Refund.Charge.Amount - balanceTransaction.Source.Refund.Charge.AmountRefunded), - InitialAmount: big.NewInt(balanceTransaction.Source.Refund.Charge.Amount), - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transactionCurrency), - Scheme: models.PaymentSchemeOther, - RawData: rawData, - CreatedAt: createdAt, - Metadata: metadata, - } - - if account != "" { - payment.DestinationAccountID = &models.AccountID{ - Reference: account, - ConnectorID: connectorID, - } - } - - adjustment = &models.PaymentAdjustment{ - PaymentID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: balanceTransaction.Source.Refund.Charge.BalanceTransaction.ID, - Type: models.PaymentTypePayIn, - }, - ConnectorID: connectorID, - }, - CreatedAt: time.Unix(balanceTransaction.Created, 0), - Reference: balanceTransaction.ID, - Amount: big.NewInt(balanceTransaction.Source.Refund.Amount), - Status: models.PaymentStatusRefunded, - RawData: rawData, - } - - case stripe.BalanceTransactionTypePaymentFailureRefund: - // Refund a charge - transactionCurrency := strings.ToUpper(string(balanceTransaction.Source.Refund.Charge.Currency)) - _, ok := supportedCurrenciesWithDecimal[transactionCurrency] - if !ok { - return ingestion.PaymentBatchElement{}, false - } - - paymentID := models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: balanceTransaction.Source.Refund.Charge.BalanceTransaction.ID, - Type: models.PaymentTypePayIn, - }, - ConnectorID: connectorID, - } - createdAt := time.Unix(balanceTransaction.Source.Refund.Charge.Created, 0) - // Created when a credit card charge refund is initiated. - // If you authorize and capture separately and the capture amount is - // less than the initial authorization, you see a balance transaction - // of type charge for the full authorization amount and another balance - // transaction of type refund for the uncaptured portion. - // cf https://stripe.com/docs/reports/balance-transaction-types - - var metadata []*models.PaymentMetadata - if balanceTransaction.Source.Refund.Charge.PaymentIntent != nil { - metadata = computeMetadata(paymentID, createdAt, balanceTransaction.Source.Refund.Charge.Metadata, balanceTransaction.Source.Refund.Charge.PaymentIntent.Metadata) - } else { - metadata = computeMetadata(paymentID, createdAt, balanceTransaction.Source.Refund.Charge.Metadata) - } - - payment = &models.Payment{ - ID: paymentID, - Reference: balanceTransaction.Source.Refund.Charge.BalanceTransaction.ID, - ConnectorID: connectorID, - Type: models.PaymentTypePayIn, - Status: models.PaymentStatusSucceeded, - Amount: big.NewInt(balanceTransaction.Source.Refund.Charge.Amount - balanceTransaction.Source.Refund.Charge.AmountRefunded), - InitialAmount: big.NewInt(balanceTransaction.Source.Refund.Charge.Amount), - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transactionCurrency), - Scheme: models.PaymentSchemeOther, - RawData: rawData, - CreatedAt: createdAt, - Metadata: metadata, - } - - if account != "" { - payment.DestinationAccountID = &models.AccountID{ - Reference: account, - ConnectorID: connectorID, - } - } - - adjustment = &models.PaymentAdjustment{ - PaymentID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: balanceTransaction.Source.Refund.Charge.BalanceTransaction.ID, - Type: models.PaymentTypePayIn, - }, - ConnectorID: connectorID, - }, - CreatedAt: time.Unix(balanceTransaction.Created, 0), - Reference: balanceTransaction.ID, - Amount: big.NewInt(balanceTransaction.Source.Refund.Amount), - Status: models.PaymentStatusRefundedFailure, - RawData: rawData, - } - - case stripe.BalanceTransactionTypePayout: - transactionCurrency := strings.ToUpper(string(balanceTransaction.Source.Payout.Currency)) - _, ok := supportedCurrenciesWithDecimal[transactionCurrency] - if !ok { - return ingestion.PaymentBatchElement{}, false - } - - paymentID := models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: balanceTransaction.ID, - Type: models.PaymentTypePayOut, - }, - ConnectorID: connectorID, - } - createdAt := time.Unix(balanceTransaction.Created, 0) - payment = &models.Payment{ - ID: paymentID, - Reference: balanceTransaction.ID, - ConnectorID: connectorID, - Type: models.PaymentTypePayOut, - Status: convertPayoutStatus(balanceTransaction.Source.Payout.Status), - Amount: big.NewInt(balanceTransaction.Source.Payout.Amount), - InitialAmount: big.NewInt(balanceTransaction.Source.Payout.Amount), - RawData: rawData, - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, string(balanceTransaction.Source.Payout.Currency)), - Scheme: func() models.PaymentScheme { - switch balanceTransaction.Source.Payout.Type { - case stripe.PayoutTypeBank: - return models.PaymentSchemeSepaCredit - case stripe.PayoutTypeCard: - return models.PaymentScheme(balanceTransaction.Source.Payout.Card.Brand) - } - - return models.PaymentSchemeUnknown - }(), - CreatedAt: createdAt, - Metadata: computeMetadata(paymentID, createdAt, balanceTransaction.Source.Payout.Metadata), - } - - if account != "" { - payment.SourceAccountID = &models.AccountID{ - Reference: account, - ConnectorID: connectorID, - } - } - - case stripe.BalanceTransactionTypePayoutFailure: - paymentID := models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: balanceTransaction.Source.Payout.BalanceTransaction.ID, - Type: models.PaymentTypePayOut, - }, - ConnectorID: connectorID, - } - createdAt := time.Unix(balanceTransaction.Source.Payout.Created, 0) - payment = &models.Payment{ - ID: paymentID, - Reference: balanceTransaction.Source.Payout.BalanceTransaction.ID, - ConnectorID: connectorID, - Type: models.PaymentTypePayOut, - Status: models.PaymentStatusFailed, - Amount: big.NewInt(balanceTransaction.Source.Payout.Amount), - InitialAmount: big.NewInt(balanceTransaction.Source.Payout.Amount), - RawData: rawData, - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, string(balanceTransaction.Source.Payout.Currency)), - Scheme: func() models.PaymentScheme { - switch balanceTransaction.Source.Payout.Type { - case stripe.PayoutTypeBank: - return models.PaymentSchemeSepaCredit - case stripe.PayoutTypeCard: - return models.PaymentScheme(balanceTransaction.Source.Payout.Card.Brand) - } - - return models.PaymentSchemeUnknown - }(), - CreatedAt: createdAt, - Metadata: computeMetadata(paymentID, createdAt, balanceTransaction.Source.Payout.Metadata), - } - - if account != "" { - payment.SourceAccountID = &models.AccountID{ - Reference: account, - ConnectorID: connectorID, - } - } - - adjustment = &models.PaymentAdjustment{ - PaymentID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: balanceTransaction.Source.Payout.BalanceTransaction.ID, - Type: models.PaymentTypePayOut, - }, - ConnectorID: connectorID, - }, - CreatedAt: time.Unix(balanceTransaction.Created, 0), - Reference: balanceTransaction.ID, - Status: models.PaymentStatusFailed, - RawData: rawData, - } - - case stripe.BalanceTransactionTypePayoutCancel: - paymentID := models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: balanceTransaction.Source.Payout.BalanceTransaction.ID, - Type: models.PaymentTypePayOut, - }, - ConnectorID: connectorID, - } - createdAt := time.Unix(balanceTransaction.Source.Payout.Created, 0) - payment = &models.Payment{ - ID: paymentID, - Reference: balanceTransaction.Source.Payout.BalanceTransaction.ID, - ConnectorID: connectorID, - Type: models.PaymentTypePayOut, - Status: models.PaymentStatusCancelled, - Amount: big.NewInt(balanceTransaction.Source.Payout.Amount), - InitialAmount: big.NewInt(balanceTransaction.Source.Payout.Amount), - RawData: rawData, - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, string(balanceTransaction.Source.Payout.Currency)), - Scheme: func() models.PaymentScheme { - switch balanceTransaction.Source.Payout.Type { - case stripe.PayoutTypeBank: - return models.PaymentSchemeSepaCredit - case stripe.PayoutTypeCard: - return models.PaymentScheme(balanceTransaction.Source.Payout.Card.Brand) - } - - return models.PaymentSchemeUnknown - }(), - CreatedAt: createdAt, - Metadata: computeMetadata(paymentID, createdAt, balanceTransaction.Source.Payout.Metadata), - } - - if account != "" { - payment.SourceAccountID = &models.AccountID{ - Reference: account, - ConnectorID: connectorID, - } - } - - adjustment = &models.PaymentAdjustment{ - PaymentID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: balanceTransaction.Source.Payout.BalanceTransaction.ID, - Type: models.PaymentTypePayOut, - }, - ConnectorID: connectorID, - }, - CreatedAt: time.Unix(balanceTransaction.Created, 0), - Reference: balanceTransaction.ID, - Status: models.PaymentStatusCancelled, - RawData: rawData, - } - - case stripe.BalanceTransactionTypeTransfer: - transactionCurrency := strings.ToUpper(string(balanceTransaction.Source.Transfer.Currency)) - _, ok := supportedCurrenciesWithDecimal[transactionCurrency] - if !ok { - return ingestion.PaymentBatchElement{}, false - } - - paymentID := models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: balanceTransaction.ID, - Type: models.PaymentTypeTransfer, - }, - ConnectorID: connectorID, - } - createdAt := time.Unix(balanceTransaction.Created, 0) - payment = &models.Payment{ - ID: paymentID, - Reference: balanceTransaction.ID, - ConnectorID: connectorID, - Type: models.PaymentTypeTransfer, - Status: models.PaymentStatusSucceeded, - Amount: big.NewInt(balanceTransaction.Source.Transfer.Amount - balanceTransaction.Source.Transfer.AmountReversed), - InitialAmount: big.NewInt(balanceTransaction.Source.Transfer.Amount), - RawData: rawData, - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, string(balanceTransaction.Source.Transfer.Currency)), - Scheme: models.PaymentSchemeOther, - CreatedAt: createdAt, - Metadata: computeMetadata(paymentID, createdAt, balanceTransaction.Source.Transfer.Metadata), - } - - if balanceTransaction.Source.Transfer.Destination != nil { - payment.DestinationAccountID = &models.AccountID{ - Reference: balanceTransaction.Source.Transfer.Destination.ID, - ConnectorID: connectorID, - } - } - - if account != "" { - payment.SourceAccountID = &models.AccountID{ - Reference: account, - ConnectorID: connectorID, - } - } - - case stripe.BalanceTransactionTypeTransferRefund: - paymentID := models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: balanceTransaction.Source.Transfer.BalanceTransaction.ID, - Type: models.PaymentTypeTransfer, - }, - ConnectorID: connectorID, - } - createdAt := time.Unix(balanceTransaction.Source.Transfer.Created, 0) - // Two things to insert here: the balance transaction at the origin - // of the refund and the balance transaction of the refund, which is an - // adjustment of the origin. - payment = &models.Payment{ - ID: paymentID, - Reference: balanceTransaction.Source.Transfer.BalanceTransaction.ID, - ConnectorID: connectorID, - Type: models.PaymentTypeTransfer, - Status: models.PaymentStatusSucceeded, - Amount: big.NewInt(balanceTransaction.Source.Transfer.Amount - balanceTransaction.Source.Transfer.AmountReversed), - InitialAmount: big.NewInt(balanceTransaction.Source.Transfer.Amount), - RawData: rawData, - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, string(balanceTransaction.Source.Transfer.Currency)), - Scheme: models.PaymentSchemeOther, - CreatedAt: createdAt, - Metadata: computeMetadata(paymentID, createdAt, balanceTransaction.Source.Transfer.Metadata), - } - - if balanceTransaction.Source.Transfer.Destination != nil { - payment.DestinationAccountID = &models.AccountID{ - Reference: balanceTransaction.Source.Transfer.Destination.ID, - ConnectorID: connectorID, - } - } - - if account != "" { - payment.SourceAccountID = &models.AccountID{ - Reference: account, - ConnectorID: connectorID, - } - } - - adjustment = &models.PaymentAdjustment{ - PaymentID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: balanceTransaction.Source.Transfer.BalanceTransaction.ID, - Type: models.PaymentTypeTransfer, - }, - ConnectorID: connectorID, - }, - CreatedAt: time.Unix(balanceTransaction.Created, 0), - Reference: balanceTransaction.ID, - Amount: big.NewInt(balanceTransaction.Amount), - Status: models.PaymentStatusRefunded, - RawData: rawData, - } - - case stripe.BalanceTransactionTypeTransferCancel: - paymentID := models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: balanceTransaction.Source.Transfer.BalanceTransaction.ID, - Type: models.PaymentTypeTransfer, - }, - ConnectorID: connectorID, - } - createdAt := time.Unix(balanceTransaction.Source.Transfer.Created, 0) - - // Two things to insert here: the balance transaction at the origin - // of the refund and the balance transaction of the refund, which is an - // adjustment of the origin. - payment = &models.Payment{ - ID: paymentID, - Reference: balanceTransaction.Source.Transfer.BalanceTransaction.ID, - ConnectorID: connectorID, - Type: models.PaymentTypeTransfer, - Status: models.PaymentStatusCancelled, - Amount: big.NewInt(balanceTransaction.Source.Transfer.Amount - balanceTransaction.Source.Transfer.AmountReversed), - InitialAmount: big.NewInt(balanceTransaction.Source.Transfer.Amount), - RawData: rawData, - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, string(balanceTransaction.Source.Transfer.Currency)), - Scheme: models.PaymentSchemeOther, - CreatedAt: createdAt, - Metadata: computeMetadata(paymentID, createdAt, balanceTransaction.Source.Transfer.Metadata), - } - - if balanceTransaction.Source.Transfer.Destination != nil { - payment.DestinationAccountID = &models.AccountID{ - Reference: balanceTransaction.Source.Transfer.Destination.ID, - ConnectorID: connectorID, - } - } - - if account != "" { - payment.SourceAccountID = &models.AccountID{ - Reference: account, - ConnectorID: connectorID, - } - } - - adjustment = &models.PaymentAdjustment{ - PaymentID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: balanceTransaction.Source.Transfer.BalanceTransaction.ID, - Type: models.PaymentTypeTransfer, - }, - ConnectorID: connectorID, - }, - CreatedAt: time.Unix(balanceTransaction.Created, 0), - Reference: balanceTransaction.ID, - Amount: big.NewInt(balanceTransaction.Amount), - Status: models.PaymentStatusCancelled, - RawData: rawData, - } - - case stripe.BalanceTransactionTypeTransferFailure: - paymentID := models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: balanceTransaction.Source.Transfer.BalanceTransaction.ID, - Type: models.PaymentTypeTransfer, - }, - ConnectorID: connectorID, - } - createdAt := time.Unix(balanceTransaction.Source.Transfer.Created, 0) - // Two things to insert here: the balance transaction at the origin - // of the refund and the balance transaction of the refund, which is an - // adjustment of the origin. - payment = &models.Payment{ - ID: paymentID, - Reference: balanceTransaction.Source.Transfer.BalanceTransaction.ID, - ConnectorID: connectorID, - Type: models.PaymentTypeTransfer, - Status: models.PaymentStatusFailed, - Amount: big.NewInt(balanceTransaction.Source.Transfer.Amount - balanceTransaction.Source.Transfer.AmountReversed), - InitialAmount: big.NewInt(balanceTransaction.Source.Transfer.Amount), - RawData: rawData, - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, string(balanceTransaction.Source.Transfer.Currency)), - Scheme: models.PaymentSchemeOther, - CreatedAt: createdAt, - Metadata: computeMetadata(paymentID, createdAt, balanceTransaction.Source.Transfer.Metadata), - } - - if balanceTransaction.Source.Transfer.Destination != nil { - payment.DestinationAccountID = &models.AccountID{ - Reference: balanceTransaction.Source.Transfer.Destination.ID, - ConnectorID: connectorID, - } - } - - if account != "" { - payment.SourceAccountID = &models.AccountID{ - Reference: account, - ConnectorID: connectorID, - } - } - - adjustment = &models.PaymentAdjustment{ - PaymentID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: balanceTransaction.Source.Transfer.BalanceTransaction.ID, - Type: models.PaymentTypeTransfer, - }, - ConnectorID: connectorID, - }, - CreatedAt: time.Unix(balanceTransaction.Created, 0), - Reference: balanceTransaction.ID, - Amount: big.NewInt(balanceTransaction.Amount), - Status: models.PaymentStatusFailed, - RawData: rawData, - } - - case stripe.BalanceTransactionTypeAdjustment: - if balanceTransaction.Source.Dispute == nil { - // We are only handle dispute adjustments - return ingestion.PaymentBatchElement{}, false - } - - transactionCurrency := strings.ToUpper(string(balanceTransaction.Source.Dispute.Charge.Currency)) - _, ok := supportedCurrenciesWithDecimal[transactionCurrency] - if !ok { - return ingestion.PaymentBatchElement{}, false - } - - disputeStatus := convertDisputeStatus(balanceTransaction.Source.Dispute.Status) - paymentStatus := models.PaymentStatusPending - switch disputeStatus { - case models.PaymentStatusDisputeWon: - paymentStatus = models.PaymentStatusSucceeded - case models.PaymentStatusDisputeLost: - paymentStatus = models.PaymentStatusFailed - default: - paymentStatus = models.PaymentStatusPending - } - - paymentID := models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: balanceTransaction.Source.Dispute.Charge.BalanceTransaction.ID, - Type: models.PaymentTypePayIn, - }, - ConnectorID: connectorID, - } - createdAt := time.Unix(balanceTransaction.Source.Dispute.Charge.Created, 0) - - var metadata []*models.PaymentMetadata - if balanceTransaction.Source.Dispute.Charge.PaymentIntent != nil { - metadata = computeMetadata(paymentID, createdAt, balanceTransaction.Source.Dispute.Charge.Metadata, balanceTransaction.Source.Dispute.Charge.PaymentIntent.Metadata) - } else { - metadata = computeMetadata(paymentID, createdAt, balanceTransaction.Source.Dispute.Charge.Metadata) - } - - payment = &models.Payment{ - ID: paymentID, - Reference: balanceTransaction.Source.Dispute.Charge.BalanceTransaction.ID, - ConnectorID: connectorID, - Type: models.PaymentTypePayIn, - Status: paymentStatus, // Dispute is occuring, we don't know the outcome yet - Amount: big.NewInt(balanceTransaction.Source.Dispute.Charge.Amount - balanceTransaction.Source.Dispute.Charge.AmountRefunded), - InitialAmount: big.NewInt(balanceTransaction.Source.Dispute.Charge.Amount), - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transactionCurrency), - RawData: rawData, - Scheme: models.PaymentScheme(balanceTransaction.Source.Dispute.Charge.PaymentMethodDetails.Card.Brand), - CreatedAt: createdAt, - Metadata: metadata, - } - - if account != "" { - payment.DestinationAccountID = &models.AccountID{ - Reference: account, - ConnectorID: connectorID, - } - } - - adjustment = &models.PaymentAdjustment{ - PaymentID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: balanceTransaction.Source.Dispute.Charge.BalanceTransaction.ID, - Type: models.PaymentTypePayIn, - }, - ConnectorID: connectorID, - }, - CreatedAt: time.Unix(balanceTransaction.Created, 0), - Reference: balanceTransaction.ID, - Status: disputeStatus, - RawData: rawData, - } - - case stripe.BalanceTransactionTypeStripeFee: - return ingestion.PaymentBatchElement{}, false - default: - return ingestion.PaymentBatchElement{}, false - } - - return ingestion.PaymentBatchElement{ - Payment: payment, - Adjustment: adjustment, - }, true -} - -func convertDisputeStatus(status stripe.DisputeStatus) models.PaymentStatus { - switch status { - case stripe.DisputeStatusNeedsResponse, stripe.DisputeStatusUnderReview: - return models.PaymentStatusDispute - case stripe.DisputeStatusLost: - return models.PaymentStatusDisputeLost - case stripe.DisputeStatusWon: - return models.PaymentStatusDisputeWon - default: - return models.PaymentStatusDispute - } -} - -func convertPayoutStatus(status stripe.PayoutStatus) models.PaymentStatus { - switch status { - case stripe.PayoutStatusCanceled: - return models.PaymentStatusCancelled - case stripe.PayoutStatusFailed: - return models.PaymentStatusFailed - case stripe.PayoutStatusInTransit, stripe.PayoutStatusPending: - return models.PaymentStatusPending - case stripe.PayoutStatusPaid: - return models.PaymentStatusSucceeded - } - - return models.PaymentStatusOther -} - -func computeMetadata(paymentID models.PaymentID, createdAt time.Time, metadatas ...map[string]string) []*models.PaymentMetadata { - res := make([]*models.PaymentMetadata, 0) - for _, metadata := range metadatas { - for k, v := range metadata { - res = append(res, &models.PaymentMetadata{ - PaymentID: paymentID, - CreatedAt: createdAt, - Key: k, - Value: v, - }) - } - } - - return res -} diff --git a/components/payments/cmd/connectors/internal/connectors/stripe/utils_test.go b/components/payments/cmd/connectors/internal/connectors/stripe/utils_test.go deleted file mode 100644 index 117bf33c68..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/stripe/utils_test.go +++ /dev/null @@ -1,237 +0,0 @@ -package stripe - -import ( - "context" - "flag" - "fmt" - "net/http" - "net/http/httptest" - "net/url" - "os" - "sync" - "testing" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors/stripe/client" - "github.com/stripe/stripe-go/v72" -) - -func TestMain(m *testing.M) { - flag.Parse() - - os.Exit(m.Run()) -} - -type ClientMockExpectation struct { - query url.Values - hasMore bool - items []*stripe.BalanceTransaction -} - -func (e *ClientMockExpectation) QueryParam(key string, value any) *ClientMockExpectation { - var qpvalue string - switch value.(type) { - case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: - qpvalue = fmt.Sprintf("%d", value) - default: - qpvalue = fmt.Sprintf("%s", value) - } - e.query.Set(key, qpvalue) - - return e -} - -func (e *ClientMockExpectation) StartingAfter(v string) *ClientMockExpectation { - e.QueryParam("starting_after", v) - - return e -} - -func (e *ClientMockExpectation) CreatedLte(v time.Time) *ClientMockExpectation { - e.QueryParam("created[lte]", v.Unix()) - - return e -} - -func (e *ClientMockExpectation) Limit(v int) *ClientMockExpectation { - e.QueryParam("limit", v) - - return e -} - -func (e *ClientMockExpectation) RespondsWith(hasMore bool, - txs ...*stripe.BalanceTransaction, -) *ClientMockExpectation { - e.hasMore = hasMore - e.items = txs - - return e -} - -func (e *ClientMockExpectation) handle(options ...client.ClientOption) ([]*stripe.BalanceTransaction, bool, error) { - req := httptest.NewRequest(http.MethodGet, "/", nil) - - for _, option := range options { - option.Apply(req) - } - - for key := range e.query { - if req.URL.Query().Get(key) != e.query.Get(key) { - return nil, false, fmt.Errorf("mismatch query params, expected query param '%s' "+ - "with value '%s', got '%s'", key, e.query.Get(key), req.URL.Query().Get(key)) - } - } - - return e.items, e.hasMore, nil -} - -type ClientMock struct { - expectations *FIFO[*ClientMockExpectation] -} - -func (m *ClientMock) ForAccount(account string) client.Client { - return m -} - -func (m *ClientMock) Accounts(ctx context.Context, - options ...client.ClientOption, -) ([]*stripe.Account, bool, error) { - return nil, false, nil -} - -func (m *ClientMock) Balance(ctx context.Context, - options ...client.ClientOption, -) (*stripe.Balance, error) { - return nil, nil -} - -func (m *ClientMock) ExternalAccounts(ctx context.Context, - options ...client.ClientOption, -) ([]*stripe.ExternalAccount, bool, error) { - return nil, false, nil -} - -func (m *ClientMock) CreateTransfer(ctx context.Context, - createTransferRequest *client.CreateTransferRequest, - options ...client.ClientOption, -) (*stripe.Transfer, error) { - return nil, nil -} - -func (m *ClientMock) ReverseTransfer(ctx context.Context, - createTransferReversalRequest *client.CreateTransferReversalRequest, - options ...client.ClientOption, -) (*stripe.Reversal, error) { - return nil, nil -} - -func (m *ClientMock) CreatePayout(ctx context.Context, - createPayoutRequest *client.CreatePayoutRequest, - options ...client.ClientOption, -) (*stripe.Payout, error) { - return nil, nil -} - -func (m *ClientMock) GetPayout(ctx context.Context, - payoutID string, - options ...client.ClientOption, -) (*stripe.Payout, error) { - return nil, nil -} - -func (m *ClientMock) BalanceTransactions(ctx context.Context, - options ...client.ClientOption, -) ([]*stripe.BalanceTransaction, bool, error) { - e, ok := m.expectations.Pop() - if !ok { - return nil, false, fmt.Errorf("no more expectation") - } - - return e.handle(options...) -} - -func (m *ClientMock) Expect() *ClientMockExpectation { - e := &ClientMockExpectation{ - query: url.Values{}, - } - m.expectations.Push(e) - - return e -} - -func NewClientMock(t *testing.T, expectationsShouldBeConsumed bool) *ClientMock { - t.Helper() - - m := &ClientMock{ - expectations: &FIFO[*ClientMockExpectation]{}, - } - - if expectationsShouldBeConsumed { - t.Cleanup(func() { - if !m.expectations.Empty() && !t.Failed() { - t.Errorf("all expectations not consumed") - } - }) - } - - return m -} - -var _ client.Client = &ClientMock{} - -type FIFO[ITEM any] struct { - mu sync.Mutex - items []ITEM -} - -func (s *FIFO[ITEM]) Pop() (ITEM, bool) { - s.mu.Lock() - defer s.mu.Unlock() - - if len(s.items) == 0 { - var i ITEM - - return i, false - } - - ret := s.items[0] - - if len(s.items) == 1 { - s.items = make([]ITEM, 0) - - return ret, true - } - - s.items = s.items[1:] - - return ret, true -} - -func (s *FIFO[ITEM]) Peek() (ITEM, bool) { - s.mu.Lock() - defer s.mu.Unlock() - - if len(s.items) == 0 { - var i ITEM - - return i, false - } - - return s.items[0], true -} - -func (s *FIFO[ITEM]) Push(i ITEM) *FIFO[ITEM] { - s.mu.Lock() - defer s.mu.Unlock() - - s.items = append(s.items, i) - - return s -} - -func (s *FIFO[ITEM]) Empty() bool { - s.mu.Lock() - defer s.mu.Unlock() - - return len(s.items) == 0 -} diff --git a/components/payments/cmd/connectors/internal/connectors/utils.go b/components/payments/cmd/connectors/internal/connectors/utils.go deleted file mode 100644 index eea7591cc1..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/utils.go +++ /dev/null @@ -1,52 +0,0 @@ -package connectors - -import ( - "context" - "os" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/metrics" - "github.com/formancehq/payments/internal/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/metric" - "go.opentelemetry.io/otel/trace" -) - -type DeferrableFunc func(ctx context.Context, timeSince time.Time) - -func ClientMetrics(ctx context.Context, connectorName, operation string) DeferrableFunc { - attributes := []attribute.KeyValue{ - attribute.String("connector", connectorName), - attribute.String("operation", operation), - } - - stack := os.Getenv("STACK") - if stack != "" { - attributes = append(attributes, attribute.String("stack", stack)) - } - - metrics.GetMetricsRegistry().ConnectorPSPCalls().Add(ctx, 1, metric.WithAttributes(attributes...)) - - return func(ctx context.Context, timeSince time.Time) { - metrics.GetMetricsRegistry().ConnectorPSPCallLatencies().Record(ctx, time.Since(timeSince).Milliseconds(), metric.WithAttributes(attributes...)) - } -} - -func StartSpan( - ctx context.Context, - spanName string, - attributes ...attribute.KeyValue, -) (context.Context, trace.Span) { - parentSpan := trace.SpanFromContext(ctx) - return otel.Tracer().Start( - ctx, - spanName, - trace.WithNewRoot(), - trace.WithLinks(trace.Link{ - SpanContext: parentSpan.SpanContext(), - }), - trace.WithAttributes( - attributes..., - ), - ) -} diff --git a/components/payments/cmd/connectors/internal/connectors/wise/client/balances.go b/components/payments/cmd/connectors/internal/connectors/wise/client/balances.go deleted file mode 100644 index eb9b5e7105..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/wise/client/balances.go +++ /dev/null @@ -1,67 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" -) - -type Balance struct { - ID uint64 `json:"id"` - Currency string `json:"currency"` - Type string `json:"type"` - Name string `json:"name"` - Amount struct { - Value json.Number `json:"value"` - Currency string `json:"currency"` - } `json:"amount"` - ReservedAmount struct { - Value json.Number `json:"value"` - Currency string `json:"currency"` - } `json:"reservedAmount"` - CashAmount struct { - Value json.Number `json:"value"` - Currency string `json:"currency"` - } `json:"cashAmount"` - TotalWorth struct { - Value json.Number `json:"value"` - Currency string `json:"currency"` - } `json:"totalWorth"` - CreationTime time.Time `json:"creationTime"` - ModificationTime time.Time `json:"modificationTime"` - Visible bool `json:"visible"` -} - -func (w *Client) GetBalances(ctx context.Context, profileID uint64) ([]*Balance, error) { - f := connectors.ClientMetrics(ctx, "wise", "list_balances") - now := time.Now() - defer f(ctx, now) - - req, err := http.NewRequestWithContext(ctx, - http.MethodGet, w.endpoint(fmt.Sprintf("v4/profiles/%d/balances?types=STANDARD", profileID)), http.NoBody) - if err != nil { - return nil, err - } - - res, err := w.httpClient.Do(req) - if err != nil { - return nil, err - } - defer res.Body.Close() - - if res.StatusCode != http.StatusOK { - return nil, unmarshalError(res.StatusCode, res.Body).Error() - } - - var balances []*Balance - err = json.NewDecoder(res.Body).Decode(&balances) - if err != nil { - return nil, fmt.Errorf("failed to decode account: %w", err) - } - - return balances, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/wise/client/profiles.go b/components/payments/cmd/connectors/internal/connectors/wise/client/profiles.go deleted file mode 100644 index 6cf4fe1907..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/wise/client/profiles.go +++ /dev/null @@ -1,48 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" -) - -type Profile struct { - ID uint64 `json:"id"` - Type string `json:"type"` -} - -func (w *Client) GetProfiles(ctx context.Context) ([]Profile, error) { - f := connectors.ClientMetrics(ctx, "wise", "list_profiles") - now := time.Now() - defer f(ctx, now) - - var profiles []Profile - - res, err := w.httpClient.Get(w.endpoint("v2/profiles")) - if err != nil { - return profiles, err - } - - defer res.Body.Close() - - body, err := io.ReadAll(res.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - - if res.StatusCode != http.StatusOK { - return nil, unmarshalError(res.StatusCode, res.Body).Error() - } - - err = json.Unmarshal(body, &profiles) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal profiles: %w", err) - } - - return profiles, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/wise/client/quotes.go b/components/payments/cmd/connectors/internal/connectors/wise/client/quotes.go deleted file mode 100644 index cdbced8ea3..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/wise/client/quotes.go +++ /dev/null @@ -1,57 +0,0 @@ -package client - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/google/uuid" -) - -type Quote struct { - ID uuid.UUID `json:"id"` -} - -func (w *Client) CreateQuote(ctx context.Context, profileID, currency string, amount json.Number) (Quote, error) { - f := connectors.ClientMetrics(ctx, "wise", "create_quote") - now := time.Now() - defer f(ctx, now) - - var response Quote - - req, err := json.Marshal(map[string]interface{}{ - "sourceCurrency": currency, - "targetCurrency": currency, - "sourceAmount": amount, - }) - if err != nil { - return response, err - } - - res, err := w.httpClient.Post(w.endpoint("v3/profiles/"+profileID+"/quotes"), "application/json", bytes.NewBuffer(req)) - if err != nil { - return response, err - } - defer res.Body.Close() - - if res.StatusCode != http.StatusOK { - return response, unmarshalError(res.StatusCode, res.Body).Error() - } - - body, err := io.ReadAll(res.Body) - if err != nil { - return response, fmt.Errorf("failed to read response body: %w", err) - } - - err = json.Unmarshal(body, &response) - if err != nil { - return response, fmt.Errorf("failed to get response from quote: %w", err) - } - - return response, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/wise/client/recipient_accounts.go b/components/payments/cmd/connectors/internal/connectors/wise/client/recipient_accounts.go deleted file mode 100644 index 83930a71fd..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/wise/client/recipient_accounts.go +++ /dev/null @@ -1,132 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" -) - -type RecipientAccountsResponse struct { - Content []*RecipientAccount `json:"content"` - SeekPositionForCurrent uint64 `json:"seekPositionForCurrent"` - SeekPositionForNext uint64 `json:"seekPositionForNext"` - Size int `json:"size"` -} - -type RecipientAccount struct { - ID uint64 `json:"id"` - Profile uint64 `json:"profileId"` - Currency string `json:"currency"` - Name struct { - FullName string `json:"fullName"` - } `json:"name"` -} - -func (w *Client) GetRecipientAccounts(ctx context.Context, profileID uint64, pageSize int, seekPositionForNext uint64) (*RecipientAccountsResponse, error) { - f := connectors.ClientMetrics(ctx, "wise", "list_recipient_accounts") - now := time.Now() - defer f(ctx, now) - - req, err := http.NewRequestWithContext(ctx, - http.MethodGet, w.endpoint("v2/accounts"), http.NoBody) - if err != nil { - return nil, err - } - - q := req.URL.Query() - q.Add("profile", fmt.Sprintf("%d", profileID)) - q.Add("size", fmt.Sprintf("%d", pageSize)) - q.Add("sort", "id,asc") - if seekPositionForNext > 0 { - q.Add("seekPosition", fmt.Sprintf("%d", seekPositionForNext)) - } - req.URL.RawQuery = q.Encode() - - res, err := w.httpClient.Do(req) - if err != nil { - return nil, err - } - defer res.Body.Close() - - if res.StatusCode != http.StatusOK { - return nil, unmarshalError(res.StatusCode, res.Body).Error() - } - - body, err := io.ReadAll(res.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - - var recipientAccounts *RecipientAccountsResponse - err = json.Unmarshal(body, &recipientAccounts) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal transfers: %w", err) - } - - return recipientAccounts, nil -} - -func (w *Client) GetRecipientAccount(ctx context.Context, accountID uint64) (*RecipientAccount, error) { - f := connectors.ClientMetrics(ctx, "wise", "get_recipient_account") - now := time.Now() - defer f(ctx, now) - - if rc, ok := w.recipientAccountsCache.Get(accountID); ok { - return rc, nil - } - - req, err := http.NewRequestWithContext(ctx, - http.MethodGet, w.endpoint(fmt.Sprintf("v1/accounts/%d", accountID)), http.NoBody) - if err != nil { - return nil, err - } - - res, err := w.httpClient.Do(req) - if err != nil { - return nil, err - } - defer res.Body.Close() - - if res.StatusCode != http.StatusOK { - type errorResponse struct { - Errors []struct { - Code string `json:"code"` - Message string `json:"message"` - } - } - - var e errorResponse - err = json.NewDecoder(res.Body).Decode(&e) - if err != nil { - return nil, fmt.Errorf("failed to decode error response: %w", err) - } - - if len(e.Errors) == 0 { - return nil, fmt.Errorf("unexpected status code: %d", res.StatusCode) - } - - switch e.Errors[0].Code { - case "RECIPIENT_MISSING": - // This is a valid response, we just don't have the account amoungs - // our recipients. - return &RecipientAccount{}, nil - } - - return nil, fmt.Errorf("unexpected status code: %d with err: %v", res.StatusCode, e) - } - - var account RecipientAccount - err = json.NewDecoder(res.Body).Decode(&account) - if err != nil { - return nil, fmt.Errorf("failed to decode account: %w", err) - } - - w.recipientAccountsCache.Add(accountID, &account) - - return &account, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/wise/client/transfers.go b/components/payments/cmd/connectors/internal/connectors/wise/client/transfers.go deleted file mode 100644 index 0de78341f5..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/wise/client/transfers.go +++ /dev/null @@ -1,264 +0,0 @@ -package client - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/metrics" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/metric" -) - -type Transfer struct { - ID uint64 `json:"id"` - Reference string `json:"reference"` - Status string `json:"status"` - SourceAccount uint64 `json:"sourceAccount"` - SourceCurrency string `json:"sourceCurrency"` - SourceValue json.Number `json:"sourceValue"` - TargetAccount uint64 `json:"targetAccount"` - TargetCurrency string `json:"targetCurrency"` - TargetValue json.Number `json:"targetValue"` - Business uint64 `json:"business"` - Created string `json:"created"` - //nolint:tagliatelle // allow for clients - CustomerTransactionID string `json:"customerTransactionId"` - Details struct { - Reference string `json:"reference"` - } `json:"details"` - Rate float64 `json:"rate"` - User uint64 `json:"user"` - - SourceBalanceID uint64 `json:"-"` - DestinationBalanceID uint64 `json:"-"` - - CreatedAt time.Time `json:"-"` -} - -func (t *Transfer) UnmarshalJSON(data []byte) error { - type Alias Transfer - - aux := &struct { - Created string `json:"created"` - *Alias - }{ - Alias: (*Alias)(t), - } - - if err := json.Unmarshal(data, &aux); err != nil { - return err - } - - var err error - - t.CreatedAt, err = time.Parse("2006-01-02 15:04:05", aux.Created) - if err != nil { - return fmt.Errorf("failed to parse created time: %w", err) - } - - return nil -} - -func (w *Client) GetTransfers(ctx context.Context, profile *Profile) ([]Transfer, error) { - f := connectors.ClientMetrics(ctx, "wise", "list_transfers") - now := time.Now() - defer f(ctx, now) - - var transfers []Transfer - - limit := 10 - offset := 0 - - for { - req, err := http.NewRequestWithContext(ctx, - http.MethodGet, w.endpoint("v1/transfers"), http.NoBody) - if err != nil { - return transfers, err - } - - q := req.URL.Query() - q.Add("limit", fmt.Sprintf("%d", limit)) - q.Add("profile", fmt.Sprintf("%d", profile.ID)) - q.Add("offset", fmt.Sprintf("%d", offset)) - req.URL.RawQuery = q.Encode() - - res, err := w.httpClient.Do(req) - if err != nil { - return transfers, err - } - defer res.Body.Close() - - if res.StatusCode != http.StatusOK { - return nil, unmarshalError(res.StatusCode, res.Body).Error() - } - - body, err := io.ReadAll(res.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - - var transferList []Transfer - - err = json.Unmarshal(body, &transferList) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal transfers: %w", err) - } - - for i, transfer := range transferList { - var sourceProfileID, targetProfileID uint64 - if transfer.SourceAccount != 0 { - recipientAccount, err := w.GetRecipientAccount(ctx, transfer.SourceAccount) - if err != nil { - return nil, fmt.Errorf("failed to get source profile id: %w", err) - } - - sourceProfileID = recipientAccount.Profile - } - - if transfer.TargetAccount != 0 { - recipientAccount, err := w.GetRecipientAccount(ctx, transfer.TargetAccount) - if err != nil { - return nil, fmt.Errorf("failed to get target profile id: %w", err) - } - - targetProfileID = recipientAccount.Profile - } - - // TODO(polo): fetching balances for each transfer is not efficient - // and can be quite long. We should consider caching balances, but - // at the same time we will develop a feature soon to get balances - // for every accounts, so caching is not a solution. - switch { - case sourceProfileID == 0 && targetProfileID == 0: - // Do nothing - case sourceProfileID == targetProfileID && sourceProfileID != 0: - // Same profile id for target and source - balances, err := w.GetBalances(ctx, sourceProfileID) - if err != nil { - return nil, fmt.Errorf("failed to get balances: %w", err) - } - for _, balance := range balances { - if balance.Currency == transfer.SourceCurrency { - transferList[i].SourceBalanceID = balance.ID - } - - if balance.Currency == transfer.TargetCurrency { - transferList[i].DestinationBalanceID = balance.ID - } - } - default: - if sourceProfileID != 0 { - balances, err := w.GetBalances(ctx, sourceProfileID) - if err != nil { - return nil, fmt.Errorf("failed to get balances: %w", err) - } - for _, balance := range balances { - if balance.Currency == transfer.SourceCurrency { - transferList[i].SourceBalanceID = balance.ID - } - } - } - - if targetProfileID != 0 { - balances, err := w.GetBalances(ctx, targetProfileID) - if err != nil { - return nil, fmt.Errorf("failed to get balances: %w", err) - } - for _, balance := range balances { - if balance.Currency == transfer.TargetCurrency { - transferList[i].DestinationBalanceID = balance.ID - } - } - } - - } - } - - transfers = append(transfers, transferList...) - - if len(transferList) < limit { - break - } - - offset += limit - } - - return transfers, nil -} - -func (w *Client) GetTransfer(ctx context.Context, transferID string) (*Transfer, error) { - f := connectors.ClientMetrics(ctx, "wise", "get_transfer") - now := time.Now() - defer f(ctx, now) - - req, err := http.NewRequestWithContext(ctx, - http.MethodGet, w.endpoint("v1/transfers/"+transferID), http.NoBody) - if err != nil { - return nil, err - } - - res, err := w.httpClient.Do(req) - if err != nil { - return nil, err - } - - body, err := io.ReadAll(res.Body) - if err != nil { - res.Body.Close() - - return nil, fmt.Errorf("failed to read response body: %w", err) - } - - if err = res.Body.Close(); err != nil { - return nil, fmt.Errorf("failed to close response body: %w", err) - } - - var transfer Transfer - err = json.Unmarshal(body, &transfer) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal transfer: %w", err) - } - - return &transfer, nil -} - -func (w *Client) CreateTransfer(ctx context.Context, quote Quote, targetAccount uint64, transactionID string) (*Transfer, error) { - metrics.GetMetricsRegistry().ConnectorPSPCalls().Add(ctx, 1, metric.WithAttributes([]attribute.KeyValue{ - attribute.String("connector", "wise"), - attribute.String("operation", "initiate_transfer"), - }...)) - - req, err := json.Marshal(map[string]interface{}{ - "targetAccount": targetAccount, - "quoteUuid": quote.ID.String(), - "customerTransactionId": transactionID, - }) - if err != nil { - return nil, err - } - - res, err := w.httpClient.Post(w.endpoint("v1/transfers"), "application/json", bytes.NewBuffer(req)) - if err != nil { - return nil, err - } - - defer res.Body.Close() - - if res.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected status code: %d", res.StatusCode) - } - - var response Transfer - err = json.NewDecoder(res.Body).Decode(&response) - if err != nil { - return nil, fmt.Errorf("failed to get response from transfer: %w", err) - } - - return &response, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/wise/config.go b/components/payments/cmd/connectors/internal/connectors/wise/config.go deleted file mode 100644 index aacab62524..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/wise/config.go +++ /dev/null @@ -1,56 +0,0 @@ -package wise - -import ( - "encoding/json" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/configtemplate" -) - -const ( - defaultPollingPeriod = 2 * time.Minute - pageSize = 100 -) - -type Config struct { - Name string `json:"name" yaml:"name" bson:"name"` - APIKey string `json:"apiKey" yaml:"apiKey" bson:"apiKey"` - PollingPeriod connectors.Duration `json:"pollingPeriod" yaml:"pollingPeriod" bson:"pollingPeriod"` -} - -// String obfuscates sensitive fields and returns a string representation of the config. -// This is used for logging. -func (c Config) String() string { - return "apiKey=***" -} - -func (c Config) Validate() error { - if c.APIKey == "" { - return ErrMissingAPIKey - } - - if c.Name == "" { - return ErrMissingName - } - - return nil -} - -func (c Config) Marshal() ([]byte, error) { - return json.Marshal(c) -} - -func (c Config) ConnectorName() string { - return c.Name -} - -func (c Config) BuildTemplate() (string, configtemplate.Config) { - cfg := configtemplate.NewConfig() - - cfg.AddParameter("name", configtemplate.TypeString, name.String(), false) - cfg.AddParameter("apiKey", configtemplate.TypeString, "", true) - cfg.AddParameter("pollingPeriod", configtemplate.TypeDurationNs, defaultPollingPeriod.String(), false) - - return name.String(), cfg -} diff --git a/components/payments/cmd/connectors/internal/connectors/wise/connector.go b/components/payments/cmd/connectors/internal/connectors/wise/connector.go deleted file mode 100644 index db043da8ee..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/wise/connector.go +++ /dev/null @@ -1,137 +0,0 @@ -package wise - -import ( - "context" - - "github.com/formancehq/payments/internal/models" - "github.com/pkg/errors" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/task" -) - -const name = models.ConnectorProviderWise - -var ( - mainTaskDescriptor = TaskDescriptor{ - Name: "Fetch profiles from client", - Key: taskNameFetchProfiles, - } -) - -type Connector struct { - logger logging.Logger - cfg Config -} - -func newConnector(logger logging.Logger, cfg Config) *Connector { - return &Connector{ - logger: logger.WithFields(map[string]any{ - "component": "connector", - }), - cfg: cfg, - } -} - -func (c *Connector) UpdateConfig(ctx task.ConnectorContext, config models.ConnectorConfigObject) error { - cfg, ok := config.(Config) - if !ok { - return connectors.ErrInvalidConfig - } - - restartTask := c.cfg.PollingPeriod.Duration != cfg.PollingPeriod.Duration - - c.cfg = cfg - - if restartTask { - descriptor, err := models.EncodeTaskDescriptor(mainTaskDescriptor) - if err != nil { - return err - } - - return ctx.Scheduler().Schedule(ctx.Context(), descriptor, models.TaskSchedulerOptions{ - // We want to polling every c.cfg.PollingPeriod.Duration seconds the users - // and their transactions. - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: c.cfg.PollingPeriod.Duration, - // No need to restart this task, since the connector is not existing or - // was uninstalled previously, the task does not exists in the database - RestartOption: models.OPTIONS_STOP_AND_RESTART, - }) - } - - return nil -} - -func (c *Connector) Install(ctx task.ConnectorContext) error { - descriptor, err := models.EncodeTaskDescriptor(mainTaskDescriptor) - if err != nil { - return err - } - - return ctx.Scheduler().Schedule(ctx.Context(), descriptor, models.TaskSchedulerOptions{ - // We want to polling every c.cfg.PollingPeriod.Duration seconds the users - // and their transactions. - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: c.cfg.PollingPeriod.Duration, - // No need to restart this task, since the connector is not existing or - // was uninstalled previously, the task does not exists in the database - RestartOption: models.OPTIONS_RESTART_NEVER, - }) -} - -func (c *Connector) Uninstall(ctx context.Context) error { - return nil -} - -func (c *Connector) Resolve(descriptor models.TaskDescriptor) task.Task { - taskDescriptor, err := models.DecodeTaskDescriptor[TaskDescriptor](descriptor) - if err != nil { - panic(err) - } - - return c.resolveTasks()(taskDescriptor) -} - -func (c *Connector) SupportedCurrenciesAndDecimals() map[string]int { - return supportedCurrenciesWithDecimal -} - -func (c *Connector) InitiatePayment(ctx task.ConnectorContext, transfer *models.TransferInitiation) error { - taskDescriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Initiate payment", - Key: taskNameInitiatePayment, - TransferID: transfer.ID.String(), - }) - if err != nil { - return err - } - - scheduleOption := models.OPTIONS_RUN_NOW_SYNC - scheduledAt := transfer.ScheduledAt - if !scheduledAt.IsZero() { - scheduleOption = models.OPTIONS_RUN_SCHEDULED_AT - } - - err = ctx.Scheduler().Schedule(ctx.Context(), taskDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: scheduleOption, - ScheduleAt: scheduledAt, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return err - } - - return nil -} - -func (c *Connector) ReversePayment(ctx task.ConnectorContext, transferReversal *models.TransferReversal) error { - return connectors.ErrNotImplemented -} - -func (c *Connector) CreateExternalBankAccount(ctx task.ConnectorContext, bankAccount *models.BankAccount) error { - return connectors.ErrNotImplemented -} - -var _ connectors.Connector = &Connector{} diff --git a/components/payments/cmd/connectors/internal/connectors/wise/errors.go b/components/payments/cmd/connectors/internal/connectors/wise/errors.go deleted file mode 100644 index 894ef089fd..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/wise/errors.go +++ /dev/null @@ -1,14 +0,0 @@ -package wise - -import "github.com/pkg/errors" - -var ( - // ErrMissingTask is returned when the task is missing. - ErrMissingTask = errors.New("task is not implemented") - - // ErrMissingAPIKey is returned when the api key is missing from config. - ErrMissingAPIKey = errors.New("missing apiKey from config") - - // ErrMissingName is returned when the name is missing from config. - ErrMissingName = errors.New("missing name from config") -) diff --git a/components/payments/cmd/connectors/internal/connectors/wise/loader.go b/components/payments/cmd/connectors/internal/connectors/wise/loader.go deleted file mode 100644 index 63cee78c3a..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/wise/loader.go +++ /dev/null @@ -1,47 +0,0 @@ -package wise - -import ( - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/gorilla/mux" -) - -type Loader struct{} - -const allowedTasks = 50 - -func (l *Loader) AllowTasks() int { - return allowedTasks -} - -func (l *Loader) Name() models.ConnectorProvider { - return name -} - -func (l *Loader) Load(logger logging.Logger, config Config) connectors.Connector { - return newConnector(logger, config) -} - -func (l *Loader) ApplyDefaults(cfg Config) Config { - if cfg.PollingPeriod.Duration == 0 { - cfg.PollingPeriod.Duration = defaultPollingPeriod - } - - if cfg.Name == "" { - cfg.Name = name.String() - } - - return cfg -} - -func (l *Loader) Router(_ *storage.Storage) *mux.Router { - // Webhooks are not implemented yet - return nil -} - -// NewLoader creates a new loader. -func NewLoader() *Loader { - return &Loader{} -} diff --git a/components/payments/cmd/connectors/internal/connectors/wise/task_fetch_profiles.go b/components/payments/cmd/connectors/internal/connectors/wise/task_fetch_profiles.go deleted file mode 100644 index 72ba0a3392..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/wise/task_fetch_profiles.go +++ /dev/null @@ -1,177 +0,0 @@ -package wise - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "strconv" - "time" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/wise/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -func taskFetchProfiles(wiseClient *client.Client) task.Task { - return func( - ctx context.Context, - logger logging.Logger, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - ) error { - sp := trace.SpanFromContext(ctx) - sp.SetName("wise.taskFetchProfiles") - sp.SetAttributes( - attribute.String("connectorID", connectorID.String()), - ) - - if err := fetchProfiles(ctx, wiseClient, connectorID, ingester, scheduler); err != nil { - otel.RecordError(sp, err) - return err - } - - return nil - } -} - -func fetchProfiles( - ctx context.Context, - wiseClient *client.Client, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, -) error { - profiles, err := wiseClient.GetProfiles(ctx) - if err != nil { - return err - } - - var descriptors []models.TaskDescriptor - for _, profile := range profiles { - balances, err := wiseClient.GetBalances(ctx, profile.ID) - if err != nil { - return err - } - - if err := ingestAccountsBatch( - ctx, - connectorID, - ingester, - profile.ID, - balances, - ); err != nil { - return err - } - - transferDescriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch transfers from client by profile", - Key: taskNameFetchTransfers, - ProfileID: profile.ID, - }) - if err != nil { - return err - } - descriptors = append(descriptors, transferDescriptor) - - recipientAccountsDescriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch recipient accounts from client by profile", - Key: taskNameFetchRecipientAccounts, - ProfileID: profile.ID, - }) - if err != nil { - return err - } - descriptors = append(descriptors, recipientAccountsDescriptor) - } - - for _, descriptor := range descriptors { - err = scheduler.Schedule(ctx, descriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return err - } - } - - return nil -} - -func ingestAccountsBatch( - ctx context.Context, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - profileID uint64, - balances []*client.Balance, -) error { - if len(balances) == 0 { - return nil - } - - accountsBatch := ingestion.AccountBatch{} - balancesBatch := ingestion.BalanceBatch{} - for _, balance := range balances { - raw, err := json.Marshal(balance) - if err != nil { - return err - } - - precision, ok := supportedCurrenciesWithDecimal[balance.Amount.Currency] - if !ok { - continue - } - - accountsBatch = append(accountsBatch, &models.Account{ - ID: models.AccountID{ - Reference: fmt.Sprintf("%d", balance.ID), - ConnectorID: connectorID, - }, - CreatedAt: balance.CreationTime, - Reference: fmt.Sprintf("%d", balance.ID), - ConnectorID: connectorID, - DefaultAsset: currency.FormatAsset(supportedCurrenciesWithDecimal, balance.Amount.Currency), - AccountName: balance.Name, - Type: models.AccountTypeInternal, - Metadata: map[string]string{ - "profile_id": strconv.FormatUint(profileID, 10), - }, - RawData: raw, - }) - - amount, err := currency.GetAmountWithPrecisionFromString(balance.Amount.Value.String(), precision) - if err != nil { - return err - } - - now := time.Now() - balancesBatch = append(balancesBatch, &models.Balance{ - AccountID: models.AccountID{ - Reference: fmt.Sprintf("%d", balance.ID), - ConnectorID: connectorID, - }, - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, balance.Amount.Currency), - Balance: amount, - CreatedAt: now, - LastUpdatedAt: now, - ConnectorID: connectorID, - }) - } - - if err := ingester.IngestAccounts(ctx, accountsBatch); err != nil { - return err - } - - if err := ingester.IngestBalances(ctx, balancesBatch, false); err != nil { - return err - } - - return nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/wise/task_fetch_recipient_accounts.go b/components/payments/cmd/connectors/internal/connectors/wise/task_fetch_recipient_accounts.go deleted file mode 100644 index f005d31b5a..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/wise/task_fetch_recipient_accounts.go +++ /dev/null @@ -1,108 +0,0 @@ -package wise - -import ( - "context" - "encoding/json" - "fmt" - "strconv" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/wise/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" -) - -type fetchRecipientAccountsState struct { - LastSeekPosition uint64 `json:"last_seek_position"` -} - -func taskFetchRecipientAccounts(wiseClient *client.Client, profileID uint64) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - resolver task.StateResolver, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "wise.taskFetchRecipientAccounts", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("profileID", strconv.FormatUint(profileID, 10)), - ) - defer span.End() - - state := task.MustResolveTo(ctx, resolver, fetchRecipientAccountsState{}) - - for { - recipientAccounts, err := wiseClient.GetRecipientAccounts(ctx, profileID, pageSize, state.LastSeekPosition) - if err != nil { - // Retryable errors already handled by the function - otel.RecordError(span, err) - return err - } - - if err := ingestRecipientAccountsBatch(ctx, connectorID, ingester, recipientAccounts.Content); err != nil { - // Retryable errors already handled by the function - otel.RecordError(span, err) - return err - } - - if recipientAccounts.SeekPositionForNext == 0 { - // No more data to fetch - break - } - - state.LastSeekPosition = recipientAccounts.SeekPositionForNext - } - - if err := ingester.UpdateTaskState(ctx, state); err != nil { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - return nil - } -} - -func ingestRecipientAccountsBatch( - ctx context.Context, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - accounts []*client.RecipientAccount, -) error { - accountsBatch := ingestion.AccountBatch{} - for _, account := range accounts { - raw, err := json.Marshal(account) - if err != nil { - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - accountsBatch = append(accountsBatch, &models.Account{ - ID: models.AccountID{ - Reference: fmt.Sprintf("%d", account.ID), - ConnectorID: connectorID, - }, - CreatedAt: time.Now(), - Reference: fmt.Sprintf("%d", account.ID), - ConnectorID: connectorID, - DefaultAsset: currency.FormatAsset(supportedCurrenciesWithDecimal, account.Currency), - AccountName: account.Name.FullName, - Type: models.AccountTypeExternal, - RawData: raw, - }) - } - - if err := ingester.IngestAccounts(ctx, accountsBatch); err != nil { - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - return nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/wise/task_fetch_transfers.go b/components/payments/cmd/connectors/internal/connectors/wise/task_fetch_transfers.go deleted file mode 100644 index 2e454112a8..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/wise/task_fetch_transfers.go +++ /dev/null @@ -1,146 +0,0 @@ -package wise - -import ( - "context" - "encoding/json" - "fmt" - "strconv" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/wise/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "go.opentelemetry.io/otel/attribute" -) - -func taskFetchTransfers(wiseClient *client.Client, profileID uint64) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - scheduler task.Scheduler, - ingester ingestion.Ingester, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "wise.taskFetchTransfers", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("profileID", strconv.FormatUint(profileID, 10)), - ) - defer span.End() - - if err := fetchTransfers(ctx, wiseClient, profileID, connectorID, scheduler, ingester); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func fetchTransfers( - ctx context.Context, - wiseClient *client.Client, - profileID uint64, - connectorID models.ConnectorID, - scheduler task.Scheduler, - ingester ingestion.Ingester, -) error { - transfers, err := wiseClient.GetTransfers(ctx, &client.Profile{ - ID: profileID, - }) - if err != nil { - return err - } - - if len(transfers) == 0 { - return nil - } - - var ( - // accountBatch ingestion.AccountBatch - paymentBatch ingestion.PaymentBatch - ) - - for _, transfer := range transfers { - - var rawData json.RawMessage - - rawData, err = json.Marshal(transfer) - if err != nil { - return fmt.Errorf("failed to marshal transfer: %w", err) - } - - precision, ok := supportedCurrenciesWithDecimal[transfer.TargetCurrency] - if !ok { - continue - } - - amount, err := currency.GetAmountWithPrecisionFromString(transfer.TargetValue.String(), precision) - if err != nil { - return err - } - - batchElement := ingestion.PaymentBatchElement{ - Payment: &models.Payment{ - ID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: fmt.Sprintf("%d", transfer.ID), - Type: models.PaymentTypeTransfer, - }, - ConnectorID: connectorID, - }, - CreatedAt: transfer.CreatedAt, - Reference: fmt.Sprintf("%d", transfer.ID), - ConnectorID: connectorID, - Type: models.PaymentTypeTransfer, - Status: matchTransferStatus(transfer.Status), - Scheme: models.PaymentSchemeOther, - Amount: amount, - Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transfer.TargetCurrency), - RawData: rawData, - }, - } - - if transfer.SourceBalanceID != 0 { - batchElement.Payment.SourceAccountID = &models.AccountID{ - Reference: fmt.Sprintf("%d", transfer.SourceBalanceID), - ConnectorID: connectorID, - } - } - - if transfer.DestinationBalanceID != 0 { - batchElement.Payment.DestinationAccountID = &models.AccountID{ - Reference: fmt.Sprintf("%d", transfer.DestinationBalanceID), - ConnectorID: connectorID, - } - } - - paymentBatch = append(paymentBatch, batchElement) - } - - if err := ingester.IngestPayments(ctx, paymentBatch); err != nil { - return err - } - - return nil -} - -func matchTransferStatus(status string) models.PaymentStatus { - switch status { - case "incoming_payment_waiting", "incoming_payment_initiated", "processing": - return models.PaymentStatusPending - case "funds_converted", "outgoing_payment_sent": - return models.PaymentStatusSucceeded - case "bounced_back", "funds_refunded": - return models.PaymentStatusFailed - case "cancelled": - return models.PaymentStatusCancelled - } - - return models.PaymentStatusOther -} diff --git a/components/payments/cmd/connectors/internal/connectors/wise/task_main.go b/components/payments/cmd/connectors/internal/connectors/wise/task_main.go deleted file mode 100644 index ea7cc20f30..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/wise/task_main.go +++ /dev/null @@ -1,50 +0,0 @@ -package wise - -import ( - "context" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" -) - -// taskMain is the main task of the connector. It launches the other tasks. -func taskMain() task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - scheduler task.Scheduler, - ) error { - ctx, span := connectors.StartSpan( - ctx, - "wise.taskMain", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - ) - defer span.End() - - taskUsers, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Fetch users from client", - Key: taskNameFetchProfiles, - }) - if err != nil { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - err = scheduler.Schedule(ctx, taskUsers, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - otel.RecordError(span, err) - return errors.Wrap(task.ErrRetryable, err.Error()) - } - - return nil - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/wise/task_payments.go b/components/payments/cmd/connectors/internal/connectors/wise/task_payments.go deleted file mode 100644 index e46b33cf09..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/wise/task_payments.go +++ /dev/null @@ -1,341 +0,0 @@ -package wise - -import ( - "context" - "encoding/json" - "fmt" - "strconv" - "time" - - "github.com/pkg/errors" - "go.opentelemetry.io/otel/attribute" - - "github.com/formancehq/go-libs/contextutil" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/wise/client" - "github.com/formancehq/payments/cmd/connectors/internal/ingestion" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/cmd/connectors/internal/task" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/internal/otel" -) - -func taskInitiatePayment( - wiseClient *client.Client, - transferID string, -) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - storageReader storage.Reader, - ) error { - transferInitiationID := models.MustTransferInitiationIDFromString(transferID) - - ctx, span := connectors.StartSpan( - ctx, - "wise.taskInitiatePayment", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("transferID", transferID), - attribute.String("reference", transferInitiationID.Reference), - ) - defer span.End() - - transfer, err := getTransfer(ctx, storageReader, transferInitiationID, true) - if err != nil { - otel.RecordError(span, err) - return err - } - - if err := initiatePayment(ctx, wiseClient, transfer, connectorID, ingester, scheduler, storageReader); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func initiatePayment( - ctx context.Context, - wiseClient *client.Client, - transfer *models.TransferInitiation, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - storageReader storage.Reader, -) error { - var err error - var paymentID *models.PaymentID - defer func() { - if err != nil { - ctx, cancel := contextutil.Detached(ctx) - defer cancel() - _ = ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusFailed, err.Error(), time.Now()) - } - }() - - err = ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusProcessing, "", time.Now()) - if err != nil { - return err - } - - if transfer.SourceAccount == nil { - err = errors.New("missing source account") - return err - } - - if transfer.SourceAccount.Type == models.AccountTypeExternal { - err = errors.New("payin not implemented: source account must be an internal account") - return err - } - - profileID, ok := transfer.SourceAccount.Metadata["profile_id"] - if !ok || profileID == "" { - err = errors.New("missing user_id in source account metadata") - return err - } - - var curr string - var precision int - curr, precision, err = currency.GetCurrencyAndPrecisionFromAsset(supportedCurrenciesWithDecimal, transfer.Asset) - if err != nil { - return err - } - - amount, err := currency.GetStringAmountFromBigIntWithPrecision(transfer.Amount, precision) - if err != nil { - return err - } - - quote, err := wiseClient.CreateQuote(ctx, profileID, curr, json.Number(amount)) - if err != nil { - return err - } - - var connectorPaymentID uint64 - var paymentType models.PaymentType - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) - defer cancel() - switch transfer.DestinationAccount.Type { - case models.AccountTypeInternal: - // Transfer between internal accounts - destinationAccount, err := strconv.ParseUint(transfer.DestinationAccount.Metadata["profile_id"], 10, 64) - if err != nil { - return err - } - - var resp *client.Transfer - resp, err = wiseClient.CreateTransfer(ctx, quote, destinationAccount, fmt.Sprintf("%s_%d", transfer.ID.Reference, len(transfer.RelatedAdjustments))) - if err != nil { - return err - } - - connectorPaymentID = resp.ID - paymentType = models.PaymentTypeTransfer - case models.AccountTypeExternal: - // Payout to an external account - - destinationAccount, err := strconv.ParseUint(transfer.DestinationAccount.Reference, 10, 64) - if err != nil { - return err - } - - var resp *client.Payout - resp, err = wiseClient.CreatePayout(ctx, quote, destinationAccount, fmt.Sprintf("%s_%d", transfer.ID.Reference, len(transfer.RelatedAdjustments))) - if err != nil { - return err - } - - connectorPaymentID = resp.ID - paymentType = models.PaymentTypePayOut - } - - paymentID = &models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: strconv.FormatUint(connectorPaymentID, 10), - Type: paymentType, - }, - ConnectorID: connectorID, - } - err = ingester.AddTransferInitiationPaymentID(ctx, transfer, paymentID, time.Now()) - if err != nil { - return err - } - - taskDescriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Update transfer initiation status", - Key: taskNameUpdatePaymentStatus, - TransferID: transfer.ID.String(), - PaymentID: paymentID.String(), - Attempt: 1, - }) - if err != nil { - return err - } - - ctx, _ = contextutil.DetachedWithTimeout(ctx, 10*time.Second) - err = scheduler.Schedule(ctx, taskDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return err - } - - return nil -} - -func taskUpdatePaymentStatus( - wiseClient *client.Client, - transferID string, - pID string, - attempt int, -) task.Task { - return func( - ctx context.Context, - taskID models.TaskID, - connectorID models.ConnectorID, - ingester ingestion.Ingester, - scheduler task.Scheduler, - storageReader storage.Reader, - ) error { - paymentID := models.MustPaymentIDFromString(pID) - transferInitiationID := models.MustTransferInitiationIDFromString(transferID) - - ctx, span := connectors.StartSpan( - ctx, - "wise.taskUpdatePaymentStatus", - attribute.String("connectorID", connectorID.String()), - attribute.String("taskID", taskID.String()), - attribute.String("transferID", transferID), - attribute.String("paymentID", pID), - attribute.Int("attempt", attempt), - attribute.String("reference", transferInitiationID.Reference), - ) - defer span.End() - - transfer, err := getTransfer(ctx, storageReader, transferInitiationID, false) - if err != nil { - otel.RecordError(span, err) - return err - } - - if err := updatePaymentStatus(ctx, wiseClient, transfer, paymentID, attempt, ingester, scheduler, storageReader); err != nil { - otel.RecordError(span, err) - return err - } - - return nil - } -} - -func updatePaymentStatus( - ctx context.Context, - wiseClient *client.Client, - transfer *models.TransferInitiation, - paymentID *models.PaymentID, - attempt int, - ingester ingestion.Ingester, - scheduler task.Scheduler, - storageReader storage.Reader, -) error { - var err error - var status string - switch transfer.Type { - case models.TransferInitiationTypeTransfer: - var resp *client.Transfer - resp, err = wiseClient.GetTransfer(ctx, paymentID.Reference) - if err != nil { - return err - } - - status = resp.Status - case models.TransferInitiationTypePayout: - var resp *client.Payout - resp, err = wiseClient.GetPayout(ctx, paymentID.Reference) - if err != nil { - return err - } - - status = resp.Status - } - - switch status { - case "incoming_payment_waiting", - "incoming_payment_initiated", - "processing", - "funds_converted", - "bounced_back", - "unknown": - taskDescriptor, err := models.EncodeTaskDescriptor(TaskDescriptor{ - Name: "Update transfer initiation status", - Key: taskNameUpdatePaymentStatus, - TransferID: transfer.ID.String(), - PaymentID: paymentID.String(), - Attempt: attempt + 1, - }) - if err != nil { - return err - } - - err = scheduler.Schedule(ctx, taskDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_IN_DURATION, - Duration: 2 * time.Minute, - RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE, - }) - if err != nil && !errors.Is(err, task.ErrAlreadyScheduled) { - return err - } - case "outgoing_payment_sent", "funds_refunded": - err = ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusProcessed, "", time.Now()) - if err != nil { - return err - } - - return nil - case "charged_back", "cancelled": - err = ingester.UpdateTransferInitiationPaymentsStatus(ctx, transfer, paymentID, models.TransferInitiationStatusFailed, "", time.Now()) - if err != nil { - return err - } - - return nil - } - - return nil -} - -func getTransfer( - ctx context.Context, - reader storage.Reader, - transferID models.TransferInitiationID, - expand bool, -) (*models.TransferInitiation, error) { - transfer, err := reader.ReadTransferInitiation(ctx, transferID) - if err != nil { - return nil, err - } - - if expand { - if transfer.SourceAccountID != nil { - sourceAccount, err := reader.GetAccount(ctx, transfer.SourceAccountID.String()) - if err != nil { - return nil, err - } - transfer.SourceAccount = sourceAccount - } - - destinationAccount, err := reader.GetAccount(ctx, transfer.DestinationAccountID.String()) - if err != nil { - return nil, err - } - transfer.DestinationAccount = destinationAccount - } - - return transfer, nil -} diff --git a/components/payments/cmd/connectors/internal/connectors/wise/task_resolve.go b/components/payments/cmd/connectors/internal/connectors/wise/task_resolve.go deleted file mode 100644 index 6e07953a9e..0000000000 --- a/components/payments/cmd/connectors/internal/connectors/wise/task_resolve.go +++ /dev/null @@ -1,53 +0,0 @@ -package wise - -import ( - "fmt" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors/wise/client" - "github.com/formancehq/payments/cmd/connectors/internal/task" -) - -const ( - taskNameMain = "main" - taskNameFetchTransfers = "fetch-transfers" - taskNameFetchProfiles = "fetch-profiles" - taskNameFetchRecipientAccounts = "fetch-recipient-accounts" - taskNameInitiatePayment = "initiate-payment" - taskNameUpdatePaymentStatus = "update-payment-status" -) - -// TaskDescriptor is the definition of a task. -type TaskDescriptor struct { - Name string `json:"name" yaml:"name" bson:"name"` - Key string `json:"key" yaml:"key" bson:"key"` - ProfileID uint64 `json:"profileID" yaml:"profileID" bson:"profileID"` - TransferID string `json:"transferID" yaml:"transferID" bson:"transferID"` - PaymentID string `json:"paymentID" yaml:"paymentID" bson:"paymentID"` - Attempt int `json:"attempt" yaml:"attempt" bson:"attempt"` -} - -func (c *Connector) resolveTasks() func(taskDefinition TaskDescriptor) task.Task { - client := client.NewClient(c.cfg.APIKey) - - return func(taskDefinition TaskDescriptor) task.Task { - switch taskDefinition.Key { - case taskNameMain: - return taskMain() - case taskNameFetchProfiles: - return taskFetchProfiles(client) - case taskNameFetchRecipientAccounts: - return taskFetchRecipientAccounts(client, taskDefinition.ProfileID) - case taskNameFetchTransfers: - return taskFetchTransfers(client, taskDefinition.ProfileID) - case taskNameInitiatePayment: - return taskInitiatePayment(client, taskDefinition.TransferID) - case taskNameUpdatePaymentStatus: - return taskUpdatePaymentStatus(client, taskDefinition.TransferID, taskDefinition.PaymentID, taskDefinition.Attempt) - } - - // This should never happen. - return func() error { - return fmt.Errorf("key '%s': %w", taskDefinition.Key, ErrMissingTask) - } - } -} diff --git a/components/payments/cmd/connectors/internal/ingestion/accounts.go b/components/payments/cmd/connectors/internal/ingestion/accounts.go deleted file mode 100644 index 05b7f37963..0000000000 --- a/components/payments/cmd/connectors/internal/ingestion/accounts.go +++ /dev/null @@ -1,67 +0,0 @@ -package ingestion - -import ( - "context" - "fmt" - "time" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/go-libs/publish" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/pkg/events" -) - -type AccountBatch []*models.Account - -type AccountIngesterFn func(ctx context.Context, batch AccountBatch, commitState any) error - -func (fn AccountIngesterFn) IngestAccounts(ctx context.Context, batch AccountBatch, commitState any) error { - return fn(ctx, batch, commitState) -} - -func (i *DefaultIngester) IngestAccounts(ctx context.Context, batch AccountBatch) error { - startingAt := time.Now() - - logging.FromContext(ctx).WithFields(map[string]interface{}{ - "size": len(batch), - "startingAt": startingAt, - }).Debugf("Ingest accounts batch") - - idsInserted, err := i.store.UpsertAccounts(ctx, batch) - if err != nil { - return fmt.Errorf("error upserting accounts: %w", err) - } - - idsInsertedMap := make(map[string]struct{}, len(idsInserted)) - for idx := range idsInserted { - idsInsertedMap[idsInserted[idx].String()] = struct{}{} - } - - for accountIdx := range batch { - _, ok := idsInsertedMap[batch[accountIdx].ID.String()] - if !ok { - // No need to publish an event for an already existing payment - continue - } - - if err := i.publisher.Publish( - events.TopicPayments, - publish.NewMessage( - ctx, - i.messages.NewEventSavedAccounts(i.provider, batch[accountIdx]), - ), - ); err != nil { - logging.FromContext(ctx).Errorf("Publishing message: %w", err) - } - } - - endedAt := time.Now() - - logging.FromContext(ctx).WithFields(map[string]interface{}{ - "size": len(batch), - "endedAt": endedAt, - "latency": endedAt.Sub(startingAt).String(), - }).Debugf("Accounts batch ingested") - - return nil -} diff --git a/components/payments/cmd/connectors/internal/ingestion/balances.go b/components/payments/cmd/connectors/internal/ingestion/balances.go deleted file mode 100644 index 334a99aebf..0000000000 --- a/components/payments/cmd/connectors/internal/ingestion/balances.go +++ /dev/null @@ -1,55 +0,0 @@ -package ingestion - -import ( - "context" - "fmt" - "time" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/go-libs/publish" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/pkg/events" -) - -type BalanceBatch []*models.Balance - -type BalanceIngesterFn func(ctx context.Context, batch BalanceBatch) error - -func (fn BalanceIngesterFn) IngestBalances(ctx context.Context, batch BalanceBatch) error { - return fn(ctx, batch) -} - -func (i *DefaultIngester) IngestBalances(ctx context.Context, batch BalanceBatch, checkIfAccountExists bool) error { - startingAt := time.Now() - - logging.FromContext(ctx).WithFields(map[string]interface{}{ - "size": len(batch), - "startingAt": startingAt, - }).Debugf("Ingest balances batch") - - if err := i.store.InsertBalances(ctx, batch, checkIfAccountExists); err != nil { - return fmt.Errorf("error inserting balances: %w", err) - } - - for _, balance := range batch { - if err := i.publisher.Publish( - events.TopicPayments, - publish.NewMessage( - ctx, - i.messages.NewEventSavedBalances(balance), - ), - ); err != nil { - logging.FromContext(ctx).Errorf("Publishing message: %w", err) - } - } - - endedAt := time.Now() - - logging.FromContext(ctx).WithFields(map[string]interface{}{ - "size": len(batch), - "endedAt": endedAt, - "latency": endedAt.Sub(startingAt).String(), - }).Debugf("Accounts batch ingested") - - return nil -} diff --git a/components/payments/cmd/connectors/internal/ingestion/bank_account.go b/components/payments/cmd/connectors/internal/ingestion/bank_account.go deleted file mode 100644 index c9a9e7b252..0000000000 --- a/components/payments/cmd/connectors/internal/ingestion/bank_account.go +++ /dev/null @@ -1,39 +0,0 @@ -package ingestion - -import ( - "context" - "time" - - "github.com/formancehq/go-libs/publish" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/pkg/events" - "github.com/google/uuid" -) - -func (i *DefaultIngester) LinkBankAccountWithAccount(ctx context.Context, bankAccount *models.BankAccount, accountID *models.AccountID) error { - adjustment := &models.BankAccountRelatedAccount{ - ID: uuid.New(), - CreatedAt: time.Now().UTC(), - BankAccountID: bankAccount.ID, - ConnectorID: accountID.ConnectorID, - AccountID: *accountID, - } - - if err := i.store.AddBankAccountRelatedAccount(ctx, adjustment); err != nil { - return err - } - - bankAccount.RelatedAccounts = append(bankAccount.RelatedAccounts, adjustment) - - if err := i.publisher.Publish( - events.TopicPayments, - publish.NewMessage( - ctx, - i.messages.NewEventSavedBankAccounts(bankAccount), - ), - ); err != nil { - return err - } - - return nil -} diff --git a/components/payments/cmd/connectors/internal/ingestion/ingester.go b/components/payments/cmd/connectors/internal/ingestion/ingester.go deleted file mode 100644 index 0c54c7d5a3..0000000000 --- a/components/payments/cmd/connectors/internal/ingestion/ingester.go +++ /dev/null @@ -1,65 +0,0 @@ -package ingestion - -import ( - "context" - "encoding/json" - "time" - - "github.com/ThreeDotsLabs/watermill/message" - "github.com/formancehq/payments/internal/messages" - "github.com/formancehq/payments/internal/models" -) - -type Ingester interface { - IngestAccounts(ctx context.Context, batch AccountBatch) error - IngestPayments(ctx context.Context, batch PaymentBatch) error - IngestBalances(ctx context.Context, batch BalanceBatch, checkIfAccountExists bool) error - UpdateTaskState(ctx context.Context, state any) error - UpdateTransferInitiationPayment(ctx context.Context, tf *models.TransferInitiation, paymentID *models.PaymentID, status models.TransferInitiationStatus, errorMessage string, updatedAt time.Time) error - UpdateTransferInitiationPaymentsStatus(ctx context.Context, tf *models.TransferInitiation, paymentID *models.PaymentID, status models.TransferInitiationStatus, errorMessage string, updatedAt time.Time) error - UpdateTransferReversalStatus(ctx context.Context, transfer *models.TransferInitiation, transferReversal *models.TransferReversal) error - AddTransferInitiationPaymentID(ctx context.Context, tf *models.TransferInitiation, paymentID *models.PaymentID, updatedAt time.Time) error - LinkBankAccountWithAccount(ctx context.Context, bankAccount *models.BankAccount, accountID *models.AccountID) error -} - -type DefaultIngester struct { - provider models.ConnectorProvider - connectorID models.ConnectorID - store Store - descriptor models.TaskDescriptor - publisher message.Publisher - messages *messages.Messages -} - -type Store interface { - UpsertAccounts(ctx context.Context, accounts []*models.Account) ([]models.AccountID, error) - UpsertPayments(ctx context.Context, payments []*models.Payment) ([]*models.PaymentID, error) - UpsertPaymentsAdjustments(ctx context.Context, paymentsAdjustment []*models.PaymentAdjustment) error - UpsertPaymentsMetadata(ctx context.Context, paymentsMetadata []*models.PaymentMetadata) error - InsertBalances(ctx context.Context, balances []*models.Balance, checkIfAccountExists bool) error - UpdateTaskState(ctx context.Context, connectorID models.ConnectorID, descriptor models.TaskDescriptor, state json.RawMessage) error - UpdateTransferInitiationPaymentsStatus(ctx context.Context, id models.TransferInitiationID, paymentID *models.PaymentID, adjustment *models.TransferInitiationAdjustment) error - UpdateTransferReversalStatus(ctx context.Context, transfer *models.TransferInitiation, transferReversal *models.TransferReversal, adjustment *models.TransferInitiationAdjustment) error - AddTransferInitiationPaymentID(ctx context.Context, id models.TransferInitiationID, paymentID *models.PaymentID, updatedAt time.Time, metadata map[string]string) error - AddBankAccountRelatedAccount(ctx context.Context, adjustment *models.BankAccountRelatedAccount) error -} - -func NewDefaultIngester( - provider models.ConnectorProvider, - connectorID models.ConnectorID, - descriptor models.TaskDescriptor, - repo Store, - publisher message.Publisher, - messages *messages.Messages, -) *DefaultIngester { - return &DefaultIngester{ - provider: provider, - connectorID: connectorID, - descriptor: descriptor, - store: repo, - publisher: publisher, - messages: messages, - } -} - -var _ Ingester = (*DefaultIngester)(nil) diff --git a/components/payments/cmd/connectors/internal/ingestion/ingester_test.go b/components/payments/cmd/connectors/internal/ingestion/ingester_test.go deleted file mode 100644 index 9c1f1895ac..0000000000 --- a/components/payments/cmd/connectors/internal/ingestion/ingester_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package ingestion - -import ( - "context" - "encoding/json" - "time" - - "github.com/ThreeDotsLabs/watermill/message" - "github.com/formancehq/payments/internal/models" -) - -type MockStore struct { - paymentIDsNotModified map[string]struct{} -} - -func NewMockStore() *MockStore { - return &MockStore{ - paymentIDsNotModified: make(map[string]struct{}), - } -} - -func (m *MockStore) WithPaymentIDsNotModified(paymentsIDs []models.PaymentID) *MockStore { - for _, id := range paymentsIDs { - m.paymentIDsNotModified[id.String()] = struct{}{} - } - return m -} - -func (m *MockStore) UpsertAccounts(ctx context.Context, accounts []*models.Account) ([]models.AccountID, error) { - return nil, nil -} - -func (m *MockStore) UpsertPayments(ctx context.Context, payments []*models.Payment) ([]*models.PaymentID, error) { - ids := make([]*models.PaymentID, 0, len(payments)) - for _, payment := range payments { - if _, ok := m.paymentIDsNotModified[payment.ID.String()]; !ok { - ids = append(ids, &payment.ID) - } - } - - return ids, nil -} - -func (m *MockStore) UpsertPaymentsAdjustments(ctx context.Context, adjustments []*models.PaymentAdjustment) error { - return nil -} - -func (m *MockStore) UpsertPaymentsMetadata(ctx context.Context, metadata []*models.PaymentMetadata) error { - return nil -} - -func (m *MockStore) InsertBalances(ctx context.Context, balances []*models.Balance, checkIfAccountExists bool) error { - return nil -} - -func (m *MockStore) UpdateTaskState(ctx context.Context, connectorID models.ConnectorID, descriptor models.TaskDescriptor, state json.RawMessage) error { - return nil -} - -func (m *MockStore) UpdateTransferInitiationPaymentsStatus(ctx context.Context, id models.TransferInitiationID, paymentID *models.PaymentID, adjustment *models.TransferInitiationAdjustment) error { - return nil -} - -func (m *MockStore) UpdateTransferReversalStatus(ctx context.Context, transfer *models.TransferInitiation, transferReversal *models.TransferReversal, adjustment *models.TransferInitiationAdjustment) error { - return nil -} - -func (m *MockStore) AddTransferInitiationPaymentID(ctx context.Context, id models.TransferInitiationID, paymentID *models.PaymentID, updatedAt time.Time, metadata map[string]string) error { - return nil -} - -func (m *MockStore) AddBankAccountRelatedAccount(ctx context.Context, adjustment *models.BankAccountRelatedAccount) error { - return nil -} - -type MockPublisher struct { - messages chan *message.Message -} - -func NewMockPublisher() *MockPublisher { - return &MockPublisher{ - messages: make(chan *message.Message, 100), - } -} - -func (m *MockPublisher) Publish(topic string, messages ...*message.Message) error { - for _, msg := range messages { - m.messages <- msg - } - - return nil -} - -func (m *MockPublisher) Close() error { - close(m.messages) - return nil -} diff --git a/components/payments/cmd/connectors/internal/ingestion/payments.go b/components/payments/cmd/connectors/internal/ingestion/payments.go deleted file mode 100644 index 856bafef5b..0000000000 --- a/components/payments/cmd/connectors/internal/ingestion/payments.go +++ /dev/null @@ -1,110 +0,0 @@ -package ingestion - -import ( - "context" - "fmt" - "time" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/go-libs/publish" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/pkg/events" -) - -type PaymentBatchElement struct { - Payment *models.Payment - Adjustment *models.PaymentAdjustment -} - -type PaymentBatch []PaymentBatchElement - -type IngesterFn func(ctx context.Context, batch PaymentBatch, commitState any) error - -func (fn IngesterFn) IngestPayments(ctx context.Context, batch PaymentBatch, commitState any) error { - return fn(ctx, batch, commitState) -} - -func (i *DefaultIngester) IngestPayments( - ctx context.Context, - batch PaymentBatch, -) error { - startingAt := time.Now() - - logging.FromContext(ctx).WithFields(map[string]interface{}{ - "size": len(batch), - "startingAt": startingAt, - }).Debugf("Ingest batch") - - var allPayments []*models.Payment //nolint:prealloc // length is unknown - var allMetadata []*models.PaymentMetadata - var allAdjustments []*models.PaymentAdjustment - - for batchIdx := range batch { - payment := batch[batchIdx].Payment - adjustment := batch[batchIdx].Adjustment - - if payment != nil { - allPayments = append(allPayments, payment) - - for _, data := range payment.Metadata { - data.Changelog = append(data.Changelog, - models.MetadataChangelog{ - CreatedAt: time.Now(), - Value: data.Value, - }) - - allMetadata = append(allMetadata, data) - } - } - - if adjustment != nil && adjustment.Reference != "" { - allAdjustments = append(allAdjustments, adjustment) - } - } - - // Insert first all payments - idsInserted, err := i.store.UpsertPayments(ctx, allPayments) - if err != nil { - return fmt.Errorf("error upserting payments: %w", err) - } - - // Then insert all metadata - if err := i.store.UpsertPaymentsMetadata(ctx, allMetadata); err != nil { - return fmt.Errorf("error upserting payments metadata: %w", err) - } - - // Then insert all adjustments - if err := i.store.UpsertPaymentsAdjustments(ctx, allAdjustments); err != nil { - return fmt.Errorf("error upserting payments adjustments: %w", err) - } - - idsInsertedMap := make(map[string]struct{}, len(idsInserted)) - for idx := range idsInserted { - idsInsertedMap[idsInserted[idx].String()] = struct{}{} - } - - for paymentIdx := range allPayments { - _, ok := idsInsertedMap[allPayments[paymentIdx].ID.String()] - if !ok { - // No need to publish an event for an already existing payment - continue - } - err = i.publisher.Publish(events.TopicPayments, - publish.NewMessage(ctx, i.messages.NewEventSavedPayments(i.provider, allPayments[paymentIdx]))) - if err != nil { - logging.FromContext(ctx).Errorf("Publishing message: %w", err) - - continue - } - } - - endedAt := time.Now() - - logging.FromContext(ctx).WithFields(map[string]interface{}{ - "size": len(batch), - "endedAt": endedAt, - "latency": endedAt.Sub(startingAt).String(), - }).Debugf("Batch ingested") - - return nil -} diff --git a/components/payments/cmd/connectors/internal/ingestion/payments_test.go b/components/payments/cmd/connectors/internal/ingestion/payments_test.go deleted file mode 100644 index 73d47dd09f..0000000000 --- a/components/payments/cmd/connectors/internal/ingestion/payments_test.go +++ /dev/null @@ -1,223 +0,0 @@ -package ingestion - -import ( - "context" - "encoding/json" - "math/big" - "testing" - "time" - - "github.com/formancehq/payments/internal/messages" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/stretchr/testify/require" -) - -var ( - connectorID = models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - - acc1 = models.AccountID{ - Reference: "acc1", - ConnectorID: connectorID, - } - acc2 = models.AccountID{ - Reference: "acc2", - ConnectorID: connectorID, - } - - p1 = &models.Payment{ - ID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: "p1", - Type: models.PaymentTypePayIn, - }, - ConnectorID: connectorID, - }, - ConnectorID: connectorID, - CreatedAt: time.Date(2023, 11, 14, 4, 55, 0, 0, time.UTC), - Reference: "p1", - Amount: big.NewInt(100), - Type: models.PaymentTypePayIn, - Status: models.PaymentStatusCancelled, - Scheme: models.PaymentSchemeA2A, - Asset: models.Asset("USD/2"), - SourceAccountID: &acc1, - } - - p2 = &models.Payment{ - ID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: "p2", - Type: models.PaymentTypeTransfer, - }, - ConnectorID: connectorID, - }, - ConnectorID: connectorID, - CreatedAt: time.Date(2023, 11, 14, 4, 54, 0, 0, time.UTC), - Reference: "p2", - Amount: big.NewInt(150), - Type: models.PaymentTypeTransfer, - Status: models.PaymentStatusSucceeded, - Scheme: models.PaymentSchemeApplePay, - Asset: models.Asset("EUR/2"), - DestinationAccountID: &acc2, - } - - p3 = &models.Payment{ - ID: models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: "p3", - Type: models.PaymentTypePayOut, - }, - ConnectorID: connectorID, - }, - ConnectorID: connectorID, - CreatedAt: time.Date(2023, 11, 14, 4, 53, 0, 0, time.UTC), - Reference: "p3", - Amount: big.NewInt(200), - Type: models.PaymentTypePayOut, - Status: models.PaymentStatusPending, - Scheme: models.PaymentSchemeCardMasterCard, - Asset: models.Asset("USD/2"), - SourceAccountID: &acc1, - DestinationAccountID: &acc2, - } -) - -type linkPayload struct { - Name string `json:"name"` - URI string `json:"uri"` -} -type paymentMessagePayload struct { - Payload struct { - ID string `json:"id"` - Links []linkPayload `json:"links"` - } `json:"payload"` -} - -func TestIngestPayments(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - batch PaymentBatch - paymentIDsNotModified []models.PaymentID - requiredPublishedPaymentIDs []models.PaymentID - } - - testCases := []testCase{ - { - name: "nominal", - batch: PaymentBatch{ - { - Payment: p1, - }, - { - Payment: p2, - }, - { - Payment: p3, - }, - }, - paymentIDsNotModified: []models.PaymentID{}, - requiredPublishedPaymentIDs: []models.PaymentID{p1.ID, p2.ID, p3.ID}, - }, - { - name: "only one payment upserted, should publish only one message", - batch: PaymentBatch{ - { - Payment: p1, - }, - { - Payment: p2, - }, - { - Payment: p3, - }, - }, - paymentIDsNotModified: []models.PaymentID{p1.ID, p2.ID}, - requiredPublishedPaymentIDs: []models.PaymentID{p3.ID}, - }, - { - name: "all payments are not modified, should not publish any message", - batch: PaymentBatch{ - { - Payment: p1, - }, - { - Payment: p2, - }, - { - Payment: p3, - }, - }, - paymentIDsNotModified: []models.PaymentID{p1.ID, p2.ID, p3.ID}, - requiredPublishedPaymentIDs: []models.PaymentID{}, - }, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - publisher := NewMockPublisher() - - ingester := NewDefaultIngester( - models.ConnectorProviderDummyPay, - connectorID, - nil, - NewMockStore().WithPaymentIDsNotModified(tc.paymentIDsNotModified), - publisher, - messages.NewMessages(""), - ) - - err := ingester.IngestPayments(context.Background(), tc.batch) - publisher.Close() - require.NoError(t, err) - - require.Len(t, publisher.messages, len(tc.requiredPublishedPaymentIDs)) - i := 0 - for msg := range publisher.messages { - var payload paymentMessagePayload - require.NoError(t, json.Unmarshal(msg.Payload, &payload)) - require.Equal(t, tc.requiredPublishedPaymentIDs[i].String(), payload.Payload.ID) - - var expectedLinks []linkPayload - p := getPayment(tc.requiredPublishedPaymentIDs[i]) - if p == nil { - continue - } - if p.SourceAccountID != nil { - expectedLinks = append(expectedLinks, linkPayload{ - Name: "source_account", - URI: "/api/payments/accounts/" + p.SourceAccountID.String(), - }) - } - if p.DestinationAccountID != nil { - expectedLinks = append(expectedLinks, linkPayload{ - Name: "destination_account", - URI: "/api/payments/accounts/" + p.DestinationAccountID.String(), - }) - } - require.Equal(t, expectedLinks, payload.Payload.Links) - - i++ - } - }) - } -} - -func getPayment(id models.PaymentID) *models.Payment { - switch id { - case p1.ID: - return p1 - case p2.ID: - return p2 - case p3.ID: - return p3 - default: - return nil - } -} diff --git a/components/payments/cmd/connectors/internal/ingestion/task.go b/components/payments/cmd/connectors/internal/ingestion/task.go deleted file mode 100644 index b8588071a1..0000000000 --- a/components/payments/cmd/connectors/internal/ingestion/task.go +++ /dev/null @@ -1,20 +0,0 @@ -package ingestion - -import ( - "context" - "encoding/json" - "fmt" -) - -func (i *DefaultIngester) UpdateTaskState(ctx context.Context, state any) error { - taskState, err := json.Marshal(state) - if err != nil { - return fmt.Errorf("error marshaling task state: %w", err) - } - - if err = i.store.UpdateTaskState(ctx, i.connectorID, i.descriptor, taskState); err != nil { - return fmt.Errorf("error updating task state: %w", err) - } - - return nil -} diff --git a/components/payments/cmd/connectors/internal/ingestion/test.json b/components/payments/cmd/connectors/internal/ingestion/test.json deleted file mode 100644 index 2c806bb71f..0000000000 --- a/components/payments/cmd/connectors/internal/ingestion/test.json +++ /dev/null @@ -1 +0,0 @@ -{"data":{"PAYMENT":[{"amount":300,"asset":"EUR/2","connectorId":"eyJQcm92aWRlciI6Ik1BTkdPUEFZIiwiUmVmZXJlbmNlIjoiNjYwNjc1ODktMDkxZC00YTQxLTg1ZTctYzEyYTRhMWY5ZGY2In0=","createdAt":"2023-10-31T15:01:10Z","id":"eyJDb25uZWN0b3JJRCI6eyJQcm92aWRlciI6Ik1BTkdPUEFZIiwiUmVmZXJlbmNlIjoiNjYwNjc1ODktMDkxZC00YTQxLTg1ZTctYzEyYTRhMWY5ZGY2In0sIlJlZmVyZW5jZSI6IjIwNzYyMTg3OSIsIlR5cGUiOiJUUkFOU0ZFUiJ9","initialAmount":300,"provider":"MANGOPAY","reference":"207621879","scheme":"other","status":"SUCCEEDED","type":"TRANSFER"},{"amount":300,"asset":"EUR/2","connectorId":"eyJQcm92aWRlciI6Ik1BTkdPUEFZIiwiUmVmZXJlbmNlIjoiNjYwNjc1ODktMDkxZC00YTQxLTg1ZTctYzEyYTRhMWY5ZGY2In0=","createdAt":"2023-10-12T15:15:00Z","id":"eyJDb25uZWN0b3JJRCI6eyJQcm92aWRlciI6Ik1BTkdPUEFZIiwiUmVmZXJlbmNlIjoiNjYwNjc1ODktMDkxZC00YTQxLTg1ZTctYzEyYTRhMWY5ZGY2In0sIlJlZmVyZW5jZSI6IjIwNjEzODczOSIsIlR5cGUiOiJUUkFOU0ZFUiJ9","initialAmount":300,"provider":"MANGOPAY","reference":"206138739","scheme":"other","status":"SUCCEEDED","type":"TRANSFER"},{"amount":300,"asset":"EUR/2","connectorId":"eyJQcm92aWRlciI6Ik1BTkdPUEFZIiwiUmVmZXJlbmNlIjoiNjYwNjc1ODktMDkxZC00YTQxLTg1ZTctYzEyYTRhMWY5ZGY2In0=","createdAt":"2023-10-11T15:45:41Z","id":"eyJDb25uZWN0b3JJRCI6eyJQcm92aWRlciI6Ik1BTkdPUEFZIiwiUmVmZXJlbmNlIjoiNjYwNjc1ODktMDkxZC00YTQxLTg1ZTctYzEyYTRhMWY5ZGY2In0sIlJlZmVyZW5jZSI6IjIwNjAzMTEyOSIsIlR5cGUiOiJUUkFOU0ZFUiJ9","initialAmount":300,"provider":"MANGOPAY","reference":"206031129","scheme":"other","status":"SUCCEEDED","type":"TRANSFER"},{"amount":300,"asset":"EUR/2","connectorId":"eyJQcm92aWRlciI6Ik1BTkdPUEFZIiwiUmVmZXJlbmNlIjoiNjYwNjc1ODktMDkxZC00YTQxLTg1ZTctYzEyYTRhMWY5ZGY2In0=","createdAt":"2023-10-12T15:09:17Z","id":"eyJDb25uZWN0b3JJRCI6eyJQcm92aWRlciI6Ik1BTkdPUEFZIiwiUmVmZXJlbmNlIjoiNjYwNjc1ODktMDkxZC00YTQxLTg1ZTctYzEyYTRhMWY5ZGY2In0sIlJlZmVyZW5jZSI6IjIwNjEzODIwOCIsIlR5cGUiOiJUUkFOU0ZFUiJ9","initialAmount":300,"provider":"MANGOPAY","reference":"206138208","scheme":"other","status":"SUCCEEDED","type":"TRANSFER"},{"amount":300,"asset":"EUR/2","connectorId":"eyJQcm92aWRlciI6Ik1BTkdPUEFZIiwiUmVmZXJlbmNlIjoiNjYwNjc1ODktMDkxZC00YTQxLTg1ZTctYzEyYTRhMWY5ZGY2In0=","createdAt":"2023-10-03T13:58:43Z","id":"eyJDb25uZWN0b3JJRCI6eyJQcm92aWRlciI6Ik1BTkdPUEFZIiwiUmVmZXJlbmNlIjoiNjYwNjc1ODktMDkxZC00YTQxLTg1ZTctYzEyYTRhMWY5ZGY2In0sIlJlZmVyZW5jZSI6IjIwNTE4Nzg0MSIsIlR5cGUiOiJUUkFOU0ZFUiJ9","initialAmount":300,"provider":"MANGOPAY","reference":"205187841","scheme":"other","status":"SUCCEEDED","type":"TRANSFER"}],"PAYMENT_ACCOUNT":[{"accountName":"Joe Blogs","connectorId":"eyJQcm92aWRlciI6Ik1BTkdPUEFZIiwiUmVmZXJlbmNlIjoiNjYwNjc1ODktMDkxZC00YTQxLTg1ZTctYzEyYTRhMWY5ZGY2In0=","createdAt":"2023-08-14T09:26:07Z","defaultAsset":"","id":"eyJDb25uZWN0b3JJRCI6eyJQcm92aWRlciI6Ik1BTkdPUEFZIiwiUmVmZXJlbmNlIjoiNjYwNjc1ODktMDkxZC00YTQxLTg1ZTctYzEyYTRhMWY5ZGY2In0sIlJlZmVyZW5jZSI6IjIwMDUxODA5NSJ9","provider":"MANGOPAY","reference":"200518095","type":"EXTERNAL"},{"accountName":"My big project transfer","connectorId":"eyJQcm92aWRlciI6Ik1BTkdPUEFZIiwiUmVmZXJlbmNlIjoiNjYwNjc1ODktMDkxZC00YTQxLTg1ZTctYzEyYTRhMWY5ZGY2In0=","createdAt":"2023-08-28T09:58:34Z","defaultAsset":"EUR/2","id":"eyJDb25uZWN0b3JJRCI6eyJQcm92aWRlciI6Ik1BTkdPUEFZIiwiUmVmZXJlbmNlIjoiNjYwNjc1ODktMDkxZC00YTQxLTg1ZTctYzEyYTRhMWY5ZGY2In0sIlJlZmVyZW5jZSI6IjIwMTY2MTE2NiJ9","provider":"MANGOPAY","reference":"201661166","type":"INTERNAL"},{"accountName":"My big project 2","connectorId":"eyJQcm92aWRlciI6Ik1BTkdPUEFZIiwiUmVmZXJlbmNlIjoiNjYwNjc1ODktMDkxZC00YTQxLTg1ZTctYzEyYTRhMWY5ZGY2In0=","createdAt":"2023-07-25T11:48:44Z","defaultAsset":"USD/2","id":"eyJDb25uZWN0b3JJRCI6eyJQcm92aWRlciI6Ik1BTkdPUEFZIiwiUmVmZXJlbmNlIjoiNjYwNjc1ODktMDkxZC00YTQxLTg1ZTctYzEyYTRhMWY5ZGY2In0sIlJlZmVyZW5jZSI6IjE5ODUzNjY4NSJ9","provider":"MANGOPAY","reference":"198536685","type":"INTERNAL"},{"accountName":"My big project","connectorId":"eyJQcm92aWRlciI6Ik1BTkdPUEFZIiwiUmVmZXJlbmNlIjoiNjYwNjc1ODktMDkxZC00YTQxLTg1ZTctYzEyYTRhMWY5ZGY2In0=","createdAt":"2021-03-25T15:11:17Z","defaultAsset":"EUR/2","id":"eyJDb25uZWN0b3JJRCI6eyJQcm92aWRlciI6Ik1BTkdPUEFZIiwiUmVmZXJlbmNlIjoiNjYwNjc1ODktMDkxZC00YTQxLTg1ZTctYzEyYTRhMWY5ZGY2In0sIlJlZmVyZW5jZSI6IjEwNDg1ODc3OCJ9","provider":"MANGOPAY","reference":"104858778","type":"INTERNAL"},{"accountName":"Joe Blogs","connectorId":"eyJQcm92aWRlciI6Ik1BTkdPUEFZIiwiUmVmZXJlbmNlIjoiNzg0NmFkYTAtN2UyNy00MDFiLTg3MDMtOGJhNDZiYzYwYTQ2In0=","createdAt":"2023-08-14T09:26:07Z","defaultAsset":"","id":"eyJDb25uZWN0b3JJRCI6eyJQcm92aWRlciI6Ik1BTkdPUEFZIiwiUmVmZXJlbmNlIjoiNzg0NmFkYTAtN2UyNy00MDFiLTg3MDMtOGJhNDZiYzYwYTQ2In0sIlJlZmVyZW5jZSI6IjIwMDUxODA5NSJ9","reference":"200518095","type":"EXTERNAL"}],"PAYMENT_BALANCE":[{"accountID":"eyJDb25uZWN0b3JJRCI6eyJQcm92aWRlciI6Ik1BTkdPUEFZIiwiUmVmZXJlbmNlIjoiNjYwNjc1ODktMDkxZC00YTQxLTg1ZTctYzEyYTRhMWY5ZGY2In0sIlJlZmVyZW5jZSI6IjIwMTY2MTE2NiJ9","asset":"EUR/2","balance":30004700,"connectorId":"eyJQcm92aWRlciI6Ik1BTkdPUEFZIiwiUmVmZXJlbmNlIjoiNjYwNjc1ODktMDkxZC00YTQxLTg1ZTctYzEyYTRhMWY5ZGY2In0=","createdAt":"2023-11-09T17:22:47.330609498Z"},{"accountID":"eyJDb25uZWN0b3JJRCI6eyJQcm92aWRlciI6Ik1BTkdPUEFZIiwiUmVmZXJlbmNlIjoiNjYwNjc1ODktMDkxZC00YTQxLTg1ZTctYzEyYTRhMWY5ZGY2In0sIlJlZmVyZW5jZSI6IjE5ODUzNjY4NSJ9","asset":"USD/2","balance":10058699,"connectorId":"eyJQcm92aWRlciI6Ik1BTkdPUEFZIiwiUmVmZXJlbmNlIjoiNjYwNjc1ODktMDkxZC00YTQxLTg1ZTctYzEyYTRhMWY5ZGY2In0=","createdAt":"2023-11-09T17:22:47.330566616Z"},{"accountID":"eyJDb25uZWN0b3JJRCI6eyJQcm92aWRlciI6Ik1PRFVMUiIsIlJlZmVyZW5jZSI6ImYxMmEzMTU4LTRjNzMtNDIzNS04MjY0LWIwMjUxZmExOTgxZSJ9LCJSZWZlcmVuY2UiOiJBMTIxNkdSMSJ9","asset":"GBP/2","balance":28200,"connectorId":"eyJQcm92aWRlciI6Ik1PRFVMUiIsIlJlZmVyZW5jZSI6ImYxMmEzMTU4LTRjNzMtNDIzNS04MjY0LWIwMjUxZmExOTgxZSJ9","createdAt":"2023-11-09T15:40:33.861824482Z"},[{"accountID":"eyJQcm92aWRlciI6Ik1PRFVMUiIsIlJlZmVyZW5jZSI6IkExMjE2RUcyIn0=","asset":"GBP/2","balance":3802050,"createdAt":"2023-11-08T12:50:52.121763165Z"},{"accountID":"eyJQcm92aWRlciI6Ik1PRFVMUiIsIlJlZmVyZW5jZSI6IkExMjE2R1IxIn0=","asset":"GBP/2","balance":28200,"createdAt":"2023-11-08T12:50:52.1217816Z"},{"accountID":"eyJQcm92aWRlciI6Ik1PRFVMUiIsIlJlZmVyZW5jZSI6IkExMjAwQkQ5OU4ifQ==","asset":"GBP/2","balance":69950,"createdAt":"2023-11-08T12:50:52.121792602Z"},{"accountID":"eyJQcm92aWRlciI6Ik1PRFVMUiIsIlJlZmVyZW5jZSI6IkExMjAwQkQ5OVEifQ==","asset":"GBP/2","balance":1099400,"createdAt":"2023-11-08T12:50:52.121805054Z"},{"accountID":"eyJQcm92aWRlciI6Ik1PRFVMUiIsIlJlZmVyZW5jZSI6IkExMjAwQkQ5QjgifQ==","asset":"GBP/2","balance":0,"createdAt":"2023-11-08T12:50:52.12181514Z"},{"accountID":"eyJQcm92aWRlciI6Ik1PRFVMUiIsIlJlZmVyZW5jZSI6IkExMjAwQkQ5QzIifQ==","asset":"GBP/2","balance":0,"createdAt":"2023-11-08T12:50:52.12182285Z"},{"accountID":"eyJQcm92aWRlciI6Ik1PRFVMUiIsIlJlZmVyZW5jZSI6IkExMjAwQkQ5QzUifQ==","asset":"GBP/2","balance":0,"createdAt":"2023-11-08T12:50:52.121833676Z"},{"accountID":"eyJQcm92aWRlciI6Ik1PRFVMUiIsIlJlZmVyZW5jZSI6IkExMjAwQkQ5QzYifQ==","asset":"GBP/2","balance":0,"createdAt":"2023-11-08T12:50:52.121843449Z"},{"accountID":"eyJQcm92aWRlciI6Ik1PRFVMUiIsIlJlZmVyZW5jZSI6IkExMjAwQkQ5QzcifQ==","asset":"GBP/2","balance":0,"createdAt":"2023-11-08T12:50:52.121854829Z"}]]}} \ No newline at end of file diff --git a/components/payments/cmd/connectors/internal/ingestion/transfer_initiation.go b/components/payments/cmd/connectors/internal/ingestion/transfer_initiation.go deleted file mode 100644 index 33fdf4db2f..0000000000 --- a/components/payments/cmd/connectors/internal/ingestion/transfer_initiation.go +++ /dev/null @@ -1,145 +0,0 @@ -package ingestion - -import ( - "context" - "fmt" - "time" - - "github.com/formancehq/go-libs/publish" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/pkg/events" - "github.com/google/uuid" -) - -// In some cases, we want to do the two udpates to the transfer initiations -// (update the payment status and add a related payment id) and send only one -// events for both of them. -func (i *DefaultIngester) UpdateTransferInitiationPayment( - ctx context.Context, - tf *models.TransferInitiation, - paymentID *models.PaymentID, - status models.TransferInitiationStatus, - errorMessage string, - updatedAt time.Time, -) error { - if err := i.addTransferInitiationPaymentID(ctx, tf, paymentID, updatedAt); err != nil { - return err - } - - if err := i.updateTransferInitiationPaymentStatus( - ctx, - tf, - paymentID, - status, - errorMessage, - updatedAt, - ); err != nil { - return err - } - - if err := i.publisher.Publish( - events.TopicPayments, - publish.NewMessage( - ctx, - i.messages.NewEventSavedTransferInitiations(tf), - ), - ); err != nil { - return err - } - - return nil -} - -// Updates only the transfer initiation payment status -func (i *DefaultIngester) UpdateTransferInitiationPaymentsStatus(ctx context.Context, tf *models.TransferInitiation, paymentID *models.PaymentID, status models.TransferInitiationStatus, errorMessage string, updatedAt time.Time) error { - if err := i.updateTransferInitiationPaymentStatus( - ctx, - tf, - paymentID, - status, - errorMessage, - updatedAt, - ); err != nil { - return err - } - - if err := i.publisher.Publish( - events.TopicPayments, - publish.NewMessage( - ctx, - i.messages.NewEventSavedTransferInitiations(tf), - ), - ); err != nil { - return err - } - - return nil -} - -// Only adds a related payment id to the transfer initiation -func (i *DefaultIngester) AddTransferInitiationPaymentID(ctx context.Context, tf *models.TransferInitiation, paymentID *models.PaymentID, updatedAt time.Time) error { - if err := i.addTransferInitiationPaymentID(ctx, tf, paymentID, updatedAt); err != nil { - return err - } - - if err := i.publisher.Publish( - events.TopicPayments, - publish.NewMessage( - ctx, - i.messages.NewEventSavedTransferInitiations(tf), - ), - ); err != nil { - return err - } - - return nil -} - -func (i *DefaultIngester) updateTransferInitiationPaymentStatus( - ctx context.Context, - tf *models.TransferInitiation, - paymentID *models.PaymentID, - status models.TransferInitiationStatus, - errorMessage string, - updatedAt time.Time, -) error { - adjustment := &models.TransferInitiationAdjustment{ - ID: uuid.New(), - TransferInitiationID: tf.ID, - CreatedAt: updatedAt.UTC(), - Status: status, - Error: errorMessage, - } - - tf.RelatedAdjustments = append([]*models.TransferInitiationAdjustment{adjustment}, tf.RelatedAdjustments...) - - if err := i.store.UpdateTransferInitiationPaymentsStatus(ctx, tf.ID, paymentID, adjustment); err != nil { - return err - } - - return nil -} - -func (i *DefaultIngester) addTransferInitiationPaymentID( - ctx context.Context, - tf *models.TransferInitiation, - paymentID *models.PaymentID, - updatedAt time.Time, -) error { - if paymentID == nil { - return fmt.Errorf("payment id is nil") - } - - tf.RelatedPayments = append(tf.RelatedPayments, &models.TransferInitiationPayment{ - TransferInitiationID: tf.ID, - PaymentID: *paymentID, - CreatedAt: updatedAt.UTC(), - Status: models.TransferInitiationStatusProcessing, - }) - - if err := i.store.AddTransferInitiationPaymentID(ctx, tf.ID, paymentID, updatedAt, tf.Metadata); err != nil { - return err - } - - return nil -} diff --git a/components/payments/cmd/connectors/internal/ingestion/transfer_reversal.go b/components/payments/cmd/connectors/internal/ingestion/transfer_reversal.go deleted file mode 100644 index c5db2cec6f..0000000000 --- a/components/payments/cmd/connectors/internal/ingestion/transfer_reversal.go +++ /dev/null @@ -1,44 +0,0 @@ -package ingestion - -import ( - "context" - "math/big" - - "github.com/formancehq/go-libs/publish" - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/pkg/events" - "github.com/google/uuid" -) - -func (i *DefaultIngester) UpdateTransferReversalStatus(ctx context.Context, tf *models.TransferInitiation, transferReversal *models.TransferReversal) error { - finalAmount := new(big.Int) - isFullyReversed := transferReversal.Status == models.TransferReversalStatusProcessed && - finalAmount.Sub(tf.Amount, transferReversal.Amount).Cmp(big.NewInt(0)) == 0 - - adjustment := &models.TransferInitiationAdjustment{ - ID: uuid.New(), - TransferInitiationID: transferReversal.TransferInitiationID, - CreatedAt: transferReversal.UpdatedAt.UTC(), - Status: transferReversal.Status.ToTransferInitiationStatus(isFullyReversed), - Error: transferReversal.Error, - Metadata: transferReversal.Metadata, - } - - if err := i.store.UpdateTransferReversalStatus(ctx, tf, transferReversal, adjustment); err != nil { - return err - } - - tf.RelatedAdjustments = append([]*models.TransferInitiationAdjustment{adjustment}, tf.RelatedAdjustments...) - - if err := i.publisher.Publish( - events.TopicPayments, - publish.NewMessage( - ctx, - i.messages.NewEventSavedTransferInitiations(tf), - ), - ); err != nil { - return err - } - - return nil -} diff --git a/components/payments/cmd/connectors/internal/metrics/metrics.go b/components/payments/cmd/connectors/internal/metrics/metrics.go deleted file mode 100644 index 37364e3773..0000000000 --- a/components/payments/cmd/connectors/internal/metrics/metrics.go +++ /dev/null @@ -1,84 +0,0 @@ -package metrics - -import ( - "go.opentelemetry.io/otel/metric" - "go.opentelemetry.io/otel/metric/noop" -) - -var registry MetricsRegistry - -func GetMetricsRegistry() MetricsRegistry { - if registry == nil { - registry = NewNoOpMetricsRegistry() - } - - return registry -} - -type MetricsRegistry interface { - ConnectorPSPCalls() metric.Int64Counter - ConnectorPSPCallLatencies() metric.Int64Histogram -} - -type metricsRegistry struct { - connectorPSPCalls metric.Int64Counter - connectorPSPCallLatencies metric.Int64Histogram -} - -func RegisterMetricsRegistry(meterProvider metric.MeterProvider) (MetricsRegistry, error) { - meter := meterProvider.Meter("payments") - - connectorPSPCalls, err := meter.Int64Counter( - "payments_connectors_psp_calls", - metric.WithUnit("1"), - metric.WithDescription("payments connectors psp calls"), - ) - if err != nil { - return nil, err - } - - connectorPSPCallLatencies, err := meter.Int64Histogram( - "payments_connectors_psp_calls_latencies", - metric.WithUnit("ms"), - metric.WithDescription("payments connectors psp calls latencies"), - ) - if err != nil { - return nil, err - } - - registry = &metricsRegistry{ - connectorPSPCalls: connectorPSPCalls, - connectorPSPCallLatencies: connectorPSPCallLatencies, - } - - return registry, nil -} - -func (m *metricsRegistry) ConnectorPSPCalls() metric.Int64Counter { - return m.connectorPSPCalls -} - -func (m *metricsRegistry) ConnectorPSPCallLatencies() metric.Int64Histogram { - return m.connectorPSPCallLatencies -} - -type NoopMetricsRegistry struct{} - -func NewNoOpMetricsRegistry() *NoopMetricsRegistry { - return &NoopMetricsRegistry{} -} - -func (m *NoopMetricsRegistry) ConnectorPSPCalls() metric.Int64Counter { - counter, _ := noop.NewMeterProvider().Meter("payments").Int64Counter("payments_connectors_psp_calls") - return counter -} - -func (m *NoopMetricsRegistry) ConnectorPSPCallLatencies() metric.Int64Histogram { - histogram, _ := noop.NewMeterProvider().Meter("payments").Int64Histogram("payments_connectors_psp_calls_latencies") - return histogram -} - -var ( - _ MetricsRegistry = (*metricsRegistry)(nil) - _ MetricsRegistry = (*NoopMetricsRegistry)(nil) -) diff --git a/components/payments/cmd/connectors/internal/storage/accounts.go b/components/payments/cmd/connectors/internal/storage/accounts.go deleted file mode 100644 index 15aa8c9593..0000000000 --- a/components/payments/cmd/connectors/internal/storage/accounts.go +++ /dev/null @@ -1,85 +0,0 @@ -package storage - -import ( - "context" - - "github.com/formancehq/payments/internal/models" -) - -func (s *Storage) UpsertAccounts(ctx context.Context, accounts []*models.Account) ([]models.AccountID, error) { - if len(accounts) == 0 { - return nil, nil - } - - var idsUpdated []string - err := s.db.NewUpdate(). - With("_data", - s.db.NewValues(&accounts). - Column( - "id", - "default_currency", - "account_name", - "metadata", - ), - ). - Model((*models.Account)(nil)). - TableExpr("_data"). - Set("default_currency = _data.default_currency"). - Set("account_name = _data.account_name"). - Set("metadata = _data.metadata"). - Where(`(account.id = _data.id) AND - (account.default_currency != _data.default_currency OR account.account_name != _data.account_name OR (account.metadata != _data.metadata))`). - Returning("account.id"). - Scan(ctx, &idsUpdated) - if err != nil { - return nil, e("failed to update accounts", err) - } - - idsUpdatedMap := make(map[string]struct{}) - for _, id := range idsUpdated { - idsUpdatedMap[id] = struct{}{} - } - - accountsToInsert := make([]*models.Account, 0, len(accounts)) - for _, account := range accounts { - if _, ok := idsUpdatedMap[account.ID.String()]; !ok { - accountsToInsert = append(accountsToInsert, account) - } - } - - var idsInserted []string - if len(accountsToInsert) > 0 { - err = s.db.NewInsert(). - Model(&accountsToInsert). - On("CONFLICT (id) DO NOTHING"). - Returning("account.id"). - Scan(ctx, &idsInserted) - if err != nil { - return nil, e("failed to create accounts", err) - } - } - - res := make([]models.AccountID, 0, len(idsUpdated)+len(idsInserted)) - for _, id := range idsUpdated { - res = append(res, models.MustAccountIDFromString(id)) - } - for _, id := range idsInserted { - res = append(res, models.MustAccountIDFromString(id)) - } - - return res, nil -} - -func (s *Storage) GetAccount(ctx context.Context, id string) (*models.Account, error) { - var account models.Account - - err := s.db.NewSelect(). - Model(&account). - Where("id = ?", id). - Scan(ctx) - if err != nil { - return nil, e("failed to get account", err) - } - - return &account, nil -} diff --git a/components/payments/cmd/connectors/internal/storage/accounts_test.go b/components/payments/cmd/connectors/internal/storage/accounts_test.go deleted file mode 100644 index b31f15afed..0000000000 --- a/components/payments/cmd/connectors/internal/storage/accounts_test.go +++ /dev/null @@ -1,174 +0,0 @@ -package storage_test - -import ( - "context" - "testing" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/stretchr/testify/require" -) - -var ( - acc1ID models.AccountID - acc1T = time.Date(2023, 11, 14, 4, 59, 0, 0, time.UTC) - - acc2ID models.AccountID - acc2T = time.Date(2023, 11, 14, 4, 58, 0, 0, time.UTC) - - acc3ID models.AccountID - acc3T = time.Date(2023, 11, 14, 4, 57, 0, 0, time.UTC) -) - -func TestAccounts(t *testing.T) { - store := newStore(t) - - testInstallConnectors(t, store) - testCreateAccounts(t, store) - testUpdateAccounts(t, store) - testUninstallConnectors(t, store) - testAccountsDeletedAfterConnectorUninstall(t, store) -} - -func testCreateAccounts(t *testing.T, store *storage.Storage) { - acc1ID = models.AccountID{ - Reference: "test1", - ConnectorID: connectorID, - } - acc2ID = models.AccountID{ - Reference: "test2", - ConnectorID: connectorID, - } - acc3ID = models.AccountID{ - Reference: "test3", - ConnectorID: connectorID, - } - - acc1 := &models.Account{ - ID: acc1ID, - CreatedAt: acc1T, - Reference: "test1", - ConnectorID: connectorID, - DefaultAsset: "USD", - AccountName: "test1", - Type: models.AccountTypeInternal, - Metadata: map[string]string{ - "foo": "bar", - }, - } - - acc2 := &models.Account{ - ID: acc2ID, - CreatedAt: acc2T, - Reference: "test2", - ConnectorID: connectorID, - Type: models.AccountTypeExternal, - } - - acc3 := &models.Account{ - ID: acc3ID, - CreatedAt: acc3T, - Reference: "test3", - ConnectorID: connectorID, - Type: models.AccountTypeInternal, - } - - connectorIDFail := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - accFail := &models.Account{ - ID: models.AccountID{Reference: "test4", ConnectorID: connectorIDFail}, - CreatedAt: acc3T, - Reference: "test4", - ConnectorID: connectorIDFail, - Type: models.AccountTypeInternal, - } - - // Try to insert accounts from a not installed connector - _, err := store.UpsertAccounts( - context.Background(), - []*models.Account{accFail}, - ) - require.Error(t, err) - - idsInserted, err := store.UpsertAccounts( - context.Background(), - []*models.Account{acc1, acc2, acc3}, - ) - require.NoError(t, err) - require.Len(t, idsInserted, 3) - require.Equal(t, acc1ID, idsInserted[0]) - require.Equal(t, acc2ID, idsInserted[1]) - require.Equal(t, acc3ID, idsInserted[2]) - - testGetAccount(t, store, acc1.ID, acc1, false) - testGetAccount(t, store, acc2.ID, acc2, false) - testGetAccount(t, store, acc3.ID, acc3, false) - testGetAccount(t, store, models.AccountID{Reference: "test4", ConnectorID: connectorID}, nil, true) -} - -func testGetAccount( - t *testing.T, - store *storage.Storage, - id models.AccountID, - expectedAccount *models.Account, - expectedError bool, -) { - account, err := store.GetAccount(context.Background(), id.String()) - if expectedError { - require.Error(t, err) - return - } else { - require.NoError(t, err) - } - - account.CreatedAt = account.CreatedAt.UTC() - require.Equal(t, expectedAccount, account) -} - -func testUpdateAccounts(t *testing.T, store *storage.Storage) { - acc1Updated := &models.Account{ - ID: acc1ID, - CreatedAt: time.Date(2023, 11, 14, 5, 59, 0, 0, time.UTC), // New timestamps, but should not be updated in the database - Reference: "test1", - ConnectorID: connectorID, - DefaultAsset: "EUR", - AccountName: "test1-update", - Type: models.AccountTypeInternal, - Metadata: map[string]string{ - "foo2": "bar2", - }, - } - - idsInserted, err := store.UpsertAccounts( - context.Background(), - []*models.Account{acc1Updated}, - ) - require.NoError(t, err) - require.Len(t, idsInserted, 1) - require.Equal(t, acc1ID, idsInserted[0]) - - // CreatedAt should not be updated - acc1Updated.CreatedAt = acc1T - testGetAccount(t, store, acc1Updated.ID, acc1Updated, false) - - // Upsert again with the same values - idsInserted, err = store.UpsertAccounts( - context.Background(), - []*models.Account{acc1Updated}, - ) - require.NoError(t, err) - require.Len(t, idsInserted, 0) // Should not be updated or inserted - - testGetAccount(t, store, acc1Updated.ID, acc1Updated, false) -} - -func testAccountsDeletedAfterConnectorUninstall(t *testing.T, store *storage.Storage) { - // Accounts should be deleted after uninstalling the connector - testGetAccount(t, store, acc1ID, nil, true) - testGetAccount(t, store, acc2ID, nil, true) - testGetAccount(t, store, acc3ID, nil, true) -} diff --git a/components/payments/cmd/connectors/internal/storage/balance_test.go b/components/payments/cmd/connectors/internal/storage/balance_test.go deleted file mode 100644 index cfb2b7a912..0000000000 --- a/components/payments/cmd/connectors/internal/storage/balance_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package storage_test - -import ( - "context" - "math/big" - "testing" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/stretchr/testify/require" -) - -var ( - b1T = time.Date(2023, 11, 14, 5, 1, 10, 0, time.UTC) - b2T = time.Date(2023, 11, 14, 5, 1, 20, 0, time.UTC) -) - -func TestBalances(t *testing.T) { - store := newStore(t) - - testInstallConnectors(t, store) - testCreateAccounts(t, store) - testCreateBalances(t, store) - testUninstallConnectors(t, store) - testBalancesDeletedAfterConnectorUninstall(t, store) -} - -func testCreateBalances(t *testing.T, store *storage.Storage) { - b1 := &models.Balance{ - AccountID: models.AccountID{ - Reference: "not_existing", - ConnectorID: connectorID, - }, - Asset: "USD", - Balance: big.NewInt(int64(100)), - CreatedAt: b1T, - LastUpdatedAt: b1T, - } - - // Cannot insert balance for non-existing account - err := store.InsertBalances(context.Background(), []*models.Balance{b1}, false) - require.Error(t, err) - - // When inserting with ignore, no error is returned - err = store.InsertBalances(context.Background(), []*models.Balance{b1}, true) - require.NoError(t, err) - - b1.AccountID = acc1ID - err = store.InsertBalances(context.Background(), []*models.Balance{b1}, true) - require.NoError(t, err) - - b2 := &models.Balance{ - AccountID: acc1ID, - Asset: "USD", - Balance: big.NewInt(int64(200)), - CreatedAt: b2T, - LastUpdatedAt: b2T, - } - err = store.InsertBalances(context.Background(), []*models.Balance{b2}, true) - require.NoError(t, err) - - testGetBalance(t, store, acc1ID, []*models.Balance{b2, b1}, nil) -} - -func testGetBalance( - t *testing.T, - store *storage.Storage, - accountID models.AccountID, - expectedBalances []*models.Balance, - expectedError error, -) { - balances, err := store.GetBalancesForAccountID(context.Background(), accountID) - require.NoError(t, err) - require.Len(t, balances, len(expectedBalances)) - for i := range balances { - if i < len(balances)-1 { - require.Equal(t, balances[i+1].LastUpdatedAt.UTC(), balances[i].CreatedAt.UTC()) - } - require.Equal(t, expectedBalances[i].CreatedAt.UTC(), balances[i].CreatedAt.UTC()) - require.Equal(t, expectedBalances[i].AccountID, balances[i].AccountID) - require.Equal(t, expectedBalances[i].Asset, balances[i].Asset) - require.Equal(t, expectedBalances[i].Balance, balances[i].Balance) - } -} - -func testBalancesDeletedAfterConnectorUninstall(t *testing.T, store *storage.Storage) { - balances, err := store.GetBalancesForAccountID(context.Background(), acc1ID) - require.NoError(t, err) - require.Len(t, balances, 0) -} diff --git a/components/payments/cmd/connectors/internal/storage/balances.go b/components/payments/cmd/connectors/internal/storage/balances.go deleted file mode 100644 index a58e75d49c..0000000000 --- a/components/payments/cmd/connectors/internal/storage/balances.go +++ /dev/null @@ -1,75 +0,0 @@ -package storage - -import ( - "context" - - "github.com/formancehq/payments/internal/models" -) - -func (s *Storage) InsertBalances(ctx context.Context, balances []*models.Balance, checkIfAccountExists bool) error { - if len(balances) == 0 { - return nil - } - - query := s.db.NewInsert(). - Model((*models.Balance)(nil)). - With("cte1", s.db.NewValues(&balances)). - Column( - "created_at", - "account_id", - "balance", - "currency", - "last_updated_at", - ) - if checkIfAccountExists { - query = query.TableExpr(` - (SELECT * - FROM cte1 - WHERE EXISTS (SELECT 1 FROM accounts.account WHERE id = cte1.account_id) - AND cte1.balance != COALESCE((SELECT balance FROM accounts.balances WHERE account_id = cte1.account_id AND last_updated_at < cte1.last_updated_at AND currency = cte1.currency ORDER BY last_updated_at DESC LIMIT 1), cte1.balance+1) - ) data`) - } else { - query = query.TableExpr(` - (SELECT * - FROM cte1 - WHERE cte1.balance != COALESCE((SELECT balance FROM accounts.balances WHERE account_id = cte1.account_id AND last_updated_at < cte1.last_updated_at AND currency = cte1.currency ORDER BY last_updated_at DESC LIMIT 1), cte1.balance+1) - ) data`) - } - - query = query.On("CONFLICT (account_id, created_at, currency) DO NOTHING") - - _, err := query.Exec(ctx) - if err != nil { - return e("failed to create balances", err) - } - - // Always update the previous row in order to keep the balance history consistent. - _, err = s.db.NewUpdate(). - Model((*models.Balance)(nil)). - With("cte1", s.db.NewValues(&balances)). - TableExpr(` - (SELECT (SELECT created_at FROM accounts.balances WHERE last_updated_at < cte1.last_updated_at AND account_id = cte1.account_id AND currency = cte1.currency ORDER BY last_updated_at DESC LIMIT 1), cte1.account_id, cte1.currency, cte1.last_updated_at FROM cte1) data - `). - Set("last_updated_at = data.last_updated_at"). - Where("balance.account_id = data.account_id AND balance.currency = data.currency AND balance.created_at = data.created_at"). - Exec(ctx) - if err != nil { - return e("failed to update balances", err) - } - - return nil -} - -func (s *Storage) GetBalancesForAccountID(ctx context.Context, accountID models.AccountID) ([]*models.Balance, error) { - var balances []*models.Balance - - err := s.db.NewSelect(). - Model(&balances). - Where("account_id = ?", accountID). - Scan(ctx) - if err != nil { - return nil, e("failed to get balances", err) - } - - return balances, nil -} diff --git a/components/payments/cmd/connectors/internal/storage/bank_accounts.go b/components/payments/cmd/connectors/internal/storage/bank_accounts.go deleted file mode 100644 index b751a94cfb..0000000000 --- a/components/payments/cmd/connectors/internal/storage/bank_accounts.go +++ /dev/null @@ -1,134 +0,0 @@ -package storage - -import ( - "context" - - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" -) - -func (s *Storage) CreateBankAccount(ctx context.Context, bankAccount *models.BankAccount) error { - account := models.BankAccount{ - CreatedAt: bankAccount.CreatedAt, - Country: bankAccount.Country, - Name: bankAccount.Name, - Metadata: bankAccount.Metadata, - } - - var id uuid.UUID - err := s.db.NewInsert().Model(&account).Returning("id").Scan(ctx, &id) - if err != nil { - return e("install connector", err) - } - bankAccount.ID = id - - return s.updateBankAccountInformation(ctx, id, bankAccount.AccountNumber, bankAccount.IBAN, bankAccount.SwiftBicCode) -} - -func (s *Storage) AddBankAccountRelatedAccount(ctx context.Context, relatedAccount *models.BankAccountRelatedAccount) error { - _, err := s.db.NewInsert().Model(relatedAccount).Exec(ctx) - if err != nil { - return e("add bank account related account", err) - } - - return nil -} - -func (s *Storage) updateBankAccountInformation(ctx context.Context, id uuid.UUID, accountNumber, iban, swiftBicCode string) error { - _, err := s.db.NewUpdate(). - Model(&models.BankAccount{}). - Set("account_number = pgp_sym_encrypt(?::TEXT, ?, ?)", accountNumber, s.configEncryptionKey, encryptionOptions). - Set("iban = pgp_sym_encrypt(?::TEXT, ?, ?)", iban, s.configEncryptionKey, encryptionOptions). - Set("swift_bic_code = pgp_sym_encrypt(?::TEXT, ?, ?)", swiftBicCode, s.configEncryptionKey, encryptionOptions). - Where("id = ?", id). - Exec(ctx) - if err != nil { - return e("update bank account information", err) - } - - return nil -} - -func (s *Storage) UpdateBankAccountMetadata(ctx context.Context, id uuid.UUID, metadata map[string]string) error { - tx, err := s.db.BeginTx(ctx, nil) - if err != nil { - return e("update bank account metadata", err) - } - defer tx.Rollback() - - var account models.BankAccount - err = tx.NewSelect(). - Model(&account). - Column("id", "metadata"). - Where("id = ?", id). - Scan(ctx) - if err != nil { - return e("update bank account metadata", err) - } - - if account.Metadata == nil { - account.Metadata = make(map[string]string) - } - - for k, v := range metadata { - account.Metadata[k] = v - } - - _, err = s.db.NewUpdate(). - Model(&account). - Column("metadata"). - Where("id = ?", id). - Exec(ctx) - if err != nil { - return e("update bank account metadata", err) - } - - return e("failed to commit transaction", tx.Commit()) -} - -func (s *Storage) LinkBankAccountWithAccount(ctx context.Context, id uuid.UUID, accountID *models.AccountID) error { - relatedAccount := &models.BankAccountRelatedAccount{ - ID: uuid.New(), - BankAccountID: id, - ConnectorID: accountID.ConnectorID, - AccountID: *accountID, - } - - return s.AddBankAccountRelatedAccount(ctx, relatedAccount) -} - -func (s *Storage) GetBankAccount(ctx context.Context, id uuid.UUID, expand bool) (*models.BankAccount, error) { - var account models.BankAccount - query := s.db.NewSelect(). - Model(&account). - Relation("RelatedAccounts"). - Column("id", "created_at", "name", "created_at", "country", "metadata") - - if expand { - query = query.ColumnExpr("pgp_sym_decrypt(account_number, ?, ?) AS decrypted_account_number", s.configEncryptionKey, encryptionOptions). - ColumnExpr("pgp_sym_decrypt(iban, ?, ?) AS decrypted_iban", s.configEncryptionKey, encryptionOptions). - ColumnExpr("pgp_sym_decrypt(swift_bic_code, ?, ?) AS decrypted_swift_bic_code", s.configEncryptionKey, encryptionOptions) - } - - err := query. - Where("id = ?", id). - Scan(ctx) - if err != nil { - return nil, e("get bank account", err) - } - - return &account, nil -} - -func (s *Storage) GetBankAccountRelatedAccounts(ctx context.Context, id uuid.UUID) ([]*models.BankAccountRelatedAccount, error) { - var relatedAccounts []*models.BankAccountRelatedAccount - err := s.db.NewSelect(). - Model(&relatedAccounts). - Where("bank_account_id = ?", id). - Scan(ctx) - if err != nil { - return nil, e("get bank account related accounts", err) - } - - return relatedAccounts, nil -} diff --git a/components/payments/cmd/connectors/internal/storage/bank_accounts_test.go b/components/payments/cmd/connectors/internal/storage/bank_accounts_test.go deleted file mode 100644 index 35772a9f85..0000000000 --- a/components/payments/cmd/connectors/internal/storage/bank_accounts_test.go +++ /dev/null @@ -1,159 +0,0 @@ -package storage_test - -import ( - "context" - "testing" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/pkg/errors" - "github.com/stretchr/testify/require" -) - -var ( - bankAccount1ID uuid.UUID - bankAccount2ID uuid.UUID - - bankAccount1T = time.Date(2023, 11, 14, 5, 2, 0, 0, time.UTC) - bankAccount2T = time.Date(2023, 11, 14, 5, 1, 0, 0, time.UTC) -) - -func TestBankAccounts(t *testing.T) { - store := newStore(t) - - testInstallConnectors(t, store) - testCreateAccounts(t, store) - testCreateBankAccounts(t, store) - testUpdateBankAccountMetadata(t, store) - testUninstallConnectors(t, store) - testBankAccountsDeletedAfterConnectorUninstall(t, store) -} - -func testCreateBankAccounts(t *testing.T, store *storage.Storage) { - bankAccount1 := &models.BankAccount{ - CreatedAt: bankAccount1T, - Name: "test1", - IBAN: "FR7630006000011234567890189", - SwiftBicCode: "BNPAFRPPXXX", - Country: "FR", - } - - err := store.CreateBankAccount(context.Background(), bankAccount1) - require.NoError(t, err) - require.NotEqual(t, uuid.Nil, bankAccount1.ID) - bankAccount1ID = bankAccount1.ID - - bankAccount2 := &models.BankAccount{ - CreatedAt: bankAccount2T, - Name: "test2", - AccountNumber: "123456789", - Country: "FR", - } - - err = store.CreateBankAccount(context.Background(), bankAccount2) - require.NoError(t, err) - require.NotEqual(t, uuid.Nil, bankAccount2.ID) - bankAccount2ID = bankAccount2.ID - - relatedAccount := &models.BankAccountRelatedAccount{ - ID: uuid.New(), - CreatedAt: bankAccount2T, - BankAccountID: bankAccount2ID, - ConnectorID: connectorID, - AccountID: acc1ID, - } - err = store.AddBankAccountRelatedAccount(context.Background(), relatedAccount) - require.NoError(t, err) - bankAccount2.RelatedAccounts = append(bankAccount2.RelatedAccounts, relatedAccount) - - err = store.AddBankAccountRelatedAccount(context.Background(), &models.BankAccountRelatedAccount{ - ID: uuid.New(), - CreatedAt: bankAccount2T, - BankAccountID: bankAccount2ID, - ConnectorID: connectorID, - AccountID: models.AccountID{ - Reference: "not_existing", - ConnectorID: connectorID, - }, - }) - require.Error(t, err) - - testGetBankAccount(t, store, bankAccount1ID, true, bankAccount1, nil) - testGetBankAccount(t, store, bankAccount2ID, true, bankAccount2, nil) -} - -func testGetBankAccount( - t *testing.T, - store *storage.Storage, - bankAccountID uuid.UUID, - expand bool, - expectedBankAccount *models.BankAccount, - expectedError error, -) { - bankAccount, err := store.GetBankAccount(context.Background(), bankAccountID, expand) - if expectedError != nil { - require.EqualError(t, err, expectedError.Error()) - return - } else { - require.NoError(t, err) - } - - require.Equal(t, bankAccount.Country, expectedBankAccount.Country) - require.Equal(t, bankAccount.CreatedAt.UTC(), expectedBankAccount.CreatedAt.UTC()) - require.Equal(t, bankAccount.Name, expectedBankAccount.Name) - - if expand { - require.Equal(t, bankAccount.SwiftBicCode, expectedBankAccount.SwiftBicCode) - require.Equal(t, bankAccount.IBAN, expectedBankAccount.IBAN) - require.Equal(t, bankAccount.AccountNumber, expectedBankAccount.AccountNumber) - } - - require.Len(t, bankAccount.RelatedAccounts, len(expectedBankAccount.RelatedAccounts)) - for i, adj := range bankAccount.RelatedAccounts { - require.Equal(t, adj.BankAccountID, expectedBankAccount.RelatedAccounts[i].BankAccountID) - require.Equal(t, adj.CreatedAt.UTC(), expectedBankAccount.RelatedAccounts[i].CreatedAt.UTC()) - require.Equal(t, adj.ConnectorID, expectedBankAccount.RelatedAccounts[i].ConnectorID) - require.Equal(t, adj.AccountID, expectedBankAccount.RelatedAccounts[i].AccountID) - } -} - -func testUpdateBankAccountMetadata(t *testing.T, store *storage.Storage) { - metadata := map[string]string{ - "key": "value", - } - - err := store.UpdateBankAccountMetadata(context.Background(), bankAccount1ID, metadata) - require.NoError(t, err) - - bankAccount, err := store.GetBankAccount(context.Background(), bankAccount1ID, false) - require.NoError(t, err) - require.Equal(t, metadata, bankAccount.Metadata) - - // Bank account not existing - err = store.UpdateBankAccountMetadata(context.Background(), uuid.New(), metadata) - require.True(t, errors.Is(err, storage.ErrNotFound)) -} - -func testBankAccountsDeletedAfterConnectorUninstall(t *testing.T, store *storage.Storage) { - // Connector has been uninstalled, related adjustments are deleted, but not the bank - // accounts themselves. - bankAccount1 := &models.BankAccount{ - CreatedAt: bankAccount1T, - Name: "test1", - IBAN: "FR7630006000011234567890189", - SwiftBicCode: "BNPAFRPPXXX", - Country: "FR", - } - - bankAccount2 := &models.BankAccount{ - CreatedAt: bankAccount2T, - Name: "test2", - AccountNumber: "123456789", - Country: "FR", - } - - testGetBankAccount(t, store, bankAccount1ID, true, bankAccount1, nil) - testGetBankAccount(t, store, bankAccount2ID, true, bankAccount2, nil) -} diff --git a/components/payments/cmd/connectors/internal/storage/connectors.go b/components/payments/cmd/connectors/internal/storage/connectors.go deleted file mode 100644 index a7f6a7f2b7..0000000000 --- a/components/payments/cmd/connectors/internal/storage/connectors.go +++ /dev/null @@ -1,131 +0,0 @@ -package storage - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/formancehq/payments/internal/models" -) - -func (s *Storage) ListConnectors(ctx context.Context) ([]*models.Connector, error) { - var connectors []*models.Connector - - err := s.db.NewSelect(). - Model(&connectors). - ColumnExpr("*, pgp_sym_decrypt(config, ?, ?) AS decrypted_config", s.configEncryptionKey, encryptionOptions). - Scan(ctx) - if err != nil { - return nil, e("list connectors", err) - } - - return connectors, nil -} - -func (s *Storage) ListConnectorsByProvider(ctx context.Context, provider models.ConnectorProvider) ([]*models.Connector, error) { - var connectors []*models.Connector - - err := s.db.NewSelect(). - Model(&connectors). - ColumnExpr("*, pgp_sym_decrypt(config, ?, ?) AS decrypted_config", s.configEncryptionKey, encryptionOptions). - Where("provider = ?", provider). - Scan(ctx) - if err != nil { - return nil, e("list connectors", err) - } - - return connectors, nil -} - -func (s *Storage) GetConfig(ctx context.Context, connectorID models.ConnectorID, destination any) error { - var connector models.Connector - - err := s.db.NewSelect(). - Model(&connector). - ColumnExpr("pgp_sym_decrypt(config, ?, ?) AS decrypted_config", s.configEncryptionKey, encryptionOptions). - Where("id = ?", connectorID). - Scan(ctx) - if err != nil { - return e(fmt.Sprintf("failed to get config for connector %s", connectorID), err) - } - - err = json.Unmarshal(connector.Config, destination) - if err != nil { - return e(fmt.Sprintf("failed to unmarshal config for connector %s", connectorID), err) - } - - return nil -} - -func (s *Storage) IsInstalledByConnectorID(ctx context.Context, connectorID models.ConnectorID) (bool, error) { - exists, err := s.db.NewSelect(). - Model(&models.Connector{}). - Where("id = ?", connectorID). - Exists(ctx) - if err != nil { - return false, e("find connector", err) - } - - return exists, nil -} - -func (s *Storage) IsInstalledByConnectorName(ctx context.Context, name string) (bool, error) { - exists, err := s.db.NewSelect(). - Model(&models.Connector{}). - Where("name = ?", name). - Exists(ctx) - if err != nil { - return false, e("find connector", err) - } - - return exists, nil -} - -func (s *Storage) Install(ctx context.Context, connector *models.Connector, config json.RawMessage) error { - _, err := s.db.NewInsert().Model(connector).Exec(ctx) - if err != nil { - return e("install connector", err) - } - - return s.UpdateConfig(ctx, connector.ID, config) -} - -func (s *Storage) Uninstall(ctx context.Context, connectorID models.ConnectorID) error { - _, err := s.db.NewDelete(). - Model(&models.Connector{}). - Where("id = ?", connectorID). - Exec(ctx) - if err != nil { - return e("uninstall connector", err) - } - - return nil -} - -func (s *Storage) UpdateConfig(ctx context.Context, connectorID models.ConnectorID, config json.RawMessage) error { - _, err := s.db.NewUpdate(). - Model(&models.Connector{}). - Set("config = pgp_sym_encrypt(?::TEXT, ?, ?)", config, s.configEncryptionKey, encryptionOptions). - Where("id = ?", connectorID). // Connector name is unique - Exec(ctx) - if err != nil { - return e("update connector config", err) - } - - return nil -} - -func (s *Storage) GetConnector(ctx context.Context, connectorID models.ConnectorID) (*models.Connector, error) { - var connector models.Connector - - err := s.db.NewSelect(). - Model(&connector). - ColumnExpr("*, pgp_sym_decrypt(config, ?, ?) AS decrypted_config", s.configEncryptionKey, encryptionOptions). - Where("id = ?", connectorID). - Scan(ctx) - if err != nil { - return nil, e("find connector", err) - } - - return &connector, nil -} diff --git a/components/payments/cmd/connectors/internal/storage/connectors_test.go b/components/payments/cmd/connectors/internal/storage/connectors_test.go deleted file mode 100644 index f941fc7a55..0000000000 --- a/components/payments/cmd/connectors/internal/storage/connectors_test.go +++ /dev/null @@ -1,99 +0,0 @@ -package storage_test - -import ( - "context" - "encoding/json" - "testing" - - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/stretchr/testify/require" -) - -var ( - connectorID = models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } -) - -func TestConnectors(t *testing.T) { - store := newStore(t) - - testInstallConnectors(t, store) - testIsInstalledConnectors(t, store) - testUpdateConfig(t, store) - testUninstallConnectors(t, store) - testAfterInstallationConnectors(t, store) -} - -func testInstallConnectors(t *testing.T, store *storage.Storage) { - connector1 := models.Connector{ - ID: connectorID, - Name: "test1", - Provider: models.ConnectorProviderDummyPay, - } - err := store.Install( - context.Background(), - &connector1, - json.RawMessage([]byte(`{"foo": "bar"}`)), - ) - require.NoError(t, err) - - err = store.Install( - context.Background(), - &connector1, - json.RawMessage([]byte(`{"foo": "bar"}`)), - ) - require.Equal(t, storage.ErrDuplicateKeyValue, err) - - testGetConnector(t, store, connectorID, []byte(`{"foo": "bar"}`)) -} - -func testGetConnector(t *testing.T, store *storage.Storage, connectorID models.ConnectorID, expectedConfig []byte) { - var config json.RawMessage - err := store.GetConfig(context.Background(), connectorID, &config) - require.NoError(t, err) - require.Equal(t, json.RawMessage(expectedConfig), config) -} - -func testUpdateConfig(t *testing.T, store *storage.Storage) { - err := store.UpdateConfig(context.Background(), connectorID, json.RawMessage([]byte(`{"foo2": "bar2"}`))) - require.NoError(t, err) - - testGetConnector(t, store, connectorID, []byte(`{"foo2": "bar2"}`)) -} - -func testIsInstalledConnectors(t *testing.T, store *storage.Storage) { - isInstalled, err := store.IsInstalledByConnectorID( - context.Background(), - models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - }) - require.NoError(t, err) - require.False(t, isInstalled) - - isInstalled, err = store.IsInstalledByConnectorID(context.Background(), connectorID) - require.NoError(t, err) - require.True(t, isInstalled) -} - -func testUninstallConnectors(t *testing.T, store *storage.Storage) { - // No error if deleting an unknown connector - err := store.Uninstall(context.Background(), models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - }) - require.NoError(t, err) - - err = store.Uninstall(context.Background(), connectorID) - require.NoError(t, err) -} - -func testAfterInstallationConnectors(t *testing.T, store *storage.Storage) { - isInstalled, err := store.IsInstalledByConnectorID(context.Background(), connectorID) - require.NoError(t, err) - require.False(t, isInstalled) -} diff --git a/components/payments/cmd/connectors/internal/storage/error.go b/components/payments/cmd/connectors/internal/storage/error.go deleted file mode 100644 index 3cd5148877..0000000000 --- a/components/payments/cmd/connectors/internal/storage/error.go +++ /dev/null @@ -1,29 +0,0 @@ -package storage - -import ( - "database/sql" - "fmt" - - "github.com/jackc/pgx/v5/pgconn" - "github.com/pkg/errors" -) - -var ErrNotFound = errors.New("not found") -var ErrDuplicateKeyValue = errors.New("duplicate key value") - -func e(msg string, err error) error { - if err == nil { - return nil - } - - var pgErr *pgconn.PgError - if errors.As(err, &pgErr) && pgErr.Code == "23505" { - return ErrDuplicateKeyValue - } - - if errors.Is(err, sql.ErrNoRows) { - return ErrNotFound - } - - return fmt.Errorf("%s: %w", msg, err) -} diff --git a/components/payments/cmd/connectors/internal/storage/main_test.go b/components/payments/cmd/connectors/internal/storage/main_test.go deleted file mode 100644 index a6ae2b2e3f..0000000000 --- a/components/payments/cmd/connectors/internal/storage/main_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package storage_test - -import ( - "context" - "crypto/rand" - "testing" - - "github.com/formancehq/go-libs/testing/docker" - "github.com/formancehq/go-libs/testing/utils" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/go-libs/testing/platform/pgtesting" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - migrationstorage "github.com/formancehq/payments/internal/storage" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/stdlib" - "github.com/stretchr/testify/require" - "github.com/uptrace/bun" - "github.com/uptrace/bun/dialect/pgdialect" -) - -var ( - srv *pgtesting.PostgresServer -) - -func TestMain(m *testing.M) { - utils.WithTestMain(func(t *utils.TestingTForMain) int { - srv = pgtesting.CreatePostgresServer(t, docker.NewPool(t, logging.Testing())) - - return m.Run() - }) -} - -func newStore(t *testing.T) *storage.Storage { - t.Helper() - - pgServer := srv.NewDatabase(t) - - config, err := pgx.ParseConfig(pgServer.ConnString()) - require.NoError(t, err) - - key := make([]byte, 64) - _, err = rand.Read(key) - require.NoError(t, err) - - db := bun.NewDB(stdlib.OpenDB(*config), pgdialect.New()) - t.Cleanup(func() { - _ = db.Close() - }) - - err = migrationstorage.Migrate(context.Background(), db) - require.NoError(t, err) - - store := storage.NewStorage( - db, - string(key), - ) - - return store -} diff --git a/components/payments/cmd/connectors/internal/storage/metadata.go b/components/payments/cmd/connectors/internal/storage/metadata.go deleted file mode 100644 index 723337bd6e..0000000000 --- a/components/payments/cmd/connectors/internal/storage/metadata.go +++ /dev/null @@ -1,39 +0,0 @@ -package storage - -import ( - "context" - "time" - - "github.com/formancehq/payments/internal/models" -) - -func (s *Storage) UpdatePaymentMetadata(ctx context.Context, paymentID models.PaymentID, metadata map[string]string) error { - var metadataToInsert []models.PaymentMetadata // nolint:prealloc // it's against a map - - for key, value := range metadata { - metadataToInsert = append(metadataToInsert, models.PaymentMetadata{ - PaymentID: paymentID, - Key: key, - Value: value, - Changelog: []models.MetadataChangelog{ - { - CreatedAt: time.Now(), - Value: value, - }, - }, - }) - } - - _, err := s.db.NewInsert(). - Model(&metadataToInsert). - On("CONFLICT (payment_id, key) DO UPDATE"). - Set("value = EXCLUDED.value"). - Set("changelog = metadata.changelog || EXCLUDED.changelog"). - Where("metadata.value != EXCLUDED.value"). - Exec(ctx) - if err != nil { - return e("failed to update payment metadata", err) - } - - return nil -} diff --git a/components/payments/cmd/connectors/internal/storage/module.go b/components/payments/cmd/connectors/internal/storage/module.go deleted file mode 100644 index cbdf9517ec..0000000000 --- a/components/payments/cmd/connectors/internal/storage/module.go +++ /dev/null @@ -1,31 +0,0 @@ -package storage - -import ( - "context" - - "github.com/formancehq/go-libs/bun/bunconnect" - "github.com/formancehq/go-libs/logging" - "github.com/uptrace/bun" - "go.uber.org/fx" -) - -func Module(connectionOptions bunconnect.ConnectionOptions, configEncryptionKey string, debug bool) fx.Option { - return fx.Options( - fx.Supply(&connectionOptions), - bunconnect.Module(connectionOptions, debug), - fx.Provide(func(db *bun.DB) *Storage { - return NewStorage(db, configEncryptionKey) - }), - fx.Invoke(func(lc fx.Lifecycle, repo *Storage) { - lc.Append(fx.Hook{ - OnStart: func(ctx context.Context) error { - logging.FromContext(ctx).Debug("Ping database...") - - // TODO: Check migrations state and panic if migrations are not applied - - return nil - }, - }) - }), - ) -} diff --git a/components/payments/cmd/connectors/internal/storage/paginate.go b/components/payments/cmd/connectors/internal/storage/paginate.go deleted file mode 100644 index ac170979be..0000000000 --- a/components/payments/cmd/connectors/internal/storage/paginate.go +++ /dev/null @@ -1,47 +0,0 @@ -package storage - -import ( - "context" - - "github.com/formancehq/go-libs/bun/bunpaginate" - "github.com/formancehq/go-libs/query" - "github.com/uptrace/bun" -) - -type PaginatedQueryOptions[T any] struct { - QueryBuilder query.Builder `json:"qb"` - Sorter Sorter - PageSize uint64 `json:"pageSize"` - Options T `json:"options"` -} - -func (opts PaginatedQueryOptions[T]) WithQueryBuilder(qb query.Builder) PaginatedQueryOptions[T] { - opts.QueryBuilder = qb - - return opts -} - -func (opts PaginatedQueryOptions[T]) WithSorter(sorter Sorter) PaginatedQueryOptions[T] { - opts.Sorter = sorter - - return opts -} - -func (opts PaginatedQueryOptions[T]) WithPageSize(pageSize uint64) PaginatedQueryOptions[T] { - opts.PageSize = pageSize - - return opts -} - -func NewPaginatedQueryOptions[T any](options T) PaginatedQueryOptions[T] { - return PaginatedQueryOptions[T]{ - Options: options, - PageSize: bunpaginate.QueryDefaultPageSize, - } -} - -func PaginateWithOffset[FILTERS any, RETURN any](s *Storage, ctx context.Context, - q *bunpaginate.OffsetPaginatedQuery[FILTERS], builders ...func(query *bun.SelectQuery) *bun.SelectQuery) (*bunpaginate.Cursor[RETURN], error) { - query := s.db.NewSelect() - return bunpaginate.UsingOffset[FILTERS, RETURN](ctx, query, *q, builders...) -} diff --git a/components/payments/cmd/connectors/internal/storage/payments.go b/components/payments/cmd/connectors/internal/storage/payments.go deleted file mode 100644 index bcdf0b69cf..0000000000 --- a/components/payments/cmd/connectors/internal/storage/payments.go +++ /dev/null @@ -1,136 +0,0 @@ -package storage - -import ( - "context" - "fmt" - - "github.com/formancehq/payments/internal/models" -) - -func (s *Storage) GetPayment(ctx context.Context, id string) (*models.Payment, error) { - var payment models.Payment - - err := s.db.NewSelect(). - Model(&payment). - Relation("Connector"). - Relation("Metadata"). - Relation("Adjustments"). - Where("payment.id = ?", id). - Scan(ctx) - if err != nil { - return nil, e(fmt.Sprintf("failed to get payment %s", id), err) - } - - return &payment, nil -} - -func (s *Storage) UpsertPayments(ctx context.Context, payments []*models.Payment) ([]*models.PaymentID, error) { - if len(payments) == 0 { - return nil, nil - } - - var idsUpdated []string - err := s.db.NewUpdate(). - With("_data", - s.db.NewValues(&payments). - Column( - "id", - "amount", - "type", - "scheme", - "asset", - "source_account_id", - "destination_account_id", - "status", - "created_at", - ), - ). - Model((*models.Payment)(nil)). - TableExpr("_data"). - Set("amount = _data.amount"). - Set("type = _data.type"). - Set("scheme = _data.scheme"). - Set("asset = _data.asset"). - Set("source_account_id = _data.source_account_id"). - Set("destination_account_id = _data.destination_account_id"). - Set("status = _data.status"). - Set("created_at = _data.created_at"). - Where(`(payment.id = _data.id) AND - (payment.created_at != _data.created_at OR payment.amount != _data.amount OR payment.type != _data.type OR - payment.scheme != _data.scheme OR payment.asset != _data.asset OR payment.source_account_id != _data.source_account_id OR - payment.destination_account_id != _data.destination_account_id OR payment.status != _data.status)`). - Returning("payment.id"). - Scan(ctx, &idsUpdated) - if err != nil { - return nil, e("failed to update payments", err) - } - - idsUpdatedMap := make(map[string]struct{}) - for _, id := range idsUpdated { - idsUpdatedMap[id] = struct{}{} - } - - paymentsToInsert := make([]*models.Payment, 0, len(payments)) - for _, payment := range payments { - if _, ok := idsUpdatedMap[payment.ID.String()]; !ok { - paymentsToInsert = append(paymentsToInsert, payment) - } - } - - var idsInserted []string - if len(paymentsToInsert) > 0 { - err = s.db.NewInsert(). - Model(&paymentsToInsert). - On("CONFLICT (id) DO NOTHING"). - Returning("payment.id"). - Scan(ctx, &idsInserted) - if err != nil { - return nil, e("failed to create payments", err) - } - } - - res := make([]*models.PaymentID, 0, len(idsUpdated)+len(idsInserted)) - for _, id := range idsUpdated { - res = append(res, models.MustPaymentIDFromString(id)) - } - for _, id := range idsInserted { - res = append(res, models.MustPaymentIDFromString(id)) - } - - return res, nil -} - -func (s *Storage) UpsertPaymentsAdjustments(ctx context.Context, adjustments []*models.PaymentAdjustment) error { - if len(adjustments) == 0 { - return nil - } - - _, err := s.db.NewInsert(). - Model(&adjustments). - On("CONFLICT (reference) DO NOTHING"). - Exec(ctx) - if err != nil { - return e("failed to create adjustments", err) - } - - return nil -} - -func (s *Storage) UpsertPaymentsMetadata(ctx context.Context, metadata []*models.PaymentMetadata) error { - if len(metadata) == 0 { - return nil - } - - _, err := s.db.NewInsert(). - Model(&metadata). - On("CONFLICT (payment_id, key) DO UPDATE"). - Set("value = EXCLUDED.value"). - Set("changelog = payment_metadata.changelog || EXCLUDED.changelog"). - Where("payment_metadata.value != EXCLUDED.value"). - Exec(ctx) - if err != nil { - return e("failed to create metadata", err) - } - - return nil -} diff --git a/components/payments/cmd/connectors/internal/storage/payments_test.go b/components/payments/cmd/connectors/internal/storage/payments_test.go deleted file mode 100644 index 3ed0385399..0000000000 --- a/components/payments/cmd/connectors/internal/storage/payments_test.go +++ /dev/null @@ -1,173 +0,0 @@ -package storage_test - -import ( - "context" - "math/big" - "testing" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/stretchr/testify/require" -) - -var ( - p1ID *models.PaymentID - p1T = time.Date(2023, 11, 14, 4, 55, 0, 0, time.UTC) - p1 *models.Payment - - p2ID *models.PaymentID - p2T = time.Date(2023, 11, 14, 4, 54, 0, 0, time.UTC) - p2 *models.Payment -) - -func TestPayments(t *testing.T) { - store := newStore(t) - - testInstallConnectors(t, store) - testCreatePayments(t, store) - testUpdatePayment(t, store) - testUninstallConnectors(t, store) - testPaymentsDeletedAfterConnectorUninstall(t, store) -} - -func testCreatePayments(t *testing.T, store *storage.Storage) { - p1ID = &models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: "test1", - Type: models.PaymentTypePayOut, - }, - ConnectorID: connectorID, - } - p1 = &models.Payment{ - ID: *p1ID, - CreatedAt: p1T, - Reference: "ref1", - Amount: big.NewInt(100), - ConnectorID: connectorID, - Type: models.PaymentTypePayOut, - Status: models.PaymentStatusSucceeded, - Scheme: models.PaymentSchemeCardVisa, - Asset: models.Asset("USD/2"), - } - - p2ID = &models.PaymentID{ - PaymentReference: models.PaymentReference{ - Reference: "test2", - Type: models.PaymentTypeTransfer, - }, - ConnectorID: connectorID, - } - p2 = &models.Payment{ - ID: *p2ID, - CreatedAt: p2T, - Reference: "ref2", - Amount: big.NewInt(150), - ConnectorID: connectorID, - Type: models.PaymentTypePayIn, - Status: models.PaymentStatusFailed, - Scheme: models.PaymentSchemeCardVisa, - Asset: models.Asset("EUR/2"), - } - - pFail := &models.Payment{ - ID: *p1ID, - CreatedAt: p1T, - Reference: "ref1", - ConnectorID: connectorID, - Amount: big.NewInt(100), - Type: models.PaymentTypePayOut, - Status: models.PaymentStatusSucceeded, - Scheme: models.PaymentSchemeCardVisa, - Asset: models.Asset("USD/2"), - SourceAccountID: &models.AccountID{ - Reference: "not_existing", - ConnectorID: connectorID, - }, - } - - ids, err := store.UpsertPayments(context.Background(), []*models.Payment{pFail}) - require.Error(t, err) - require.Len(t, ids, 0) - - ids, err = store.UpsertPayments(context.Background(), []*models.Payment{p1}) - require.NoError(t, err) - require.Len(t, ids, 1) - - ids, err = store.UpsertPayments(context.Background(), []*models.Payment{p2}) - require.NoError(t, err) - require.Len(t, ids, 1) - - p1.Status = models.PaymentStatusPending - p2.Status = models.PaymentStatusSucceeded - ids, err = store.UpsertPayments(context.Background(), []*models.Payment{p1, p2}) - require.NoError(t, err) - require.Len(t, ids, 2) - - ids, err = store.UpsertPayments(context.Background(), []*models.Payment{p1, p2}) - require.NoError(t, err) - require.Len(t, ids, 0) - - testGetPayment(t, store, *p1ID, p1, nil) - testGetPayment(t, store, *p2ID, p2, nil) -} - -func testGetPayment( - t *testing.T, - store *storage.Storage, - paymentID models.PaymentID, - expected *models.Payment, - expectedErr error, -) { - payment, err := store.GetPayment(context.Background(), paymentID.String()) - if expectedErr != nil { - require.EqualError(t, err, expectedErr.Error()) - return - } else { - require.NoError(t, err) - } - - payment.CreatedAt = payment.CreatedAt.UTC() - checkPaymentsEqual(t, expected, payment) -} - -func checkPaymentsEqual(t *testing.T, p1, p2 *models.Payment) { - require.Equal(t, p1.ID, p2.ID) - require.Equal(t, p1.CreatedAt.UTC(), p2.CreatedAt.UTC()) - require.Equal(t, p1.Reference, p2.Reference) - require.Equal(t, p1.Amount, p2.Amount) - require.Equal(t, p1.Type, p2.Type) - require.Equal(t, p1.Status, p2.Status) - require.Equal(t, p1.Scheme, p2.Scheme) - require.Equal(t, p1.Asset, p2.Asset) - require.Equal(t, p1.SourceAccountID, p2.SourceAccountID) - require.Equal(t, p1.DestinationAccountID, p2.DestinationAccountID) - require.Equal(t, p1.RawData, p2.RawData) -} - -func testUpdatePayment(t *testing.T, store *storage.Storage) { - p1.CreatedAt = time.Date(2023, 11, 14, 5, 55, 0, 0, time.UTC) - p1.Reference = "ref1_updated" - p1.Amount = big.NewInt(150) - p1.Type = models.PaymentTypePayIn - p1.Status = models.PaymentStatusPending - p1.Scheme = models.PaymentSchemeCardVisa - p1.Asset = models.Asset("USD/2") - - ids, err := store.UpsertPayments(context.Background(), []*models.Payment{p1}) - require.NoError(t, err) - require.Len(t, ids, 1) - - payment, err := store.GetPayment(context.Background(), p1ID.String()) - require.NoError(t, err) - - require.NotEqual(t, p1.Reference, payment.Reference) - - p1.Reference = payment.Reference - testGetPayment(t, store, *p1ID, p1, nil) -} - -func testPaymentsDeletedAfterConnectorUninstall(t *testing.T, store *storage.Storage) { - testGetPayment(t, store, *p1ID, nil, storage.ErrNotFound) - testGetPayment(t, store, *p2ID, nil, storage.ErrNotFound) -} diff --git a/components/payments/cmd/connectors/internal/storage/ping.go b/components/payments/cmd/connectors/internal/storage/ping.go deleted file mode 100644 index 2832abb0b1..0000000000 --- a/components/payments/cmd/connectors/internal/storage/ping.go +++ /dev/null @@ -1,5 +0,0 @@ -package storage - -func (s *Storage) Ping() error { - return s.db.Ping() -} diff --git a/components/payments/cmd/connectors/internal/storage/repository.go b/components/payments/cmd/connectors/internal/storage/repository.go deleted file mode 100644 index f342f965fd..0000000000 --- a/components/payments/cmd/connectors/internal/storage/repository.go +++ /dev/null @@ -1,35 +0,0 @@ -package storage - -import ( - "context" - - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/uptrace/bun" - "github.com/uptrace/bun/extra/bundebug" -) - -type Storage struct { - db *bun.DB - configEncryptionKey string -} - -const encryptionOptions = "compress-algo=1, cipher-algo=aes256" - -func NewStorage(db *bun.DB, configEncryptionKey string) *Storage { - return &Storage{db: db, configEncryptionKey: configEncryptionKey} -} - -//nolint:unused // used in debug mode -func (s *Storage) debug() { - s.db.AddQueryHook(bundebug.NewQueryHook(bundebug.WithVerbose(true))) -} - -type Reader interface { - ReadTransferInitiation(ctx context.Context, id models.TransferInitiationID) (*models.TransferInitiation, error) - GetAccount(ctx context.Context, id string) (*models.Account, error) - GetBankAccount(ctx context.Context, id uuid.UUID, expand bool) (*models.BankAccount, error) - GetWebhook(ctx context.Context, id uuid.UUID) (*models.Webhook, error) - GetPayment(ctx context.Context, id string) (*models.Payment, error) - GetTransferReversal(ctx context.Context, id models.TransferReversalID) (*models.TransferReversal, error) -} diff --git a/components/payments/cmd/connectors/internal/storage/sort.go b/components/payments/cmd/connectors/internal/storage/sort.go deleted file mode 100644 index 2ec3d5c0a0..0000000000 --- a/components/payments/cmd/connectors/internal/storage/sort.go +++ /dev/null @@ -1,33 +0,0 @@ -package storage - -import ( - "fmt" - - "github.com/uptrace/bun" -) - -type SortOrder string - -const ( - SortOrderAsc SortOrder = "asc" - SortOrderDesc SortOrder = "desc" -) - -type sortExpression struct { - Column string `json:"column"` - Order SortOrder `json:"order"` -} - -type Sorter []sortExpression - -func (s Sorter) Add(column string, order SortOrder) Sorter { - return append(s, sortExpression{column, order}) -} - -func (s Sorter) Apply(query *bun.SelectQuery) *bun.SelectQuery { - for _, expr := range s { - query = query.Order(fmt.Sprintf("%s %s", expr.Column, expr.Order)) - } - - return query -} diff --git a/components/payments/cmd/connectors/internal/storage/task.go b/components/payments/cmd/connectors/internal/storage/task.go deleted file mode 100644 index 045c24780f..0000000000 --- a/components/payments/cmd/connectors/internal/storage/task.go +++ /dev/null @@ -1,165 +0,0 @@ -package storage - -import ( - "context" - "encoding/json" - - "github.com/formancehq/go-libs/bun/bunpaginate" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/pkg/errors" - "github.com/uptrace/bun" -) - -func (s *Storage) UpdateTaskStatus(ctx context.Context, connectorID models.ConnectorID, descriptor models.TaskDescriptor, status models.TaskStatus, taskError string) error { - _, err := s.db.NewUpdate().Model(&models.Task{}). - Set("status = ?", status). - Set("error = ?", taskError). - Where("descriptor::TEXT = ?::TEXT", descriptor.ToMessage()). - Where("connector_id = ?", connectorID). - Exec(ctx) - if err != nil { - return e("failed to update task", err) - } - - return nil -} - -func (s *Storage) UpdateTaskState(ctx context.Context, connectorID models.ConnectorID, descriptor models.TaskDescriptor, state json.RawMessage) error { - _, err := s.db.NewUpdate().Model(&models.Task{}). - Set("state = ?", state). - Where("descriptor::TEXT = ?::TEXT", descriptor.ToMessage()). - Where("connector_id = ?", connectorID). - Exec(ctx) - if err != nil { - return e("failed to update task", err) - } - - return nil -} - -func (s *Storage) FindAndUpsertTask( - ctx context.Context, - connectorID models.ConnectorID, - descriptor models.TaskDescriptor, - status models.TaskStatus, - schedulerOptions models.TaskSchedulerOptions, - taskErr string, -) (*models.Task, error) { - _, err := s.GetTaskByDescriptor(ctx, connectorID, descriptor) - if err != nil && !errors.Is(err, ErrNotFound) { - return nil, e("failed to get task", err) - } - - if err == nil { - err = s.UpdateTaskStatus(ctx, connectorID, descriptor, status, taskErr) - if err != nil { - return nil, e("failed to update task", err) - } - } else { - err = s.CreateTask(ctx, connectorID, descriptor, status, schedulerOptions) - if err != nil { - return nil, e("failed to upsert task", err) - } - } - - return s.GetTaskByDescriptor(ctx, connectorID, descriptor) -} - -func (s *Storage) CreateTask(ctx context.Context, connectorID models.ConnectorID, descriptor models.TaskDescriptor, status models.TaskStatus, schedulerOptions models.TaskSchedulerOptions) error { - _, err := s.db.NewInsert().Model(&models.Task{ - ConnectorID: connectorID, - Descriptor: descriptor.ToMessage(), - Status: status, - SchedulerOptions: schedulerOptions, - }).Exec(ctx) - if err != nil { - return e("failed to create task", err) - } - - return nil -} - -func (s *Storage) ListTasksByStatus(ctx context.Context, connectorID models.ConnectorID, status models.TaskStatus) ([]*models.Task, error) { - var tasks []*models.Task - - err := s.db.NewSelect().Model(&tasks). - Where("connector_id = ?", connectorID). - Where("status = ?", status). - Scan(ctx) - if err != nil { - return nil, e("failed to get tasks", err) - } - - return tasks, nil -} - -type TaskQuery struct{} - -type ListTasksQuery bunpaginate.OffsetPaginatedQuery[PaginatedQueryOptions[TaskQuery]] - -func NewListTasksQuery(opts PaginatedQueryOptions[TaskQuery]) ListTasksQuery { - return ListTasksQuery{ - PageSize: opts.PageSize, - Order: bunpaginate.OrderAsc, - Options: opts, - } -} - -func (s *Storage) ListTasks(ctx context.Context, connectorID models.ConnectorID, q ListTasksQuery) (*bunpaginate.Cursor[models.Task], error) { - return PaginateWithOffset[PaginatedQueryOptions[TaskQuery], models.Task](s, ctx, - (*bunpaginate.OffsetPaginatedQuery[PaginatedQueryOptions[TaskQuery]])(&q), - func(query *bun.SelectQuery) *bun.SelectQuery { - query = query. - Where("connector_id = ?", connectorID). - Order("created_at DESC") - - if q.Options.Sorter != nil { - query = q.Options.Sorter.Apply(query) - } - - return query - }, - ) -} - -func (s *Storage) ReadOldestPendingTask(ctx context.Context, connectorID models.ConnectorID) (*models.Task, error) { - var task models.Task - err := s.db.NewSelect().Model(&task). - Where("connector_id = ?", connectorID). - Where("status = ?", models.TaskStatusPending). - Order("created_at ASC"). - Limit(1). - Scan(ctx) - if err != nil { - return nil, e("failed to get task", err) - } - - return &task, nil -} - -func (s *Storage) GetTask(ctx context.Context, id uuid.UUID) (*models.Task, error) { - var task models.Task - - err := s.db.NewSelect().Model(&task). - Where("id = ?", id). - Scan(ctx) - if err != nil { - return nil, e("failed to get task", err) - } - - return &task, nil -} - -func (s *Storage) GetTaskByDescriptor(ctx context.Context, connectorID models.ConnectorID, descriptor models.TaskDescriptor) (*models.Task, error) { - var task models.Task - err := s.db.NewSelect().Model(&task). - Where("connector_id = ?", connectorID). - Where("descriptor::TEXT = ?::TEXT", descriptor.ToMessage()). - Scan(ctx) - if err != nil { - return nil, e("failed to get task", err) - } - - return &task, nil -} diff --git a/components/payments/cmd/connectors/internal/storage/transfer_initiation.go b/components/payments/cmd/connectors/internal/storage/transfer_initiation.go deleted file mode 100644 index cbaa4dd319..0000000000 --- a/components/payments/cmd/connectors/internal/storage/transfer_initiation.go +++ /dev/null @@ -1,182 +0,0 @@ -package storage - -import ( - "context" - "database/sql" - "time" - - "github.com/formancehq/payments/internal/models" - "github.com/pkg/errors" -) - -func (s *Storage) CreateTransferInitiation(ctx context.Context, transferInitiation *models.TransferInitiation) error { - tx, err := s.db.BeginTx(ctx, &sql.TxOptions{}) - if err != nil { - return err - } - defer func() { - _ = tx.Rollback() - }() - - query := tx.NewInsert(). - Column("id", "created_at", "scheduled_at", "description", "type", "destination_account_id", "provider", "connector_id", "initial_amount", "amount", "asset", "metadata"). - Model(transferInitiation) - - if transferInitiation.SourceAccountID != nil { - query = query.Column("source_account_id") - } - - _, err = query.Exec(ctx) - if err != nil { - return e("failed to create transfer initiation", err) - } - - for _, adjustment := range transferInitiation.RelatedAdjustments { - adj := adjustment - if _, err := tx.NewInsert().Model(adj).Exec(ctx); err != nil { - return e("failed to add adjustment", err) - } - } - - return e("failed to commit transaction", tx.Commit()) -} - -func (s *Storage) AddTransferInitiationAdjustment(ctx context.Context, adjustment *models.TransferInitiationAdjustment) error { - if _, err := s.db.NewInsert().Model(adjustment).Exec(ctx); err != nil { - return e("failed to add adjustment", err) - } - - return nil -} - -func (s *Storage) ReadTransferInitiation(ctx context.Context, id models.TransferInitiationID) (*models.TransferInitiation, error) { - var transferInitiation models.TransferInitiation - - query := s.db.NewSelect(). - Column("id", "created_at", "scheduled_at", "description", "type", "source_account_id", "destination_account_id", "provider", "connector_id", "amount", "asset", "metadata"). - Model(&transferInitiation). - Relation("RelatedAdjustments"). - Where("id = ?", id) - - err := query.Scan(ctx) - if err != nil { - return nil, e("failed to get transfer initiation", err) - } - - transferInitiation.SortRelatedAdjustments() - - transferInitiation.RelatedPayments, err = s.ReadTransferInitiationPayments(ctx, id) - if err != nil { - return nil, e("failed to get transfer initiation payments", err) - } - - return &transferInitiation, nil -} - -func (s *Storage) ReadTransferInitiationPayments(ctx context.Context, id models.TransferInitiationID) ([]*models.TransferInitiationPayment, error) { - var payments []*models.TransferInitiationPayment - - query := s.db.NewSelect(). - Column("transfer_initiation_id", "payment_id", "created_at", "status", "error"). - Model(&payments). - Where("transfer_initiation_id = ?", id). - Order("created_at DESC") - - err := query.Scan(ctx) - if err != nil { - return nil, e("failed to get transfer initiation payments", err) - } - - return payments, nil -} - -func (s *Storage) AddTransferInitiationPaymentID(ctx context.Context, id models.TransferInitiationID, paymentID *models.PaymentID, createdAt time.Time, metadata map[string]string) error { - tx, err := s.db.BeginTx(ctx, &sql.TxOptions{}) - if err != nil { - return err - } - defer func() { - _ = tx.Rollback() - }() - - if paymentID == nil { - return errors.New("payment id is nil") - } - - _, err = tx.NewInsert(). - Column("transfer_initiation_id", "payment_id", "created_at", "status"). - Model(&models.TransferInitiationPayment{ - TransferInitiationID: id, - PaymentID: *paymentID, - CreatedAt: createdAt, - Status: models.TransferInitiationStatusProcessing, - }). - Exec(ctx) - if err != nil { - return e("failed to add transfer initiation payment id", err) - } - - if metadata != nil { - _, err := tx.NewUpdate(). - Model((*models.TransferInitiation)(nil)). - Set("metadata = ?", metadata). - Where("id = ?", id). - Exec(ctx) - if err != nil { - return e("failed to add metadata", err) - } - } - - return e("failed to commit transaction", tx.Commit()) -} - -func (s *Storage) UpdateTransferInitiationPaymentsStatus( - ctx context.Context, - id models.TransferInitiationID, - paymentID *models.PaymentID, - adjustment *models.TransferInitiationAdjustment, -) error { - tx, err := s.db.BeginTx(ctx, &sql.TxOptions{}) - if err != nil { - return err - } - defer func() { - _ = tx.Rollback() - }() - - if paymentID != nil { - query := tx.NewUpdate(). - Model((*models.TransferInitiationPayment)(nil)). - Set("status = ?", adjustment.Status) - - if adjustment.Error != "" { - query = query.Set("error = ?", adjustment.Error) - } - - _, err := query. - Where("transfer_initiation_id = ?", id). - Where("payment_id = ?", paymentID). - Exec(ctx) - if err != nil { - return e("failed to update transfer initiation status", err) - } - } - - if _, err = tx.NewInsert().Model(adjustment).Exec(ctx); err != nil { - return e("failed to add adjustment", err) - } - - return e("failed to commit transaction", tx.Commit()) -} - -func (s *Storage) DeleteTransferInitiation(ctx context.Context, id models.TransferInitiationID) error { - _, err := s.db.NewDelete(). - Model((*models.TransferInitiation)(nil)). - Where("id = ?", id). - Exec(ctx) - if err != nil { - return e("failed to delete transfer initiation", err) - } - - return nil -} diff --git a/components/payments/cmd/connectors/internal/storage/transfer_initiation_test.go b/components/payments/cmd/connectors/internal/storage/transfer_initiation_test.go deleted file mode 100644 index 1dc127c48e..0000000000 --- a/components/payments/cmd/connectors/internal/storage/transfer_initiation_test.go +++ /dev/null @@ -1,275 +0,0 @@ -package storage_test - -import ( - "context" - "math/big" - "testing" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/stretchr/testify/require" -) - -var ( - t1ID models.TransferInitiationID - t1T = time.Date(2023, 11, 14, 5, 8, 0, 0, time.UTC) - t1 *models.TransferInitiation - adjumentID1 = uuid.New() - - t2ID models.TransferInitiationID - t2T = time.Date(2023, 11, 14, 5, 7, 0, 0, time.UTC) - t2 *models.TransferInitiation - adjumentID2 = uuid.New() - - tAddPayments = time.Date(2023, 11, 14, 5, 9, 10, 0, time.UTC) - tUpdateStatus1 = time.Date(2023, 11, 14, 5, 9, 15, 0, time.UTC) - tUpdateStatus2 = time.Date(2023, 11, 14, 5, 9, 16, 0, time.UTC) -) - -func TestTransferInitiations(t *testing.T) { - store := newStore(t) - - testInstallConnectors(t, store) - testCreateAccounts(t, store) - testCreatePayments(t, store) - testCreateTransferInitiations(t, store) - testAddTransferInitiationPayments(t, store) - testUpdateTransferInitiationStatus(t, store) - testDeleteTransferInitiations(t, store) - testUninstallConnectors(t, store) - testTransferInitiationsDeletedAfterConnectorUninstall(t, store) -} - -func testCreateTransferInitiations(t *testing.T, store *storage.Storage) { - t1ID = models.TransferInitiationID{ - Reference: "test1", - ConnectorID: connectorID, - } - t1 = &models.TransferInitiation{ - ID: t1ID, - CreatedAt: t1T, - ScheduledAt: t1T, - Description: "test_description", - Type: models.TransferInitiationTypeTransfer, - ConnectorID: connectorID, - Provider: models.ConnectorProviderDummyPay, - Amount: big.NewInt(100), - Asset: models.Asset("USD/2"), - RelatedAdjustments: []*models.TransferInitiationAdjustment{ - { - ID: adjumentID1, - TransferInitiationID: t1ID, - CreatedAt: t1T, - Status: models.TransferInitiationStatusWaitingForValidation, - }, - }, - } - - t2ID = models.TransferInitiationID{ - Reference: "test2", - ConnectorID: connectorID, - } - t2 = &models.TransferInitiation{ - ID: t2ID, - CreatedAt: t2T, - ScheduledAt: t2T, - Description: "test_description2", - Type: models.TransferInitiationTypeTransfer, - ConnectorID: connectorID, - Provider: models.ConnectorProviderDummyPay, - Amount: big.NewInt(150), - Asset: models.Asset("USD/2"), - SourceAccountID: &acc1ID, - DestinationAccountID: acc2ID, - RelatedAdjustments: []*models.TransferInitiationAdjustment{ - { - ID: adjumentID2, - TransferInitiationID: t2ID, - CreatedAt: t2T, - Status: models.TransferInitiationStatusWaitingForValidation, - }, - }, - } - - // Missing source account id and destination account id - err := store.CreateTransferInitiation(context.Background(), t1) - require.Error(t, err) - - t1.SourceAccountID = &acc1ID - t1.DestinationAccountID = acc2ID - err = store.CreateTransferInitiation(context.Background(), t1) - require.NoError(t, err) - - err = store.CreateTransferInitiation(context.Background(), t2) - require.NoError(t, err) - - testGetTransferInitiation(t, store, t1ID, false, t1, nil) - testGetTransferInitiation(t, store, t2ID, false, t2, nil) -} - -func testGetTransferInitiation( - t *testing.T, - store *storage.Storage, - id models.TransferInitiationID, - expand bool, - expected *models.TransferInitiation, - expectedErr error, -) { - tf, err := store.ReadTransferInitiation(context.Background(), id) - if expectedErr != nil { - require.EqualError(t, err, expectedErr.Error()) - return - } else { - require.NoError(t, err) - } - - if expand { - payments, err := store.ReadTransferInitiationPayments(context.Background(), id) - require.NoError(t, err) - tf.RelatedPayments = payments - } - - checkTransferInitiationsEqual(t, expected, tf, true) -} - -func checkTransferInitiationsEqual(t *testing.T, t1, t2 *models.TransferInitiation, checkRelatedAdjusment bool) { - require.Equal(t, t1.ID, t2.ID) - require.Equal(t, t1.CreatedAt.UTC(), t2.CreatedAt.UTC()) - require.Equal(t, t1.ScheduledAt.UTC(), t2.ScheduledAt.UTC()) - require.Equal(t, t1.Description, t2.Description) - require.Equal(t, t1.Type, t2.Type) - require.Equal(t, t1.Provider, t2.Provider) - require.Equal(t, t1.Amount, t2.Amount) - require.Equal(t, t1.Asset, t2.Asset) - require.Equal(t, t1.SourceAccountID, t2.SourceAccountID) - require.Equal(t, t1.DestinationAccountID, t2.DestinationAccountID) - for i := range t1.RelatedPayments { - require.Equal(t, t1.RelatedPayments[i].TransferInitiationID, t2.RelatedPayments[i].TransferInitiationID) - require.Equal(t, t1.RelatedPayments[i].PaymentID, t2.RelatedPayments[i].PaymentID) - require.Equal(t, t1.RelatedPayments[i].CreatedAt.UTC(), t2.RelatedPayments[i].CreatedAt.UTC()) - require.Equal(t, t1.RelatedPayments[i].Status, t2.RelatedPayments[i].Status) - require.Equal(t, t1.RelatedPayments[i].Error, t2.RelatedPayments[i].Error) - } - if checkRelatedAdjusment { - for i := range t1.RelatedAdjustments { - require.Equal(t, t1.RelatedAdjustments[i].TransferInitiationID, t2.RelatedAdjustments[i].TransferInitiationID) - require.Equal(t, t1.RelatedAdjustments[i].CreatedAt.UTC(), t2.RelatedAdjustments[i].CreatedAt.UTC()) - require.Equal(t, t1.RelatedAdjustments[i].Status, t2.RelatedAdjustments[i].Status) - require.Equal(t, t1.RelatedAdjustments[i].Error, t2.RelatedAdjustments[i].Error) - require.Equal(t, t1.RelatedAdjustments[i].Metadata, t2.RelatedAdjustments[i].Metadata) - } - } -} - -func testAddTransferInitiationPayments(t *testing.T, store *storage.Storage) { - err := store.AddTransferInitiationPaymentID( - context.Background(), - t1ID, - p1ID, - tAddPayments, - map[string]string{ - "test": "test", - }, - ) - require.NoError(t, err) - - t1.RelatedPayments = []*models.TransferInitiationPayment{ - { - TransferInitiationID: t1ID, - PaymentID: *p1ID, - CreatedAt: tAddPayments, - Status: models.TransferInitiationStatusProcessing, - Error: "", - }, - } - t1.Metadata = map[string]string{ - "test": "test", - } - testGetTransferInitiation(t, store, t1ID, true, t1, nil) - - err = store.AddTransferInitiationPaymentID( - context.Background(), - t1ID, - nil, - tAddPayments, - nil, - ) - require.Error(t, err) - - err = store.AddTransferInitiationPaymentID( - context.Background(), - models.TransferInitiationID{ - Reference: "not_existing", - ConnectorID: connectorID, - }, - p1ID, - tAddPayments, - nil, - ) - require.Error(t, err) -} - -func testUpdateTransferInitiationStatus(t *testing.T, store *storage.Storage) { - adjustment1 := &models.TransferInitiationAdjustment{ - ID: uuid.New(), - TransferInitiationID: t1ID, - CreatedAt: tUpdateStatus1, - Status: models.TransferInitiationStatusRejected, - Error: "test_error", - } - err := store.UpdateTransferInitiationPaymentsStatus( - context.Background(), - t1ID, - nil, - adjustment1, - ) - require.NoError(t, err) - - t1.RelatedAdjustments = append([]*models.TransferInitiationAdjustment{ - adjustment1, - }, t1.RelatedAdjustments...) - testGetTransferInitiation(t, store, t1ID, true, t1, nil) - - adjustment2 := &models.TransferInitiationAdjustment{ - ID: uuid.New(), - TransferInitiationID: t1ID, - CreatedAt: tUpdateStatus2, - Status: models.TransferInitiationStatusFailed, - Error: "test_error2", - } - err = store.UpdateTransferInitiationPaymentsStatus( - context.Background(), - t1ID, - p1ID, - adjustment2, - ) - require.NoError(t, err) - - t1.RelatedPayments[0].Status = models.TransferInitiationStatusFailed - t1.RelatedPayments[0].Error = "test_error2" - t1.RelatedAdjustments = append([]*models.TransferInitiationAdjustment{ - adjustment2, - }, t1.RelatedAdjustments...) - testGetTransferInitiation(t, store, t1ID, true, t1, nil) -} - -func testDeleteTransferInitiations(t *testing.T, store *storage.Storage) { - err := store.DeleteTransferInitiation(context.Background(), t1ID) - require.NoError(t, err) - - testGetTransferInitiation(t, store, t1ID, false, nil, storage.ErrNotFound) - - // Delete does not generate an error when not existing - err = store.DeleteTransferInitiation(context.Background(), models.TransferInitiationID{ - Reference: "not_existing", - ConnectorID: connectorID, - }) - require.NoError(t, err) -} - -func testTransferInitiationsDeletedAfterConnectorUninstall(t *testing.T, store *storage.Storage) { - testGetTransferInitiation(t, store, t1ID, false, nil, storage.ErrNotFound) - testGetTransferInitiation(t, store, t2ID, false, nil, storage.ErrNotFound) -} diff --git a/components/payments/cmd/connectors/internal/storage/transfer_reversal.go b/components/payments/cmd/connectors/internal/storage/transfer_reversal.go deleted file mode 100644 index 3a46dc33a9..0000000000 --- a/components/payments/cmd/connectors/internal/storage/transfer_reversal.go +++ /dev/null @@ -1,116 +0,0 @@ -package storage - -import ( - "context" - "database/sql" - "math/big" - "time" - - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/pkg/errors" -) - -func (s *Storage) CreateTransferReversal(ctx context.Context, transferReversal *models.TransferReversal) error { - tx, err := s.db.BeginTx(ctx, &sql.TxOptions{}) - if err != nil { - return err - } - defer func() { - _ = tx.Rollback() - }() - - _, err = tx.NewInsert().Model(transferReversal).Exec(ctx) - if err != nil { - return e("failed to create transfer reversal", err) - } - - adjustment := &models.TransferInitiationAdjustment{ - ID: uuid.New(), - TransferInitiationID: transferReversal.TransferInitiationID, - CreatedAt: time.Now().UTC(), - Status: models.TransferInitiationStatusAskReversed, - Error: "", - Metadata: transferReversal.Metadata, - } - - if _, err = tx.NewInsert().Model(adjustment).Exec(ctx); err != nil { - return e("failed to create adjustment", err) - } - - return e("failed to commit transaction", tx.Commit()) -} - -func (s *Storage) UpdateTransferReversalStatus( - ctx context.Context, - transfer *models.TransferInitiation, - transferReversal *models.TransferReversal, - adjustment *models.TransferInitiationAdjustment, -) error { - tx, err := s.db.BeginTx(ctx, &sql.TxOptions{}) - if err != nil { - return err - } - defer func() { - _ = tx.Rollback() - }() - - now := time.Now().UTC() - - _, err = tx.NewUpdate(). - Model(transferReversal). - Set("status = ?", transferReversal.Status). - Set("error = ?", transferReversal.Error). - Set("updated_at = ?", now). - Where("id = ?", transferReversal.ID). - Exec(ctx) - if err != nil { - return e("failed to update transfer reversal status", err) - } - - if transferReversal.Status == models.TransferReversalStatusProcessed { - var amount *big.Int - err = tx.NewUpdate(). - Model((*models.TransferInitiation)(nil)). - Set("amount = amount - ?", transferReversal.Amount). - Where("id = ?", transferReversal.TransferInitiationID). - Returning("amount"). - Scan(ctx, &amount) - if err != nil { - return e("failed to update transfer initiation amount", err) - } - - switch amount.Cmp(big.NewInt(0)) { - case 0: - // amount == 0, so we can mark the transfer as reversed - adjustment.Status = models.TransferInitiationStatusReversed - case 1: - // amount > 0, so we can mark the transfer as partially reversed - adjustment.Status = models.TransferInitiationStatusPartiallyReversed - case -1: - // Should not happened since we have checks in postgres - return errors.New("transfer reversal amount is greater than transfer initiation amount") - } - - transfer.Amount = amount - } - - if _, err := tx.NewInsert().Model(adjustment).Exec(ctx); err != nil { - return e("failed to add adjustment", err) - } - - return e("failed to commit transaction", tx.Commit()) -} - -func (s *Storage) GetTransferReversal(ctx context.Context, id models.TransferReversalID) (*models.TransferReversal, error) { - var ret models.TransferReversal - err := s.db.NewSelect(). - Model(&ret). - Where("id = ?", id). - Scan(ctx) - if err != nil { - return nil, e("failed to get transfer reversal", err) - } - - return &ret, nil -} diff --git a/components/payments/cmd/connectors/internal/storage/webhooks.go b/components/payments/cmd/connectors/internal/storage/webhooks.go deleted file mode 100644 index 68d1a67f4d..0000000000 --- a/components/payments/cmd/connectors/internal/storage/webhooks.go +++ /dev/null @@ -1,41 +0,0 @@ -package storage - -import ( - "context" - - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/pkg/errors" -) - -func (s *Storage) CreateWebhook(ctx context.Context, webhook *models.Webhook) error { - _, err := s.db.NewInsert().Model(webhook).Exec(ctx) - if err != nil { - return err - } - - return nil -} - -func (s *Storage) UpdateWebhookRequestBody(ctx context.Context, webhookID uuid.UUID, requestBody []byte) error { - if len(requestBody) == 0 { - return errors.New("requestBody cannot be empty") - } - - _, err := s.db.NewUpdate().Model((*models.Webhook)(nil)).Set("request_body = ?", requestBody).Where("id = ?", webhookID).Exec(ctx) - if err != nil { - return err - } - - return nil -} - -func (s *Storage) GetWebhook(ctx context.Context, id uuid.UUID) (*models.Webhook, error) { - webhook := &models.Webhook{} - err := s.db.NewSelect().Model(webhook).Where("id = ?", id).Scan(ctx) - if err != nil { - return nil, err - } - - return webhook, nil -} diff --git a/components/payments/cmd/connectors/internal/task/context.go b/components/payments/cmd/connectors/internal/task/context.go deleted file mode 100644 index 039bba4dac..0000000000 --- a/components/payments/cmd/connectors/internal/task/context.go +++ /dev/null @@ -1,42 +0,0 @@ -package task - -import ( - "context" -) - -type ConnectorContext interface { - Context() context.Context - Scheduler() Scheduler -} - -type ConnectorCtx struct { - ctx context.Context - scheduler Scheduler -} - -func (ctx *ConnectorCtx) Context() context.Context { - return ctx.ctx -} - -func (ctx *ConnectorCtx) Scheduler() Scheduler { - return ctx.scheduler -} - -func NewConnectorContext(ctx context.Context, scheduler Scheduler) *ConnectorCtx { - return &ConnectorCtx{ - ctx: ctx, - scheduler: scheduler, - } -} - -type taskContextKey struct{} - -var _taskContextKey = taskContextKey{} - -func ContextWithConnectorContext(ctx context.Context, task ConnectorContext) context.Context { - return context.WithValue(ctx, _taskContextKey, task) -} - -func ConnectorContextFromContext(ctx context.Context) ConnectorContext { - return ctx.Value(_taskContextKey).(ConnectorContext) -} diff --git a/components/payments/cmd/connectors/internal/task/errors.go b/components/payments/cmd/connectors/internal/task/errors.go deleted file mode 100644 index 9a1e43d7fb..0000000000 --- a/components/payments/cmd/connectors/internal/task/errors.go +++ /dev/null @@ -1,13 +0,0 @@ -package task - -import "github.com/pkg/errors" - -var ( - // ErrRetryable will be sent by the task if we can retry the task, - // e.g. if the task failed because of a temporary network issue. - ErrRetryable = errors.New("retryable error") - - // ErrNonRetryable will be sent by the task if we can't retry the task, - // e.g. if the task failed because of a validation error. - ErrNonRetryable = errors.New("non-retryable error") -) diff --git a/components/payments/cmd/connectors/internal/task/resolver.go b/components/payments/cmd/connectors/internal/task/resolver.go deleted file mode 100644 index e12057ad7b..0000000000 --- a/components/payments/cmd/connectors/internal/task/resolver.go +++ /dev/null @@ -1,13 +0,0 @@ -package task - -import "github.com/formancehq/payments/internal/models" - -type Resolver interface { - Resolve(descriptor models.TaskDescriptor) Task -} - -type ResolverFn func(descriptor models.TaskDescriptor) Task - -func (fn ResolverFn) Resolve(descriptor models.TaskDescriptor) Task { - return fn(descriptor) -} diff --git a/components/payments/cmd/connectors/internal/task/scheduler.go b/components/payments/cmd/connectors/internal/task/scheduler.go deleted file mode 100644 index 2fd8845e2c..0000000000 --- a/components/payments/cmd/connectors/internal/task/scheduler.go +++ /dev/null @@ -1,739 +0,0 @@ -package task - -import ( - "context" - "encoding/json" - "fmt" - "runtime/debug" - "sync" - "time" - - "github.com/formancehq/go-libs/bun/bunpaginate" - - "github.com/alitto/pond" - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/metrics" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/pkg/errors" - "go.uber.org/dig" -) - -var ( - ErrValidation = errors.New("validation error") - ErrAlreadyScheduled = errors.New("already scheduled") - ErrUnableToResolve = errors.New("unable to resolve task") -) - -type Scheduler interface { - Schedule(ctx context.Context, p models.TaskDescriptor, options models.TaskSchedulerOptions) error -} - -type taskHolder struct { - descriptor models.TaskDescriptor - cancel func() - logger logging.Logger - stopChan StopChan -} - -type ContainerCreateFunc func(ctx context.Context, descriptor models.TaskDescriptor, taskID uuid.UUID) (*dig.Container, error) - -type DefaultTaskScheduler struct { - connectorID models.ConnectorID - store Repository - metricsRegistry metrics.MetricsRegistry - containerFactory ContainerCreateFunc - tasks map[string]*taskHolder - mu sync.Mutex - resolver Resolver - stopped bool - workerPool *pond.WorkerPool -} - -func (s *DefaultTaskScheduler) ListTasks(ctx context.Context, q storage.ListTasksQuery) (*bunpaginate.Cursor[models.Task], error) { - return s.store.ListTasks(ctx, s.connectorID, q) -} - -func (s *DefaultTaskScheduler) ReadTask(ctx context.Context, taskID uuid.UUID) (*models.Task, error) { - return s.store.GetTask(ctx, taskID) -} - -func (s *DefaultTaskScheduler) ReadTaskByDescriptor(ctx context.Context, descriptor models.TaskDescriptor) (*models.Task, error) { - taskDescriptor, err := json.Marshal(descriptor) - if err != nil { - return nil, err - } - - return s.store.GetTaskByDescriptor(ctx, s.connectorID, taskDescriptor) -} - -// Schedule schedules a task to be executed. -// Schedule waits for: -// - Context to be done -// - Task creation if the scheduler option is not equal to OPTIONS_RUN_NOW_SYNC -// - Task termination if the scheduler option is equal to OPTIONS_RUN_NOW_SYNC -func (s *DefaultTaskScheduler) Schedule(ctx context.Context, descriptor models.TaskDescriptor, options models.TaskSchedulerOptions) error { - select { - case err := <-s.schedule(ctx, descriptor, options): - return err - case <-ctx.Done(): - return nil - } -} - -// schedule schedules a task to be executed. -// It returns an error chan that will be closed when the task is terminated if -// the scheduler option is equal to OPTIONS_RUN_NOW_SYNC. Otherwise, it will -// return an error chan that will be closed immediately after task creation. -func (s *DefaultTaskScheduler) schedule(ctx context.Context, descriptor models.TaskDescriptor, options models.TaskSchedulerOptions) <-chan error { - s.mu.Lock() - defer s.mu.Unlock() - - returnErrorFunc := func(err error) <-chan error { - errChan := make(chan error, 1) - if err != nil { - errChan <- err - } - close(errChan) - return errChan - } - - taskID, err := descriptor.EncodeToString() - if err != nil { - return returnErrorFunc(err) - } - - if _, ok := s.tasks[taskID]; ok { - switch options.RestartOption { - case models.OPTIONS_STOP_AND_RESTART, models.OPTIONS_RESTART_ALWAYS: - // We still want to restart the task - default: - return returnErrorFunc(ErrAlreadyScheduled) - } - } - - switch options.RestartOption { - case models.OPTIONS_RESTART_NEVER: - _, err := s.ReadTaskByDescriptor(ctx, descriptor) - if err == nil { - return returnErrorFunc(nil) - } - case models.OPTIONS_RESTART_IF_NOT_ACTIVE: - task, err := s.ReadTaskByDescriptor(ctx, descriptor) - if err == nil && task.Status == models.TaskStatusActive { - return nil - } - case models.OPTIONS_STOP_AND_RESTART: - err := s.stopTask(ctx, descriptor) - if err != nil { - return returnErrorFunc(err) - } - case models.OPTIONS_RESTART_ALWAYS: - // Do nothing - } - - errChan := s.startTask(ctx, descriptor, options) - - return errChan -} - -func (s *DefaultTaskScheduler) Shutdown(ctx context.Context) error { - s.mu.Lock() - defer s.mu.Unlock() - - s.stopped = true - - s.logger(ctx).Infof("Stopping scheduler...") - s.workerPool.Stop() - - for name, task := range s.tasks { - task.logger.Debugf("Stopping task") - - if task.stopChan != nil { - errCh := make(chan struct{}) - task.stopChan <- errCh - select { - case <-errCh: - case <-time.After(time.Second): // TODO: Make configurable - task.logger.Debugf("Stopping using stop chan timeout, canceling context") - task.cancel() - } - } else { - task.cancel() - } - - delete(s.tasks, name) - } - - return nil -} - -func (s *DefaultTaskScheduler) Restore(ctx context.Context) error { - tasks, err := s.store.ListTasksByStatus(ctx, s.connectorID, models.TaskStatusActive) - if err != nil { - return err - } - - for _, task := range tasks { - if task.SchedulerOptions.Restart { - task.SchedulerOptions.RestartOption = models.OPTIONS_RESTART_ALWAYS - } - - errChan := s.startTask(ctx, task.GetDescriptor(), task.SchedulerOptions) - select { - case err := <-errChan: - if err != nil { - s.logger(ctx).Errorf("Unable to restore task %s: %s", task.ID, err) - } - case <-ctx.Done(): - } - } - - return nil -} - -func (s *DefaultTaskScheduler) registerTaskError(ctx context.Context, holder *taskHolder, taskErr any) { - var taskError string - - switch v := taskErr.(type) { - case error: - taskError = v.Error() - default: - taskError = fmt.Sprintf("%s", v) - } - - holder.logger.Errorf("Task terminated with error: %s", taskErr) - - err := s.store.UpdateTaskStatus(ctx, s.connectorID, holder.descriptor, models.TaskStatusFailed, taskError) - if err != nil { - holder.logger.Errorf("Error updating task status: %s", taskError) - } -} - -func (s *DefaultTaskScheduler) deleteTask(ctx context.Context, holder *taskHolder) { - s.mu.Lock() - defer s.mu.Unlock() - - taskID, err := holder.descriptor.EncodeToString() - if err != nil { - holder.logger.Errorf("Error encoding task descriptor: %s", err) - - return - } - - delete(s.tasks, taskID) - - if s.stopped { - return - } - - oldestPendingTask, err := s.store.ReadOldestPendingTask(ctx, s.connectorID) - if err != nil { - if errors.Is(err, storage.ErrNotFound) { - return - } - - logging.FromContext(ctx).Error(err) - - return - } - - p := s.resolver.Resolve(oldestPendingTask.GetDescriptor()) - if p == nil { - logging.FromContext(ctx).Errorf("unable to resolve task") - return - } - - errChan := s.startTask(ctx, oldestPendingTask.GetDescriptor(), models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - }) - select { - case err, ok := <-errChan: - if !ok { - return - } - if err != nil { - logging.FromContext(ctx).Error(err) - } - case <-ctx.Done(): - return - } -} - -type StopChan chan chan struct{} - -// Lock should be held when calling this function -func (s *DefaultTaskScheduler) stopTask(ctx context.Context, descriptor models.TaskDescriptor) error { - taskID, err := descriptor.EncodeToString() - if err != nil { - return err - } - - task, ok := s.tasks[taskID] - if !ok { - return nil - } - - task.logger.Infof("Stopping task...") - - if task.stopChan != nil { - errCh := make(chan struct{}) - task.stopChan <- errCh - select { - case <-errCh: - case <-time.After(time.Second): // TODO: Make configurable - task.logger.Debugf("Stopping using stop chan timeout, canceling context") - task.cancel() - } - } else { - task.cancel() - } - - err = s.store.UpdateTaskStatus(ctx, s.connectorID, descriptor, models.TaskStatusStopped, "") - if err != nil { - task.logger.Errorf("Error updating task status: %s", err) - return err - } - - delete(s.tasks, taskID) - - return nil -} - -func (s *DefaultTaskScheduler) startTask(ctx context.Context, descriptor models.TaskDescriptor, options models.TaskSchedulerOptions) <-chan error { - errChan := make(chan error, 1) - - task, err := s.store.FindAndUpsertTask(ctx, s.connectorID, descriptor, - models.TaskStatusActive, options, "") - if err != nil { - errChan <- errors.Wrap(err, "finding task and update") - close(errChan) - return errChan - } - - logger := s.logger(ctx).WithFields(map[string]interface{}{ - "task-id": task.ID, - }) - - taskResolver := s.resolver.Resolve(task.GetDescriptor()) - if taskResolver == nil { - errChan <- ErrUnableToResolve - close(errChan) - return errChan - } - - ctx, cancel := context.WithCancel(ctx) - - holder := &taskHolder{ - cancel: cancel, - logger: logger, - descriptor: descriptor, - } - - container, err := s.containerFactory(ctx, descriptor, task.ID) - if err != nil { - // TODO: Handle error - panic(err) - } - - err = container.Provide(func() context.Context { - return ctx - }) - if err != nil { - panic(err) - } - - err = container.Provide(func() Scheduler { - return s - }) - if err != nil { - panic(err) - } - - err = container.Provide(func() models.ConnectorID { - return s.connectorID - }) - if err != nil { - panic(err) - } - - err = container.Provide(func() models.TaskID { - return models.TaskID(task.ID) - }) - if err != nil { - panic(err) - } - - err = container.Provide(func() StopChan { - s.mu.Lock() - defer s.mu.Unlock() - - holder.stopChan = make(StopChan, 1) - - return holder.stopChan - }) - if err != nil { - panic(err) - } - - err = container.Provide(func() logging.Logger { - return logger - }) - if err != nil { - panic(err) - } - - err = container.Provide(func() metrics.MetricsRegistry { - return s.metricsRegistry - }) - if err != nil { - panic(err) - } - - err = container.Provide(func() StateResolver { - return StateResolverFn(func(ctx context.Context, v any) error { - t, err := s.store.GetTask(ctx, task.ID) - if err != nil { - return err - } - - if t.State == nil || len(t.State) == 0 { - return nil - } - - return json.Unmarshal(t.State, v) - }) - }) - if err != nil { - panic(err) - } - - taskID, err := holder.descriptor.EncodeToString() - if err != nil { - errChan <- err - close(errChan) - return errChan - } - - s.tasks[taskID] = holder - - sendError := false - switch options.ScheduleOption { - case models.OPTIONS_RUN_NOW_SYNC: - sendError = true - fallthrough - case models.OPTIONS_RUN_NOW: - options.Duration = 0 - fallthrough - case models.OPTIONS_RUN_SCHEDULED_AT: - if !options.ScheduleAt.IsZero() { - options.Duration = time.Until(options.ScheduleAt) - if options.Duration < 0 { - options.Duration = 0 - } - } - fallthrough - case models.OPTIONS_RUN_IN_DURATION: - go s.runTaskOnce( - ctx, - logger, - holder, - descriptor, - options, - taskResolver, - container, - sendError, - errChan, - 1, - ) - case models.OPTIONS_RUN_PERIODICALLY: - go s.runTaskPeriodically( - ctx, - logger, - holder, - descriptor, - options, - taskResolver, - container, - ) - } - - if !sendError { - close(errChan) - } - - return errChan -} - -func (s *DefaultTaskScheduler) runTaskOnce( - ctx context.Context, - logger logging.Logger, - holder *taskHolder, - descriptor models.TaskDescriptor, - options models.TaskSchedulerOptions, - taskResolver Task, - container *dig.Container, - sendError bool, - errChan chan error, - attempt int, -) { - // If attempt is > 1, it means that the task is being retried, so no need - // to wait again - if options.Duration > 0 && attempt == 1 { - logger.Infof("Waiting %s before starting task...", options.Duration) - select { - case <-ctx.Done(): - return - case ch := <-holder.stopChan: - logger.Infof("Stopping task...") - close(ch) - return - case <-time.After(options.Duration): - } - } - - logger.Infof("Starting task...") - - defer func() { - defer s.deleteTask(ctx, holder) - - if sendError { - defer close(errChan) - } - - if e := recover(); e != nil { - switch v := e.(type) { - case error: - if errors.Is(v, pond.ErrSubmitOnStoppedPool) { - // Pool is stopped and task is marked as active, - // nothing to do as they will be restarted on - // next startup - return - } - } - - s.registerTaskError(ctx, holder, e) - debug.PrintStack() - - if sendError { - switch v := e.(type) { - case error: - errChan <- v - default: - errChan <- fmt.Errorf("%s", v) - } - } - } - }() - - runF := func() (err error) { - defer func() { - if e := recover(); e != nil { - switch v := e.(type) { - case error: - if errors.Is(v, pond.ErrSubmitOnStoppedPool) { - // In this case, the scheduler is stopped, it means that - // either the connector is uninstalled or the service - // is stopped. In case of the connector being uninstalled, - // it doesn't matter if we send an error or not since - // all data will be deleted. In case of the service being - // stopped, the task should be restarted on next startup, - // so we have to mark it as Retryable. - err = errors.Wrap(ErrRetryable, v.Error()) - return - } else { - panic(e) - } - default: - panic(v) - } - } - }() - - done := make(chan struct{}) - s.workerPool.Submit(func() { - defer close(done) - err = container.Invoke(taskResolver) - }) - select { - case <-done: - case <-ctx.Done(): - return ctx.Err() - } - - return err - } - -loop: - for { - select { - case <-ctx.Done(): - return - default: - } - - err := runF() - switch { - case err == nil: - break loop - case errors.Is(err, ErrRetryable): - logger.Infof("Task terminated with retryable error: %s", err) - continue - case errors.Is(err, ErrNonRetryable): - logger.Infof("Task terminated with non retryable error: %s", err) - fallthrough - default: - if err == context.Canceled { - // Context was canceled, which means the scheduler was stopped - // either by the application being stopped or by the connector - // being removed. In this case, we don't want to update the - // task status, as it will be restarted on next startup. - return - } - - // All other errors - s.registerTaskError(ctx, holder, err) - - if sendError { - errChan <- err - } - - return - } - } - - logger.Infof("Task terminated with success") - - err := s.store.UpdateTaskStatus(ctx, s.connectorID, descriptor, models.TaskStatusTerminated, "") - if err != nil { - logger.Errorf("Error updating task status: %s", err) - if sendError { - errChan <- err - } - } -} - -func (s *DefaultTaskScheduler) runTaskPeriodically( - ctx context.Context, - logger logging.Logger, - holder *taskHolder, - descriptor models.TaskDescriptor, - options models.TaskSchedulerOptions, - taskResolver Task, - container *dig.Container, -) { - defer func() { - defer s.deleteTask(ctx, holder) - - if e := recover(); e != nil { - switch v := e.(type) { - case error: - if errors.Is(v, pond.ErrSubmitOnStoppedPool) { - // In this case, the scheduler is stopped, it means that - // either the connector is uninstalled or the service - // is stopped. In case of the connector being uninstalled, - // it doesn't matter if we send an error or not since - // all data will be deleted. In case of the service being - // stopped, the task should be restarted on next startup, - // so we need to not mark is as an error. - return - } else { - s.registerTaskError(ctx, holder, e) - debug.PrintStack() - } - default: - s.registerTaskError(ctx, holder, e) - debug.PrintStack() - } - - return - } - }() - - processFunc := func() (bool, error) { - var err error - done := make(chan struct{}) - s.workerPool.Submit(func() { - defer close(done) - err = container.Invoke(taskResolver) - }) - select { - case <-done: - case <-ctx.Done(): - return true, nil - case ch := <-holder.stopChan: - logger.Infof("Stopping task...") - close(ch) - return true, nil - } - if err != nil { - return false, err - } - - return false, err - } - - logger.Infof("Starting task...") - ticker := time.NewTicker(options.Duration) - for { - stopped, err := processFunc() - switch { - case err == nil: - // Doing nothing, waiting for the next tick - case errors.Is(err, ErrRetryable): - ticker.Reset(options.Duration) - continue - case errors.Is(err, ErrNonRetryable): - fallthrough - default: - // All other errors - s.registerTaskError(ctx, holder, err) - return - } - - if stopped { - // Task is stopped or context is done - return - } - - select { - case ch := <-holder.stopChan: - logger.Infof("Stopping task...") - close(ch) - return - case <-ctx.Done(): - return - case <-ticker.C: - logger.Infof("Polling trigger, running task...") - } - } -} - -func (s *DefaultTaskScheduler) logger(ctx context.Context) logging.Logger { - return logging.FromContext(ctx).WithFields(map[string]any{ - "component": "scheduler", - "connectorID": s.connectorID, - }) -} - -var _ Scheduler = &DefaultTaskScheduler{} - -func NewDefaultScheduler( - connectorID models.ConnectorID, - store Repository, - containerFactory ContainerCreateFunc, - resolver Resolver, - metricsRegistry metrics.MetricsRegistry, - maxTasks int, -) *DefaultTaskScheduler { - return &DefaultTaskScheduler{ - connectorID: connectorID, - store: store, - metricsRegistry: metricsRegistry, - tasks: map[string]*taskHolder{}, - containerFactory: containerFactory, - resolver: resolver, - workerPool: pond.New(maxTasks, maxTasks), - } -} diff --git a/components/payments/cmd/connectors/internal/task/scheduler_test.go b/components/payments/cmd/connectors/internal/task/scheduler_test.go deleted file mode 100644 index 95734b7d44..0000000000 --- a/components/payments/cmd/connectors/internal/task/scheduler_test.go +++ /dev/null @@ -1,419 +0,0 @@ -package task - -import ( - "context" - "testing" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/metrics" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - "github.com/stretchr/testify/require" - "go.uber.org/dig" -) - -//nolint:gochecknoglobals // allow in tests -var DefaultContainerFactory = ContainerCreateFunc(func(ctx context.Context, descriptor models.TaskDescriptor, taskID uuid.UUID) (*dig.Container, error) { - return dig.New(), nil -}) - -func newDescriptor() models.TaskDescriptor { - return []byte(uuid.New().String()) -} - -func TaskTerminatedWithStatus( - store *InMemoryStore, - connectorID models.ConnectorID, - descriptor models.TaskDescriptor, - expectedStatus models.TaskStatus, - errString string, -) func() bool { - return func() bool { - status, resultErr, ok := store.Result(connectorID, descriptor) - if !ok { - return false - } - - if resultErr != errString { - return false - } - - return status == expectedStatus - } -} - -func TaskTerminated(store *InMemoryStore, connectorID models.ConnectorID, descriptor models.TaskDescriptor) func() bool { - return TaskTerminatedWithStatus(store, connectorID, descriptor, models.TaskStatusTerminated, "") -} - -func TaskFailed(store *InMemoryStore, connectorID models.ConnectorID, descriptor models.TaskDescriptor, errStr string) func() bool { - return TaskTerminatedWithStatus(store, connectorID, descriptor, models.TaskStatusFailed, errStr) -} - -func TaskPending(store *InMemoryStore, connectorID models.ConnectorID, descriptor models.TaskDescriptor) func() bool { - return TaskTerminatedWithStatus(store, connectorID, descriptor, models.TaskStatusPending, "") -} - -func TaskActive(store *InMemoryStore, connectorID models.ConnectorID, descriptor models.TaskDescriptor) func() bool { - return TaskTerminatedWithStatus(store, connectorID, descriptor, models.TaskStatusActive, "") -} - -func TestTaskScheduler(t *testing.T) { - t.Parallel() - - l := logrus.New() - if testing.Verbose() { - l.SetLevel(logrus.DebugLevel) - } - - t.Run("Nominal", func(t *testing.T) { - t.Parallel() - - store := NewInMemoryStore() - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - done := make(chan struct{}) - scheduler := NewDefaultScheduler(connectorID, store, - DefaultContainerFactory, ResolverFn(func(descriptor models.TaskDescriptor) Task { - return func(ctx context.Context) error { - select { - case <-ctx.Done(): - return ctx.Err() - case <-done: - return nil - } - } - }), metrics.NewNoOpMetricsRegistry(), 1) - - descriptor := newDescriptor() - err := scheduler.Schedule(context.TODO(), descriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_NEVER, - }) - require.NoError(t, err) - - require.Eventually(t, TaskActive(store, connectorID, descriptor), time.Second, 100*time.Millisecond) - close(done) - require.Eventually(t, TaskTerminated(store, connectorID, descriptor), time.Second, 100*time.Millisecond) - }) - - t.Run("Duplicate task", func(t *testing.T) { - t.Parallel() - - store := NewInMemoryStore() - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - scheduler := NewDefaultScheduler(connectorID, store, DefaultContainerFactory, - ResolverFn(func(descriptor models.TaskDescriptor) Task { - return func(ctx context.Context) error { - <-ctx.Done() - - return ctx.Err() - } - }), metrics.NewNoOpMetricsRegistry(), 1) - - descriptor := newDescriptor() - err := scheduler.Schedule(context.TODO(), descriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_NEVER, - }) - require.NoError(t, err) - require.Eventually(t, TaskActive(store, connectorID, descriptor), time.Second, 100*time.Millisecond) - - err = scheduler.Schedule(context.TODO(), descriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_NEVER, - }) - require.Equal(t, ErrAlreadyScheduled, err) - }) - - t.Run("Error", func(t *testing.T) { - t.Parallel() - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - store := NewInMemoryStore() - scheduler := NewDefaultScheduler(connectorID, store, DefaultContainerFactory, - ResolverFn(func(descriptor models.TaskDescriptor) Task { - return func() error { - return errors.New("test") - } - }), metrics.NewNoOpMetricsRegistry(), 1) - - descriptor := newDescriptor() - err := scheduler.Schedule(context.TODO(), descriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_NEVER, - }) - require.NoError(t, err) - require.Eventually(t, TaskFailed(store, connectorID, descriptor, "test"), time.Second, - 100*time.Millisecond) - }) - - t.Run("Pending", func(t *testing.T) { - t.Parallel() - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - store := NewInMemoryStore() - descriptor1 := newDescriptor() - descriptor2 := newDescriptor() - - task1Launched := make(chan struct{}) - task2Launched := make(chan struct{}) - - task1Terminated := make(chan struct{}) - task2Terminated := make(chan struct{}) - - scheduler := NewDefaultScheduler(connectorID, store, DefaultContainerFactory, - ResolverFn(func(descriptor models.TaskDescriptor) Task { - switch string(descriptor) { - case string(descriptor1): - return func(ctx context.Context) error { - close(task1Launched) - select { - case <-task1Terminated: - return nil - case <-ctx.Done(): - return ctx.Err() - } - } - case string(descriptor2): - return func(ctx context.Context) error { - close(task2Launched) - select { - case <-task2Terminated: - return nil - case <-ctx.Done(): - return ctx.Err() - } - } - } - - panic("unknown descriptor") - }), metrics.NewNoOpMetricsRegistry(), 1) - - require.NoError(t, scheduler.Schedule(context.TODO(), descriptor1, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_NEVER, - })) - require.NoError(t, scheduler.Schedule(context.TODO(), descriptor2, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_NEVER, - })) - - select { - case <-task1Launched: - require.Eventually(t, TaskActive(store, connectorID, descriptor1), time.Second, 100*time.Millisecond) - require.Eventually(t, TaskActive(store, connectorID, descriptor2), time.Second, 100*time.Millisecond) - close(task1Terminated) - require.Eventually(t, TaskTerminated(store, connectorID, descriptor1), time.Second, 100*time.Millisecond) - require.Eventually(t, TaskActive(store, connectorID, descriptor2), time.Second, 100*time.Millisecond) - close(task2Terminated) - require.Eventually(t, TaskTerminated(store, connectorID, descriptor2), time.Second, 100*time.Millisecond) - case <-task2Launched: - require.Eventually(t, TaskActive(store, connectorID, descriptor1), time.Second, 100*time.Millisecond) - require.Eventually(t, TaskActive(store, connectorID, descriptor2), time.Second, 100*time.Millisecond) - close(task2Terminated) - require.Eventually(t, TaskTerminated(store, connectorID, descriptor2), time.Second, 100*time.Millisecond) - require.Eventually(t, TaskActive(store, connectorID, descriptor1), time.Second, 100*time.Millisecond) - close(task1Terminated) - require.Eventually(t, TaskTerminated(store, connectorID, descriptor1), time.Second, 100*time.Millisecond) - } - }) - - t.Run("Stop scheduler", func(t *testing.T) { - t.Parallel() - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - store := NewInMemoryStore() - mainDescriptor := newDescriptor() - workerDescriptor := newDescriptor() - - scheduler := NewDefaultScheduler(connectorID, store, DefaultContainerFactory, - ResolverFn(func(descriptor models.TaskDescriptor) Task { - switch string(descriptor) { - case string(mainDescriptor): - return func(ctx context.Context, scheduler Scheduler) { - <-ctx.Done() - require.NoError(t, scheduler.Schedule(ctx, workerDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_NEVER, - })) - } - default: - return func() { - - } - } - }), metrics.NewNoOpMetricsRegistry(), 1) - - require.NoError(t, scheduler.Schedule(context.TODO(), mainDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_NEVER, - })) - require.Eventually(t, TaskActive(store, connectorID, mainDescriptor), time.Second, 100*time.Millisecond) - require.NoError(t, scheduler.Shutdown(context.TODO())) - // the main task should be still marked as active since it failed to - // schedule the worker task because the scheduler was stopped - require.Eventually(t, TaskActive(store, connectorID, mainDescriptor), time.Second, 100*time.Millisecond) - require.Eventually(t, TaskActive(store, connectorID, workerDescriptor), time.Second, 100*time.Millisecond) - }) - - t.Run("errors and retryable errors", func(t *testing.T) { - t.Parallel() - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - store := NewInMemoryStore() - nonRetryableDescriptor := newDescriptor() - retryableDescriptor := newDescriptor() - otherErrorDescriptor := newDescriptor() - noErrorDescriptor := newDescriptor() - - scheduler := NewDefaultScheduler(connectorID, store, DefaultContainerFactory, - ResolverFn(func(descriptor models.TaskDescriptor) Task { - switch string(descriptor) { - case string(nonRetryableDescriptor): - return func(ctx context.Context, scheduler Scheduler) error { - return ErrNonRetryable - } - case string(retryableDescriptor): - return func(ctx context.Context, scheduler Scheduler) error { - return ErrRetryable - } - case string(otherErrorDescriptor): - return func(ctx context.Context, scheduler Scheduler) error { - return errors.New("test") - } - case string(noErrorDescriptor): - return func(ctx context.Context, scheduler Scheduler) error { - return nil - } - default: - return func() { - - } - } - }), metrics.NewNoOpMetricsRegistry(), 1) - - require.NoError(t, scheduler.Schedule(context.TODO(), nonRetryableDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_NEVER, - })) - require.Eventually(t, TaskFailed(store, connectorID, nonRetryableDescriptor, "non-retryable error"), time.Second, 100*time.Millisecond) - - require.NoError(t, scheduler.Schedule(context.TODO(), otherErrorDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_NEVER, - })) - require.Eventually(t, TaskFailed(store, connectorID, otherErrorDescriptor, "test"), time.Second, 100*time.Millisecond) - - require.NoError(t, scheduler.Schedule(context.TODO(), noErrorDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_NEVER, - })) - require.Eventually(t, TaskTerminated(store, connectorID, noErrorDescriptor), time.Second, 100*time.Millisecond) - - require.NoError(t, scheduler.Schedule(context.TODO(), retryableDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_NOW, - RestartOption: models.OPTIONS_RESTART_NEVER, - })) - require.Eventually(t, TaskActive(store, connectorID, retryableDescriptor), time.Second, 100*time.Millisecond) - require.NoError(t, scheduler.Shutdown(context.TODO())) - - require.Eventually(t, TaskFailed(store, connectorID, nonRetryableDescriptor, "non-retryable error"), time.Second, 100*time.Millisecond) - require.Eventually(t, TaskFailed(store, connectorID, otherErrorDescriptor, "test"), time.Second, 100*time.Millisecond) - require.Eventually(t, TaskTerminated(store, connectorID, noErrorDescriptor), time.Second, 100*time.Millisecond) - require.Eventually(t, TaskActive(store, connectorID, retryableDescriptor), time.Second, 100*time.Millisecond) - }) - - t.Run("errors and retryable errors", func(t *testing.T) { - t.Parallel() - - connectorID := models.ConnectorID{ - Reference: uuid.New(), - Provider: models.ConnectorProviderDummyPay, - } - store := NewInMemoryStore() - nonRetryableDescriptor := newDescriptor() - retryableDescriptor := newDescriptor() - otherErrorDescriptor := newDescriptor() - noErrorDescriptor := newDescriptor() - - scheduler := NewDefaultScheduler(connectorID, store, DefaultContainerFactory, - ResolverFn(func(descriptor models.TaskDescriptor) Task { - switch string(descriptor) { - case string(nonRetryableDescriptor): - return func(ctx context.Context, scheduler Scheduler) error { - return ErrNonRetryable - } - case string(retryableDescriptor): - return func(ctx context.Context, scheduler Scheduler) error { - return ErrRetryable - } - case string(otherErrorDescriptor): - return func(ctx context.Context, scheduler Scheduler) error { - return errors.New("test") - } - case string(noErrorDescriptor): - return func(ctx context.Context, scheduler Scheduler) error { - return nil - } - default: - return func() { - - } - } - }), metrics.NewNoOpMetricsRegistry(), 1) - - require.NoError(t, scheduler.Schedule(context.TODO(), nonRetryableDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: 1 * time.Second, - RestartOption: models.OPTIONS_RESTART_NEVER, - })) - require.Eventually(t, TaskFailed(store, connectorID, nonRetryableDescriptor, "non-retryable error"), time.Second, 100*time.Millisecond) - - require.NoError(t, scheduler.Schedule(context.TODO(), otherErrorDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: 1 * time.Second, - RestartOption: models.OPTIONS_RESTART_NEVER, - })) - require.Eventually(t, TaskFailed(store, connectorID, otherErrorDescriptor, "test"), time.Second, 100*time.Millisecond) - - require.NoError(t, scheduler.Schedule(context.TODO(), noErrorDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: 1 * time.Second, - RestartOption: models.OPTIONS_RESTART_NEVER, - })) - require.Eventually(t, TaskActive(store, connectorID, noErrorDescriptor), time.Second, 100*time.Millisecond) - - require.NoError(t, scheduler.Schedule(context.TODO(), retryableDescriptor, models.TaskSchedulerOptions{ - ScheduleOption: models.OPTIONS_RUN_PERIODICALLY, - Duration: 1 * time.Second, - RestartOption: models.OPTIONS_RESTART_NEVER, - })) - require.Eventually(t, TaskActive(store, connectorID, retryableDescriptor), time.Second, 100*time.Millisecond) - require.NoError(t, scheduler.Shutdown(context.TODO())) - - require.Eventually(t, TaskFailed(store, connectorID, nonRetryableDescriptor, "non-retryable error"), time.Second, 100*time.Millisecond) - require.Eventually(t, TaskFailed(store, connectorID, otherErrorDescriptor, "test"), time.Second, 100*time.Millisecond) - require.Eventually(t, TaskActive(store, connectorID, noErrorDescriptor), time.Second, 100*time.Millisecond) - require.Eventually(t, TaskActive(store, connectorID, retryableDescriptor), time.Second, 100*time.Millisecond) - }) -} diff --git a/components/payments/cmd/connectors/internal/task/state.go b/components/payments/cmd/connectors/internal/task/state.go deleted file mode 100644 index f1e811f8e4..0000000000 --- a/components/payments/cmd/connectors/internal/task/state.go +++ /dev/null @@ -1,39 +0,0 @@ -package task - -import ( - "context" - - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/pkg/errors" -) - -type StateResolver interface { - ResolveTo(ctx context.Context, v any) error -} -type StateResolverFn func(ctx context.Context, v any) error - -func (fn StateResolverFn) ResolveTo(ctx context.Context, v any) error { - return fn(ctx, v) -} - -func ResolveTo[State any](ctx context.Context, resolver StateResolver, to *State) (*State, error) { - err := resolver.ResolveTo(ctx, to) - if err != nil { - return nil, err - } - - return to, nil -} - -func MustResolveTo[State any](ctx context.Context, resolver StateResolver, to State) State { - state, err := ResolveTo(ctx, resolver, &to) - if errors.Is(err, storage.ErrNotFound) { - return to - } - - if err != nil { - panic(err) - } - - return *state -} diff --git a/components/payments/cmd/connectors/internal/task/store.go b/components/payments/cmd/connectors/internal/task/store.go deleted file mode 100644 index 63d7f9a3ab..0000000000 --- a/components/payments/cmd/connectors/internal/task/store.go +++ /dev/null @@ -1,21 +0,0 @@ -package task - -import ( - "context" - - "github.com/formancehq/go-libs/bun/bunpaginate" - - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" -) - -type Repository interface { - UpdateTaskStatus(ctx context.Context, connectorID models.ConnectorID, descriptor models.TaskDescriptor, status models.TaskStatus, err string) error - FindAndUpsertTask(ctx context.Context, connectorID models.ConnectorID, descriptor models.TaskDescriptor, status models.TaskStatus, schedulerOptions models.TaskSchedulerOptions, err string) (*models.Task, error) - ListTasksByStatus(ctx context.Context, connectorID models.ConnectorID, status models.TaskStatus) ([]*models.Task, error) - ListTasks(ctx context.Context, connectorID models.ConnectorID, q storage.ListTasksQuery) (*bunpaginate.Cursor[models.Task], error) - ReadOldestPendingTask(ctx context.Context, connectorID models.ConnectorID) (*models.Task, error) - GetTask(ctx context.Context, taskID uuid.UUID) (*models.Task, error) - GetTaskByDescriptor(ctx context.Context, connectorID models.ConnectorID, descriptor models.TaskDescriptor) (*models.Task, error) -} diff --git a/components/payments/cmd/connectors/internal/task/storememory.go b/components/payments/cmd/connectors/internal/task/storememory.go deleted file mode 100644 index 90112c715e..0000000000 --- a/components/payments/cmd/connectors/internal/task/storememory.go +++ /dev/null @@ -1,250 +0,0 @@ -package task - -import ( - "context" - "encoding/base64" - "encoding/json" - "fmt" - "strings" - "sync" - "time" - - "github.com/formancehq/go-libs/bun/bunpaginate" - - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/formancehq/payments/internal/models" - "github.com/google/uuid" -) - -type InMemoryStore struct { - mu sync.RWMutex - tasks map[uuid.UUID]models.Task - statuses map[string]models.TaskStatus - created map[string]time.Time - errors map[string]string -} - -func (s *InMemoryStore) GetTask(ctx context.Context, id uuid.UUID) (*models.Task, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - task, ok := s.tasks[id] - if !ok { - return nil, storage.ErrNotFound - } - - return &task, nil -} - -func (s *InMemoryStore) GetTaskByDescriptor( - ctx context.Context, - connectorID models.ConnectorID, - descriptor models.TaskDescriptor, -) (*models.Task, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - id, err := descriptor.EncodeToString() - if err != nil { - return nil, err - } - - status, ok := s.statuses[id] - if !ok { - return nil, storage.ErrNotFound - } - - return &models.Task{ - Descriptor: descriptor.ToMessage(), - Status: status, - Error: s.errors[id], - State: nil, - CreatedAt: s.created[id], - }, nil -} - -func (s *InMemoryStore) ListTasks(ctx context.Context, - connectorID models.ConnectorID, - q storage.ListTasksQuery, -) (*bunpaginate.Cursor[models.Task], error) { - s.mu.RLock() - defer s.mu.RUnlock() - - ret := make([]models.Task, 0) - - for id, status := range s.statuses { - if !strings.HasPrefix(id, fmt.Sprintf("%s/", connectorID)) { - continue - } - - var descriptor models.TaskDescriptor - - ret = append(ret, models.Task{ - Descriptor: descriptor.ToMessage(), - Status: status, - Error: s.errors[id], - State: nil, - CreatedAt: s.created[id], - }) - } - - return &bunpaginate.Cursor[models.Task]{ - PageSize: 15, - HasMore: false, - Previous: "", - Next: "", - Data: ret, - }, nil -} - -func (s *InMemoryStore) ReadOldestPendingTask( - ctx context.Context, - connectorID models.ConnectorID, -) (*models.Task, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - var ( - oldestDate time.Time - oldestID string - ) - - for id, status := range s.statuses { - if status != models.TaskStatusPending { - continue - } - - if oldestDate.IsZero() || s.created[id].Before(oldestDate) { - oldestDate = s.created[id] - oldestID = id - } - } - - if oldestDate.IsZero() { - return nil, storage.ErrNotFound - } - - descriptorStr := strings.Split(oldestID, "/")[1] - - var descriptor models.TaskDescriptor - - data, err := base64.StdEncoding.DecodeString(descriptorStr) - if err != nil { - return nil, err - } - - err = json.Unmarshal(data, &descriptor) - if err != nil { - return nil, err - } - - return &models.Task{ - Descriptor: descriptor.ToMessage(), - Status: models.TaskStatusPending, - State: nil, - CreatedAt: s.created[oldestID], - }, nil -} - -func (s *InMemoryStore) ListTasksByStatus( - ctx context.Context, - connectorID models.ConnectorID, - taskStatus models.TaskStatus, -) ([]*models.Task, error) { - cursor, err := s.ListTasks(ctx, connectorID, storage.NewListTasksQuery(storage.NewPaginatedQueryOptions(storage.TaskQuery{}))) - if err != nil { - return nil, err - } - - ret := make([]*models.Task, 0) - - for _, v := range cursor.Data { - if v.Status != taskStatus { - continue - } - - ret = append(ret, &v) - } - - return ret, nil -} - -func (s *InMemoryStore) FindAndUpsertTask( - ctx context.Context, - connectorID models.ConnectorID, - descriptor models.TaskDescriptor, - status models.TaskStatus, - options models.TaskSchedulerOptions, - taskErr string, -) (*models.Task, error) { - err := s.UpdateTaskStatus(ctx, connectorID, descriptor, status, taskErr) - if err != nil { - return nil, err - } - - return &models.Task{ - Descriptor: descriptor.ToMessage(), - Status: status, - Error: taskErr, - State: nil, - }, nil -} - -func (s *InMemoryStore) UpdateTaskStatus( - ctx context.Context, - connectorID models.ConnectorID, - descriptor models.TaskDescriptor, - status models.TaskStatus, - taskError string, -) error { - s.mu.Lock() - defer s.mu.Unlock() - - taskID, err := descriptor.EncodeToString() - if err != nil { - return err - } - - key := fmt.Sprintf("%s/%s", connectorID, taskID) - - s.statuses[key] = status - - s.errors[key] = taskError - if _, ok := s.created[key]; !ok { - s.created[key] = time.Now() - } - - return nil -} - -func (s *InMemoryStore) Result( - connectorID models.ConnectorID, - descriptor models.TaskDescriptor, -) (models.TaskStatus, string, bool) { - s.mu.RLock() - defer s.mu.RUnlock() - - taskID, err := descriptor.EncodeToString() - if err != nil { - panic(err) - } - - key := fmt.Sprintf("%s/%s", connectorID, taskID) - - status, ok := s.statuses[key] - if !ok { - return "", "", false - } - - return status, s.errors[key], true -} - -func NewInMemoryStore() *InMemoryStore { - return &InMemoryStore{ - statuses: make(map[string]models.TaskStatus), - errors: make(map[string]string), - created: make(map[string]time.Time), - } -} - -var _ Repository = &InMemoryStore{} diff --git a/components/payments/cmd/connectors/internal/task/task.go b/components/payments/cmd/connectors/internal/task/task.go deleted file mode 100644 index ce2673226a..0000000000 --- a/components/payments/cmd/connectors/internal/task/task.go +++ /dev/null @@ -1,3 +0,0 @@ -package task - -type Task any diff --git a/components/payments/cmd/connectors/root.go b/components/payments/cmd/connectors/root.go deleted file mode 100644 index 8c46a05a23..0000000000 --- a/components/payments/cmd/connectors/root.go +++ /dev/null @@ -1,46 +0,0 @@ -package connectors - -import ( - "github.com/formancehq/go-libs/auth" - "github.com/formancehq/go-libs/aws/iam" - "github.com/formancehq/go-libs/otlp" - "github.com/formancehq/go-libs/otlp/otlpmetrics" - "github.com/formancehq/go-libs/otlp/otlptraces" - "github.com/formancehq/go-libs/publish" - "github.com/formancehq/go-libs/service" - "github.com/spf13/cobra" -) - -func NewConnectors( - version string, - addAutoMigrateCommandFunc func(cmd *cobra.Command), -) *cobra.Command { - - root := &cobra.Command{ - Use: "connectors", - Short: "connectors", - DisableAutoGenTag: true, - } - - cobra.EnableTraverseRunHooks = true - - server := newServer(version) - addAutoMigrateCommandFunc(server) - root.AddCommand(server) - - server.Flags().BoolP("toggle", "t", false, "Help message for toggle") - server.Flags().String(postgresURIFlag, "postgres://localhost/payments", "PostgreSQL DB address") - server.Flags().String(configEncryptionKeyFlag, "", "Config encryption key") - server.Flags().String(envFlag, "local", "Environment") - server.Flags().String(listenFlag, ":8080", "Listen address") - - service.AddFlags(server.Flags()) - otlp.AddFlags(server.Flags()) - otlptraces.AddFlags(server.Flags()) - otlpmetrics.AddFlags(server.Flags()) - publish.AddFlags(serviceName, server.Flags()) - iam.AddFlags(server.Flags()) - auth.AddFlags(server.Flags()) - - return root -} diff --git a/components/payments/cmd/connectors/serve.go b/components/payments/cmd/connectors/serve.go deleted file mode 100644 index 2a9f364912..0000000000 --- a/components/payments/cmd/connectors/serve.go +++ /dev/null @@ -1,94 +0,0 @@ -package connectors - -import ( - "github.com/bombsimon/logrusr/v3" - sharedapi "github.com/formancehq/go-libs/api" - "github.com/formancehq/go-libs/auth" - "github.com/formancehq/go-libs/bun/bunconnect" - "github.com/formancehq/go-libs/otlp/otlpmetrics" - "github.com/formancehq/go-libs/otlp/otlptraces" - "github.com/formancehq/go-libs/publish" - "github.com/formancehq/go-libs/service" - "github.com/formancehq/payments/cmd/connectors/internal/api" - "github.com/formancehq/payments/cmd/connectors/internal/metrics" - "github.com/formancehq/payments/cmd/connectors/internal/storage" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/metric" - "go.opentelemetry.io/otel/metric/noop" - "go.uber.org/fx" -) - -const ( - stackURLFlag = "stack-url" - postgresURIFlag = "postgres-uri" - configEncryptionKeyFlag = "config-encryption-key" - envFlag = "env" - listenFlag = "listen" - - serviceName = "Payments" -) - -func newServer(version string) *cobra.Command { - return &cobra.Command{ - Use: "serve", - Aliases: []string{"server"}, - Short: "Launch server", - SilenceUsage: true, - RunE: runServer(version), - } -} - -func runServer(version string) func(cmd *cobra.Command, args []string) error { - return func(cmd *cobra.Command, args []string) error { - setLogger() - - databaseOptions, err := prepareDatabaseOptions(cmd, service.IsDebug(cmd)) - if err != nil { - return err - } - - options := make([]fx.Option, 0) - - options = append(options, databaseOptions) - options = append(options, - otlptraces.FXModuleFromFlags(cmd), - otlpmetrics.FXModuleFromFlags(cmd), - auth.FXModuleFromFlags(cmd), - fx.Provide(fx.Annotate(noop.NewMeterProvider, fx.As(new(metric.MeterProvider)))), - fx.Provide(metrics.RegisterMetricsRegistry), - ) - options = append(options, publish.FXModuleFromFlags(cmd, service.IsDebug(cmd))) - listen, _ := cmd.Flags().GetString(listenFlag) - stackURL, _ := cmd.Flags().GetString(stackURLFlag) - otelTraces, _ := cmd.Flags().GetBool(otlptraces.OtelTracesFlag) - - options = append(options, api.HTTPModule(sharedapi.ServiceInfo{ - Version: version, - Debug: service.IsDebug(cmd), - }, listen, stackURL, otelTraces)) - - return service.New(cmd.OutOrStdout(), options...).Run(cmd) - } -} - -func setLogger() { - // Add a dedicated logger for opentelemetry in case of error - otel.SetLogger(logrusr.New(logrus.New().WithField("component", "otlp"))) -} - -func prepareDatabaseOptions(cmd *cobra.Command, debug bool) (fx.Option, error) { - configEncryptionKey, _ := cmd.Flags().GetString(configEncryptionKeyFlag) - if configEncryptionKey == "" { - return nil, errors.New("missing config encryption key") - } - - connectionOptions, err := bunconnect.ConnectionOptionsFromFlags(cmd) - if err != nil { - return nil, err - } - - return storage.Module(*connectionOptions, configEncryptionKey, debug), nil -} diff --git a/components/payments/cmd/migrate.go b/components/payments/cmd/migrate.go index 050f361b64..9f50cdd7a0 100644 --- a/components/payments/cmd/migrate.go +++ b/components/payments/cmd/migrate.go @@ -2,7 +2,6 @@ package cmd import ( "github.com/formancehq/go-libs/bun/bunmigrate" - "github.com/formancehq/payments/internal/storage" "github.com/spf13/cobra" "github.com/uptrace/bun" @@ -12,14 +11,15 @@ import ( ) var ( - configEncryptionKeyFlag = "config-encryption-key" - autoMigrateFlag = "auto-migrate" + autoMigrateFlag = "auto-migrate" ) func newMigrate() *cobra.Command { - return bunmigrate.NewDefaultCommand(Migrate, func(cmd *cobra.Command) { + cmd := bunmigrate.NewDefaultCommand(Migrate, func(cmd *cobra.Command) { cmd.Flags().String(configEncryptionKeyFlag, "", "Config encryption key") }) + + return cmd } func Migrate(cmd *cobra.Command, args []string, db *bun.DB) error { diff --git a/components/payments/cmd/root.go b/components/payments/cmd/root.go index 59ce23bb51..8833e1e1b0 100644 --- a/components/payments/cmd/root.go +++ b/components/payments/cmd/root.go @@ -1,20 +1,44 @@ -//nolint:gochecknoglobals,golint,revive // allow for cobra & logrus init package cmd import ( - "github.com/formancehq/go-libs/bun/bunmigrate" - "github.com/formancehq/go-libs/service" + "errors" + "os" _ "github.com/bombsimon/logrusr/v3" - "github.com/formancehq/payments/cmd/api" - "github.com/formancehq/payments/cmd/connectors" + sharedapi "github.com/formancehq/go-libs/api" + "github.com/formancehq/go-libs/auth" + "github.com/formancehq/go-libs/bun/bunconnect" + "github.com/formancehq/go-libs/bun/bunmigrate" + "github.com/formancehq/go-libs/health" + "github.com/formancehq/go-libs/licence" + "github.com/formancehq/go-libs/otlp/otlptraces" + "github.com/formancehq/go-libs/publish" + "github.com/formancehq/go-libs/service" + "github.com/formancehq/go-libs/temporal" + "github.com/formancehq/payments/internal/api" + v2 "github.com/formancehq/payments/internal/api/v2" + v3 "github.com/formancehq/payments/internal/api/v3" + "github.com/formancehq/payments/internal/connectors/engine" + "github.com/formancehq/payments/internal/storage" "github.com/spf13/cobra" + "go.uber.org/fx" ) +// TODO(polo/crimson): add profiling + var ( - Version = "develop" - BuildDate = "-" - Commit = "-" + ServiceName = "payments" + Version = "develop" + BuildDate = "-" + Commit = "-" +) + +const ( + pluginsDirectoryPathFlag = "plugin-directory-path" + configEncryptionKeyFlag = "config-encryption-key" + listenFlag = "listen" + stackFlag = "stack" + stackPublicURLFlag = "stack-public-url" ) func NewRootCommand() *cobra.Command { @@ -25,17 +49,20 @@ func NewRootCommand() *cobra.Command { Version: Version, } + root.PersistentFlags().String(configEncryptionKeyFlag, "", "Config encryption key") + version := newVersion() root.AddCommand(version) migrate := newMigrate() root.AddCommand(migrate) - api := api.NewAPI(Version, addAutoMigrateCommand) - root.AddCommand(api) - - connectors := connectors.NewConnectors(Version, addAutoMigrateCommand) - root.AddCommand(connectors) + server := newServer() + addAutoMigrateCommand(server) + server.Flags().String(listenFlag, ":8080", "Listen address") + server.Flags().String(pluginsDirectoryPathFlag, "", "Plugin directory path") + server.Flags().String(stackFlag, "", "Stack name") + root.AddCommand(server) return root } @@ -54,3 +81,75 @@ func addAutoMigrateCommand(cmd *cobra.Command) { return nil } } + +func commonOptions(cmd *cobra.Command) (fx.Option, error) { + configEncryptionKey, _ := cmd.Flags().GetString(configEncryptionKeyFlag) + if configEncryptionKey == "" { + return nil, errors.New("missing config encryption key") + } + + connectionOptions, err := bunconnect.ConnectionOptionsFromFlags(cmd) + if err != nil { + return nil, err + } + + path, _ := cmd.Flags().GetString(pluginsDirectoryPathFlag) + pluginPaths, err := getPluginsMap(path) + if err != nil { + return nil, err + } + + listen, _ := cmd.Flags().GetString(listenFlag) + stack, _ := cmd.Flags().GetString(stackFlag) + stackPublicURL, _ := cmd.Flags().GetString(stackPublicURLFlag) + + return fx.Options( + fx.Provide(func() *bunconnect.ConnectionOptions { + return connectionOptions + }), + fx.Provide(func() sharedapi.ServiceInfo { + return sharedapi.ServiceInfo{ + Version: Version, + } + }), + otlptraces.FXModuleFromFlags(cmd), + temporal.FXModuleFromFlags( + cmd, + engine.Tracer, + temporal.SearchAttributes{ + SearchAttributes: engine.SearchAttributes, + }, + ), + auth.FXModuleFromFlags(cmd), + health.Module(), + publish.FXModuleFromFlags(cmd, service.IsDebug(cmd)), + licence.FXModuleFromFlags(cmd, ServiceName), + storage.Module(cmd, *connectionOptions, configEncryptionKey), + api.NewModule(listen, service.IsDebug(cmd)), + engine.Module(pluginPaths, stack, stackPublicURL), + v2.NewModule(), + v3.NewModule(), + ), nil +} + +func getPluginsMap(pluginsDirectoryPath string) (map[string]string, error) { + if pluginsDirectoryPath == "" { + return nil, errors.New("missing plugin directory path") + } + + files, err := os.ReadDir(pluginsDirectoryPath) + if err != nil { + return nil, errors.New("failed to read plugins directory") + } + + plugins := make(map[string]string) + for _, file := range files { + if file.IsDir() { + continue + } + + plugins[file.Name()] = pluginsDirectoryPath + "/" + file.Name() + } + + return plugins, nil +} diff --git a/components/payments/cmd/server.go b/components/payments/cmd/server.go new file mode 100644 index 0000000000..114e8f87ab --- /dev/null +++ b/components/payments/cmd/server.go @@ -0,0 +1,57 @@ +package cmd + +import ( + "github.com/bombsimon/logrusr/v3" + "github.com/formancehq/go-libs/auth" + "github.com/formancehq/go-libs/aws/iam" + "github.com/formancehq/go-libs/bun/bunconnect" + "github.com/formancehq/go-libs/licence" + "github.com/formancehq/go-libs/otlp/otlpmetrics" + "github.com/formancehq/go-libs/otlp/otlptraces" + "github.com/formancehq/go-libs/publish" + "github.com/formancehq/go-libs/service" + "github.com/formancehq/go-libs/temporal" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "go.opentelemetry.io/otel" +) + +func newServer() *cobra.Command { + cmd := &cobra.Command{ + Use: "serve", + Aliases: []string{"server"}, + Short: "Launch api server", + SilenceUsage: true, + RunE: runServer(), + } + + service.AddFlags(cmd.Flags()) + otlpmetrics.AddFlags(cmd.Flags()) + otlptraces.AddFlags(cmd.Flags()) + auth.AddFlags(cmd.Flags()) + publish.AddFlags(ServiceName, cmd.Flags()) + bunconnect.AddFlags(cmd.Flags()) + iam.AddFlags(cmd.Flags()) + temporal.AddFlags(cmd.Flags()) + licence.AddFlags(cmd.Flags()) + + return cmd +} + +func runServer() func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { + setLogger() + + options, err := commonOptions(cmd) + if err != nil { + return err + } + + return service.New(cmd.OutOrStdout(), options).Run(cmd) + } +} + +func setLogger() { + // Add a dedicated logger for opentelemetry in case of error + otel.SetLogger(logrusr.New(logrus.New().WithField("component", "otlp"))) +} diff --git a/components/payments/docker-compose.yml b/components/payments/docker-compose.yml index e58c15f77a..8ae8ea6ad7 100644 --- a/components/payments/docker-compose.yml +++ b/components/payments/docker-compose.yml @@ -33,9 +33,9 @@ services: environment: POSTGRES_URI: postgres://payments:payments@postgres:${POSTGRES_PORT:-5432}/payments?sslmode=disable - payments-api: + payments: image: golang:1.22.4-alpine3.19 - command: go run ./ api server + command: go run ./ server healthcheck: test: [ "CMD", "curl", "-f", "http://127.0.0.1:8080/_healthcheck" ] interval: 10s @@ -57,26 +57,4 @@ services: POSTGRES_URI: postgres://payments:payments@postgres:${POSTGRES_PORT:-5432}/payments?sslmode=disable CONFIG_ENCRYPTION_KEY: mysuperencryptionkey - payments-connectors: - image: golang:1.22.4-alpine3.19 - command: go run ./ connectors server - healthcheck: - test: [ "CMD", "curl", "-f", "http://127.0.0.1:8081/_healthcheck" ] - interval: 10s - timeout: 5s - retries: 5 - depends_on: - postgres: - condition: service_healthy - payments-migrate: - condition: service_completed_successfully - ports: - - "8081:8080" - volumes: - - .:/app/components/payments - - ../../libs:/app/libs - working_dir: /app/components/payments - environment: - DEBUG: ${DEBUG:-"true"} - POSTGRES_URI: postgres://payments:payments@postgres:${POSTGRES_PORT:-5432}/payments?sslmode=disable - CONFIG_ENCRYPTION_KEY: mysuperencryptionkey \ No newline at end of file +# TODO(polo): add temporal \ No newline at end of file diff --git a/components/payments/go.mod b/components/payments/go.mod index ce4081d2f1..d8b8d410af 100644 --- a/components/payments/go.mod +++ b/components/payments/go.mod @@ -6,60 +6,46 @@ toolchain go1.22.7 require ( github.com/ThreeDotsLabs/watermill v1.3.7 - github.com/adyen/adyen-go-api-library/v7 v7.3.1 - github.com/alitto/pond v1.8.3 github.com/bombsimon/logrusr/v3 v3.1.0 - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc - github.com/formancehq/go-libs v1.7.1 - github.com/formancehq/payments/genericclient v0.0.0-00010101000000-000000000000 - github.com/get-momo/atlar-v1-go-client v1.2.1 + github.com/formancehq/go-libs v1.7.2-0.20240925132527-7627842ea9b5 github.com/gibson042/canonicaljson-go v1.0.3 - github.com/go-openapi/runtime v0.26.0 - github.com/go-openapi/strfmt v0.21.8 + github.com/go-chi/chi/v5 v5.1.0 github.com/golang/mock v1.6.0 github.com/google/uuid v1.6.0 - github.com/gorilla/mux v1.8.1 + github.com/hashicorp/go-hclog v1.6.3 + github.com/hashicorp/go-plugin v1.6.1 github.com/hashicorp/golang-lru/v2 v2.0.4 github.com/jackc/pgx/v5 v5.7.1 github.com/lib/pq v1.10.9 + github.com/onsi/ginkgo/v2 v2.20.2 + github.com/onsi/gomega v1.34.2 github.com/pkg/errors v0.9.1 - github.com/rs/cors v1.11.1 github.com/sirupsen/logrus v1.9.3 - github.com/spf13/afero v1.11.0 github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.9.0 - github.com/stripe/stripe-go/v72 v72.122.0 github.com/uptrace/bun v1.2.3 - github.com/uptrace/bun/dialect/pgdialect v1.2.3 - github.com/uptrace/bun/extra/bundebug v1.2.3 - go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.44.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 go.opentelemetry.io/otel v1.30.0 - go.opentelemetry.io/otel/metric v1.30.0 go.opentelemetry.io/otel/trace v1.30.0 - go.uber.org/dig v1.18.0 + go.temporal.io/api v1.39.0 + go.temporal.io/sdk v1.29.1 go.uber.org/fx v1.22.2 - go.uber.org/mock v0.4.0 golang.org/x/oauth2 v0.23.0 - golang.org/x/sync v0.8.0 + google.golang.org/grpc v1.67.0 + google.golang.org/protobuf v1.34.2 ) require ( - dario.cat/mergo v1.0.1 // indirect filippo.io/edwards25519 v1.1.0 // indirect - github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/IBM/sarama v1.43.3 // indirect - github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/ThreeDotsLabs/watermill-http/v2 v2.3.1 // indirect github.com/ThreeDotsLabs/watermill-kafka/v3 v3.0.5 // indirect github.com/ThreeDotsLabs/watermill-nats/v2 v2.1.1 // indirect github.com/ajg/form v1.5.1 // indirect - github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aws/aws-msk-iam-sasl-signer-go v1.0.0 // indirect github.com/aws/aws-sdk-go-v2 v1.31.0 // indirect - github.com/aws/aws-sdk-go-v2/config v1.27.36 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.17.34 // indirect + github.com/aws/aws-sdk-go-v2/config v1.27.37 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.35 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14 // indirect github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.18 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18 // indirect @@ -67,95 +53,91 @@ require ( github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.23.0 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.31.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.23.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.31.1 // indirect github.com/aws/smithy-go v1.21.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/containerd/continuity v0.4.3 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect github.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 // indirect - github.com/docker/cli v27.3.1+incompatible // indirect - github.com/docker/docker v27.3.1+incompatible // indirect - github.com/docker/go-connections v0.5.0 // indirect - github.com/docker/go-units v0.5.0 // indirect github.com/eapache/go-resiliency v1.7.0 // indirect github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect github.com/eapache/queue v1.1.0 // indirect + github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect github.com/fatih/color v1.17.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-chi/chi v4.1.2+incompatible // indirect - github.com/go-chi/chi/v5 v5.1.0 // indirect github.com/go-chi/render v1.0.3 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect - github.com/go-openapi/analysis v0.21.4 // indirect - github.com/go-openapi/errors v0.20.4 // indirect - github.com/go-openapi/jsonpointer v0.20.0 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/loads v0.21.2 // indirect - github.com/go-openapi/spec v0.20.9 // indirect - github.com/go-openapi/swag v0.22.4 // indirect - github.com/go-openapi/validate v0.22.2 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect - github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/goccy/go-json v0.10.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect - github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/schema v1.4.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect + github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/hashicorp/yamux v0.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jcmturner/aescts/v2 v2.0.0 // indirect github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect github.com/jcmturner/gofork v1.7.6 // indirect github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/jinzhu/inflection v1.0.0 // indirect - github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.17.9 // indirect + github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect + github.com/lestrrat-go/blackmagic v1.0.2 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/iter v1.0.2 // indirect + github.com/lestrrat-go/jwx v1.2.30 // indirect + github.com/lestrrat-go/option v1.0.1 // indirect github.com/lithammer/shortuuid/v3 v3.0.7 // indirect github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect - github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/term v0.5.0 // indirect + github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 // indirect github.com/muhlemmer/gu v0.3.1 // indirect github.com/muhlemmer/httpforwarded v0.1.0 // indirect github.com/nats-io/nats.go v1.37.0 // indirect github.com/nats-io/nkeys v0.4.7 // indirect github.com/nats-io/nuid v1.0.1 // indirect + github.com/nexus-rpc/sdk-go v0.0.10 // indirect + github.com/oklog/run v1.0.0 // indirect github.com/oklog/ulid v1.3.1 // indirect - github.com/onsi/ginkgo/v2 v2.20.2 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0 // indirect - github.com/opencontainers/runc v1.1.14 // indirect - github.com/opentracing/opentracing-go v1.2.0 // indirect - github.com/ory/dockertest/v3 v3.11.0 // indirect + github.com/pborman/uuid v1.2.1 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/puzpuzpuz/xsync/v3 v3.4.0 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect github.com/riandyrn/otelchi v0.10.0 // indirect + github.com/robfig/cron v1.2.0 // indirect + github.com/rs/cors v1.11.1 // indirect github.com/shirou/gopsutil/v4 v4.24.8 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/tklauser/go-sysconf v0.3.14 // indirect github.com/tklauser/numcpus v0.8.0 // indirect github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect + github.com/uptrace/bun/dialect/pgdialect v1.2.3 // indirect github.com/uptrace/bun/extra/bunotel v1.2.3 // indirect github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 // indirect github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect @@ -165,13 +147,9 @@ require ( github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect - github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect - github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xo/dburl v0.23.2 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/zitadel/oidc/v2 v2.12.2 // indirect - go.mongodb.org/mongo-driver v1.12.0 // indirect go.opentelemetry.io/contrib/instrumentation/host v0.55.0 // indirect go.opentelemetry.io/contrib/instrumentation/runtime v0.55.0 // indirect go.opentelemetry.io/contrib/propagators/b3 v1.30.0 // indirect @@ -183,23 +161,27 @@ require ( go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.30.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.30.0 // indirect go.opentelemetry.io/otel/log v0.6.0 // indirect + go.opentelemetry.io/otel/metric v1.30.0 // indirect go.opentelemetry.io/otel/sdk v1.30.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.30.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect + go.temporal.io/sdk/contrib/opentelemetry v0.6.0 // indirect + go.uber.org/dig v1.18.0 // indirect + go.uber.org/mock v0.4.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.27.0 // indirect + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect golang.org/x/net v0.29.0 // indirect + golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.25.0 // indirect golang.org/x/text v0.18.0 // indirect + golang.org/x/time v0.6.0 // indirect golang.org/x/tools v0.25.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect - google.golang.org/grpc v1.67.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) -replace github.com/formancehq/payments/genericclient => ./cmd/connectors/internal/connectors/generic/client/generated +// replace github.com/formancehq/payments/genericclient => ./cmd/connectors/internal/connectors/generic/client/generated diff --git a/components/payments/go.sum b/components/payments/go.sum index fc175621c1..328766b481 100644 --- a/components/payments/go.sum +++ b/components/payments/go.sum @@ -1,618 +1,17 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= -cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= -cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= -cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= -cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= -cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= -cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= -cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= -cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= -cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= -cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= -cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= -cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= -cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= -cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= -cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= -cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw= -cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= -cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= -cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= -cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o= -cloud.google.com/go/accesscontextmanager v1.4.0/go.mod h1:/Kjh7BBu/Gh83sv+K60vN9QE5NJcd80sU33vIe2IFPE= -cloud.google.com/go/accesscontextmanager v1.6.0/go.mod h1:8XCvZWfYw3K/ji0iVnp+6pu7huxoQTLmxAbVjbloTtM= -cloud.google.com/go/accesscontextmanager v1.7.0/go.mod h1:CEGLewx8dwa33aDAZQujl7Dx+uYhS0eay198wB/VumQ= -cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= -cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= -cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg= -cloud.google.com/go/aiplatform v1.35.0/go.mod h1:7MFT/vCaOyZT/4IIFfxH4ErVg/4ku6lKv3w0+tFTgXQ= -cloud.google.com/go/aiplatform v1.36.1/go.mod h1:WTm12vJRPARNvJ+v6P52RDHCNe4AhvjcIZ/9/RRHy/k= -cloud.google.com/go/aiplatform v1.37.0/go.mod h1:IU2Cv29Lv9oCn/9LkFiiuKfwrRTq+QQMbW+hPCxJGZw= -cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= -cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= -cloud.google.com/go/analytics v0.17.0/go.mod h1:WXFa3WSym4IZ+JiKmavYdJwGG/CvpqiqczmL59bTD9M= -cloud.google.com/go/analytics v0.18.0/go.mod h1:ZkeHGQlcIPkw0R/GW+boWHhCOR43xz9RN/jn7WcqfIE= -cloud.google.com/go/analytics v0.19.0/go.mod h1:k8liqf5/HCnOUkbawNtrWWc+UAzyDlW89doe8TtoDsE= -cloud.google.com/go/apigateway v1.3.0/go.mod h1:89Z8Bhpmxu6AmUxuVRg/ECRGReEdiP3vQtk4Z1J9rJk= -cloud.google.com/go/apigateway v1.4.0/go.mod h1:pHVY9MKGaH9PQ3pJ4YLzoj6U5FUDeDFBllIz7WmzJoc= -cloud.google.com/go/apigateway v1.5.0/go.mod h1:GpnZR3Q4rR7LVu5951qfXPJCHquZt02jf7xQx7kpqN8= -cloud.google.com/go/apigeeconnect v1.3.0/go.mod h1:G/AwXFAKo0gIXkPTVfZDd2qA1TxBXJ3MgMRBQkIi9jc= -cloud.google.com/go/apigeeconnect v1.4.0/go.mod h1:kV4NwOKqjvt2JYR0AoIWo2QGfoRtn/pkS3QlHp0Ni04= -cloud.google.com/go/apigeeconnect v1.5.0/go.mod h1:KFaCqvBRU6idyhSNyn3vlHXc8VMDJdRmwDF6JyFRqZ8= -cloud.google.com/go/apigeeregistry v0.4.0/go.mod h1:EUG4PGcsZvxOXAdyEghIdXwAEi/4MEaoqLMLDMIwKXY= -cloud.google.com/go/apigeeregistry v0.5.0/go.mod h1:YR5+s0BVNZfVOUkMa5pAR2xGd0A473vA5M7j247o1wM= -cloud.google.com/go/apigeeregistry v0.6.0/go.mod h1:BFNzW7yQVLZ3yj0TKcwzb8n25CFBri51GVGOEUcgQsc= -cloud.google.com/go/apikeys v0.4.0/go.mod h1:XATS/yqZbaBK0HOssf+ALHp8jAlNHUgyfprvNcBIszU= -cloud.google.com/go/apikeys v0.5.0/go.mod h1:5aQfwY4D+ewMMWScd3hm2en3hCj+BROlyrt3ytS7KLI= -cloud.google.com/go/apikeys v0.6.0/go.mod h1:kbpXu5upyiAlGkKrJgQl8A0rKNNJ7dQ377pdroRSSi8= -cloud.google.com/go/appengine v1.4.0/go.mod h1:CS2NhuBuDXM9f+qscZ6V86m1MIIqPj3WC/UoEuR1Sno= -cloud.google.com/go/appengine v1.5.0/go.mod h1:TfasSozdkFI0zeoxW3PTBLiNqRmzraodCWatWI9Dmak= -cloud.google.com/go/appengine v1.6.0/go.mod h1:hg6i0J/BD2cKmDJbaFSYHFyZkgBEfQrDg/X0V5fJn84= -cloud.google.com/go/appengine v1.7.0/go.mod h1:eZqpbHFCqRGa2aCdope7eC0SWLV1j0neb/QnMJVWx6A= -cloud.google.com/go/appengine v1.7.1/go.mod h1:IHLToyb/3fKutRysUlFO0BPt5j7RiQ45nrzEJmKTo6E= -cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= -cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= -cloud.google.com/go/area120 v0.7.0/go.mod h1:a3+8EUD1SX5RUcCs3MY5YasiO1z6yLiNLRiFrykbynY= -cloud.google.com/go/area120 v0.7.1/go.mod h1:j84i4E1RboTWjKtZVWXPqvK5VHQFJRF2c1Nm69pWm9k= -cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= -cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= -cloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0= -cloud.google.com/go/artifactregistry v1.9.0/go.mod h1:2K2RqvA2CYvAeARHRkLDhMDJ3OXy26h3XW+3/Jh2uYc= -cloud.google.com/go/artifactregistry v1.11.1/go.mod h1:lLYghw+Itq9SONbCa1YWBoWs1nOucMH0pwXN1rOBZFI= -cloud.google.com/go/artifactregistry v1.11.2/go.mod h1:nLZns771ZGAwVLzTX/7Al6R9ehma4WUEhZGWV6CeQNQ= -cloud.google.com/go/artifactregistry v1.12.0/go.mod h1:o6P3MIvtzTOnmvGagO9v/rOjjA0HmhJ+/6KAXrmYDCI= -cloud.google.com/go/artifactregistry v1.13.0/go.mod h1:uy/LNfoOIivepGhooAUpL1i30Hgee3Cu0l4VTWHUC08= -cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= -cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= -cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= -cloud.google.com/go/asset v1.9.0/go.mod h1:83MOE6jEJBMqFKadM9NLRcs80Gdw76qGuHn8m3h8oHQ= -cloud.google.com/go/asset v1.10.0/go.mod h1:pLz7uokL80qKhzKr4xXGvBQXnzHn5evJAEAtZiIb0wY= -cloud.google.com/go/asset v1.11.1/go.mod h1:fSwLhbRvC9p9CXQHJ3BgFeQNM4c9x10lqlrdEUYXlJo= -cloud.google.com/go/asset v1.12.0/go.mod h1:h9/sFOa4eDIyKmH6QMpm4eUK3pDojWnUhTgJlk762Hg= -cloud.google.com/go/asset v1.13.0/go.mod h1:WQAMyYek/b7NBpYq/K4KJWcRqzoalEsxz/t/dTk4THw= -cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= -cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= -cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= -cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= -cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= -cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= -cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= -cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= -cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= -cloud.google.com/go/automl v1.8.0/go.mod h1:xWx7G/aPEe/NP+qzYXktoBSDfjO+vnKMGgsApGJJquM= -cloud.google.com/go/automl v1.12.0/go.mod h1:tWDcHDp86aMIuHmyvjuKeeHEGq76lD7ZqfGLN6B0NuU= -cloud.google.com/go/baremetalsolution v0.3.0/go.mod h1:XOrocE+pvK1xFfleEnShBlNAXf+j5blPPxrhjKgnIFc= -cloud.google.com/go/baremetalsolution v0.4.0/go.mod h1:BymplhAadOO/eBa7KewQ0Ppg4A4Wplbn+PsFKRLo0uI= -cloud.google.com/go/baremetalsolution v0.5.0/go.mod h1:dXGxEkmR9BMwxhzBhV0AioD0ULBmuLZI8CdwalUxuss= -cloud.google.com/go/batch v0.3.0/go.mod h1:TR18ZoAekj1GuirsUsR1ZTKN3FC/4UDnScjT8NXImFE= -cloud.google.com/go/batch v0.4.0/go.mod h1:WZkHnP43R/QCGQsZ+0JyG4i79ranE2u8xvjq/9+STPE= -cloud.google.com/go/batch v0.7.0/go.mod h1:vLZN95s6teRUqRQ4s3RLDsH8PvboqBK+rn1oevL159g= -cloud.google.com/go/beyondcorp v0.2.0/go.mod h1:TB7Bd+EEtcw9PCPQhCJtJGjk/7TC6ckmnSFS+xwTfm4= -cloud.google.com/go/beyondcorp v0.3.0/go.mod h1:E5U5lcrcXMsCuoDNyGrpyTm/hn7ne941Jz2vmksAxW8= -cloud.google.com/go/beyondcorp v0.4.0/go.mod h1:3ApA0mbhHx6YImmuubf5pyW8srKnCEPON32/5hj+RmM= -cloud.google.com/go/beyondcorp v0.5.0/go.mod h1:uFqj9X+dSfrheVp7ssLTaRHd2EHqSL4QZmH4e8WXGGU= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= -cloud.google.com/go/bigquery v1.43.0/go.mod h1:ZMQcXHsl+xmU1z36G2jNGZmKp9zNY5BUua5wDgmNCfw= -cloud.google.com/go/bigquery v1.44.0/go.mod h1:0Y33VqXTEsbamHJvJHdFmtqHvMIY28aK1+dFsvaChGc= -cloud.google.com/go/bigquery v1.47.0/go.mod h1:sA9XOgy0A8vQK9+MWhEQTY6Tix87M/ZurWFIxmF9I/E= -cloud.google.com/go/bigquery v1.48.0/go.mod h1:QAwSz+ipNgfL5jxiaK7weyOhzdoAy1zFm0Nf1fysJac= -cloud.google.com/go/bigquery v1.49.0/go.mod h1:Sv8hMmTFFYBlt/ftw2uN6dFdQPzBlREY9yBh7Oy7/4Q= -cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU= -cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= -cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= -cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI= -cloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y= -cloud.google.com/go/billing v1.12.0/go.mod h1:yKrZio/eu+okO/2McZEbch17O5CB5NpZhhXG6Z766ss= -cloud.google.com/go/billing v1.13.0/go.mod h1:7kB2W9Xf98hP9Sr12KfECgfGclsH3CQR0R08tnRlRbc= -cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= -cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= -cloud.google.com/go/binaryauthorization v1.3.0/go.mod h1:lRZbKgjDIIQvzYQS1p99A7/U1JqvqeZg0wiI5tp6tg0= -cloud.google.com/go/binaryauthorization v1.4.0/go.mod h1:tsSPQrBd77VLplV70GUhBf/Zm3FsKmgSqgm4UmiDItk= -cloud.google.com/go/binaryauthorization v1.5.0/go.mod h1:OSe4OU1nN/VswXKRBmciKpo9LulY41gch5c68htf3/Q= -cloud.google.com/go/certificatemanager v1.3.0/go.mod h1:n6twGDvcUBFu9uBgt4eYvvf3sQ6My8jADcOVwHmzadg= -cloud.google.com/go/certificatemanager v1.4.0/go.mod h1:vowpercVFyqs8ABSmrdV+GiFf2H/ch3KyudYQEMM590= -cloud.google.com/go/certificatemanager v1.6.0/go.mod h1:3Hh64rCKjRAX8dXgRAyOcY5vQ/fE1sh8o+Mdd6KPgY8= -cloud.google.com/go/channel v1.8.0/go.mod h1:W5SwCXDJsq/rg3tn3oG0LOxpAo6IMxNa09ngphpSlnk= -cloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL8yBMbKUk= -cloud.google.com/go/channel v1.11.0/go.mod h1:IdtI0uWGqhEeatSB62VOoJ8FSUhJ9/+iGkJVqp74CGE= -cloud.google.com/go/channel v1.12.0/go.mod h1:VkxCGKASi4Cq7TbXxlaBezonAYpp1GCnKMY6tnMQnLU= -cloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U= -cloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA= -cloud.google.com/go/cloudbuild v1.6.0/go.mod h1:UIbc/w9QCbH12xX+ezUsgblrWv+Cv4Tw83GiSMHOn9M= -cloud.google.com/go/cloudbuild v1.7.0/go.mod h1:zb5tWh2XI6lR9zQmsm1VRA+7OCuve5d8S+zJUul8KTg= -cloud.google.com/go/cloudbuild v1.9.0/go.mod h1:qK1d7s4QlO0VwfYn5YuClDGg2hfmLZEb4wQGAbIgL1s= -cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM= -cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk= -cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA= -cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= -cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= -cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4= -cloud.google.com/go/cloudtasks v1.8.0/go.mod h1:gQXUIwCSOI4yPVK7DgTVFiiP0ZW/eQkydWzwVMdHxrI= -cloud.google.com/go/cloudtasks v1.9.0/go.mod h1:w+EyLsVkLWHcOaqNEyvcKAsWp9p29dL6uL9Nst1cI7Y= -cloud.google.com/go/cloudtasks v1.10.0/go.mod h1:NDSoTLkZ3+vExFEWu2UJV1arUyzVDAiZtdWcsUyNwBs= -cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= -cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= -cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= -cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= -cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= -cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= -cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= -cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= -cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= -cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE= -cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= -cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= -cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= -cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= -cloud.google.com/go/compute v1.19.3/go.mod h1:qxvISKp/gYnXkSAD1ppcSOveRAmzxicEv/JlizULFrI= -cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= -cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= -cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= -cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= -cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= -cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= -cloud.google.com/go/container v1.6.0/go.mod h1:Xazp7GjJSeUYo688S+6J5V+n/t+G5sKBTFkKNudGRxg= -cloud.google.com/go/container v1.7.0/go.mod h1:Dp5AHtmothHGX3DwwIHPgq45Y8KmNsgN3amoYfxVkLo= -cloud.google.com/go/container v1.13.1/go.mod h1:6wgbMPeQRw9rSnKBCAJXnds3Pzj03C4JHamr8asWKy4= -cloud.google.com/go/container v1.14.0/go.mod h1:3AoJMPhHfLDxLvrlVWaK57IXzaPnLaZq63WX59aQBfM= -cloud.google.com/go/container v1.15.0/go.mod h1:ft+9S0WGjAyjDggg5S06DXj+fHJICWg8L7isCQe9pQA= -cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= -cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= -cloud.google.com/go/containeranalysis v0.7.0/go.mod h1:9aUL+/vZ55P2CXfuZjS4UjQ9AgXoSw8Ts6lemfmxBxI= -cloud.google.com/go/containeranalysis v0.9.0/go.mod h1:orbOANbwk5Ejoom+s+DUCTTJ7IBdBQJDcSylAx/on9s= -cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= -cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= -cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= -cloud.google.com/go/datacatalog v1.7.0/go.mod h1:9mEl4AuDYWw81UGc41HonIHH7/sn52H0/tc8f8ZbZIE= -cloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOXxZoKYF5wdISM= -cloud.google.com/go/datacatalog v1.8.1/go.mod h1:RJ58z4rMp3gvETA465Vg+ag8BGgBdnRPEMMSTr5Uv+M= -cloud.google.com/go/datacatalog v1.12.0/go.mod h1:CWae8rFkfp6LzLumKOnmVh4+Zle4A3NXLzVJ1d1mRm0= -cloud.google.com/go/datacatalog v1.13.0/go.mod h1:E4Rj9a5ZtAxcQJlEBTLgMTphfP11/lNaAshpoBgemX8= -cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= -cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= -cloud.google.com/go/dataflow v0.8.0/go.mod h1:Rcf5YgTKPtQyYz8bLYhFoIV/vP39eL7fWNcSOyFfLJE= -cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= -cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= -cloud.google.com/go/dataform v0.5.0/go.mod h1:GFUYRe8IBa2hcomWplodVmUx/iTL0FrsauObOM3Ipr0= -cloud.google.com/go/dataform v0.6.0/go.mod h1:QPflImQy33e29VuapFdf19oPbE4aYTJxr31OAPV+ulA= -cloud.google.com/go/dataform v0.7.0/go.mod h1:7NulqnVozfHvWUBpMDfKMUESr+85aJsC/2O0o3jWPDE= -cloud.google.com/go/datafusion v1.4.0/go.mod h1:1Zb6VN+W6ALo85cXnM1IKiPw+yQMKMhB9TsTSRDo/38= -cloud.google.com/go/datafusion v1.5.0/go.mod h1:Kz+l1FGHB0J+4XF2fud96WMmRiq/wj8N9u007vyXZ2w= -cloud.google.com/go/datafusion v1.6.0/go.mod h1:WBsMF8F1RhSXvVM8rCV3AeyWVxcC2xY6vith3iw3S+8= -cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= -cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= -cloud.google.com/go/datalabeling v0.7.0/go.mod h1:WPQb1y08RJbmpM3ww0CSUAGweL0SxByuW2E+FU+wXcM= -cloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX8gdmFcZzA= -cloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A= -cloud.google.com/go/dataplex v1.5.2/go.mod h1:cVMgQHsmfRoI5KFYq4JtIBEUbYwc3c7tXmIDhRmNNVQ= -cloud.google.com/go/dataplex v1.6.0/go.mod h1:bMsomC/aEJOSpHXdFKFGQ1b0TDPIeL28nJObeO1ppRs= -cloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s= -cloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI= -cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4= -cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= -cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= -cloud.google.com/go/dataqna v0.7.0/go.mod h1:Lx9OcIIeqCrw1a6KdO3/5KMP1wAmTc0slZWwP12Qq3c= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/datastore v1.10.0/go.mod h1:PC5UzAmDEkAmkfaknstTYbNpgE49HAgW2J1gcgUfmdM= -cloud.google.com/go/datastore v1.11.0/go.mod h1:TvGxBIHCS50u8jzG+AW/ppf87v1of8nwzFNgEZU1D3c= -cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= -cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= -cloud.google.com/go/datastream v1.4.0/go.mod h1:h9dpzScPhDTs5noEMQVWP8Wx8AFBRyS0s8KWPx/9r0g= -cloud.google.com/go/datastream v1.5.0/go.mod h1:6TZMMNPwjUqZHBKPQ1wwXpb0d5VDVPl2/XoS5yi88q4= -cloud.google.com/go/datastream v1.6.0/go.mod h1:6LQSuswqLa7S4rPAOZFVjHIG3wJIjZcZrw8JDEDJuIs= -cloud.google.com/go/datastream v1.7.0/go.mod h1:uxVRMm2elUSPuh65IbZpzJNMbuzkcvu5CjMqVIUHrww= -cloud.google.com/go/deploy v1.4.0/go.mod h1:5Xghikd4VrmMLNaF6FiRFDlHb59VM59YoDQnOUdsH/c= -cloud.google.com/go/deploy v1.5.0/go.mod h1:ffgdD0B89tToyW/U/D2eL0jN2+IEV/3EMuXHA0l4r+s= -cloud.google.com/go/deploy v1.6.0/go.mod h1:f9PTHehG/DjCom3QH0cntOVRm93uGBDt2vKzAPwpXQI= -cloud.google.com/go/deploy v1.8.0/go.mod h1:z3myEJnA/2wnB4sgjqdMfgxCA0EqC3RBTNcVPs93mtQ= -cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= -cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= -cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= -cloud.google.com/go/dialogflow v1.18.0/go.mod h1:trO7Zu5YdyEuR+BhSNOqJezyFQ3aUzz0njv7sMx/iek= -cloud.google.com/go/dialogflow v1.19.0/go.mod h1:JVmlG1TwykZDtxtTXujec4tQ+D8SBFMoosgy+6Gn0s0= -cloud.google.com/go/dialogflow v1.29.0/go.mod h1:b+2bzMe+k1s9V+F2jbJwpHPzrnIyHihAdRFMtn2WXuM= -cloud.google.com/go/dialogflow v1.31.0/go.mod h1:cuoUccuL1Z+HADhyIA7dci3N5zUssgpBJmCzI6fNRB4= -cloud.google.com/go/dialogflow v1.32.0/go.mod h1:jG9TRJl8CKrDhMEcvfcfFkkpp8ZhgPz3sBGmAUYJ2qE= -cloud.google.com/go/dlp v1.6.0/go.mod h1:9eyB2xIhpU0sVwUixfBubDoRwP+GjeUoxxeueZmqvmM= -cloud.google.com/go/dlp v1.7.0/go.mod h1:68ak9vCiMBjbasxeVD17hVPxDEck+ExiHavX8kiHG+Q= -cloud.google.com/go/dlp v1.9.0/go.mod h1:qdgmqgTyReTz5/YNSSuueR8pl7hO0o9bQ39ZhtgkWp4= -cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= -cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= -cloud.google.com/go/documentai v1.9.0/go.mod h1:FS5485S8R00U10GhgBC0aNGrJxBP8ZVpEeJ7PQDZd6k= -cloud.google.com/go/documentai v1.10.0/go.mod h1:vod47hKQIPeCfN2QS/jULIvQTugbmdc0ZvxxfQY1bg4= -cloud.google.com/go/documentai v1.16.0/go.mod h1:o0o0DLTEZ+YnJZ+J4wNfTxmDVyrkzFvttBXXtYRMHkM= -cloud.google.com/go/documentai v1.18.0/go.mod h1:F6CK6iUH8J81FehpskRmhLq/3VlwQvb7TvwOceQ2tbs= -cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= -cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= -cloud.google.com/go/domains v0.8.0/go.mod h1:M9i3MMDzGFXsydri9/vW+EWz9sWb4I6WyHqdlAk0idE= -cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= -cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= -cloud.google.com/go/edgecontainer v0.3.0/go.mod h1:FLDpP4nykgwwIfcLt6zInhprzw0lEi2P1fjO6Ie0qbc= -cloud.google.com/go/edgecontainer v1.0.0/go.mod h1:cttArqZpBB2q58W/upSG++ooo6EsblxDIolxa3jSjbY= -cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= -cloud.google.com/go/essentialcontacts v1.3.0/go.mod h1:r+OnHa5jfj90qIfZDO/VztSFqbQan7HV75p8sA+mdGI= -cloud.google.com/go/essentialcontacts v1.4.0/go.mod h1:8tRldvHYsmnBCHdFpvU+GL75oWiBKl80BiqlFh9tp+8= -cloud.google.com/go/essentialcontacts v1.5.0/go.mod h1:ay29Z4zODTuwliK7SnX8E86aUF2CTzdNtvv42niCX0M= -cloud.google.com/go/eventarc v1.7.0/go.mod h1:6ctpF3zTnaQCxUjHUdcfgcA1A2T309+omHZth7gDfmc= -cloud.google.com/go/eventarc v1.8.0/go.mod h1:imbzxkyAU4ubfsaKYdQg04WS1NvncblHEup4kvF+4gw= -cloud.google.com/go/eventarc v1.10.0/go.mod h1:u3R35tmZ9HvswGRBnF48IlYgYeBcPUCjkr4BTdem2Kw= -cloud.google.com/go/eventarc v1.11.0/go.mod h1:PyUjsUKPWoRBCHeOxZd/lbOOjahV41icXyUY5kSTvVY= -cloud.google.com/go/filestore v1.3.0/go.mod h1:+qbvHGvXU1HaKX2nD0WEPo92TP/8AQuCVEBXNY9z0+w= -cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLYoINEK8XQAI= -cloud.google.com/go/filestore v1.5.0/go.mod h1:FqBXDWBp4YLHqRnVGveOkHDf8svj9r5+mUDLupOWEDs= -cloud.google.com/go/filestore v1.6.0/go.mod h1:di5unNuss/qfZTw2U9nhFqo8/ZDSc466dre85Kydllg= -cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= -cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= -cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= -cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY= -cloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5UwtlpzoyquQ08= -cloud.google.com/go/functions v1.10.0/go.mod h1:0D3hEOe3DbEvCXtYOZHQZmD+SzYsi1YbI7dGvHfldXw= -cloud.google.com/go/functions v1.12.0/go.mod h1:AXWGrF3e2C/5ehvwYo/GH6O5s09tOPksiKhz+hH8WkA= -cloud.google.com/go/functions v1.13.0/go.mod h1:EU4O007sQm6Ef/PwRsI8N2umygGqPBS/IZQKBQBcJ3c= -cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= -cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= -cloud.google.com/go/gaming v1.7.0/go.mod h1:LrB8U7MHdGgFG851iHAfqUdLcKBdQ55hzXy9xBJz0+w= -cloud.google.com/go/gaming v1.8.0/go.mod h1:xAqjS8b7jAVW0KFYeRUxngo9My3f33kFmua++Pi+ggM= -cloud.google.com/go/gaming v1.9.0/go.mod h1:Fc7kEmCObylSWLO334NcO+O9QMDyz+TKC4v1D7X+Bc0= -cloud.google.com/go/gkebackup v0.2.0/go.mod h1:XKvv/4LfG829/B8B7xRkk8zRrOEbKtEam6yNfuQNH60= -cloud.google.com/go/gkebackup v0.3.0/go.mod h1:n/E671i1aOQvUxT541aTkCwExO/bTer2HDlj4TsBRAo= -cloud.google.com/go/gkebackup v0.4.0/go.mod h1:byAyBGUwYGEEww7xsbnUTBHIYcOPy/PgUWUtOeRm9Vg= -cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= -cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= -cloud.google.com/go/gkeconnect v0.7.0/go.mod h1:SNfmVqPkaEi3bF/B3CNZOAYPYdg7sU+obZ+QTky2Myw= -cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= -cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= -cloud.google.com/go/gkehub v0.11.0/go.mod h1:JOWHlmN+GHyIbuWQPl47/C2RFhnFKH38jH9Ascu3n0E= -cloud.google.com/go/gkehub v0.12.0/go.mod h1:djiIwwzTTBrF5NaXCGv3mf7klpEMcST17VBTVVDcuaw= -cloud.google.com/go/gkemulticloud v0.3.0/go.mod h1:7orzy7O0S+5kq95e4Hpn7RysVA7dPs8W/GgfUtsPbrA= -cloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI= -cloud.google.com/go/gkemulticloud v0.5.0/go.mod h1:W0JDkiyi3Tqh0TJr//y19wyb1yf8llHVto2Htf2Ja3Y= -cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= -cloud.google.com/go/gsuiteaddons v1.3.0/go.mod h1:EUNK/J1lZEZO8yPtykKxLXI6JSVN2rg9bN8SXOa0bgM= -cloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSushomRlg+3ua1o= -cloud.google.com/go/gsuiteaddons v1.5.0/go.mod h1:TFCClYLd64Eaa12sFVmUyG62tk4mdIsI7pAnSXRkcFo= -cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= -cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= -cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= -cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= -cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= -cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= -cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY= -cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY= -cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= -cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= -cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= -cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk= -cloud.google.com/go/iap v1.7.0/go.mod h1:beqQx56T9O1G1yNPph+spKpNibDlYIiIixiqsQXxLIo= -cloud.google.com/go/iap v1.7.1/go.mod h1:WapEwPc7ZxGt2jFGB/C/bm+hP0Y6NXzOYGjpPnmMS74= -cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM= -cloud.google.com/go/ids v1.2.0/go.mod h1:5WXvp4n25S0rA/mQWAg1YEEBBq6/s+7ml1RDCW1IrcY= -cloud.google.com/go/ids v1.3.0/go.mod h1:JBdTYwANikFKaDP6LtW5JAi4gubs57SVNQjemdt6xV4= -cloud.google.com/go/iot v1.3.0/go.mod h1:r7RGh2B61+B8oz0AGE+J72AhA0G7tdXItODWsaA2oLs= -cloud.google.com/go/iot v1.4.0/go.mod h1:dIDxPOn0UvNDUMD8Ger7FIaTuvMkj+aGk94RPP0iV+g= -cloud.google.com/go/iot v1.5.0/go.mod h1:mpz5259PDl3XJthEmh9+ap0affn/MqNSP4My77Qql9o= -cloud.google.com/go/iot v1.6.0/go.mod h1:IqdAsmE2cTYYNO1Fvjfzo9po179rAtJeVGUvkLN3rLE= -cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= -cloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg= -cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0= -cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4jMAg= -cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w= -cloud.google.com/go/kms v1.10.0/go.mod h1:ng3KTUtQQU9bPX3+QGLsflZIHlkbn8amFAMY63m8d24= -cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI= -cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= -cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= -cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= -cloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8= -cloud.google.com/go/language v1.9.0/go.mod h1:Ns15WooPM5Ad/5no/0n81yUetis74g3zrbeJBE+ptUY= -cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= -cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= -cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= -cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= -cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= -cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= -cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= -cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= -cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= -cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= -cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= -cloud.google.com/go/maps v0.1.0/go.mod h1:BQM97WGyfw9FWEmQMpZ5T6cpovXXSd1cGmFma94eubI= -cloud.google.com/go/maps v0.6.0/go.mod h1:o6DAMMfb+aINHz/p/jbcY+mYeXBoZoxTfdSQ8VAJaCw= -cloud.google.com/go/maps v0.7.0/go.mod h1:3GnvVl3cqeSvgMcpRlQidXsPYuDGQ8naBis7MVzpXsY= -cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= -cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= -cloud.google.com/go/mediatranslation v0.7.0/go.mod h1:LCnB/gZr90ONOIQLgSXagp8XUW1ODs2UmUMvcgMfI2I= -cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= -cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= -cloud.google.com/go/memcache v1.6.0/go.mod h1:XS5xB0eQZdHtTuTF9Hf8eJkKtR3pVRCcvJwtm68T3rA= -cloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY= -cloud.google.com/go/memcache v1.9.0/go.mod h1:8oEyzXCu+zo9RzlEaEjHl4KkgjlNDaXbCQeQWlzNFJM= -cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= -cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= -cloud.google.com/go/metastore v1.7.0/go.mod h1:s45D0B4IlsINu87/AsWiEVYbLaIMeUSoxlKKDqBGFS8= -cloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI= -cloud.google.com/go/metastore v1.10.0/go.mod h1:fPEnH3g4JJAk+gMRnrAnoqyv2lpUCqJPWOodSaf45Eo= -cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk= -cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= -cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w= -cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw= -cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= -cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= -cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= -cloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8= -cloud.google.com/go/networkconnectivity v1.10.0/go.mod h1:UP4O4sWXJG13AqrTdQCD9TnLGEbtNRqjuaaA7bNjF5E= -cloud.google.com/go/networkconnectivity v1.11.0/go.mod h1:iWmDD4QF16VCDLXUqvyspJjIEtBR/4zq5hwnY2X3scM= -cloud.google.com/go/networkmanagement v1.4.0/go.mod h1:Q9mdLLRn60AsOrPc8rs8iNV6OHXaGcDdsIQe1ohekq8= -cloud.google.com/go/networkmanagement v1.5.0/go.mod h1:ZnOeZ/evzUdUsnvRt792H0uYEnHQEMaz+REhhzJRcf4= -cloud.google.com/go/networkmanagement v1.6.0/go.mod h1:5pKPqyXjB/sgtvB5xqOemumoQNB7y95Q7S+4rjSOPYY= -cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= -cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= -cloud.google.com/go/networksecurity v0.7.0/go.mod h1:mAnzoxx/8TBSyXEeESMy9OOYwo1v+gZ5eMRnsT5bC8k= -cloud.google.com/go/networksecurity v0.8.0/go.mod h1:B78DkqsxFG5zRSVuwYFRZ9Xz8IcQ5iECsNrPn74hKHU= -cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= -cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= -cloud.google.com/go/notebooks v1.4.0/go.mod h1:4QPMngcwmgb6uw7Po99B2xv5ufVoIQ7nOGDyL4P8AgA= -cloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0= -cloud.google.com/go/notebooks v1.7.0/go.mod h1:PVlaDGfJgj1fl1S3dUwhFMXFgfYGhYQt2164xOMONmE= -cloud.google.com/go/notebooks v1.8.0/go.mod h1:Lq6dYKOYOWUCTvw5t2q1gp1lAp0zxAxRycayS0iJcqQ= -cloud.google.com/go/optimization v1.1.0/go.mod h1:5po+wfvX5AQlPznyVEZjGJTMr4+CAkJf2XSTQOOl9l4= -cloud.google.com/go/optimization v1.2.0/go.mod h1:Lr7SOHdRDENsh+WXVmQhQTrzdu9ybg0NecjHidBq6xs= -cloud.google.com/go/optimization v1.3.1/go.mod h1:IvUSefKiwd1a5p0RgHDbWCIbDFgKuEdB+fPPuP0IDLI= -cloud.google.com/go/orchestration v1.3.0/go.mod h1:Sj5tq/JpWiB//X/q3Ngwdl5K7B7Y0KZ7bfv0wL6fqVA= -cloud.google.com/go/orchestration v1.4.0/go.mod h1:6W5NLFWs2TlniBphAViZEVhrXRSMgUGDfW7vrWKvsBk= -cloud.google.com/go/orchestration v1.6.0/go.mod h1:M62Bevp7pkxStDfFfTuCOaXgaaqRAga1yKyoMtEoWPQ= -cloud.google.com/go/orgpolicy v1.4.0/go.mod h1:xrSLIV4RePWmP9P3tBl8S93lTmlAxjm06NSm2UTmKvE= -cloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc= -cloud.google.com/go/orgpolicy v1.10.0/go.mod h1:w1fo8b7rRqlXlIJbVhOMPrwVljyuW5mqssvBtU18ONc= -cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= -cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= -cloud.google.com/go/osconfig v1.9.0/go.mod h1:Yx+IeIZJ3bdWmzbQU4fxNl8xsZ4amB+dygAwFPlvnNo= -cloud.google.com/go/osconfig v1.10.0/go.mod h1:uMhCzqC5I8zfD9zDEAfvgVhDS8oIjySWh+l4WK6GnWw= -cloud.google.com/go/osconfig v1.11.0/go.mod h1:aDICxrur2ogRd9zY5ytBLV89KEgT2MKB2L/n6x1ooPw= -cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= -cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= -cloud.google.com/go/oslogin v1.6.0/go.mod h1:zOJ1O3+dTU8WPlGEkFSh7qeHPPSoxrcMbbK1Nm2iX70= -cloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo= -cloud.google.com/go/oslogin v1.9.0/go.mod h1:HNavntnH8nzrn8JCTT5fj18FuJLFJc4NaZJtBnQtKFs= -cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= -cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= -cloud.google.com/go/phishingprotection v0.7.0/go.mod h1:8qJI4QKHoda/sb/7/YmMQ2omRLSLYSu9bU0EKCNI+Lk= -cloud.google.com/go/policytroubleshooter v1.3.0/go.mod h1:qy0+VwANja+kKrjlQuOzmlvscn4RNsAc0e15GGqfMxg= -cloud.google.com/go/policytroubleshooter v1.4.0/go.mod h1:DZT4BcRw3QoO8ota9xw/LKtPa8lKeCByYeKTIf/vxdE= -cloud.google.com/go/policytroubleshooter v1.5.0/go.mod h1:Rz1WfV+1oIpPdN2VvvuboLVRsB1Hclg3CKQ53j9l8vw= -cloud.google.com/go/policytroubleshooter v1.6.0/go.mod h1:zYqaPTsmfvpjm5ULxAyD/lINQxJ0DDsnWOP/GZ7xzBc= -cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= -cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= -cloud.google.com/go/privatecatalog v0.7.0/go.mod h1:2s5ssIFO69F5csTXcwBP7NPFTZvps26xGzvQ2PQaBYg= -cloud.google.com/go/privatecatalog v0.8.0/go.mod h1:nQ6pfaegeDAq/Q5lrfCQzQLhubPiZhSaNhIgfJlnIXs= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/pubsub v1.26.0/go.mod h1:QgBH3U/jdJy/ftjPhTkyXNj543Tin1pRYcdcPRnFIRI= -cloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhzp5O6qsW0= -cloud.google.com/go/pubsub v1.28.0/go.mod h1:vuXFpwaVoIPQMGXqRyUQigu/AX1S3IWugR9xznmcXX8= -cloud.google.com/go/pubsub v1.30.0/go.mod h1:qWi1OPS0B+b5L+Sg6Gmc9zD1Y+HaM0MdUr7LsupY1P4= -cloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg= -cloud.google.com/go/pubsublite v1.6.0/go.mod h1:1eFCS0U11xlOuMFV/0iBqw3zP12kddMeCbj/F3FSj9k= -cloud.google.com/go/pubsublite v1.7.0/go.mod h1:8hVMwRXfDfvGm3fahVbtDbiLePT3gpoiJYJY+vxWxVM= -cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= -cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= -cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= -cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= -cloud.google.com/go/recaptchaenterprise/v2 v2.4.0/go.mod h1:Am3LHfOuBstrLrNCBrlI5sbwx9LBg3te2N6hGvHn2mE= -cloud.google.com/go/recaptchaenterprise/v2 v2.5.0/go.mod h1:O8LzcHXN3rz0j+LBC91jrwI3R+1ZSZEWrfL7XHgNo9U= -cloud.google.com/go/recaptchaenterprise/v2 v2.6.0/go.mod h1:RPauz9jeLtB3JVzg6nCbe12qNoaa8pXc4d/YukAmcnA= -cloud.google.com/go/recaptchaenterprise/v2 v2.7.0/go.mod h1:19wVj/fs5RtYtynAPJdDTb69oW0vNHYDBTbB4NvMD9c= -cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= -cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= -cloud.google.com/go/recommendationengine v0.7.0/go.mod h1:1reUcE3GIu6MeBz/h5xZJqNLuuVjNg1lmWMPyjatzac= -cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= -cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= -cloud.google.com/go/recommender v1.7.0/go.mod h1:XLHs/W+T8olwlGOgfQenXBTbIseGclClff6lhFVe9Bs= -cloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70= -cloud.google.com/go/recommender v1.9.0/go.mod h1:PnSsnZY7q+VL1uax2JWkt/UegHssxjUVVCrX52CuEmQ= -cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= -cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= -cloud.google.com/go/redis v1.9.0/go.mod h1:HMYQuajvb2D0LvMgZmLDZW8V5aOC/WxstZHiy4g8OiA= -cloud.google.com/go/redis v1.10.0/go.mod h1:ThJf3mMBQtW18JzGgh41/Wld6vnDDc/F/F35UolRZPM= -cloud.google.com/go/redis v1.11.0/go.mod h1:/X6eicana+BWcUda5PpwZC48o37SiFVTFSs0fWAJ7uQ= -cloud.google.com/go/resourcemanager v1.3.0/go.mod h1:bAtrTjZQFJkiWTPDb1WBjzvc6/kifjj4QBYuKCCoqKA= -cloud.google.com/go/resourcemanager v1.4.0/go.mod h1:MwxuzkumyTX7/a3n37gmsT3py7LIXwrShilPh3P1tR0= -cloud.google.com/go/resourcemanager v1.5.0/go.mod h1:eQoXNAiAvCf5PXxWxXjhKQoTMaUSNrEfg+6qdf/wots= -cloud.google.com/go/resourcemanager v1.6.0/go.mod h1:YcpXGRs8fDzcUl1Xw8uOVmI8JEadvhRIkoXXUNVYcVo= -cloud.google.com/go/resourcemanager v1.7.0/go.mod h1:HlD3m6+bwhzj9XCouqmeiGuni95NTrExfhoSrkC/3EI= -cloud.google.com/go/resourcesettings v1.3.0/go.mod h1:lzew8VfESA5DQ8gdlHwMrqZs1S9V87v3oCnKCWoOuQU= -cloud.google.com/go/resourcesettings v1.4.0/go.mod h1:ldiH9IJpcrlC3VSuCGvjR5of/ezRrOxFtpJoJo5SmXg= -cloud.google.com/go/resourcesettings v1.5.0/go.mod h1:+xJF7QSG6undsQDfsCJyqWXyBwUoJLhetkRMDRnIoXA= -cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= -cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= -cloud.google.com/go/retail v1.10.0/go.mod h1:2gDk9HsL4HMS4oZwz6daui2/jmKvqShXKQuB2RZ+cCc= -cloud.google.com/go/retail v1.11.0/go.mod h1:MBLk1NaWPmh6iVFSz9MeKG/Psyd7TAgm6y/9L2B4x9Y= -cloud.google.com/go/retail v1.12.0/go.mod h1:UMkelN/0Z8XvKymXFbD4EhFJlYKRx1FGhQkVPU5kF14= -cloud.google.com/go/run v0.2.0/go.mod h1:CNtKsTA1sDcnqqIFR3Pb5Tq0usWxJJvsWOCPldRU3Do= -cloud.google.com/go/run v0.3.0/go.mod h1:TuyY1+taHxTjrD0ZFk2iAR+xyOXEA0ztb7U3UNA0zBo= -cloud.google.com/go/run v0.8.0/go.mod h1:VniEnuBwqjigv0A7ONfQUaEItaiCRVujlMqerPPiktM= -cloud.google.com/go/run v0.9.0/go.mod h1:Wwu+/vvg8Y+JUApMwEDfVfhetv30hCG4ZwDR/IXl2Qg= -cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= -cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= -cloud.google.com/go/scheduler v1.6.0/go.mod h1:SgeKVM7MIwPn3BqtcBntpLyrIJftQISRrYB5ZtT+KOk= -cloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44= -cloud.google.com/go/scheduler v1.8.0/go.mod h1:TCET+Y5Gp1YgHT8py4nlg2Sew8nUHMqcpousDgXJVQc= -cloud.google.com/go/scheduler v1.9.0/go.mod h1:yexg5t+KSmqu+njTIh3b7oYPheFtBWGcbVUYF1GGMIc= -cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= -cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= -cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= -cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= -cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= -cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= -cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= -cloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq/t9dzI+2Q= -cloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA= -cloud.google.com/go/security v1.12.0/go.mod h1:rV6EhrpbNHrrxqlvW0BWAIawFWq3X90SduMJdFwtLB8= -cloud.google.com/go/security v1.13.0/go.mod h1:Q1Nvxl1PAgmeW0y3HTt54JYIvUdtcpYKVfIB8AOMZ+0= -cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= -cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= -cloud.google.com/go/securitycenter v1.15.0/go.mod h1:PeKJ0t8MoFmmXLXWm41JidyzI3PJjd8sXWaVqg43WWk= -cloud.google.com/go/securitycenter v1.16.0/go.mod h1:Q9GMaLQFUD+5ZTabrbujNWLtSLZIZF7SAR0wWECrjdk= -cloud.google.com/go/securitycenter v1.18.1/go.mod h1:0/25gAzCM/9OL9vVx4ChPeM/+DlfGQJDwBy/UC8AKK0= -cloud.google.com/go/securitycenter v1.19.0/go.mod h1:LVLmSg8ZkkyaNy4u7HCIshAngSQ8EcIRREP3xBnyfag= -cloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU= -cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s= -cloud.google.com/go/servicecontrol v1.10.0/go.mod h1:pQvyvSRh7YzUF2efw7H87V92mxU8FnFDawMClGCNuAA= -cloud.google.com/go/servicecontrol v1.11.0/go.mod h1:kFmTzYzTUIuZs0ycVqRHNaNhgR+UMUpw9n02l/pY+mc= -cloud.google.com/go/servicecontrol v1.11.1/go.mod h1:aSnNNlwEFBY+PWGQ2DoM0JJ/QUXqV5/ZD9DOLB7SnUk= -cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= -cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= -cloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPjsRs1RlmJ4pqiNjVL4= -cloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U= -cloud.google.com/go/servicedirectory v1.8.0/go.mod h1:srXodfhY1GFIPvltunswqXpVxFPpZjf8nkKQT7XcXaY= -cloud.google.com/go/servicedirectory v1.9.0/go.mod h1:29je5JjiygNYlmsGz8k6o+OZ8vd4f//bQLtvzkPPT/s= -cloud.google.com/go/servicemanagement v1.4.0/go.mod h1:d8t8MDbezI7Z2R1O/wu8oTggo3BI2GKYbdG4y/SJTco= -cloud.google.com/go/servicemanagement v1.5.0/go.mod h1:XGaCRe57kfqu4+lRxaFEAuqmjzF0r+gWHjWqKqBvKFo= -cloud.google.com/go/servicemanagement v1.6.0/go.mod h1:aWns7EeeCOtGEX4OvZUWCCJONRZeFKiptqKf1D0l/Jc= -cloud.google.com/go/servicemanagement v1.8.0/go.mod h1:MSS2TDlIEQD/fzsSGfCdJItQveu9NXnUniTrq/L8LK4= -cloud.google.com/go/serviceusage v1.3.0/go.mod h1:Hya1cozXM4SeSKTAgGXgj97GlqUvF5JaoXacR1JTP/E= -cloud.google.com/go/serviceusage v1.4.0/go.mod h1:SB4yxXSaYVuUBYUml6qklyONXNLt83U0Rb+CXyhjEeU= -cloud.google.com/go/serviceusage v1.5.0/go.mod h1:w8U1JvqUqwJNPEOTQjrMHkw3IaIFLoLsPLvsE3xueec= -cloud.google.com/go/serviceusage v1.6.0/go.mod h1:R5wwQcbOWsyuOfbP9tGdAnCAc6B9DRwPG1xtWMDeuPA= -cloud.google.com/go/shell v1.3.0/go.mod h1:VZ9HmRjZBsjLGXusm7K5Q5lzzByZmJHf1d0IWHEN5X4= -cloud.google.com/go/shell v1.4.0/go.mod h1:HDxPzZf3GkDdhExzD/gs8Grqk+dmYcEjGShZgYa9URw= -cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+qE2f9A= -cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos= -cloud.google.com/go/spanner v1.44.0/go.mod h1:G8XIgYdOK+Fbcpbs7p2fiprDw4CaZX63whnSMLVBxjk= -cloud.google.com/go/spanner v1.45.0/go.mod h1:FIws5LowYz8YAE1J8fOS7DJup8ff7xJeetWEo5REA2M= -cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= -cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= -cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= -cloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco= -cloud.google.com/go/speech v1.14.1/go.mod h1:gEosVRPJ9waG7zqqnsHpYTOoAS4KouMRLDFMekpJ0J0= -cloud.google.com/go/speech v1.15.0/go.mod h1:y6oH7GhqCaZANH7+Oe0BhgIogsNInLlz542tg3VqeYI= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= -cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= -cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= -cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= -cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= -cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= -cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= -cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= -cloud.google.com/go/storagetransfer v1.8.0/go.mod h1:JpegsHHU1eXg7lMHkvf+KE5XDJ7EQu0GwNJbbVGanEw= -cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= -cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= -cloud.google.com/go/talent v1.3.0/go.mod h1:CmcxwJ/PKfRgd1pBjQgU6W3YBwiewmUzQYH5HHmSCmM= -cloud.google.com/go/talent v1.4.0/go.mod h1:ezFtAgVuRf8jRsvyE6EwmbTK5LKciD4KVnHuDEFmOOA= -cloud.google.com/go/talent v1.5.0/go.mod h1:G+ODMj9bsasAEJkQSzO2uHQWXHHXUomArjWQQYkqK6c= -cloud.google.com/go/texttospeech v1.4.0/go.mod h1:FX8HQHA6sEpJ7rCMSfXuzBcysDAuWusNNNvN9FELDd8= -cloud.google.com/go/texttospeech v1.5.0/go.mod h1:oKPLhR4n4ZdQqWKURdwxMy0uiTS1xU161C8W57Wkea4= -cloud.google.com/go/texttospeech v1.6.0/go.mod h1:YmwmFT8pj1aBblQOI3TfKmwibnsfvhIBzPXcW4EBovc= -cloud.google.com/go/tpu v1.3.0/go.mod h1:aJIManG0o20tfDQlRIej44FcwGGl/cD0oiRyMKG19IQ= -cloud.google.com/go/tpu v1.4.0/go.mod h1:mjZaX8p0VBgllCzF6wcU2ovUXN9TONFLd7iz227X2Xg= -cloud.google.com/go/tpu v1.5.0/go.mod h1:8zVo1rYDFuW2l4yZVY0R0fb/v44xLh3llq7RuV61fPM= -cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28= -cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= -cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= -cloud.google.com/go/trace v1.9.0/go.mod h1:lOQqpE5IaWY0Ixg7/r2SjixMuc6lfTFeO4QGM4dQWOk= -cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= -cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= -cloud.google.com/go/translate v1.5.0/go.mod h1:29YDSYveqqpA1CQFD7NQuP49xymq17RXNaUDdc0mNu0= -cloud.google.com/go/translate v1.6.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= -cloud.google.com/go/translate v1.7.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= -cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk= -cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw= -cloud.google.com/go/video v1.12.0/go.mod h1:MLQew95eTuaNDEGriQdcYn0dTwf9oWiA4uYebxM5kdg= -cloud.google.com/go/video v1.13.0/go.mod h1:ulzkYlYgCp15N2AokzKjy7MQ9ejuynOJdf1tR5lGthk= -cloud.google.com/go/video v1.14.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= -cloud.google.com/go/video v1.15.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= -cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= -cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= -cloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M= -cloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU= -cloud.google.com/go/videointelligence v1.10.0/go.mod h1:LHZngX1liVtUhZvi2uNS0VQuOzNi2TkY1OakiuoUOjU= -cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= -cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= -cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= -cloud.google.com/go/vision/v2 v2.4.0/go.mod h1:VtI579ll9RpVTrdKdkMzckdnwMyX2JILb+MhPqRbPsY= -cloud.google.com/go/vision/v2 v2.5.0/go.mod h1:MmaezXOOE+IWa+cS7OhRRLK2cNv1ZL98zhqFFZaaH2E= -cloud.google.com/go/vision/v2 v2.6.0/go.mod h1:158Hes0MvOS9Z/bDMSFpjwsUrZ5fPrdwuyyvKSGAGMY= -cloud.google.com/go/vision/v2 v2.7.0/go.mod h1:H89VysHy21avemp6xcf9b9JvZHVehWbET0uT/bcuY/0= -cloud.google.com/go/vmmigration v1.2.0/go.mod h1:IRf0o7myyWFSmVR1ItrBSFLFD/rJkfDCUTO4vLlJvsE= -cloud.google.com/go/vmmigration v1.3.0/go.mod h1:oGJ6ZgGPQOFdjHuocGcLqX4lc98YQ7Ygq8YQwHh9A7g= -cloud.google.com/go/vmmigration v1.5.0/go.mod h1:E4YQ8q7/4W9gobHjQg4JJSgXXSgY21nA5r8swQV+Xxc= -cloud.google.com/go/vmmigration v1.6.0/go.mod h1:bopQ/g4z+8qXzichC7GW1w2MjbErL54rk3/C843CjfY= -cloud.google.com/go/vmwareengine v0.1.0/go.mod h1:RsdNEf/8UDvKllXhMz5J40XxDrNJNN4sagiox+OI208= -cloud.google.com/go/vmwareengine v0.2.2/go.mod h1:sKdctNJxb3KLZkE/6Oui94iw/xs9PRNC2wnNLXsHvH8= -cloud.google.com/go/vmwareengine v0.3.0/go.mod h1:wvoyMvNWdIzxMYSpH/R7y2h5h3WFkx6d+1TIsP39WGY= -cloud.google.com/go/vpcaccess v1.4.0/go.mod h1:aQHVbTWDYUR1EbTApSVvMq1EnT57ppDmQzZ3imqIk4w= -cloud.google.com/go/vpcaccess v1.5.0/go.mod h1:drmg4HLk9NkZpGfCmZ3Tz0Bwnm2+DKqViEpeEpOq0m8= -cloud.google.com/go/vpcaccess v1.6.0/go.mod h1:wX2ILaNhe7TlVa4vC5xce1bCnqE3AeH27RV31lnmZes= -cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= -cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= -cloud.google.com/go/webrisk v1.6.0/go.mod h1:65sW9V9rOosnc9ZY7A7jsy1zoHS5W9IAXv6dGqhMQMc= -cloud.google.com/go/webrisk v1.7.0/go.mod h1:mVMHgEYH0r337nmt1JyLthzMr6YxwN1aAIEc2fTcq7A= -cloud.google.com/go/webrisk v1.8.0/go.mod h1:oJPDuamzHXgUc+b8SiHRcVInZQuybnvEW72PqTc7sSg= -cloud.google.com/go/websecurityscanner v1.3.0/go.mod h1:uImdKm2wyeXQevQJXeh8Uun/Ym1VqworNDlBXQevGMo= -cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ= -cloud.google.com/go/websecurityscanner v1.5.0/go.mod h1:Y6xdCPy81yi0SQnDY1xdNTNpfY1oAgXUlcfN3B3eSng= -cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= -cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= -cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= -cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= -cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= -git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/IBM/sarama v1.43.3 h1:Yj6L2IaNvb2mRBop39N7mmJAHBVY3dTPncr3qGVkxPA= github.com/IBM/sarama v1.43.3/go.mod h1:FVIRaLrhK3Cla/9FfRF5X9Zua2KpS3SYIXxhac1H+FQ= -github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/ThreeDotsLabs/watermill v1.3.7 h1:NV0PSTmuACVEOV4dMxRnmGXrmbz8U83LENOvpHekN7o= github.com/ThreeDotsLabs/watermill v1.3.7/go.mod h1:lBnrLbxOjeMRgcJbv+UiZr8Ylz8RkJ4m6i/VN/Nk+to= github.com/ThreeDotsLabs/watermill-http/v2 v2.3.1 h1:M0iYM5HsGcoxtiQqprRlYZNZnGk3w5LsE9RbC2R8myQ= @@ -621,32 +20,16 @@ github.com/ThreeDotsLabs/watermill-kafka/v3 v3.0.5 h1:ud+4txnRgtr3kZXfXZ5+C7kVQE github.com/ThreeDotsLabs/watermill-kafka/v3 v3.0.5/go.mod h1:t4o+4A6GB+XC8WL3DandhzPwd265zQuyWMQC/I+WIOU= github.com/ThreeDotsLabs/watermill-nats/v2 v2.1.1 h1:afAkAFzeooBRQvxElR+6xoigXKCukcZXnE9ACxhwlPI= github.com/ThreeDotsLabs/watermill-nats/v2 v2.1.1/go.mod h1:stjbT+s4u/s5ime5jdIyvPyjBGwGeJewIN7jxH8gp4k= -github.com/adyen/adyen-go-api-library/v7 v7.3.1 h1:NToWy5oZDH5Juz45h9GTlidGFldW10xvaihCJIOWZcw= -github.com/adyen/adyen-go-api-library/v7 v7.3.1/go.mod h1:z9oHJsUpqgCkBhKa8hpBgQvTU8ObRfvO0NKEYUoocx0= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= -github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= -github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= -github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= -github.com/alitto/pond v1.8.3 h1:ydIqygCLVPqIX/USe5EaV/aSRXTRXDEI9JwuDdu+/xs= -github.com/alitto/pond v1.8.3/go.mod h1:CmvIIGd5jKLasGI3D87qDkQxjzChdKMmnXMg3fG6M6Q= -github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= -github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= -github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= -github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-msk-iam-sasl-signer-go v1.0.0 h1:UyjtGmO0Uwl/K+zpzPwLoXzMhcN9xmnR2nrqJoBrg3c= github.com/aws/aws-msk-iam-sasl-signer-go v1.0.0/go.mod h1:TJAXuFs2HcMib3sN5L0gUC+Q01Qvy3DemvA55WuC+iA= github.com/aws/aws-sdk-go-v2 v1.31.0 h1:3V05LbxTSItI5kUqNwhJrrrY1BAXxXt0sN0l72QmG5U= github.com/aws/aws-sdk-go-v2 v1.31.0/go.mod h1:ztolYtaEUtdpf9Wftr31CJfLVjOnD/CVRkKOOYgF8hA= -github.com/aws/aws-sdk-go-v2/config v1.27.36 h1:4IlvHh6Olc7+61O1ktesh0jOcqmq/4WG6C2Aj5SKXy0= -github.com/aws/aws-sdk-go-v2/config v1.27.36/go.mod h1:IiBpC0HPAGq9Le0Xxb1wpAKzEfAQ3XlYgJLYKEVYcfw= -github.com/aws/aws-sdk-go-v2/credentials v1.17.34 h1:gmkk1l/cDGSowPRzkdxYi8edw+gN4HmVK151D/pqGNc= -github.com/aws/aws-sdk-go-v2/credentials v1.17.34/go.mod h1:4R9OEV3tgFMsok4ZeFpExn7zQaZRa9MRGFYnI/xC/vs= +github.com/aws/aws-sdk-go-v2/config v1.27.37 h1:xaoIwzHVuRWRHFI0jhgEdEGc8xE1l91KaeRDsWEIncU= +github.com/aws/aws-sdk-go-v2/config v1.27.37/go.mod h1:S2e3ax9/8KnMSyRVNd3sWTKs+1clJ2f1U6nE0lpvQRg= +github.com/aws/aws-sdk-go-v2/credentials v1.17.35 h1:7QknrZhYySEB1lEXJxGAmuD5sWwys5ZXNr4m5oEz0IE= +github.com/aws/aws-sdk-go-v2/credentials v1.17.35/go.mod h1:8Vy4kk7at4aPSmibr7K+nLTzG6qUQAUO4tW49fzUV4E= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14 h1:C/d03NAmh8C4BZXhuRNboF/DqhBkBCeDiJDcaqIT5pA= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14/go.mod h1:7I0Ju7p9mCIdlrfS+JCgqcYD0VXz/N4yozsox+0o078= github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.18 h1:k51348zRERIvv01FflXAOQj50NeUiZUGOEedT4Vg+UE= @@ -661,53 +44,33 @@ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5 h1:QFASJGf github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5/go.mod h1:QdZ3OmoIjSX+8D1OPAzPxDfjXASbBMDsz9qvtyIhtik= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20 h1:Xbwbmk44URTiHNx6PNo0ujDE6ERlsCKJD3u1zfnzAPg= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20/go.mod h1:oAfOFzUB14ltPZj1rWwRc3d/6OgD76R8KlvU3EqM9Fg= -github.com/aws/aws-sdk-go-v2/service/sso v1.23.0 h1:fHySkG0IGj2nepgGJPmmhZYL9ndnsq1Tvc6MeuVQCaQ= -github.com/aws/aws-sdk-go-v2/service/sso v1.23.0/go.mod h1:XRlMvmad0ZNL+75C5FYdMvbbLkd6qiqz6foR1nA1PXY= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.0 h1:cU/OeQPNReyMj1JEBgjE29aclYZYtXcsPMXbTkVGMFk= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.0/go.mod h1:FnvDM4sfa+isJ3kDXIzAB9GAwVSzFzSy97uZ3IsHo4E= -github.com/aws/aws-sdk-go-v2/service/sts v1.31.0 h1:GNVxIHBTi2EgwCxpNiozhNasMOK+ROUA2Z3X+cSBX58= -github.com/aws/aws-sdk-go-v2/service/sts v1.31.0/go.mod h1:yMWe0F+XG0DkRZK5ODZhG7BEFYhLXi2dqGsv6tX0cgI= +github.com/aws/aws-sdk-go-v2/service/sso v1.23.1 h1:2jrVsMHqdLD1+PA4BA6Nh1eZp0Gsy3mFSB5MxDvcJtU= +github.com/aws/aws-sdk-go-v2/service/sso v1.23.1/go.mod h1:XRlMvmad0ZNL+75C5FYdMvbbLkd6qiqz6foR1nA1PXY= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.1 h1:0L7yGCg3Hb3YQqnSgBTZM5wepougtL1aEccdcdYhHME= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.1/go.mod h1:FnvDM4sfa+isJ3kDXIzAB9GAwVSzFzSy97uZ3IsHo4E= +github.com/aws/aws-sdk-go-v2/service/sts v1.31.1 h1:8K0UNOkZiK9Uh3HIF6Bx0rcNCftqGCeKmOaR7Gp5BSo= +github.com/aws/aws-sdk-go-v2/service/sts v1.31.1/go.mod h1:yMWe0F+XG0DkRZK5ODZhG7BEFYhLXi2dqGsv6tX0cgI= github.com/aws/smithy-go v1.21.0 h1:H7L8dtDRk0P1Qm6y0ji7MCYMQObJ5R9CRpyPhRUkLYA= github.com/aws/smithy-go v1.21.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/bombsimon/logrusr/v3 v3.1.0 h1:zORbLM943D+hDMGgyjMhSAz/iDz86ZV72qaak/CA0zQ= github.com/bombsimon/logrusr/v3 v3.1.0/go.mod h1:PksPPgSFEL2I52pla2glgCyyd2OqOHAnFF5E+g8Ixco= -github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= +github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20230310173818-32f1caf87195/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0 h1:R2zQhFwSCyyd7L43igYjDrH0wkC/i+QBPELuY0HOu84= github.com/dnwe/otelsarama v0.0.0-20240308230250-9388d9d40bc0/go.mod h1:2MqLKYJfjs3UriXXF9Fd0Qmh/lhxi/6tHXkqtXxyIHc= github.com/docker/cli v27.3.1+incompatible h1:qEGdFBF3Xu6SCvCYhc7CzaQTlBmqDuzxPDpigSyeKQQ= @@ -718,8 +81,6 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA= github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws= @@ -729,31 +90,18 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= -github.com/envoyproxy/go-control-plane v0.11.0/go.mod h1:VnHyVMpzcLvCFt9yUz1UnCwHLhwx1WguiVDV7pTG/tI= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= -github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= -github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= +github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= +github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/formancehq/go-libs v1.7.1 h1:9D5cxKWFlVtdX5AYDXeUz1Nb9PdoEfQX0f/yeLsU324= -github.com/formancehq/go-libs v1.7.1/go.mod h1:pWTScpoyieF7OoJ6WVmXNG9NhDjbZbAmFqd7UOw85iI= +github.com/formancehq/go-libs v1.7.2-0.20240925132527-7627842ea9b5 h1:6UcoXXm5hzFk7c2aOonPUPO3bNrU7CvTiE/nZQWMvY4= +github.com/formancehq/go-libs v1.7.2-0.20240925132527-7627842ea9b5/go.mod h1:ynmWBbsdhVyjE+MxneMErtgd/RnNAk892VuIhZE2fps= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= -github.com/get-momo/atlar-v1-go-client v1.2.1 h1:sKWd0maMshxBErGXsYVGhGIB+zFxynrWLNHnegB4lXs= -github.com/get-momo/atlar-v1-go-client v1.2.1/go.mod h1:qcLoXEhjTCOeBqAzG2tucpvxGJS2LYNwaU7WnJYnO64= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gibson042/canonicaljson-go v1.0.3 h1:EAyF8L74AWabkyUmrvEFHEt/AGFQeD6RfwbAuf0j1bI= github.com/gibson042/canonicaljson-go v1.0.3/go.mod h1:DsLpJTThXyGNO+KZlI85C1/KDcImpP67k/RKVjcaEqo= github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= @@ -762,16 +110,8 @@ github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= -github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= -github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= -github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= -github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= -github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= -github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -780,166 +120,43 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-openapi/analysis v0.21.4 h1:ZDFLvSNxpDaomuCueM0BlSXxpANBlFYiBvr+GXrvIHc= -github.com/go-openapi/analysis v0.21.4/go.mod h1:4zQ35W4neeZTqh3ol0rv/O8JBbka9QyAgQRPp9y3pfo= -github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= -github.com/go-openapi/errors v0.20.4 h1:unTcVm6PispJsMECE3zWgvG4xTiKda1LIR5rCRWLG6M= -github.com/go-openapi/errors v0.20.4/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuAOhlsB1FSgk= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ= -github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA= -github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= -github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= -github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/loads v0.21.2 h1:r2a/xFIYeZ4Qd2TnGpWDIQNcP80dIaZgf704za8enro= -github.com/go-openapi/loads v0.21.2/go.mod h1:Jq58Os6SSGz0rzh62ptiu8Z31I+OTHqmULx5e/gJbNw= -github.com/go-openapi/runtime v0.26.0 h1:HYOFtG00FM1UvqrcxbEJg/SwvDRvYLQKGhw2zaQjTcc= -github.com/go-openapi/runtime v0.26.0/go.mod h1:QgRGeZwrUcSHdeh4Ka9Glvo0ug1LC5WyE+EV88plZrQ= -github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= -github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8= -github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= -github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= -github.com/go-openapi/strfmt v0.21.8 h1:VYBUoKYRLAlgKDrIxR/I0lKrztDQ0tuTDrbhLVP8Erg= -github.com/go-openapi/strfmt v0.21.8/go.mod h1:adeGTkxE44sPyLk0JV235VQAO/ZXUr8KAzYjclFs3ew= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= -github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-openapi/validate v0.22.2 h1:Lda8nadL/5kIvS5mdXCAIuZ7IVXvKFIppLnw+EZh+n0= -github.com/go-openapi/validate v0.22.2/go.mod h1:kVxh31KbfsxU8ZyoHaDbLBWU5CnMdqBUEtadQ2G4d5M= -github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= -github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= -github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= -github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 h1:c5FlPPgxOn7kJz3VoPLkQYQXGBS3EklQ4Zfi57uOuqQ= github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/s2a-go v0.1.0/go.mod h1:OJpEgntRZo8ugHpF9hkoLJbS5dSI20XZeXJ9JVywLlM= -github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= -github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= -github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= -github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= -github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= -github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= -github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= -github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= -github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= -github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= -github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= -github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= -github.com/googleapis/gax-go/v2 v2.10.0/go.mod h1:4UOEnMCrxsSqQ940WnTiD6qJ63le2ev3xfyagutxiPw= -github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI= -github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= @@ -948,9 +165,8 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -962,18 +178,17 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1 github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-plugin v1.6.1 h1:P7MR2UP6gNKGPp+y7EZw2kOiq4IR9WiqLvp0XOsVdwI= +github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1hZyMK0PWAw0= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.4 h1:7GHuZcgid37q8o5i3QI9KMT4nCWQQ3Kx3Ov6bb9MfK0= github.com/hashicorp/golang-lru/v2 v2.0.4/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= +github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -998,69 +213,58 @@ github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZ github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jeremija/gosubmit v0.2.7 h1:At0OhGCFGPXyjPYAsCchoBUhE099pcBXmsb4iZqROIc= github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= +github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= +github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= +github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= +github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx v1.2.30 h1:VKIFrmjYn0z2J51iLPadqoHIVLzvWNa1kCsTqNDHYPA= +github.com/lestrrat-go/jwx v1.2.30/go.mod h1:vMxrwFhunGZ3qddmfmEm2+uced8MSI6QFWGTKygjSzQ= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8= github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts= github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= -github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= -github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= -github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= -github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q= github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= -github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 h1:7GoSOOW2jpsfkntVKaS2rAr1TJqfcxotyaUcuxoZSzg= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= -github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY= @@ -1075,7 +279,10 @@ github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nexus-rpc/sdk-go v0.0.10 h1:7jEPUlsghxoD4OJ2H8YbFJ1t4wbxsUef7yZgBfyY3uA= +github.com/nexus-rpc/sdk-go v0.0.10/go.mod h1:TpfkM2Cw0Rlk9drGkoiSMpFqflKTiQLWUNyKJjF8mKQ= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= @@ -1088,84 +295,65 @@ github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQ github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opencontainers/runc v1.1.14 h1:rgSuzbmgz5DUJjeSnw337TxDbRuqjs6iqQck/2weR6w= github.com/opencontainers/runc v1.1.14/go.mod h1:E4C2z+7BxR7GHXp0hAY53mek+x49X1LjPNeMTfRGvOA= -github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= -github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA= github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI= -github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= -github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= -github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= -github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= +github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= -github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4= github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/riandyrn/otelchi v0.10.0 h1:QMbR/FMDWBOkej6dfyWteYefUKqIFxnyrpaoWRJ9RPQ= github.com/riandyrn/otelchi v0.10.0/go.mod h1:zBaX2FavWMlsvq4GqHit+QXxF1c5wIMZZFaYyW4+7FA= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= +github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= -github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= github.com/shirou/gopsutil/v4 v4.24.8 h1:pVQjIenQkIhqO81mwTaXjTzOMT7d3TZkf43PlVFHENI= github.com/shirou/gopsutil/v4 v4.24.8/go.mod h1:wE0OrJtj4dG+hYkxqDH3QiBICdKSf04/npcvLLc/oRg= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stripe/stripe-go/v72 v72.122.0 h1:eRXWqnEwGny6dneQ5BsxGzUCED5n180u8n665JHlut8= -github.com/stripe/stripe-go/v72 v72.122.0/go.mod h1:QwqJQtduHubZht9mek5sds9CtQcKFdsykV9ZepRWwo0= -github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= @@ -1192,13 +380,10 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= -github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= @@ -1207,33 +392,14 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xo/dburl v0.23.2 h1:Fl88cvayrgE56JA/sqhNMLljCW/b7RmG1mMkKMZUFgA= github.com/xo/dburl v0.23.2/go.mod h1:uazlaAQxj4gkshhfuuYyvwCBouOmNnG2aDxTCFZpmL4= -github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= -github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= github.com/zitadel/oidc/v2 v2.12.2 h1:3kpckg4rurgw7w7aLJrq7yvRxb2pkNOtD08RH42vPEs= github.com/zitadel/oidc/v2 v2.12.2/go.mod h1:vhP26g1g4YVntcTi0amMYW3tJuid70nxqxf+kb6XKgg= -go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= -go.mongodb.org/mongo-driver v1.12.0 h1:aPx33jmn/rQuJXPQLZQ8NtfPQG8CaqgLThFtqRb0PiE= -go.mongodb.org/mongo-driver v1.12.0/go.mod h1:AZkxhPnFJUoH7kZlFkVKucV20K387miPfm7oimrSmK0= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.44.0 h1:QaNUlLvmettd1vnmFHrgBYQHearxWP3uO4h4F3pVtkM= -go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.44.0/go.mod h1:cJu+5jZwoZfkBOECSFtBZK/O7h/pY5djn0fwnIGnQ4A= go.opentelemetry.io/contrib/instrumentation/host v0.55.0 h1:V/Cy5A2ydwvyED4ewwXJ441R3QllG+U8tXXVOjPeX4Y= go.opentelemetry.io/contrib/instrumentation/host v0.55.0/go.mod h1:fsY+EfHPwa1bQcxOUPv1FWaQXAwY+RliLRs6B6qgJes= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs= @@ -1268,759 +434,162 @@ go.opentelemetry.io/otel/sdk/metric v1.30.0 h1:QJLT8Pe11jyHBHfSAgYH7kEmT24eX792j go.opentelemetry.io/otel/sdk/metric v1.30.0/go.mod h1:waS6P3YqFNzeP01kuo/MBBYqaoBJl7efRQHOaydhy1Y= go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= -go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.temporal.io/api v1.39.0 h1:pbhcfvNDB7mllb8lIBqPcg+m6LMG/IhTpdiFxe+0mYk= +go.temporal.io/api v1.39.0/go.mod h1:1WwYUMo6lao8yl0371xWUm13paHExN5ATYT/B7QtFis= +go.temporal.io/sdk v1.29.1 h1:y+sUMbUhTU9rj50mwIZAPmcXCtgUdOWS9xHDYRYSgZ0= +go.temporal.io/sdk v1.29.1/go.mod h1:kp//DRvn3CqQVBCtjL51Oicp9wrZYB2s6row1UgzcKQ= +go.temporal.io/sdk/contrib/opentelemetry v0.6.0 h1:rNBArDj5iTUkcMwKocUShoAW59o6HdS7Nq4CTp4ldj8= +go.temporal.io/sdk/contrib/opentelemetry v0.6.0/go.mod h1:Lem8VrE2ks8P+FYcRM3UphPoBr+tfM3v/Kaf0qStzSg= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw= go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= go.uber.org/fx v1.22.2 h1:iPW+OPxv0G8w75OemJ1RAnTUrF55zOJlXlo1TbJ0Buw= go.uber.org/fx v1.22.2/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= -golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= -golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= -golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= -golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= -golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= -golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= -gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= -gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= -gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= -gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= -gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= -gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= -gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= -google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= -google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= -google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= -google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= -google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= -google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= -google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= -google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= -google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= -google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= -google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= -google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= -google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= -google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= -google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= -google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= -google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= -google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= -google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= -google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= -google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91A08= -google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= -google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo= -google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= -google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= -google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= -google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= -google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI= -google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0= -google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= -google.golang.org/api v0.118.0/go.mod h1:76TtD3vkgmZ66zZzp72bUUklpmQmKlhh6sYtIjYK+5E= -google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms= -google.golang.org/api v0.124.0/go.mod h1:xu2HQurE5gi/3t1aFCvhPD781p0a3p11sdunTJ2BlP4= -google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= -google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= -google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= -google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= -google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= -google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= -google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= -google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= -google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= -google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= -google.golang.org/genproto v0.0.0-20221024153911-1573dae28c9c/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= -google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= -google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo= -google.golang.org/genproto v0.0.0-20221109142239-94d6d90a7d66/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221117204609-8f9c96812029/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE= -google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230112194545-e10362b5ecf9/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230123190316-2c411cf9d197/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230124163310-31e0e69b6fc2/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230127162408-596548ed4efa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA= -google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= -google.golang.org/genproto v0.0.0-20230223222841-637eb2293923/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= -google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488/go.mod h1:TvhZT5f700eVlTNwND1xoEZQeWTB2RY/65kplwl/bFA= -google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= -google.golang.org/genproto v0.0.0-20230320184635-7606e756e683/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= -google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= -google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= -google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= -google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= -google.golang.org/genproto v0.0.0-20230525234025-438c736192d0/go.mod h1:9ExIQyXL5hZrHzQceCwuSYwZZ5QZBazOcprJ5rgs3lY= -google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64= -google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go.mod h1:ts19tUU+Z0ZShN1y3aPyq2+O3d5FUNNgT6FtOzmrNn8= -google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= -google.golang.org/genproto/googleapis/api v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= -google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= -google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234015-3fc162c6f38a/go.mod h1:xURIpW9ES5+/GZhnV6beoEtxQrnkRGIfP5VQG2tCBLc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= -google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= -google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= -google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= -google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs= gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= -gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= -lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= -lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= -modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= -modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= -modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= -modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= -modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= -modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= -modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= -modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws= -modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= -modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= -modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= -modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= -modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= -modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= -modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= -modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= -modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0= -modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s= -modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= -modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= -modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= -modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= -modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= -modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= -modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= -modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/components/payments/internal/api/backend/backend.go b/components/payments/internal/api/backend/backend.go new file mode 100644 index 0000000000..c5c915663a --- /dev/null +++ b/components/payments/internal/api/backend/backend.go @@ -0,0 +1,62 @@ +package backend + +import ( + "context" + "encoding/json" + "time" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/payments/internal/connectors/plugins" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/storage" + "github.com/google/uuid" +) + +//go:generate mockgen -source backend.go -destination backend_generated.go -package backend . Backend +type Backend interface { + // Accounts + AccountsList(ctx context.Context, query storage.ListAccountsQuery) (*bunpaginate.Cursor[models.Account], error) + AccountsGet(ctx context.Context, id models.AccountID) (*models.Account, error) + BankAccountsList(ctx context.Context, query storage.ListBankAccountsQuery) (*bunpaginate.Cursor[models.BankAccount], error) + BankAccountsUpdateMetadata(ctx context.Context, id uuid.UUID, metadata map[string]string) error + BankAccountsForwardToConnector(ctx context.Context, bankAccountID uuid.UUID, connectorID models.ConnectorID) (*models.BankAccount, error) + + // Balances + BalancesList(ctx context.Context, query storage.ListBalancesQuery) (*bunpaginate.Cursor[models.Balance], error) + PoolsBalancesAt(ctx context.Context, poolID uuid.UUID, at time.Time) ([]models.AggregatedBalance, error) + + // Bank Accounts + BankAccountsCreate(ctx context.Context, bankAccount models.BankAccount) error + BankAccountsGet(ctx context.Context, id uuid.UUID) (*models.BankAccount, error) + + // Connectors + ConnectorsConfigs() plugins.Configs + ConnectorsConfig(ctx context.Context, connectorID models.ConnectorID) (json.RawMessage, error) + ConnectorsList(ctx context.Context, query storage.ListConnectorsQuery) (*bunpaginate.Cursor[models.Connector], error) + ConnectorsInstall(ctx context.Context, provider string, config json.RawMessage) (models.ConnectorID, error) + ConnectorsUninstall(ctx context.Context, connectorID models.ConnectorID) error + ConnectorsReset(ctx context.Context, connectorID models.ConnectorID) error + + // Payments + PaymentsUpdateMetadata(ctx context.Context, id models.PaymentID, metadata map[string]string) error + PaymentsList(ctx context.Context, query storage.ListPaymentsQuery) (*bunpaginate.Cursor[models.Payment], error) + PaymentsGet(ctx context.Context, id models.PaymentID) (*models.Payment, error) + + // Pools + PoolsCreate(ctx context.Context, pool models.Pool) error + PoolsGet(ctx context.Context, id uuid.UUID) (*models.Pool, error) + PoolsList(ctx context.Context, query storage.ListPoolsQuery) (*bunpaginate.Cursor[models.Pool], error) + PoolsDelete(ctx context.Context, id uuid.UUID) error + PoolsAddAccount(ctx context.Context, id uuid.UUID, accountID models.AccountID) error + PoolsRemoveAccount(ctx context.Context, id uuid.UUID, accountID models.AccountID) error + + // Schedules + SchedulesList(ctx context.Context, query storage.ListSchedulesQuery) (*bunpaginate.Cursor[models.Schedule], error) + SchedulesGet(ctx context.Context, id string, connectorID models.ConnectorID) (*models.Schedule, error) + + // Webhooks + ConnectorsHandleWebhooks(ctx context.Context, urlPath string, webhook models.Webhook) error + + // Workflows Instances + WorkflowsInstancesList(ctx context.Context, query storage.ListInstancesQuery) (*bunpaginate.Cursor[models.Instance], error) +} diff --git a/components/payments/internal/api/backend/backend_generated.go b/components/payments/internal/api/backend/backend_generated.go new file mode 100644 index 0000000000..a54fde6d8f --- /dev/null +++ b/components/payments/internal/api/backend/backend_generated.go @@ -0,0 +1,451 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: backend.go + +// Package backend is a generated GoMock package. +package backend + +import ( + context "context" + json "encoding/json" + reflect "reflect" + time "time" + + plugins "github.com/formancehq/payments/internal/connectors/plugins" + models "github.com/formancehq/payments/internal/models" + storage "github.com/formancehq/payments/internal/storage" + bunpaginate "github.com/formancehq/go-libs/bun/bunpaginate" + gomock "github.com/golang/mock/gomock" + uuid "github.com/google/uuid" +) + +// MockBackend is a mock of Backend interface. +type MockBackend struct { + ctrl *gomock.Controller + recorder *MockBackendMockRecorder +} + +// MockBackendMockRecorder is the mock recorder for MockBackend. +type MockBackendMockRecorder struct { + mock *MockBackend +} + +// NewMockBackend creates a new mock instance. +func NewMockBackend(ctrl *gomock.Controller) *MockBackend { + mock := &MockBackend{ctrl: ctrl} + mock.recorder = &MockBackendMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBackend) EXPECT() *MockBackendMockRecorder { + return m.recorder +} + +// AccountsGet mocks base method. +func (m *MockBackend) AccountsGet(ctx context.Context, id models.AccountID) (*models.Account, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AccountsGet", ctx, id) + ret0, _ := ret[0].(*models.Account) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AccountsGet indicates an expected call of AccountsGet. +func (mr *MockBackendMockRecorder) AccountsGet(ctx, id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AccountsGet", reflect.TypeOf((*MockBackend)(nil).AccountsGet), ctx, id) +} + +// AccountsList mocks base method. +func (m *MockBackend) AccountsList(ctx context.Context, query storage.ListAccountsQuery) (*bunpaginate.Cursor[models.Account], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AccountsList", ctx, query) + ret0, _ := ret[0].(*bunpaginate.Cursor[models.Account]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AccountsList indicates an expected call of AccountsList. +func (mr *MockBackendMockRecorder) AccountsList(ctx, query interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AccountsList", reflect.TypeOf((*MockBackend)(nil).AccountsList), ctx, query) +} + +// BalancesList mocks base method. +func (m *MockBackend) BalancesList(ctx context.Context, query storage.ListBalancesQuery) (*bunpaginate.Cursor[models.Balance], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BalancesList", ctx, query) + ret0, _ := ret[0].(*bunpaginate.Cursor[models.Balance]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BalancesList indicates an expected call of BalancesList. +func (mr *MockBackendMockRecorder) BalancesList(ctx, query interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BalancesList", reflect.TypeOf((*MockBackend)(nil).BalancesList), ctx, query) +} + +// BankAccountsCreate mocks base method. +func (m *MockBackend) BankAccountsCreate(ctx context.Context, bankAccount models.BankAccount) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BankAccountsCreate", ctx, bankAccount) + ret0, _ := ret[0].(error) + return ret0 +} + +// BankAccountsCreate indicates an expected call of BankAccountsCreate. +func (mr *MockBackendMockRecorder) BankAccountsCreate(ctx, bankAccount interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BankAccountsCreate", reflect.TypeOf((*MockBackend)(nil).BankAccountsCreate), ctx, bankAccount) +} + +// BankAccountsForwardToConnector mocks base method. +func (m *MockBackend) BankAccountsForwardToConnector(ctx context.Context, bankAccountID uuid.UUID, connectorID models.ConnectorID) (*models.BankAccount, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BankAccountsForwardToConnector", ctx, bankAccountID, connectorID) + ret0, _ := ret[0].(*models.BankAccount) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BankAccountsForwardToConnector indicates an expected call of BankAccountsForwardToConnector. +func (mr *MockBackendMockRecorder) BankAccountsForwardToConnector(ctx, bankAccountID, connectorID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BankAccountsForwardToConnector", reflect.TypeOf((*MockBackend)(nil).BankAccountsForwardToConnector), ctx, bankAccountID, connectorID) +} + +// BankAccountsGet mocks base method. +func (m *MockBackend) BankAccountsGet(ctx context.Context, id uuid.UUID) (*models.BankAccount, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BankAccountsGet", ctx, id) + ret0, _ := ret[0].(*models.BankAccount) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BankAccountsGet indicates an expected call of BankAccountsGet. +func (mr *MockBackendMockRecorder) BankAccountsGet(ctx, id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BankAccountsGet", reflect.TypeOf((*MockBackend)(nil).BankAccountsGet), ctx, id) +} + +// BankAccountsList mocks base method. +func (m *MockBackend) BankAccountsList(ctx context.Context, query storage.ListBankAccountsQuery) (*bunpaginate.Cursor[models.BankAccount], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BankAccountsList", ctx, query) + ret0, _ := ret[0].(*bunpaginate.Cursor[models.BankAccount]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BankAccountsList indicates an expected call of BankAccountsList. +func (mr *MockBackendMockRecorder) BankAccountsList(ctx, query interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BankAccountsList", reflect.TypeOf((*MockBackend)(nil).BankAccountsList), ctx, query) +} + +// BankAccountsUpdateMetadata mocks base method. +func (m *MockBackend) BankAccountsUpdateMetadata(ctx context.Context, id uuid.UUID, metadata map[string]string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BankAccountsUpdateMetadata", ctx, id, metadata) + ret0, _ := ret[0].(error) + return ret0 +} + +// BankAccountsUpdateMetadata indicates an expected call of BankAccountsUpdateMetadata. +func (mr *MockBackendMockRecorder) BankAccountsUpdateMetadata(ctx, id, metadata interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BankAccountsUpdateMetadata", reflect.TypeOf((*MockBackend)(nil).BankAccountsUpdateMetadata), ctx, id, metadata) +} + +// ConnectorsConfig mocks base method. +func (m *MockBackend) ConnectorsConfig(ctx context.Context, connectorID models.ConnectorID) (json.RawMessage, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConnectorsConfig", ctx, connectorID) + ret0, _ := ret[0].(json.RawMessage) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ConnectorsConfig indicates an expected call of ConnectorsConfig. +func (mr *MockBackendMockRecorder) ConnectorsConfig(ctx, connectorID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConnectorsConfig", reflect.TypeOf((*MockBackend)(nil).ConnectorsConfig), ctx, connectorID) +} + +// ConnectorsConfigs mocks base method. +func (m *MockBackend) ConnectorsConfigs() plugins.Configs { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConnectorsConfigs") + ret0, _ := ret[0].(plugins.Configs) + return ret0 +} + +// ConnectorsConfigs indicates an expected call of ConnectorsConfigs. +func (mr *MockBackendMockRecorder) ConnectorsConfigs() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConnectorsConfigs", reflect.TypeOf((*MockBackend)(nil).ConnectorsConfigs)) +} + +// ConnectorsHandleWebhooks mocks base method. +func (m *MockBackend) ConnectorsHandleWebhooks(ctx context.Context, urlPath string, webhook models.Webhook) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConnectorsHandleWebhooks", ctx, urlPath, webhook) + ret0, _ := ret[0].(error) + return ret0 +} + +// ConnectorsHandleWebhooks indicates an expected call of ConnectorsHandleWebhooks. +func (mr *MockBackendMockRecorder) ConnectorsHandleWebhooks(ctx, urlPath, webhook interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConnectorsHandleWebhooks", reflect.TypeOf((*MockBackend)(nil).ConnectorsHandleWebhooks), ctx, urlPath, webhook) +} + +// ConnectorsInstall mocks base method. +func (m *MockBackend) ConnectorsInstall(ctx context.Context, provider string, config json.RawMessage) (models.ConnectorID, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConnectorsInstall", ctx, provider, config) + ret0, _ := ret[0].(models.ConnectorID) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ConnectorsInstall indicates an expected call of ConnectorsInstall. +func (mr *MockBackendMockRecorder) ConnectorsInstall(ctx, provider, config interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConnectorsInstall", reflect.TypeOf((*MockBackend)(nil).ConnectorsInstall), ctx, provider, config) +} + +// ConnectorsList mocks base method. +func (m *MockBackend) ConnectorsList(ctx context.Context, query storage.ListConnectorsQuery) (*bunpaginate.Cursor[models.Connector], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConnectorsList", ctx, query) + ret0, _ := ret[0].(*bunpaginate.Cursor[models.Connector]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ConnectorsList indicates an expected call of ConnectorsList. +func (mr *MockBackendMockRecorder) ConnectorsList(ctx, query interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConnectorsList", reflect.TypeOf((*MockBackend)(nil).ConnectorsList), ctx, query) +} + +// ConnectorsReset mocks base method. +func (m *MockBackend) ConnectorsReset(ctx context.Context, connectorID models.ConnectorID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConnectorsReset", ctx, connectorID) + ret0, _ := ret[0].(error) + return ret0 +} + +// ConnectorsReset indicates an expected call of ConnectorsReset. +func (mr *MockBackendMockRecorder) ConnectorsReset(ctx, connectorID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConnectorsReset", reflect.TypeOf((*MockBackend)(nil).ConnectorsReset), ctx, connectorID) +} + +// ConnectorsUninstall mocks base method. +func (m *MockBackend) ConnectorsUninstall(ctx context.Context, connectorID models.ConnectorID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConnectorsUninstall", ctx, connectorID) + ret0, _ := ret[0].(error) + return ret0 +} + +// ConnectorsUninstall indicates an expected call of ConnectorsUninstall. +func (mr *MockBackendMockRecorder) ConnectorsUninstall(ctx, connectorID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConnectorsUninstall", reflect.TypeOf((*MockBackend)(nil).ConnectorsUninstall), ctx, connectorID) +} + +// PaymentsGet mocks base method. +func (m *MockBackend) PaymentsGet(ctx context.Context, id models.PaymentID) (*models.Payment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentsGet", ctx, id) + ret0, _ := ret[0].(*models.Payment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PaymentsGet indicates an expected call of PaymentsGet. +func (mr *MockBackendMockRecorder) PaymentsGet(ctx, id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentsGet", reflect.TypeOf((*MockBackend)(nil).PaymentsGet), ctx, id) +} + +// PaymentsList mocks base method. +func (m *MockBackend) PaymentsList(ctx context.Context, query storage.ListPaymentsQuery) (*bunpaginate.Cursor[models.Payment], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentsList", ctx, query) + ret0, _ := ret[0].(*bunpaginate.Cursor[models.Payment]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PaymentsList indicates an expected call of PaymentsList. +func (mr *MockBackendMockRecorder) PaymentsList(ctx, query interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentsList", reflect.TypeOf((*MockBackend)(nil).PaymentsList), ctx, query) +} + +// PaymentsUpdateMetadata mocks base method. +func (m *MockBackend) PaymentsUpdateMetadata(ctx context.Context, id models.PaymentID, metadata map[string]string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentsUpdateMetadata", ctx, id, metadata) + ret0, _ := ret[0].(error) + return ret0 +} + +// PaymentsUpdateMetadata indicates an expected call of PaymentsUpdateMetadata. +func (mr *MockBackendMockRecorder) PaymentsUpdateMetadata(ctx, id, metadata interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentsUpdateMetadata", reflect.TypeOf((*MockBackend)(nil).PaymentsUpdateMetadata), ctx, id, metadata) +} + +// PoolsAddAccount mocks base method. +func (m *MockBackend) PoolsAddAccount(ctx context.Context, id uuid.UUID, accountID models.AccountID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PoolsAddAccount", ctx, id, accountID) + ret0, _ := ret[0].(error) + return ret0 +} + +// PoolsAddAccount indicates an expected call of PoolsAddAccount. +func (mr *MockBackendMockRecorder) PoolsAddAccount(ctx, id, accountID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PoolsAddAccount", reflect.TypeOf((*MockBackend)(nil).PoolsAddAccount), ctx, id, accountID) +} + +// PoolsBalancesAt mocks base method. +func (m *MockBackend) PoolsBalancesAt(ctx context.Context, poolID uuid.UUID, at time.Time) ([]models.AggregatedBalance, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PoolsBalancesAt", ctx, poolID, at) + ret0, _ := ret[0].([]models.AggregatedBalance) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PoolsBalancesAt indicates an expected call of PoolsBalancesAt. +func (mr *MockBackendMockRecorder) PoolsBalancesAt(ctx, poolID, at interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PoolsBalancesAt", reflect.TypeOf((*MockBackend)(nil).PoolsBalancesAt), ctx, poolID, at) +} + +// PoolsCreate mocks base method. +func (m *MockBackend) PoolsCreate(ctx context.Context, pool models.Pool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PoolsCreate", ctx, pool) + ret0, _ := ret[0].(error) + return ret0 +} + +// PoolsCreate indicates an expected call of PoolsCreate. +func (mr *MockBackendMockRecorder) PoolsCreate(ctx, pool interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PoolsCreate", reflect.TypeOf((*MockBackend)(nil).PoolsCreate), ctx, pool) +} + +// PoolsDelete mocks base method. +func (m *MockBackend) PoolsDelete(ctx context.Context, id uuid.UUID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PoolsDelete", ctx, id) + ret0, _ := ret[0].(error) + return ret0 +} + +// PoolsDelete indicates an expected call of PoolsDelete. +func (mr *MockBackendMockRecorder) PoolsDelete(ctx, id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PoolsDelete", reflect.TypeOf((*MockBackend)(nil).PoolsDelete), ctx, id) +} + +// PoolsGet mocks base method. +func (m *MockBackend) PoolsGet(ctx context.Context, id uuid.UUID) (*models.Pool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PoolsGet", ctx, id) + ret0, _ := ret[0].(*models.Pool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PoolsGet indicates an expected call of PoolsGet. +func (mr *MockBackendMockRecorder) PoolsGet(ctx, id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PoolsGet", reflect.TypeOf((*MockBackend)(nil).PoolsGet), ctx, id) +} + +// PoolsList mocks base method. +func (m *MockBackend) PoolsList(ctx context.Context, query storage.ListPoolsQuery) (*bunpaginate.Cursor[models.Pool], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PoolsList", ctx, query) + ret0, _ := ret[0].(*bunpaginate.Cursor[models.Pool]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PoolsList indicates an expected call of PoolsList. +func (mr *MockBackendMockRecorder) PoolsList(ctx, query interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PoolsList", reflect.TypeOf((*MockBackend)(nil).PoolsList), ctx, query) +} + +// PoolsRemoveAccount mocks base method. +func (m *MockBackend) PoolsRemoveAccount(ctx context.Context, id uuid.UUID, accountID models.AccountID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PoolsRemoveAccount", ctx, id, accountID) + ret0, _ := ret[0].(error) + return ret0 +} + +// PoolsRemoveAccount indicates an expected call of PoolsRemoveAccount. +func (mr *MockBackendMockRecorder) PoolsRemoveAccount(ctx, id, accountID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PoolsRemoveAccount", reflect.TypeOf((*MockBackend)(nil).PoolsRemoveAccount), ctx, id, accountID) +} + +// SchedulesGet mocks base method. +func (m *MockBackend) SchedulesGet(ctx context.Context, id string, connectorID models.ConnectorID) (*models.Schedule, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SchedulesGet", ctx, id, connectorID) + ret0, _ := ret[0].(*models.Schedule) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SchedulesGet indicates an expected call of SchedulesGet. +func (mr *MockBackendMockRecorder) SchedulesGet(ctx, id, connectorID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SchedulesGet", reflect.TypeOf((*MockBackend)(nil).SchedulesGet), ctx, id, connectorID) +} + +// SchedulesList mocks base method. +func (m *MockBackend) SchedulesList(ctx context.Context, query storage.ListSchedulesQuery) (*bunpaginate.Cursor[models.Schedule], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SchedulesList", ctx, query) + ret0, _ := ret[0].(*bunpaginate.Cursor[models.Schedule]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SchedulesList indicates an expected call of SchedulesList. +func (mr *MockBackendMockRecorder) SchedulesList(ctx, query interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SchedulesList", reflect.TypeOf((*MockBackend)(nil).SchedulesList), ctx, query) +} + +// WorkflowsInstancesList mocks base method. +func (m *MockBackend) WorkflowsInstancesList(ctx context.Context, query storage.ListInstancesQuery) (*bunpaginate.Cursor[models.Instance], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WorkflowsInstancesList", ctx, query) + ret0, _ := ret[0].(*bunpaginate.Cursor[models.Instance]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// WorkflowsInstancesList indicates an expected call of WorkflowsInstancesList. +func (mr *MockBackendMockRecorder) WorkflowsInstancesList(ctx, query interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WorkflowsInstancesList", reflect.TypeOf((*MockBackend)(nil).WorkflowsInstancesList), ctx, query) +} diff --git a/components/payments/internal/api/module.go b/components/payments/internal/api/module.go new file mode 100644 index 0000000000..2fcbfba31e --- /dev/null +++ b/components/payments/internal/api/module.go @@ -0,0 +1,38 @@ +package api + +import ( + "github.com/formancehq/go-libs/api" + "github.com/formancehq/go-libs/auth" + "github.com/formancehq/go-libs/health" + "github.com/formancehq/go-libs/httpserver" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/api/services" + "github.com/formancehq/payments/internal/connectors/engine" + "github.com/formancehq/payments/internal/storage" + "github.com/go-chi/chi/v5" + "go.uber.org/fx" +) + +func TagVersion() fx.Annotation { + return fx.ResultTags(`group:"apiVersions"`) +} + +func NewModule(bind string, debug bool) fx.Option { + return fx.Options( + fx.Invoke(func(m *chi.Mux, lc fx.Lifecycle) { + lc.Append(httpserver.NewHook(m, httpserver.WithAddress(bind))) + }), + fx.Provide(fx.Annotate(func( + backend backend.Backend, + info api.ServiceInfo, + healthController *health.HealthController, + a auth.Authenticator, + versions ...Version, + ) *chi.Mux { + return NewRouter(backend, info, healthController, a, debug, versions...) + }, fx.ParamTags(``, ``, ``, ``, `group:"apiVersions"`))), + fx.Provide(func(storage storage.Storage, engine engine.Engine) backend.Backend { + return services.New(storage, engine) + }), + ) +} diff --git a/components/payments/internal/api/router.go b/components/payments/internal/api/router.go new file mode 100644 index 0000000000..5211e82c9a --- /dev/null +++ b/components/payments/internal/api/router.go @@ -0,0 +1,64 @@ +package api + +import ( + "fmt" + "net/http" + "sort" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/go-libs/auth" + "github.com/formancehq/go-libs/health" + "github.com/formancehq/payments/internal/api/backend" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" +) + +type Version struct { + Version int + Builder func(backend backend.Backend, a auth.Authenticator, debug bool) *chi.Mux +} + +type versionsSlice []Version + +func (v versionsSlice) Len() int { + return len(v) +} + +func (v versionsSlice) Less(i, j int) bool { + return v[i].Version < v[j].Version +} + +func (v versionsSlice) Swap(i, j int) { + v[i], v[j] = v[j], v[i] +} + +func NewRouter( + backend backend.Backend, + info api.ServiceInfo, + healthController *health.HealthController, + a auth.Authenticator, + debug bool, + versions ...Version) *chi.Mux { + r := chi.NewRouter() + r.Use(middleware.Recoverer) + r.Use(func(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + handler.ServeHTTP(w, r) + }) + }) + r.Get("/_healthcheck", healthController.Check) + r.Get("/_info", api.InfoHandler(info)) + + sortedVersions := versionsSlice(versions) + sort.Stable(sortedVersions) + + for _, version := range sortedVersions[1:] { + prefix := fmt.Sprintf("/v%d", version.Version) + r.Handle(prefix+"/*", http.StripPrefix(prefix, version.Builder(backend, a, debug))) + } + + r.Handle("/*", versions[0].Builder(backend, a, debug)) // V1 and V2 have no prefix + + return r +} diff --git a/components/payments/internal/api/services/accounts_get.go b/components/payments/internal/api/services/accounts_get.go new file mode 100644 index 0000000000..836aa5c1b0 --- /dev/null +++ b/components/payments/internal/api/services/accounts_get.go @@ -0,0 +1,11 @@ +package services + +import ( + "context" + + "github.com/formancehq/payments/internal/models" +) + +func (s *Service) AccountsGet(ctx context.Context, id models.AccountID) (*models.Account, error) { + return s.storage.AccountsGet(ctx, id) +} diff --git a/components/payments/internal/api/services/accounts_list.go b/components/payments/internal/api/services/accounts_list.go new file mode 100644 index 0000000000..fce64c25be --- /dev/null +++ b/components/payments/internal/api/services/accounts_list.go @@ -0,0 +1,13 @@ +package services + +import ( + "context" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/storage" +) + +func (s *Service) AccountsList(ctx context.Context, query storage.ListAccountsQuery) (*bunpaginate.Cursor[models.Account], error) { + return s.storage.AccountsList(ctx, query) +} diff --git a/components/payments/internal/api/services/balances_list.go b/components/payments/internal/api/services/balances_list.go new file mode 100644 index 0000000000..134d36ac15 --- /dev/null +++ b/components/payments/internal/api/services/balances_list.go @@ -0,0 +1,13 @@ +package services + +import ( + "context" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/storage" +) + +func (s *Service) BalancesList(ctx context.Context, query storage.ListBalancesQuery) (*bunpaginate.Cursor[models.Balance], error) { + return s.storage.BalancesList(ctx, query) +} diff --git a/components/payments/internal/api/services/bank_accounts_create.go b/components/payments/internal/api/services/bank_accounts_create.go new file mode 100644 index 0000000000..5776c0e8c6 --- /dev/null +++ b/components/payments/internal/api/services/bank_accounts_create.go @@ -0,0 +1,11 @@ +package services + +import ( + "context" + + "github.com/formancehq/payments/internal/models" +) + +func (s *Service) BankAccountsCreate(ctx context.Context, bankAccount models.BankAccount) error { + return s.storage.BankAccountsUpsert(ctx, bankAccount) +} diff --git a/components/payments/internal/api/services/bank_accounts_forward_to_connector.go b/components/payments/internal/api/services/bank_accounts_forward_to_connector.go new file mode 100644 index 0000000000..7fbcb20a89 --- /dev/null +++ b/components/payments/internal/api/services/bank_accounts_forward_to_connector.go @@ -0,0 +1,12 @@ +package services + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "github.com/google/uuid" +) + +func (s *Service) BankAccountsForwardToConnector(ctx context.Context, bankAccountID uuid.UUID, connectorID models.ConnectorID) (*models.BankAccount, error) { + return s.engine.ForwardBankAccount(ctx, bankAccountID, connectorID) +} diff --git a/components/payments/internal/api/services/bank_accounts_get.go b/components/payments/internal/api/services/bank_accounts_get.go new file mode 100644 index 0000000000..de20b76036 --- /dev/null +++ b/components/payments/internal/api/services/bank_accounts_get.go @@ -0,0 +1,12 @@ +package services + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "github.com/google/uuid" +) + +func (s *Service) BankAccountsGet(ctx context.Context, id uuid.UUID) (*models.BankAccount, error) { + return s.storage.BankAccountsGet(ctx, id, true) +} diff --git a/components/payments/internal/api/services/bank_accounts_list.go b/components/payments/internal/api/services/bank_accounts_list.go new file mode 100644 index 0000000000..c01b66bf6c --- /dev/null +++ b/components/payments/internal/api/services/bank_accounts_list.go @@ -0,0 +1,13 @@ +package services + +import ( + "context" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/storage" +) + +func (s *Service) BankAccountsList(ctx context.Context, query storage.ListBankAccountsQuery) (*bunpaginate.Cursor[models.BankAccount], error) { + return s.storage.BankAccountsList(ctx, query) +} diff --git a/components/payments/internal/api/services/bank_accounts_update_metadata.go b/components/payments/internal/api/services/bank_accounts_update_metadata.go new file mode 100644 index 0000000000..7164ad3193 --- /dev/null +++ b/components/payments/internal/api/services/bank_accounts_update_metadata.go @@ -0,0 +1,11 @@ +package services + +import ( + "context" + + "github.com/google/uuid" +) + +func (s *Service) BankAccountsUpdateMetadata(ctx context.Context, id uuid.UUID, metadata map[string]string) error { + return s.storage.BankAccountsUpdateMetadata(ctx, id, metadata) +} diff --git a/components/payments/internal/api/services/connector_configs.go b/components/payments/internal/api/services/connector_configs.go new file mode 100644 index 0000000000..e0c6ce8f34 --- /dev/null +++ b/components/payments/internal/api/services/connector_configs.go @@ -0,0 +1,22 @@ +package services + +import ( + "context" + "encoding/json" + + "github.com/formancehq/payments/internal/connectors/plugins" + "github.com/formancehq/payments/internal/models" +) + +func (s *Service) ConnectorsConfigs() plugins.Configs { + return plugins.GetConfigs() +} + +func (s *Service) ConnectorsConfig(ctx context.Context, connectorID models.ConnectorID) (json.RawMessage, error) { + connector, err := s.storage.ConnectorsGet(ctx, connectorID) + if err != nil { + return nil, newStorageError(err, "get connector") + } + + return connector.Config, nil +} diff --git a/components/payments/internal/api/services/connectors_handle_webhooks.go b/components/payments/internal/api/services/connectors_handle_webhooks.go new file mode 100644 index 0000000000..e71f02fd32 --- /dev/null +++ b/components/payments/internal/api/services/connectors_handle_webhooks.go @@ -0,0 +1,15 @@ +package services + +import ( + "context" + + "github.com/formancehq/payments/internal/models" +) + +func (s *Service) ConnectorsHandleWebhooks( + ctx context.Context, + urlPath string, + webhook models.Webhook, +) error { + return s.engine.HandleWebhook(ctx, urlPath, webhook) +} diff --git a/components/payments/internal/api/services/connectors_install.go b/components/payments/internal/api/services/connectors_install.go new file mode 100644 index 0000000000..9484c08903 --- /dev/null +++ b/components/payments/internal/api/services/connectors_install.go @@ -0,0 +1,13 @@ +package services + +import ( + "context" + "encoding/json" + + "github.com/formancehq/payments/internal/models" +) + +func (s *Service) ConnectorsInstall(ctx context.Context, provider string, config json.RawMessage) (models.ConnectorID, error) { + connectorID, err := s.engine.InstallConnector(ctx, provider, config) + return connectorID, handleEngineErrors(err) +} diff --git a/components/payments/internal/api/services/connectors_list.go b/components/payments/internal/api/services/connectors_list.go new file mode 100644 index 0000000000..f8b149a633 --- /dev/null +++ b/components/payments/internal/api/services/connectors_list.go @@ -0,0 +1,14 @@ +package services + +import ( + "context" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/storage" +) + +func (s *Service) ConnectorsList(ctx context.Context, query storage.ListConnectorsQuery) (*bunpaginate.Cursor[models.Connector], error) { + cursor, err := s.storage.ConnectorsList(ctx, query) + return cursor, newStorageError(err, "failed to list connectors") +} diff --git a/components/payments/internal/api/services/connectors_reset.go b/components/payments/internal/api/services/connectors_reset.go new file mode 100644 index 0000000000..b43a222d93 --- /dev/null +++ b/components/payments/internal/api/services/connectors_reset.go @@ -0,0 +1,12 @@ +package services + +import ( + "context" + + "github.com/formancehq/payments/internal/models" +) + +func (s *Service) ConnectorsReset(ctx context.Context, connectorID models.ConnectorID) error { + err := s.engine.ResetConnector(ctx, connectorID) + return handleEngineErrors(err) +} diff --git a/components/payments/internal/api/services/connectors_uninstall.go b/components/payments/internal/api/services/connectors_uninstall.go new file mode 100644 index 0000000000..bbd212cea2 --- /dev/null +++ b/components/payments/internal/api/services/connectors_uninstall.go @@ -0,0 +1,16 @@ +package services + +import ( + "context" + + "github.com/formancehq/payments/internal/models" +) + +func (s *Service) ConnectorsUninstall(ctx context.Context, connectorID models.ConnectorID) error { + _, err := s.storage.ConnectorsGet(ctx, connectorID) + if err != nil { + return newStorageError(err, "get connector") + } + + return s.engine.UninstallConnector(ctx, connectorID) +} diff --git a/components/payments/cmd/connectors/internal/api/service/errors.go b/components/payments/internal/api/services/errors.go similarity index 52% rename from components/payments/cmd/connectors/internal/api/service/errors.go rename to components/payments/internal/api/services/errors.go index fff30a8380..6d22fd34f9 100644 --- a/components/payments/cmd/connectors/internal/api/service/errors.go +++ b/components/payments/internal/api/services/errors.go @@ -1,14 +1,15 @@ -package service +package services import ( - "errors" "fmt" + + "github.com/formancehq/payments/internal/connectors/engine" + "github.com/pkg/errors" ) var ( ErrValidation = errors.New("validation error") - ErrPublish = errors.New("publish error") - ErrInvalidID = errors.New("invalid id") + ErrNotFound = errors.New("not found") ) type storageError struct { @@ -38,3 +39,18 @@ func newStorageError(err error, msg string) error { msg: msg, } } + +func handleEngineErrors(err error) error { + if err == nil { + return nil + } + + switch { + case errors.Is(err, engine.ErrValidation): + return errors.Wrap(ErrValidation, err.Error()) + case errors.Is(err, engine.ErrNotFound): + return errors.Wrap(ErrNotFound, err.Error()) + default: + return err + } +} diff --git a/components/payments/internal/api/services/payments_get.go b/components/payments/internal/api/services/payments_get.go new file mode 100644 index 0000000000..b2692a794d --- /dev/null +++ b/components/payments/internal/api/services/payments_get.go @@ -0,0 +1,11 @@ +package services + +import ( + "context" + + "github.com/formancehq/payments/internal/models" +) + +func (s *Service) PaymentsGet(ctx context.Context, id models.PaymentID) (*models.Payment, error) { + return s.storage.PaymentsGet(ctx, id) +} diff --git a/components/payments/internal/api/services/payments_list.go b/components/payments/internal/api/services/payments_list.go new file mode 100644 index 0000000000..0b60cc8f3d --- /dev/null +++ b/components/payments/internal/api/services/payments_list.go @@ -0,0 +1,13 @@ +package services + +import ( + "context" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/storage" +) + +func (s *Service) PaymentsList(ctx context.Context, query storage.ListPaymentsQuery) (*bunpaginate.Cursor[models.Payment], error) { + return s.storage.PaymentsList(ctx, query) +} diff --git a/components/payments/internal/api/services/payments_update_metadata.go b/components/payments/internal/api/services/payments_update_metadata.go new file mode 100644 index 0000000000..8924880081 --- /dev/null +++ b/components/payments/internal/api/services/payments_update_metadata.go @@ -0,0 +1,11 @@ +package services + +import ( + "context" + + "github.com/formancehq/payments/internal/models" +) + +func (s *Service) PaymentsUpdateMetadata(ctx context.Context, id models.PaymentID, metadata map[string]string) error { + return s.storage.PaymentsUpdateMetadata(ctx, id, metadata) +} diff --git a/components/payments/internal/api/services/pools_add_account.go b/components/payments/internal/api/services/pools_add_account.go new file mode 100644 index 0000000000..f7cca80ee5 --- /dev/null +++ b/components/payments/internal/api/services/pools_add_account.go @@ -0,0 +1,13 @@ +package services + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "github.com/google/uuid" +) + +func (s *Service) PoolsAddAccount(ctx context.Context, id uuid.UUID, accountID models.AccountID) error { + err := s.engine.AddAccountToPool(ctx, id, accountID) + return handleEngineErrors(err) +} diff --git a/components/payments/internal/api/services/pools_balances_at.go b/components/payments/internal/api/services/pools_balances_at.go new file mode 100644 index 0000000000..4d4c6efe62 --- /dev/null +++ b/components/payments/internal/api/services/pools_balances_at.go @@ -0,0 +1,44 @@ +package services + +import ( + "context" + "math/big" + "time" + + "github.com/formancehq/payments/internal/models" + "github.com/google/uuid" +) + +func (s *Service) PoolsBalancesAt(ctx context.Context, poolID uuid.UUID, at time.Time) ([]models.AggregatedBalance, error) { + pool, err := s.storage.PoolsGet(ctx, poolID) + if err != nil { + return nil, newStorageError(err, "getting pool") + } + res := make(map[string]*big.Int) + for _, poolAccount := range pool.PoolAccounts { + balances, err := s.storage.BalancesGetAt(ctx, poolAccount.AccountID, at) + if err != nil { + return nil, newStorageError(err, "getting balances") + } + + for _, balance := range balances { + amount, ok := res[balance.Asset] + if !ok { + amount = big.NewInt(0) + } + + amount.Add(amount, balance.Balance) + res[balance.Asset] = amount + } + } + + balances := make([]models.AggregatedBalance, 0, len(res)) + for asset, amount := range res { + balances = append(balances, models.AggregatedBalance{ + Asset: asset, + Amount: amount, + }) + } + + return balances, nil +} diff --git a/components/payments/internal/api/services/pools_create.go b/components/payments/internal/api/services/pools_create.go new file mode 100644 index 0000000000..929cd942ee --- /dev/null +++ b/components/payments/internal/api/services/pools_create.go @@ -0,0 +1,12 @@ +package services + +import ( + "context" + + "github.com/formancehq/payments/internal/models" +) + +func (s *Service) PoolsCreate(ctx context.Context, pool models.Pool) error { + err := s.engine.CreatePool(ctx, pool) + return handleEngineErrors(err) +} diff --git a/components/payments/internal/api/services/pools_delete.go b/components/payments/internal/api/services/pools_delete.go new file mode 100644 index 0000000000..2e41a23ba0 --- /dev/null +++ b/components/payments/internal/api/services/pools_delete.go @@ -0,0 +1,12 @@ +package services + +import ( + "context" + + "github.com/google/uuid" +) + +func (s *Service) PoolsDelete(ctx context.Context, id uuid.UUID) error { + err := s.engine.DeletePool(ctx, id) + return handleEngineErrors(err) +} diff --git a/components/payments/internal/api/services/pools_get.go b/components/payments/internal/api/services/pools_get.go new file mode 100644 index 0000000000..fae8ac1656 --- /dev/null +++ b/components/payments/internal/api/services/pools_get.go @@ -0,0 +1,12 @@ +package services + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "github.com/google/uuid" +) + +func (s *Service) PoolsGet(ctx context.Context, id uuid.UUID) (*models.Pool, error) { + return s.storage.PoolsGet(ctx, id) +} diff --git a/components/payments/internal/api/services/pools_list.go b/components/payments/internal/api/services/pools_list.go new file mode 100644 index 0000000000..ff85baa2d5 --- /dev/null +++ b/components/payments/internal/api/services/pools_list.go @@ -0,0 +1,13 @@ +package services + +import ( + "context" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/storage" +) + +func (s *Service) PoolsList(ctx context.Context, query storage.ListPoolsQuery) (*bunpaginate.Cursor[models.Pool], error) { + return s.storage.PoolsList(ctx, query) +} diff --git a/components/payments/internal/api/services/pools_remove_account.go b/components/payments/internal/api/services/pools_remove_account.go new file mode 100644 index 0000000000..84163cb460 --- /dev/null +++ b/components/payments/internal/api/services/pools_remove_account.go @@ -0,0 +1,13 @@ +package services + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "github.com/google/uuid" +) + +func (s *Service) PoolsRemoveAccount(ctx context.Context, id uuid.UUID, accountID models.AccountID) error { + err := s.engine.RemoveAccountFromPool(ctx, id, accountID) + return handleEngineErrors(err) +} diff --git a/components/payments/internal/api/services/schedules_get.go b/components/payments/internal/api/services/schedules_get.go new file mode 100644 index 0000000000..016568cf76 --- /dev/null +++ b/components/payments/internal/api/services/schedules_get.go @@ -0,0 +1,11 @@ +package services + +import ( + "context" + + "github.com/formancehq/payments/internal/models" +) + +func (s *Service) SchedulesGet(ctx context.Context, id string, connectorID models.ConnectorID) (*models.Schedule, error) { + return s.storage.SchedulesGet(ctx, id, connectorID) +} diff --git a/components/payments/internal/api/services/schedules_list.go b/components/payments/internal/api/services/schedules_list.go new file mode 100644 index 0000000000..2f0b4a316f --- /dev/null +++ b/components/payments/internal/api/services/schedules_list.go @@ -0,0 +1,14 @@ +package services + +import ( + "context" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/storage" +) + +func (s *Service) SchedulesList(ctx context.Context, query storage.ListSchedulesQuery) (*bunpaginate.Cursor[models.Schedule], error) { + cursor, err := s.storage.SchedulesList(ctx, query) + return cursor, newStorageError(err, "failed to list schedules") +} diff --git a/components/payments/internal/api/services/services.go b/components/payments/internal/api/services/services.go new file mode 100644 index 0000000000..88b34eec37 --- /dev/null +++ b/components/payments/internal/api/services/services.go @@ -0,0 +1,19 @@ +package services + +import ( + "github.com/formancehq/payments/internal/connectors/engine" + "github.com/formancehq/payments/internal/storage" +) + +type Service struct { + storage storage.Storage + + engine engine.Engine +} + +func New(storage storage.Storage, engine engine.Engine) *Service { + return &Service{ + storage: storage, + engine: engine, + } +} diff --git a/components/payments/internal/api/services/workflows_instances_list.go b/components/payments/internal/api/services/workflows_instances_list.go new file mode 100644 index 0000000000..0a32828737 --- /dev/null +++ b/components/payments/internal/api/services/workflows_instances_list.go @@ -0,0 +1,14 @@ +package services + +import ( + "context" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/storage" +) + +func (s *Service) WorkflowsInstancesList(ctx context.Context, query storage.ListInstancesQuery) (*bunpaginate.Cursor[models.Instance], error) { + cursor, err := s.storage.InstancesList(ctx, query) + return cursor, newStorageError(err, "failed to list instances") +} diff --git a/components/payments/internal/api/v2/errors.go b/components/payments/internal/api/v2/errors.go new file mode 100644 index 0000000000..6ea662becb --- /dev/null +++ b/components/payments/internal/api/v2/errors.go @@ -0,0 +1,35 @@ +package v2 + +import ( + "errors" + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/services" + "github.com/formancehq/payments/internal/storage" +) + +const ( + ErrUniqueReference = "CONFLICT" + ErrNotFound = "NOT_FOUND" + ErrInvalidID = "INVALID_ID" + ErrMissingOrInvalidBody = "MISSING_OR_INVALID_BODY" + ErrValidation = "VALIDATION" +) + +func handleServiceErrors(w http.ResponseWriter, r *http.Request, err error) { + switch { + case errors.Is(err, storage.ErrDuplicateKeyValue): + api.BadRequest(w, ErrUniqueReference, err) + case errors.Is(err, storage.ErrNotFound): + api.NotFound(w, err) + case errors.Is(err, storage.ErrValidation): + api.BadRequest(w, ErrValidation, err) + case errors.Is(err, services.ErrValidation): + api.BadRequest(w, ErrValidation, err) + case errors.Is(err, services.ErrNotFound): + api.NotFound(w, err) + default: + api.InternalServerError(w, r, err) + } +} diff --git a/components/payments/cmd/api/internal/api/balances.go b/components/payments/internal/api/v2/handler_accounts_balances.go similarity index 65% rename from components/payments/cmd/api/internal/api/balances.go rename to components/payments/internal/api/v2/handler_accounts_balances.go index 52d8c734c6..a81e5f43fc 100644 --- a/components/payments/cmd/api/internal/api/balances.go +++ b/components/payments/internal/api/v2/handler_accounts_balances.go @@ -1,39 +1,33 @@ -package api +package v2 import ( "encoding/json" "math/big" "net/http" - "strconv" "time" "github.com/formancehq/go-libs/api" "github.com/formancehq/go-libs/bun/bunpaginate" "github.com/formancehq/go-libs/pointer" - "github.com/formancehq/payments/cmd/api/internal/api/backend" - "github.com/formancehq/payments/cmd/api/internal/storage" + "github.com/formancehq/payments/internal/api/backend" "github.com/formancehq/payments/internal/models" "github.com/formancehq/payments/internal/otel" - "github.com/gorilla/mux" - "go.opentelemetry.io/otel/attribute" + "github.com/formancehq/payments/internal/storage" ) type balancesResponse struct { AccountID string `json:"accountId"` CreatedAt time.Time `json:"createdAt"` LastUpdatedAt time.Time `json:"lastUpdatedAt"` - Currency string `json:"currency"` // Deprecated: should be removed soon Asset string `json:"asset"` Balance *big.Int `json:"balance"` } -func listBalancesForAccount(b backend.Backend) http.HandlerFunc { +func accountsBalances(backend backend.Backend) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - ctx, span := otel.Tracer().Start(r.Context(), "listBalancesForAccount") + ctx, span := otel.Tracer().Start(r.Context(), "v2_accountsBalances") defer span.End() - w.Header().Set("Content-Type", "application/json") - balanceQuery, err := populateBalanceQueryFromRequest(r) if err != nil { otel.RecordError(span, err) @@ -41,13 +35,6 @@ func listBalancesForAccount(b backend.Backend) http.HandlerFunc { return } - span.SetAttributes( - attribute.String("request.accountID", balanceQuery.AccountID.String()), - attribute.String("request.currency", balanceQuery.Currency), - attribute.String("request.from", balanceQuery.From.String()), - attribute.String("request.to", balanceQuery.To.String()), - ) - query, err := bunpaginate.Extract[storage.ListBalancesQuery](r, func() (*storage.ListBalancesQuery, error) { options, err := getPagination(r, balanceQuery) if err != nil { @@ -61,23 +48,7 @@ func listBalancesForAccount(b backend.Backend) http.HandlerFunc { return } - // In order to support the legacy API, we need to check if the limit query parameter is set - // and if so, we need to override the pageSize pagination option - if r.URL.Query().Get("limit") != "" { - limit, err := strconv.ParseInt(r.URL.Query().Get("limit"), 10, 64) - if err != nil { - otel.RecordError(span, err) - api.BadRequest(w, ErrValidation, err) - return - } - - if limit > 0 { - query.PageSize = uint64(limit) - query.Options.PageSize = uint64(limit) - } - } - - cursor, err := b.GetService().ListBalances(ctx, *query) + cursor, err := backend.BalancesList(ctx, *query) if err != nil { otel.RecordError(span, err) handleServiceErrors(w, r, err) @@ -91,8 +62,7 @@ func listBalancesForAccount(b backend.Backend) http.HandlerFunc { data[i] = &balancesResponse{ AccountID: ret[i].AccountID.String(), CreatedAt: ret[i].CreatedAt, - Currency: ret[i].Asset.String(), - Asset: ret[i].Asset.String(), + Asset: ret[i].Asset, Balance: ret[i].Balance, LastUpdatedAt: ret[i].LastUpdatedAt, } @@ -118,13 +88,13 @@ func listBalancesForAccount(b backend.Backend) http.HandlerFunc { func populateBalanceQueryFromRequest(r *http.Request) (storage.BalanceQuery, error) { var balanceQuery storage.BalanceQuery - balanceQuery = balanceQuery.WithCurrency(r.URL.Query().Get("asset")) + balanceQuery = balanceQuery.WithAsset(r.URL.Query().Get("asset")) - accountID, err := models.AccountIDFromString(mux.Vars(r)["accountID"]) + accountID, err := models.AccountIDFromString(accountID(r)) if err != nil { return balanceQuery, err } - balanceQuery = balanceQuery.WithAccountID(accountID) + balanceQuery = balanceQuery.WithAccountID(&accountID) var startTimeParsed, endTimeParsed time.Time diff --git a/components/payments/internal/api/v2/handler_accounts_get.go b/components/payments/internal/api/v2/handler_accounts_get.go new file mode 100644 index 0000000000..8030fedafd --- /dev/null +++ b/components/payments/internal/api/v2/handler_accounts_get.go @@ -0,0 +1,61 @@ +package v2 + +import ( + "encoding/json" + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" +) + +func accountsGet(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_accountsGet") + defer span.End() + + id, err := models.AccountIDFromString(accountID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + account, err := backend.AccountsGet(ctx, id) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + data := &accountResponse{ + ID: account.ID.String(), + Reference: account.Reference, + CreatedAt: account.CreatedAt, + ConnectorID: account.ConnectorID.String(), + Provider: account.ConnectorID.Provider, + Type: string(account.Type), + Metadata: account.Metadata, + Raw: account.Raw, + } + + if account.DefaultAsset != nil { + data.DefaultCurrency = *account.DefaultAsset + data.DefaultAsset = *account.DefaultAsset + } + + if account.Name != nil { + data.AccountName = *account.Name + } + + err = json.NewEncoder(w).Encode(api.BaseResponse[accountResponse]{ + Data: data, + }) + if err != nil { + otel.RecordError(span, err) + api.InternalServerError(w, r, err) + return + } + } +} diff --git a/components/payments/internal/api/v2/handler_accounts_list.go b/components/payments/internal/api/v2/handler_accounts_list.go new file mode 100644 index 0000000000..5b0fc543fc --- /dev/null +++ b/components/payments/internal/api/v2/handler_accounts_list.go @@ -0,0 +1,97 @@ +package v2 + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/otel" + "github.com/formancehq/payments/internal/storage" +) + +// NOTE: in order to maintain previous version compatibility, we need to keep the +// same response structure as the previous version of the API +type accountResponse struct { + ID string `json:"id"` + Reference string `json:"reference"` + CreatedAt time.Time `json:"createdAt"` + ConnectorID string `json:"connectorID"` + Provider string `json:"provider"` + DefaultCurrency string `json:"defaultCurrency"` // Deprecated: should be removed soon + DefaultAsset string `json:"defaultAsset"` + AccountName string `json:"accountName"` + Type string `json:"type"` + Metadata map[string]string `json:"metadata"` + // TODO(polo): add pools + // Pools []uuid.UUID `json:"pools"` + Raw interface{} `json:"raw"` +} + +func accountsList(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_accountsList") + defer span.End() + + query, err := bunpaginate.Extract[storage.ListAccountsQuery](r, func() (*storage.ListAccountsQuery, error) { + options, err := getPagination(r, storage.AccountQuery{}) + if err != nil { + return nil, err + } + return pointer.For(storage.NewListAccountsQuery(*options)), nil + }) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + cursor, err := backend.AccountsList(ctx, *query) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + data := make([]*accountResponse, len(cursor.Data)) + for i := range cursor.Data { + data[i] = &accountResponse{ + ID: cursor.Data[i].ID.String(), + Reference: cursor.Data[i].Reference, + CreatedAt: cursor.Data[i].CreatedAt, + ConnectorID: cursor.Data[i].ConnectorID.String(), + Provider: cursor.Data[i].ConnectorID.Provider, + Type: string(cursor.Data[i].Type), + Metadata: cursor.Data[i].Metadata, + Raw: cursor.Data[i].Raw, + } + + if cursor.Data[i].DefaultAsset != nil { + data[i].DefaultCurrency = *cursor.Data[i].DefaultAsset + data[i].DefaultAsset = *cursor.Data[i].DefaultAsset + } + + if cursor.Data[i].Name != nil { + data[i].AccountName = *cursor.Data[i].Name + } + } + + err = json.NewEncoder(w).Encode(api.BaseResponse[*accountResponse]{ + Cursor: &bunpaginate.Cursor[*accountResponse]{ + PageSize: cursor.PageSize, + HasMore: cursor.HasMore, + Previous: cursor.Previous, + Next: cursor.Next, + Data: data, + }, + }) + if err != nil { + otel.RecordError(span, err) + api.InternalServerError(w, r, err) + return + } + } +} diff --git a/components/payments/internal/api/v2/handler_bank_accounts_create.go b/components/payments/internal/api/v2/handler_bank_accounts_create.go new file mode 100644 index 0000000000..cd6aec4559 --- /dev/null +++ b/components/payments/internal/api/v2/handler_bank_accounts_create.go @@ -0,0 +1,161 @@ +package v2 + +import ( + "encoding/json" + "errors" + "net/http" + "time" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" + "github.com/google/uuid" +) + +// NOTE: in order to maintain previous version compatibility, we need to keep the +// same response structure as the previous version of the API +type bankAccountRelatedAccountsResponse struct { + ID string `json:"id"` + CreatedAt time.Time `json:"createdAt"` + AccountID string `json:"accountID"` + ConnectorID string `json:"connectorID"` + Provider string `json:"provider"` +} + +type bankAccountResponse struct { + ID string `json:"id"` + Name string `json:"name"` + CreatedAt time.Time `json:"createdAt"` + Country string `json:"country"` + Iban string `json:"iban,omitempty"` + AccountNumber string `json:"accountNumber,omitempty"` + SwiftBicCode string `json:"swiftBicCode,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` + RelatedAccounts []*bankAccountRelatedAccountsResponse `json:"relatedAccounts,omitempty"` +} + +type bankAccountsCreateRequest struct { + Name string `json:"name"` + + AccountNumber *string `json:"accountNumber"` + IBAN *string `json:"iban"` + SwiftBicCode *string `json:"swiftBicCode"` + Country *string `json:"country"` + ConnectorID *string `json:"connectorID"` + + Metadata map[string]string `json:"metadata"` +} + +func (r *bankAccountsCreateRequest) Validate() error { + if r.AccountNumber == nil && r.IBAN == nil { + return errors.New("either accountNumber or iban must be provided") + } + + if r.Name == "" { + return errors.New("name must be provided") + } + + return nil +} + +func bankAccountsCreate(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_bankAccountsCreate") + defer span.End() + + var req bankAccountsCreateRequest + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrMissingOrInvalidBody, err) + return + } + + if err := req.Validate(); err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + var connectorID *models.ConnectorID + if req.ConnectorID != nil { + c, err := models.ConnectorIDFromString(*req.ConnectorID) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + connectorID = &c + } + + bankAccount := &models.BankAccount{ + ID: uuid.New(), + CreatedAt: time.Now().UTC(), + Name: req.Name, + AccountNumber: req.AccountNumber, + IBAN: req.IBAN, + SwiftBicCode: req.SwiftBicCode, + Country: req.Country, + Metadata: req.Metadata, + } + + err = backend.BankAccountsCreate(ctx, *bankAccount) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + if connectorID != nil { + bankAccount, err = backend.BankAccountsForwardToConnector(ctx, bankAccount.ID, *connectorID) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + } + + data := &bankAccountResponse{ + ID: bankAccount.ID.String(), + Name: bankAccount.Name, + CreatedAt: bankAccount.CreatedAt, + Metadata: bankAccount.Metadata, + } + + if bankAccount.IBAN != nil { + data.Iban = *bankAccount.IBAN + } + + if bankAccount.AccountNumber != nil { + data.AccountNumber = *bankAccount.AccountNumber + } + + if bankAccount.SwiftBicCode != nil { + data.SwiftBicCode = *bankAccount.SwiftBicCode + } + + if bankAccount.Country != nil { + data.Country = *bankAccount.Country + } + + for _, relatedAccount := range bankAccount.RelatedAccounts { + data.RelatedAccounts = append(data.RelatedAccounts, &bankAccountRelatedAccountsResponse{ + ID: "", + CreatedAt: relatedAccount.CreatedAt, + AccountID: relatedAccount.AccountID.String(), + ConnectorID: relatedAccount.ConnectorID.String(), + Provider: relatedAccount.ConnectorID.Provider, + }) + } + + err = json.NewEncoder(w).Encode(api.BaseResponse[bankAccountResponse]{ + Data: data, + }) + if err != nil { + otel.RecordError(span, err) + api.InternalServerError(w, r, err) + return + } + } +} diff --git a/components/payments/internal/api/v2/handler_bank_accounts_forward_to_connector.go b/components/payments/internal/api/v2/handler_bank_accounts_forward_to_connector.go new file mode 100644 index 0000000000..02ec70f8e9 --- /dev/null +++ b/components/payments/internal/api/v2/handler_bank_accounts_forward_to_connector.go @@ -0,0 +1,110 @@ +package v2 + +import ( + "encoding/json" + "errors" + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" + "github.com/google/uuid" +) + +type bankAccountsForwardToConnectorRequest struct { + ConnectorID string `json:"connectorID"` +} + +func (f *bankAccountsForwardToConnectorRequest) Validate() error { + if f.ConnectorID == "" { + return errors.New("connectorID must be provided") + } + + return nil +} + +func bankAccountsForwardToConnector(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_bankAccountsForwardToConnector") + defer span.End() + + id, err := uuid.Parse(bankAccountID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + var req bankAccountsForwardToConnectorRequest + err = json.NewDecoder(r.Body).Decode(&req) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrMissingOrInvalidBody, err) + return + } + + err = req.Validate() + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrMissingOrInvalidBody, err) + return + } + + connectorID, err := models.ConnectorIDFromString(req.ConnectorID) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + bankAccount, err := backend.BankAccountsForwardToConnector(ctx, id, connectorID) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + data := &bankAccountResponse{ + ID: bankAccount.ID.String(), + Name: bankAccount.Name, + CreatedAt: bankAccount.CreatedAt, + Metadata: bankAccount.Metadata, + } + + if bankAccount.IBAN != nil { + data.Iban = *bankAccount.IBAN + } + + if bankAccount.AccountNumber != nil { + data.AccountNumber = *bankAccount.AccountNumber + } + + if bankAccount.SwiftBicCode != nil { + data.SwiftBicCode = *bankAccount.SwiftBicCode + } + + if bankAccount.Country != nil { + data.Country = *bankAccount.Country + } + + for _, relatedAccount := range bankAccount.RelatedAccounts { + data.RelatedAccounts = append(data.RelatedAccounts, &bankAccountRelatedAccountsResponse{ + ID: "", + CreatedAt: relatedAccount.CreatedAt, + AccountID: relatedAccount.AccountID.String(), + ConnectorID: relatedAccount.ConnectorID.String(), + Provider: relatedAccount.ConnectorID.Provider, + }) + } + + err = json.NewEncoder(w).Encode(api.BaseResponse[bankAccountResponse]{ + Data: data, + }) + if err != nil { + otel.RecordError(span, err) + api.InternalServerError(w, r, err) + return + } + } +} diff --git a/components/payments/internal/api/v2/handler_bank_accounts_get.go b/components/payments/internal/api/v2/handler_bank_accounts_get.go new file mode 100644 index 0000000000..fb0186eb33 --- /dev/null +++ b/components/payments/internal/api/v2/handler_bank_accounts_get.go @@ -0,0 +1,74 @@ +package v2 + +import ( + "encoding/json" + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/otel" + "github.com/google/uuid" +) + +func bankAccountsGet(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_bankAccountsGet") + defer span.End() + + id, err := uuid.Parse(bankAccountID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + bankAccount, err := backend.BankAccountsGet(ctx, id) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + data := &bankAccountResponse{ + ID: bankAccount.ID.String(), + Name: bankAccount.Name, + CreatedAt: bankAccount.CreatedAt, + Metadata: bankAccount.Metadata, + } + + if bankAccount.IBAN != nil { + data.Iban = *bankAccount.IBAN + } + + if bankAccount.AccountNumber != nil { + data.AccountNumber = *bankAccount.AccountNumber + } + + if bankAccount.SwiftBicCode != nil { + data.SwiftBicCode = *bankAccount.SwiftBicCode + } + + if bankAccount.Country != nil { + data.Country = *bankAccount.Country + } + + for _, relatedAccount := range bankAccount.RelatedAccounts { + data.RelatedAccounts = append(data.RelatedAccounts, &bankAccountRelatedAccountsResponse{ + ID: "", + CreatedAt: relatedAccount.CreatedAt, + AccountID: relatedAccount.AccountID.String(), + ConnectorID: relatedAccount.ConnectorID.String(), + Provider: relatedAccount.ConnectorID.Provider, + }) + } + + err = json.NewEncoder(w).Encode(api.BaseResponse[bankAccountResponse]{ + Data: data, + }) + if err != nil { + otel.RecordError(span, err) + api.InternalServerError(w, r, err) + return + } + } +} diff --git a/components/payments/internal/api/v2/handler_bank_accounts_list.go b/components/payments/internal/api/v2/handler_bank_accounts_list.go new file mode 100644 index 0000000000..8f0527fdf7 --- /dev/null +++ b/components/payments/internal/api/v2/handler_bank_accounts_list.go @@ -0,0 +1,93 @@ +package v2 + +import ( + "encoding/json" + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/otel" + "github.com/formancehq/payments/internal/storage" +) + +func bankAccountsList(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_bankAccountsList") + defer span.End() + + query, err := bunpaginate.Extract[storage.ListBankAccountsQuery](r, func() (*storage.ListBankAccountsQuery, error) { + options, err := getPagination(r, storage.BankAccountQuery{}) + if err != nil { + otel.RecordError(span, err) + return nil, err + } + return pointer.For(storage.NewListBankAccountsQuery(*options)), nil + }) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + cursor, err := backend.BankAccountsList(ctx, *query) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + data := make([]*bankAccountResponse, len(cursor.Data)) + for i := range cursor.Data { + data[i] = &bankAccountResponse{ + ID: cursor.Data[i].ID.String(), + Name: cursor.Data[i].Name, + CreatedAt: cursor.Data[i].CreatedAt, + Metadata: cursor.Data[i].Metadata, + } + + if cursor.Data[i].IBAN != nil { + data[i].Iban = *cursor.Data[i].IBAN + } + + if cursor.Data[i].AccountNumber != nil { + data[i].AccountNumber = *cursor.Data[i].AccountNumber + } + + if cursor.Data[i].SwiftBicCode != nil { + data[i].SwiftBicCode = *cursor.Data[i].SwiftBicCode + } + + if cursor.Data[i].Country != nil { + data[i].Country = *cursor.Data[i].Country + } + + data[i].RelatedAccounts = make([]*bankAccountRelatedAccountsResponse, len(cursor.Data[i].RelatedAccounts)) + for j := range cursor.Data[i].RelatedAccounts { + data[i].RelatedAccounts[j] = &bankAccountRelatedAccountsResponse{ + ID: "", + CreatedAt: cursor.Data[i].RelatedAccounts[j].CreatedAt, + AccountID: cursor.Data[i].RelatedAccounts[j].AccountID.String(), + ConnectorID: cursor.Data[i].RelatedAccounts[j].ConnectorID.String(), + Provider: cursor.Data[i].RelatedAccounts[j].ConnectorID.Provider, + } + } + } + + err = json.NewEncoder(w).Encode(api.BaseResponse[*bankAccountResponse]{ + Cursor: &bunpaginate.Cursor[*bankAccountResponse]{ + PageSize: cursor.PageSize, + HasMore: cursor.HasMore, + Previous: cursor.Previous, + Next: cursor.Next, + Data: data, + }, + }) + if err != nil { + otel.RecordError(span, err) + api.InternalServerError(w, r, err) + return + } + } +} diff --git a/components/payments/internal/api/v2/handler_bank_accounts_update_metadata.go b/components/payments/internal/api/v2/handler_bank_accounts_update_metadata.go new file mode 100644 index 0000000000..40c8063246 --- /dev/null +++ b/components/payments/internal/api/v2/handler_bank_accounts_update_metadata.go @@ -0,0 +1,62 @@ +package v2 + +import ( + "encoding/json" + "errors" + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/otel" + "github.com/google/uuid" +) + +type bankAccountsUpdateMetadataRequest struct { + Metadata map[string]string `json:"metadata"` +} + +func (u *bankAccountsUpdateMetadataRequest) Validate() error { + if len(u.Metadata) == 0 { + return errors.New("metadata must be provided") + } + + return nil +} + +func bankAccountsUpdateMetadata(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_bankAccountsUpdateMetadata") + defer span.End() + + id, err := uuid.Parse(bankAccountID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + var req bankAccountsUpdateMetadataRequest + err = json.NewDecoder(r.Body).Decode(&req) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrMissingOrInvalidBody, err) + return + } + + err = req.Validate() + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + err = backend.BankAccountsUpdateMetadata(ctx, id, req.Metadata) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.NoContent(w) + } +} diff --git a/components/payments/internal/api/v2/handler_connectors_config.go b/components/payments/internal/api/v2/handler_connectors_config.go new file mode 100644 index 0000000000..6a644a6cfe --- /dev/null +++ b/components/payments/internal/api/v2/handler_connectors_config.go @@ -0,0 +1,33 @@ +package v2 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" +) + +func connectorsConfig(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_connectorsConfig") + defer span.End() + + connectorID, err := models.ConnectorIDFromString(connectorID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + config, err := backend.ConnectorsConfig(ctx, connectorID) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.Ok(w, config) + } +} diff --git a/components/payments/internal/api/v2/handler_connectors_configs.go b/components/payments/internal/api/v2/handler_connectors_configs.go new file mode 100644 index 0000000000..fcdf8b2e3f --- /dev/null +++ b/components/payments/internal/api/v2/handler_connectors_configs.go @@ -0,0 +1,29 @@ +package v2 + +import ( + "encoding/json" + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/connectors/plugins" + "github.com/formancehq/payments/internal/otel" +) + +func connectorsConfigs(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + _, span := otel.Tracer().Start(r.Context(), "v2_connectorsConfigs") + defer span.End() + + configs := backend.ConnectorsConfigs() + + err := json.NewEncoder(w).Encode(api.BaseResponse[plugins.Configs]{ + Data: &configs, + }) + if err != nil { + otel.RecordError(span, err) + api.InternalServerError(w, r, err) + return + } + } +} diff --git a/components/payments/internal/api/v2/handler_connectors_install.go b/components/payments/internal/api/v2/handler_connectors_install.go new file mode 100644 index 0000000000..c6eaa9a47a --- /dev/null +++ b/components/payments/internal/api/v2/handler_connectors_install.go @@ -0,0 +1,46 @@ +package v2 + +import ( + "errors" + "io" + "net/http" + "strings" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/go-libs/contextutil" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/otel" +) + +func connectorsInstall(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_connectorsInstall") + defer span.End() + + provider := strings.ToLower(connectorProvider(r)) + if provider == "" { + otel.RecordError(span, errors.New("provider is required")) + api.BadRequest(w, ErrValidation, errors.New("provider is required")) + return + } + + config, err := io.ReadAll(r.Body) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrMissingOrInvalidBody, err) + return + } + + // Detach the context to avoid cancellation of the installation process + // leading to a partial installation + ctx, _ = contextutil.Detached(ctx) + connectorID, err := backend.ConnectorsInstall(ctx, provider, config) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.Created(w, connectorID.String()) + } +} diff --git a/components/payments/internal/api/v2/handler_connectors_list.go b/components/payments/internal/api/v2/handler_connectors_list.go new file mode 100644 index 0000000000..397e0244d6 --- /dev/null +++ b/components/payments/internal/api/v2/handler_connectors_list.go @@ -0,0 +1,63 @@ +package v2 + +import ( + "encoding/json" + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/otel" + "github.com/formancehq/payments/internal/storage" +) + +// NOTE: in order to maintain previous version compatibility, we need to keep the +// same response structure as the previous version of the API +type connectorsListElement struct { + Provider string `json:"provider"` + ConnectorID string `json:"connectorID"` + Name string `json:"name"` + Enabled bool `json:"enabled"` +} + +func connectorsList(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_connectorsList") + defer span.End() + + connectors, err := backend.ConnectorsList( + ctx, + storage.NewListConnectorsQuery( + bunpaginate.NewPaginatedQueryOptions(storage.ConnectorQuery{}). + // NOTE: previous version of payments did not have pagination, so + // fetch everything and return it all + WithPageSize(1000), + ), + ) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + data := make([]*connectorsListElement, len(connectors.Data)) + for i := range connectors.Data { + data[i] = &connectorsListElement{ + Provider: connectors.Data[i].Provider, + ConnectorID: connectors.Data[i].ID.String(), + Name: connectors.Data[i].Name, + Enabled: true, + } + } + + err = json.NewEncoder(w).Encode( + api.BaseResponse[[]*connectorsListElement]{ + Data: &data, + }) + if err != nil { + otel.RecordError(span, err) + api.InternalServerError(w, r, err) + return + } + } +} diff --git a/components/payments/internal/api/v2/handler_connectors_reset.go b/components/payments/internal/api/v2/handler_connectors_reset.go new file mode 100644 index 0000000000..1108c38312 --- /dev/null +++ b/components/payments/internal/api/v2/handler_connectors_reset.go @@ -0,0 +1,32 @@ +package v2 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" +) + +func connectorsReset(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_connectorsReset") + defer span.End() + + connectorID, err := models.ConnectorIDFromString(connectorID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + if err := backend.ConnectorsReset(ctx, connectorID); err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.NoContent(w) + } +} diff --git a/components/payments/internal/api/v2/handler_connectors_uninstall.go b/components/payments/internal/api/v2/handler_connectors_uninstall.go new file mode 100644 index 0000000000..7d6359649f --- /dev/null +++ b/components/payments/internal/api/v2/handler_connectors_uninstall.go @@ -0,0 +1,32 @@ +package v2 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" +) + +func connectorsUninstall(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_connectorsUninstall") + defer span.End() + + connectorID, err := models.ConnectorIDFromString(connectorID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + if err := backend.ConnectorsUninstall(ctx, connectorID); err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.NoContent(w) + } +} diff --git a/components/payments/internal/api/v2/handler_connectors_webhooks.go b/components/payments/internal/api/v2/handler_connectors_webhooks.go new file mode 100644 index 0000000000..620d15599c --- /dev/null +++ b/components/payments/internal/api/v2/handler_connectors_webhooks.go @@ -0,0 +1,54 @@ +package v2 + +import ( + "io" + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" + "github.com/google/uuid" +) + +func connectorsWebhooks(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_connectorsWebhooks") + defer span.End() + + connectorID, err := models.ConnectorIDFromString(connectorID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + body, err := io.ReadAll(r.Body) + if err != nil && err != io.EOF { + otel.RecordError(span, err) + api.BadRequest(w, ErrMissingOrInvalidBody, err) + return + } + + headers := r.Header + queryValues := r.URL.Query() + path := r.URL.Path + + webhook := models.Webhook{ + ID: uuid.New().String(), + ConnectorID: connectorID, + QueryValues: queryValues, + Headers: headers, + Body: body, + } + + err = backend.ConnectorsHandleWebhooks(ctx, path, webhook) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.RawOk(w, nil) + } +} diff --git a/components/payments/internal/api/v2/handler_payments_get.go b/components/payments/internal/api/v2/handler_payments_get.go new file mode 100644 index 0000000000..8d6729096b --- /dev/null +++ b/components/payments/internal/api/v2/handler_payments_get.go @@ -0,0 +1,75 @@ +package v2 + +import ( + "encoding/json" + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" +) + +func paymentsGet(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_paymentsGet") + defer span.End() + + id, err := models.PaymentIDFromString(paymentID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + payment, err := backend.PaymentsGet(ctx, id) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + data := paymentResponse{ + ID: payment.ID.String(), + Reference: payment.Reference, + Type: payment.Type.String(), + Provider: payment.ConnectorID.Provider, + ConnectorID: payment.ConnectorID.String(), + Status: payment.Status.String(), + Amount: payment.Amount, + InitialAmount: payment.InitialAmount, + Scheme: payment.Scheme.String(), + Asset: payment.Asset, + CreatedAt: payment.CreatedAt, + Metadata: payment.Metadata, + } + + if payment.SourceAccountID != nil { + data.SourceAccountID = payment.SourceAccountID.String() + } + + if payment.DestinationAccountID != nil { + data.DestinationAccountID = payment.DestinationAccountID.String() + } + + data.Adjustments = make([]paymentAdjustment, len(payment.Adjustments)) + for i := range payment.Adjustments { + data.Adjustments[i] = paymentAdjustment{ + Reference: payment.Adjustments[i].ID.Reference, + CreatedAt: payment.Adjustments[i].CreatedAt, + Status: payment.Adjustments[i].Status.String(), + Amount: payment.Adjustments[i].Amount, + Raw: payment.Adjustments[i].Raw, + } + } + + err = json.NewEncoder(w).Encode(api.BaseResponse[paymentResponse]{ + Data: &data, + }) + if err != nil { + otel.RecordError(span, err) + api.InternalServerError(w, r, err) + return + } + } +} diff --git a/components/payments/internal/api/v2/handler_payments_list.go b/components/payments/internal/api/v2/handler_payments_list.go new file mode 100644 index 0000000000..613ef18d3c --- /dev/null +++ b/components/payments/internal/api/v2/handler_payments_list.go @@ -0,0 +1,123 @@ +package v2 + +import ( + "encoding/json" + "math/big" + "net/http" + "time" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/otel" + "github.com/formancehq/payments/internal/storage" +) + +type paymentResponse struct { + ID string `json:"id"` + Reference string `json:"reference"` + SourceAccountID string `json:"sourceAccountID"` + DestinationAccountID string `json:"destinationAccountID"` + Type string `json:"type"` + Provider string `json:"provider"` + ConnectorID string `json:"connectorID"` + Status string `json:"status"` + Amount *big.Int `json:"amount"` + InitialAmount *big.Int `json:"initialAmount"` + Scheme string `json:"scheme"` + Asset string `json:"asset"` + CreatedAt time.Time `json:"createdAt"` + Raw interface{} `json:"raw"` + Adjustments []paymentAdjustment `json:"adjustments"` + Metadata map[string]string `json:"metadata"` +} + +type paymentAdjustment struct { + Reference string `json:"reference" bson:"reference"` + CreatedAt time.Time `json:"createdAt" bson:"createdAt"` + Status string `json:"status" bson:"status"` + Amount *big.Int `json:"amount" bson:"amount"` + Raw interface{} `json:"raw" bson:"raw"` +} + +func paymentsList(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_paymentsList") + defer span.End() + + query, err := bunpaginate.Extract[storage.ListPaymentsQuery](r, func() (*storage.ListPaymentsQuery, error) { + options, err := getPagination(r, storage.PaymentQuery{}) + if err != nil { + otel.RecordError(span, err) + return nil, err + } + return pointer.For(storage.NewListPaymentsQuery(*options)), nil + }) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + cursor, err := backend.PaymentsList(ctx, *query) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + data := make([]*paymentResponse, len(cursor.Data)) + for i := range cursor.Data { + data[i] = &paymentResponse{ + ID: cursor.Data[i].ID.String(), + Reference: cursor.Data[i].Reference, + Type: cursor.Data[i].Type.String(), + Provider: cursor.Data[i].ConnectorID.Provider, + ConnectorID: cursor.Data[i].ConnectorID.String(), + Status: cursor.Data[i].Status.String(), + Amount: cursor.Data[i].Amount, + InitialAmount: cursor.Data[i].InitialAmount, + Scheme: cursor.Data[i].Scheme.String(), + Asset: cursor.Data[i].Asset, + CreatedAt: cursor.Data[i].CreatedAt, + Adjustments: []paymentAdjustment{}, + Metadata: cursor.Data[i].Metadata, + } + + if cursor.Data[i].SourceAccountID != nil { + data[i].SourceAccountID = cursor.Data[i].SourceAccountID.String() + } + + if cursor.Data[i].DestinationAccountID != nil { + data[i].DestinationAccountID = cursor.Data[i].DestinationAccountID.String() + } + + data[i].Adjustments = make([]paymentAdjustment, len(cursor.Data[i].Adjustments)) + for j := range cursor.Data[i].Adjustments { + data[i].Adjustments[j] = paymentAdjustment{ + Reference: cursor.Data[i].Adjustments[j].ID.Reference, + CreatedAt: cursor.Data[i].Adjustments[j].CreatedAt, + Status: cursor.Data[i].Adjustments[j].Status.String(), + Amount: cursor.Data[i].Adjustments[j].Amount, + Raw: cursor.Data[i].Adjustments[j].Raw, + } + } + } + + err = json.NewEncoder(w).Encode(api.BaseResponse[*paymentResponse]{ + Cursor: &bunpaginate.Cursor[*paymentResponse]{ + PageSize: cursor.PageSize, + HasMore: cursor.HasMore, + Previous: cursor.Previous, + Next: cursor.Next, + Data: data, + }, + }) + if err != nil { + otel.RecordError(span, err) + api.InternalServerError(w, r, err) + return + } + } +} diff --git a/components/payments/internal/api/v2/handler_payments_update_metadata.go b/components/payments/internal/api/v2/handler_payments_update_metadata.go new file mode 100644 index 0000000000..fec43170d0 --- /dev/null +++ b/components/payments/internal/api/v2/handler_payments_update_metadata.go @@ -0,0 +1,42 @@ +package v2 + +import ( + "encoding/json" + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" +) + +func paymentsUpdateMetadata(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_paymentsUpdateMetadata") + defer span.End() + + id, err := models.PaymentIDFromString(paymentID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + var metadata map[string]string + err = json.NewDecoder(r.Body).Decode(&metadata) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + err = backend.PaymentsUpdateMetadata(ctx, id, metadata) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.NoContent(w) + } +} diff --git a/components/payments/internal/api/v2/handler_pools_add_account.go b/components/payments/internal/api/v2/handler_pools_add_account.go new file mode 100644 index 0000000000..594fc96810 --- /dev/null +++ b/components/payments/internal/api/v2/handler_pools_add_account.go @@ -0,0 +1,69 @@ +package v2 + +import ( + "encoding/json" + "errors" + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" + "github.com/google/uuid" +) + +type poolsAddAccountRequest struct { + AccountID string `json:"accountID"` +} + +func (c *poolsAddAccountRequest) Validate() error { + if c.AccountID == "" { + return errors.New("accountID is required") + } + + return nil +} + +func poolsAddAccount(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_poolsAddAccount") + defer span.End() + + id, err := uuid.Parse(poolID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + var poolsAddAccountRequest poolsAddAccountRequest + err = json.NewDecoder(r.Body).Decode(&poolsAddAccountRequest) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrMissingOrInvalidBody, err) + return + } + + if err := poolsAddAccountRequest.Validate(); err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + accountID, err := models.AccountIDFromString(poolsAddAccountRequest.AccountID) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + err = backend.PoolsAddAccount(ctx, id, accountID) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.NoContent(w) + } +} diff --git a/components/payments/internal/api/v2/handler_pools_balances_at.go b/components/payments/internal/api/v2/handler_pools_balances_at.go new file mode 100644 index 0000000000..2c23fb3857 --- /dev/null +++ b/components/payments/internal/api/v2/handler_pools_balances_at.go @@ -0,0 +1,80 @@ +package v2 + +import ( + "encoding/json" + "math/big" + "net/http" + "time" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/otel" + "github.com/google/uuid" + "github.com/pkg/errors" +) + +// NOTE: in order to maintain previous version compatibility, we need to keep the +// same response structure as the previous version of the API +type poolBalancesResponse struct { + Balances []*poolBalanceResponse `json:"balances"` +} + +type poolBalanceResponse struct { + Amount *big.Int `json:"amount"` + Asset string `json:"asset"` +} + +func poolsBalancesAt(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_poolsBalancesAt") + defer span.End() + + id, err := uuid.Parse(poolID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + atTime := r.URL.Query().Get("at") + if atTime == "" { + otel.RecordError(span, errors.New("missing atTime")) + api.BadRequest(w, ErrValidation, errors.New("missing atTime")) + return + } + + at, err := time.Parse(time.RFC3339, atTime) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, errors.Wrap(err, "invalid atTime")) + return + } + + balances, err := backend.PoolsBalancesAt(ctx, id, at) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + data := &poolBalancesResponse{ + Balances: make([]*poolBalanceResponse, len(balances)), + } + + for i := range balances { + data.Balances[i] = &poolBalanceResponse{ + Amount: balances[i].Amount, + Asset: balances[i].Asset, + } + } + + err = json.NewEncoder(w).Encode(api.BaseResponse[poolBalancesResponse]{ + Data: data, + }) + if err != nil { + otel.RecordError(span, err) + api.InternalServerError(w, r, err) + return + } + } +} diff --git a/components/payments/internal/api/v2/handler_pools_create.go b/components/payments/internal/api/v2/handler_pools_create.go new file mode 100644 index 0000000000..43fc1816cc --- /dev/null +++ b/components/payments/internal/api/v2/handler_pools_create.go @@ -0,0 +1,83 @@ +package v2 + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" + "github.com/google/uuid" +) + +type createPoolRequest struct { + Name string `json:"name"` + AccountIDs []string `json:"accountIDs"` +} + +type poolResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Accounts []string `json:"accounts"` +} + +func poolsCreate(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_poolsBalancesAt") + defer span.End() + + var createPoolRequest createPoolRequest + err := json.NewDecoder(r.Body).Decode(&createPoolRequest) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrMissingOrInvalidBody, err) + return + } + + pool := models.Pool{ + ID: uuid.New(), + Name: createPoolRequest.Name, + CreatedAt: time.Now().UTC(), + } + + accounts := make([]models.PoolAccounts, len(createPoolRequest.AccountIDs)) + for i, accountID := range createPoolRequest.AccountIDs { + aID, err := models.AccountIDFromString(accountID) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + accounts[i] = models.PoolAccounts{ + PoolID: pool.ID, + AccountID: aID, + } + } + pool.PoolAccounts = accounts + + err = backend.PoolsCreate(ctx, pool) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + data := &poolResponse{ + ID: pool.ID.String(), + Name: pool.Name, + Accounts: createPoolRequest.AccountIDs, + } + + err = json.NewEncoder(w).Encode(api.BaseResponse[poolResponse]{ + Data: data, + }) + if err != nil { + otel.RecordError(span, err) + api.InternalServerError(w, r, err) + return + } + } +} diff --git a/components/payments/internal/api/v2/handler_pools_delete.go b/components/payments/internal/api/v2/handler_pools_delete.go new file mode 100644 index 0000000000..9ade474954 --- /dev/null +++ b/components/payments/internal/api/v2/handler_pools_delete.go @@ -0,0 +1,33 @@ +package v2 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/otel" + "github.com/google/uuid" +) + +func poolsDelete(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_poolsDelete") + defer span.End() + + id, err := uuid.Parse(poolID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + err = backend.PoolsDelete(ctx, id) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.NoContent(w) + } +} diff --git a/components/payments/internal/api/v2/handler_pools_get.go b/components/payments/internal/api/v2/handler_pools_get.go new file mode 100644 index 0000000000..3a0f3fc527 --- /dev/null +++ b/components/payments/internal/api/v2/handler_pools_get.go @@ -0,0 +1,52 @@ +package v2 + +import ( + "encoding/json" + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/otel" + "github.com/google/uuid" +) + +func poolsGet(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_poolsGet") + defer span.End() + + id, err := uuid.Parse(poolID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + pool, err := backend.PoolsGet(ctx, id) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + data := &poolResponse{ + ID: pool.ID.String(), + Name: pool.Name, + } + + accounts := make([]string, len(pool.PoolAccounts)) + for i := range pool.PoolAccounts { + accounts[i] = pool.PoolAccounts[i].AccountID.String() + } + data.Accounts = accounts + + err = json.NewEncoder(w).Encode(api.BaseResponse[poolResponse]{ + Data: data, + }) + if err != nil { + otel.RecordError(span, err) + api.InternalServerError(w, r, err) + return + } + } +} diff --git a/components/payments/internal/api/v2/handler_pools_list.go b/components/payments/internal/api/v2/handler_pools_list.go new file mode 100644 index 0000000000..bb897908bb --- /dev/null +++ b/components/payments/internal/api/v2/handler_pools_list.go @@ -0,0 +1,70 @@ +package v2 + +import ( + "encoding/json" + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/otel" + "github.com/formancehq/payments/internal/storage" +) + +func poolsList(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_poolsList") + defer span.End() + + query, err := bunpaginate.Extract[storage.ListPoolsQuery](r, func() (*storage.ListPoolsQuery, error) { + options, err := getPagination(r, storage.PoolQuery{}) + if err != nil { + return nil, err + } + return pointer.For(storage.NewListPoolsQuery(*options)), nil + }) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + cursor, err := backend.PoolsList(ctx, *query) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + data := make([]*poolResponse, len(cursor.Data)) + for i := range cursor.Data { + data[i] = &poolResponse{ + ID: cursor.Data[i].ID.String(), + Name: cursor.Data[i].Name, + } + + accounts := make([]string, len(cursor.Data[i].PoolAccounts)) + for j := range cursor.Data[i].PoolAccounts { + accounts[j] = cursor.Data[i].PoolAccounts[j].AccountID.String() + } + + data[i].Accounts = accounts + } + + err = json.NewEncoder(w).Encode(api.BaseResponse[*poolResponse]{ + Cursor: &bunpaginate.Cursor[*poolResponse]{ + PageSize: cursor.PageSize, + HasMore: cursor.HasMore, + Previous: cursor.Previous, + Next: cursor.Next, + Data: data, + }, + }) + if err != nil { + otel.RecordError(span, err) + api.InternalServerError(w, r, err) + return + } + } +} diff --git a/components/payments/internal/api/v2/handler_pools_remove_account.go b/components/payments/internal/api/v2/handler_pools_remove_account.go new file mode 100644 index 0000000000..e030c03b44 --- /dev/null +++ b/components/payments/internal/api/v2/handler_pools_remove_account.go @@ -0,0 +1,41 @@ +package v2 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" + "github.com/google/uuid" +) + +func poolsRemoveAccount(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_poolRemoveAccount") + defer span.End() + + id, err := uuid.Parse(poolID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + accountID, err := models.AccountIDFromString(accountID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + err = backend.PoolsRemoveAccount(ctx, id, accountID) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.NoContent(w) + } +} diff --git a/components/payments/internal/api/v2/handler_tasks_get.go b/components/payments/internal/api/v2/handler_tasks_get.go new file mode 100644 index 0000000000..b78bc20d9a --- /dev/null +++ b/components/payments/internal/api/v2/handler_tasks_get.go @@ -0,0 +1,60 @@ +package v2 + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" +) + +func tasksGet(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_tasksGet") + defer span.End() + + connectorID, err := models.ConnectorIDFromString(connectorID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + taskID := taskID(r) + + schedule, err := backend.SchedulesGet(ctx, taskID, connectorID) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + raw, err := json.Marshal(schedule) + if err != nil { + otel.RecordError(span, err) + api.InternalServerError(w, r, err) + return + } + + data := listTasksResponseElement{ + ID: schedule.ID, + ConnectorID: schedule.ConnectorID.String(), + CreatedAt: schedule.CreatedAt.Format(time.RFC3339), + UpdatedAt: schedule.CreatedAt.Format(time.RFC3339), + Descriptor: raw, + Status: "ACTIVE", + } + + err = json.NewEncoder(w).Encode(api.BaseResponse[listTasksResponseElement]{ + Data: &data, + }) + if err != nil { + otel.RecordError(span, err) + api.InternalServerError(w, r, err) + return + } + } +} diff --git a/components/payments/internal/api/v2/handler_tasks_list.go b/components/payments/internal/api/v2/handler_tasks_list.go new file mode 100644 index 0000000000..d4b15e31b3 --- /dev/null +++ b/components/payments/internal/api/v2/handler_tasks_list.go @@ -0,0 +1,87 @@ +package v2 + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/otel" + "github.com/formancehq/payments/internal/storage" +) + +type listTasksResponseElement struct { + ID string `json:"id"` + ConnectorID string `json:"connectorID"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + Descriptor json.RawMessage `json:"descriptor"` + Status string `json:"status"` + State json.RawMessage `json:"state"` + Error string `json:"error"` +} + +func tasksList(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_tasksList") + defer span.End() + + query, err := bunpaginate.Extract[storage.ListSchedulesQuery](r, func() (*storage.ListSchedulesQuery, error) { + pageSize, err := bunpaginate.GetPageSize(r) + if err != nil { + return nil, err + } + + return pointer.For(storage.NewListSchedulesQuery(bunpaginate.NewPaginatedQueryOptions(storage.ScheduleQuery{}).WithPageSize(pageSize))), nil + }) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + cursor, err := backend.SchedulesList(ctx, *query) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + data := make([]listTasksResponseElement, len(cursor.Data)) + for i := range cursor.Data { + raw, err := json.Marshal(&cursor.Data[i]) + if err != nil { + otel.RecordError(span, err) + api.InternalServerError(w, r, err) + return + } + + data[i] = listTasksResponseElement{ + ID: cursor.Data[i].ID, + ConnectorID: cursor.Data[i].ConnectorID.String(), + CreatedAt: cursor.Data[i].CreatedAt.Format(time.RFC3339), + UpdatedAt: cursor.Data[i].CreatedAt.Format(time.RFC3339), + Descriptor: raw, + Status: "ACTIVE", + } + } + + err = json.NewEncoder(w).Encode(api.BaseResponse[listTasksResponseElement]{ + Cursor: &bunpaginate.Cursor[listTasksResponseElement]{ + PageSize: cursor.PageSize, + HasMore: cursor.HasMore, + Previous: cursor.Previous, + Next: cursor.Next, + Data: data, + }, + }) + if err != nil { + otel.RecordError(span, err) + api.InternalServerError(w, r, err) + return + } + } +} diff --git a/components/payments/internal/api/v2/module.go b/components/payments/internal/api/v2/module.go new file mode 100644 index 0000000000..4356f8fca0 --- /dev/null +++ b/components/payments/internal/api/v2/module.go @@ -0,0 +1,15 @@ +package v2 + +import ( + "github.com/formancehq/payments/internal/api" + "go.uber.org/fx" +) + +func NewModule() fx.Option { + return fx.Options( + fx.Supply(fx.Annotate(api.Version{ + Version: 2, + Builder: newRouter, + }, api.TagVersion())), + ) +} diff --git a/components/payments/internal/api/v2/router.go b/components/payments/internal/api/v2/router.go new file mode 100644 index 0000000000..431deeabd2 --- /dev/null +++ b/components/payments/internal/api/v2/router.go @@ -0,0 +1,131 @@ +package v2 + +import ( + "net/http" + + "github.com/formancehq/go-libs/auth" + "github.com/formancehq/go-libs/service" + "github.com/formancehq/payments/internal/api/backend" + "github.com/go-chi/chi/v5" +) + +func newRouter(backend backend.Backend, a auth.Authenticator, debug bool) *chi.Mux { + r := chi.NewRouter() + + r.Group(func(r chi.Router) { + r.Use(service.OTLPMiddleware("payments", debug)) + + // Public routes + r.Group(func(r chi.Router) { + r.Post("/connectors/webhooks/{connector}/connectorID", connectorsWebhooks(backend)) + }) + + // Authenticated routes + r.Group(func(r chi.Router) { + r.Use(auth.Middleware(a)) + + // Accounts + r.Route("/accounts", func(r chi.Router) { + r.Get("/", accountsList(backend)) + + r.Route("/{accountID}", func(r chi.Router) { + r.Get("/", accountsGet(backend)) + r.Get("/balances", accountsBalances(backend)) + // TODO(polo): add create account handler + }) + }) + + // Bank Accounts + r.Route("/bank-accounts", func(r chi.Router) { + r.Post("/", bankAccountsCreate(backend)) + r.Get("/", bankAccountsList(backend)) + + r.Route("/{bankAccountID}", func(r chi.Router) { + r.Get("/", bankAccountsGet(backend)) + r.Patch("/metadata", bankAccountsUpdateMetadata(backend)) + r.Post("/forward", bankAccountsForwardToConnector(backend)) + }) + }) + + // Payments + r.Route("/payments", func(r chi.Router) { + r.Get("/", paymentsList(backend)) + + r.Route("/{paymentID}", func(r chi.Router) { + r.Get("/", paymentsGet(backend)) + r.Patch("/metadata", paymentsUpdateMetadata(backend)) + // TODO(polo): add create payment handler + }) + }) + + // Pools + r.Route("/pools", func(r chi.Router) { + r.Post("/", poolsCreate(backend)) + r.Get("/", poolsList(backend)) + + r.Route("/{poolID}", func(r chi.Router) { + r.Get("/", poolsGet(backend)) + r.Delete("/", poolsDelete(backend)) + r.Get("/balances", poolsBalancesAt(backend)) + + r.Route("/accounts", func(r chi.Router) { + r.Post("/", poolsAddAccount(backend)) + r.Delete("/{accountID}", poolsRemoveAccount(backend)) + }) + }) + }) + + // Connectors + r.Route("/connectors", func(r chi.Router) { + r.Get("/", connectorsList(backend)) + r.Get("/configs", connectorsConfigs(backend)) + + r.Route("/{connector}", func(r chi.Router) { + r.Post("/", connectorsInstall(backend)) + connectorsRouter(backend, r) + }) + }) + }) + }) + + return r +} + +func connectorsRouter(backend backend.Backend, r chi.Router) { + r.Route("/{connectorID}", func(r chi.Router) { + r.Delete("/", connectorsUninstall(backend)) + r.Get("/config", connectorsConfig(backend)) + r.Post("/reset", connectorsReset(backend)) + r.Get("/tasks", tasksList(backend)) + r.Get("/tasks/{taskID}", tasksGet(backend)) + // TODO(polo): add update config handler + }) +} + +func connectorID(r *http.Request) string { + return chi.URLParam(r, "connectorID") +} + +func connectorProvider(r *http.Request) string { + return chi.URLParam(r, "connector") +} + +func accountID(r *http.Request) string { + return chi.URLParam(r, "accountID") +} + +func paymentID(r *http.Request) string { + return chi.URLParam(r, "paymentID") +} + +func poolID(r *http.Request) string { + return chi.URLParam(r, "poolID") +} + +func bankAccountID(r *http.Request) string { + return chi.URLParam(r, "bankAccountID") +} + +func taskID(r *http.Request) string { + return chi.URLParam(r, "taskID") +} diff --git a/components/payments/internal/api/v2/utils.go b/components/payments/internal/api/v2/utils.go new file mode 100644 index 0000000000..608cfa9a01 --- /dev/null +++ b/components/payments/internal/api/v2/utils.go @@ -0,0 +1,38 @@ +package v2 + +import ( + "io" + "net/http" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/go-libs/query" +) + +func getQueryBuilder(r *http.Request) (query.Builder, error) { + data, err := io.ReadAll(r.Body) + if err != nil { + return nil, err + } + + if len(data) > 0 { + return query.ParseJSON(string(data)) + } else { + // In order to be backward compatible + return query.ParseJSON(r.URL.Query().Get("query")) + } +} + +func getPagination[T any](r *http.Request, options T) (*bunpaginate.PaginatedQueryOptions[T], error) { + qb, err := getQueryBuilder(r) + if err != nil { + return nil, err + } + + pageSize, err := bunpaginate.GetPageSize(r) + if err != nil { + return nil, err + } + + return pointer.For(bunpaginate.NewPaginatedQueryOptions(options).WithQueryBuilder(qb).WithPageSize(pageSize)), nil +} diff --git a/components/payments/internal/api/v3/errors.go b/components/payments/internal/api/v3/errors.go new file mode 100644 index 0000000000..d2db51cb21 --- /dev/null +++ b/components/payments/internal/api/v3/errors.go @@ -0,0 +1,35 @@ +package v3 + +import ( + "errors" + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/services" + "github.com/formancehq/payments/internal/storage" +) + +const ( + ErrUniqueReference = "CONFLICT" + ErrNotFound = "NOT_FOUND" + ErrInvalidID = "INVALID_ID" + ErrMissingOrInvalidBody = "MISSING_OR_INVALID_BODY" + ErrValidation = "VALIDATION" +) + +func handleServiceErrors(w http.ResponseWriter, r *http.Request, err error) { + switch { + case errors.Is(err, storage.ErrDuplicateKeyValue): + api.BadRequest(w, ErrUniqueReference, err) + case errors.Is(err, storage.ErrNotFound): + api.NotFound(w, err) + case errors.Is(err, storage.ErrValidation): + api.BadRequest(w, ErrValidation, err) + case errors.Is(err, services.ErrValidation): + api.BadRequest(w, ErrValidation, err) + case errors.Is(err, services.ErrNotFound): + api.NotFound(w, err) + default: + api.InternalServerError(w, r, err) + } +} diff --git a/components/payments/internal/api/v3/handler_accounts_balances.go b/components/payments/internal/api/v3/handler_accounts_balances.go new file mode 100644 index 0000000000..b46efafa1a --- /dev/null +++ b/components/payments/internal/api/v3/handler_accounts_balances.go @@ -0,0 +1,97 @@ +package v3 + +import ( + "net/http" + "time" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" + "github.com/formancehq/payments/internal/storage" +) + +func accountsBalances(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_accountsBalances") + defer span.End() + + balanceQuery, err := populateBalanceQueryFromRequest(r) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + query, err := bunpaginate.Extract[storage.ListBalancesQuery](r, func() (*storage.ListBalancesQuery, error) { + options, err := getPagination(r, balanceQuery) + if err != nil { + return nil, err + } + return pointer.For(storage.NewListBalancesQuery(*options)), nil + }) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + cursor, err := backend.BalancesList(ctx, *query) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.RenderCursor(w, *cursor) + } +} + +func populateBalanceQueryFromRequest(r *http.Request) (storage.BalanceQuery, error) { + var balanceQuery storage.BalanceQuery + + balanceQuery = balanceQuery.WithAsset(r.URL.Query().Get("asset")) + + accountID, err := models.AccountIDFromString(accountID(r)) + if err != nil { + return balanceQuery, err + } + balanceQuery = balanceQuery.WithAccountID(&accountID) + + var startTimeParsed, endTimeParsed time.Time + + from, to := r.URL.Query().Get("from"), r.URL.Query().Get("to") + if from != "" { + startTimeParsed, err = time.Parse(time.RFC3339Nano, from) + if err != nil { + return balanceQuery, err + } + } + if to != "" { + endTimeParsed, err = time.Parse(time.RFC3339Nano, to) + if err != nil { + return balanceQuery, err + } + } + + switch { + case startTimeParsed.IsZero() && endTimeParsed.IsZero(): + balanceQuery = balanceQuery. + WithTo(time.Now()) + case !startTimeParsed.IsZero() && endTimeParsed.IsZero(): + balanceQuery = balanceQuery. + WithFrom(startTimeParsed). + WithTo(time.Now()) + case startTimeParsed.IsZero() && !endTimeParsed.IsZero(): + balanceQuery = balanceQuery. + WithTo(endTimeParsed) + default: + balanceQuery = balanceQuery. + WithFrom(startTimeParsed). + WithTo(endTimeParsed) + } + + return balanceQuery, nil +} diff --git a/components/payments/internal/api/v3/handler_accounts_get.go b/components/payments/internal/api/v3/handler_accounts_get.go new file mode 100644 index 0000000000..0c77503ab5 --- /dev/null +++ b/components/payments/internal/api/v3/handler_accounts_get.go @@ -0,0 +1,33 @@ +package v3 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" +) + +func accountsGet(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_accountsGet") + defer span.End() + + id, err := models.AccountIDFromString(accountID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + account, err := backend.AccountsGet(ctx, id) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.Ok(w, account) + } +} diff --git a/components/payments/internal/api/v3/handler_accounts_list.go b/components/payments/internal/api/v3/handler_accounts_list.go new file mode 100644 index 0000000000..15d64852ca --- /dev/null +++ b/components/payments/internal/api/v3/handler_accounts_list.go @@ -0,0 +1,41 @@ +package v3 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/otel" + "github.com/formancehq/payments/internal/storage" +) + +func accountsList(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_accountsList") + defer span.End() + + query, err := bunpaginate.Extract[storage.ListAccountsQuery](r, func() (*storage.ListAccountsQuery, error) { + options, err := getPagination(r, storage.AccountQuery{}) + if err != nil { + return nil, err + } + return pointer.For(storage.NewListAccountsQuery(*options)), nil + }) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + accounts, err := backend.AccountsList(ctx, *query) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.RenderCursor(w, *accounts) + } +} diff --git a/components/payments/internal/api/v3/handler_bank_accounts_create.go b/components/payments/internal/api/v3/handler_bank_accounts_create.go new file mode 100644 index 0000000000..1f80ab067d --- /dev/null +++ b/components/payments/internal/api/v3/handler_bank_accounts_create.go @@ -0,0 +1,78 @@ +package v3 + +import ( + "encoding/json" + "errors" + "net/http" + "time" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" + "github.com/google/uuid" +) + +type bankAccountsCreateRequest struct { + Name string `json:"name"` + + AccountNumber *string `json:"accountNumber"` + IBAN *string `json:"iban"` + SwiftBicCode *string `json:"swiftBicCode"` + Country *string `json:"country"` + + Metadata map[string]string `json:"metadata"` +} + +func (r *bankAccountsCreateRequest) Validate() error { + if r.AccountNumber == nil && r.IBAN == nil { + return errors.New("either accountNumber or iban must be provided") + } + + if r.Name == "" { + return errors.New("name must be provided") + } + + return nil +} + +func bankAccountsCreate(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_bankAccountsCreate") + defer span.End() + + var req bankAccountsCreateRequest + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrMissingOrInvalidBody, err) + return + } + + if err := req.Validate(); err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + bankAccount := &models.BankAccount{ + ID: uuid.New(), + CreatedAt: time.Now().UTC(), + Name: req.Name, + AccountNumber: req.AccountNumber, + IBAN: req.IBAN, + SwiftBicCode: req.SwiftBicCode, + Country: req.Country, + Metadata: req.Metadata, + } + + err = backend.BankAccountsCreate(ctx, *bankAccount) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.Created(w, bankAccount.ID.String()) + } +} diff --git a/components/payments/internal/api/v3/handler_bank_accounts_forward_to_connector.go b/components/payments/internal/api/v3/handler_bank_accounts_forward_to_connector.go new file mode 100644 index 0000000000..c08f890cac --- /dev/null +++ b/components/payments/internal/api/v3/handler_bank_accounts_forward_to_connector.go @@ -0,0 +1,70 @@ +package v3 + +import ( + "encoding/json" + "errors" + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" + "github.com/google/uuid" +) + +type bankAccountsForwardToConnectorRequest struct { + ConnectorID string `json:"connectorID"` +} + +func (f *bankAccountsForwardToConnectorRequest) Validate() error { + if f.ConnectorID == "" { + return errors.New("connectorID must be provided") + } + + return nil +} + +func bankAccountsForwardToConnector(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_bankAccountsForwardToConnector") + defer span.End() + + id, err := uuid.Parse(bankAccountID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + var req bankAccountsForwardToConnectorRequest + err = json.NewDecoder(r.Body).Decode(&req) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrMissingOrInvalidBody, err) + return + } + + err = req.Validate() + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrMissingOrInvalidBody, err) + return + } + + connectorID, err := models.ConnectorIDFromString(req.ConnectorID) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + bankAccount, err := backend.BankAccountsForwardToConnector(ctx, id, connectorID) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.Ok(w, bankAccount) + } +} diff --git a/components/payments/internal/api/v3/handler_bank_accounts_get.go b/components/payments/internal/api/v3/handler_bank_accounts_get.go new file mode 100644 index 0000000000..b16bf8adc6 --- /dev/null +++ b/components/payments/internal/api/v3/handler_bank_accounts_get.go @@ -0,0 +1,33 @@ +package v3 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/otel" + "github.com/google/uuid" +) + +func bankAccountsGet(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_bankAccountsGet") + defer span.End() + + id, err := uuid.Parse(bankAccountID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + bankAccount, err := backend.BankAccountsGet(ctx, id) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.Ok(w, bankAccount) + } +} diff --git a/components/payments/internal/api/v3/handler_bank_accounts_list.go b/components/payments/internal/api/v3/handler_bank_accounts_list.go new file mode 100644 index 0000000000..3cd39ee40b --- /dev/null +++ b/components/payments/internal/api/v3/handler_bank_accounts_list.go @@ -0,0 +1,41 @@ +package v3 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/otel" + "github.com/formancehq/payments/internal/storage" +) + +func bankAccountsList(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_bankAccountsList") + defer span.End() + + query, err := bunpaginate.Extract[storage.ListBankAccountsQuery](r, func() (*storage.ListBankAccountsQuery, error) { + options, err := getPagination(r, storage.BankAccountQuery{}) + if err != nil { + return nil, err + } + return pointer.For(storage.NewListBankAccountsQuery(*options)), nil + }) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + cursor, err := backend.BankAccountsList(ctx, *query) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.RenderCursor(w, *cursor) + } +} diff --git a/components/payments/internal/api/v3/handler_bank_accounts_update_metadata.go b/components/payments/internal/api/v3/handler_bank_accounts_update_metadata.go new file mode 100644 index 0000000000..1111482bdd --- /dev/null +++ b/components/payments/internal/api/v3/handler_bank_accounts_update_metadata.go @@ -0,0 +1,62 @@ +package v3 + +import ( + "encoding/json" + "errors" + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/otel" + "github.com/google/uuid" +) + +type bankAccountsUpdateMetadataRequest struct { + Metadata map[string]string `json:"metadata"` +} + +func (u *bankAccountsUpdateMetadataRequest) Validate() error { + if len(u.Metadata) == 0 { + return errors.New("metadata must be provided") + } + + return nil +} + +func bankAccountsUpdateMetadata(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_bankAccountsUpdateMetadata") + defer span.End() + + id, err := uuid.Parse(bankAccountID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + var req bankAccountsUpdateMetadataRequest + err = json.NewDecoder(r.Body).Decode(&req) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrMissingOrInvalidBody, err) + return + } + + err = req.Validate() + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + err = backend.BankAccountsUpdateMetadata(ctx, id, req.Metadata) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.NoContent(w) + } +} diff --git a/components/payments/internal/api/v3/handler_connectors_config.go b/components/payments/internal/api/v3/handler_connectors_config.go new file mode 100644 index 0000000000..58b2f7a1a4 --- /dev/null +++ b/components/payments/internal/api/v3/handler_connectors_config.go @@ -0,0 +1,33 @@ +package v3 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" +) + +func connectorsConfig(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_connectorsConfig") + defer span.End() + + connectorID, err := models.ConnectorIDFromString(connectorID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + config, err := backend.ConnectorsConfig(ctx, connectorID) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.Ok(w, config) + } +} diff --git a/components/payments/internal/api/v3/handler_connectors_configs.go b/components/payments/internal/api/v3/handler_connectors_configs.go new file mode 100644 index 0000000000..c7ec4ec5dc --- /dev/null +++ b/components/payments/internal/api/v3/handler_connectors_configs.go @@ -0,0 +1,29 @@ +package v3 + +import ( + "encoding/json" + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/connectors/plugins" + "github.com/formancehq/payments/internal/otel" +) + +func connectorsConfigs(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + _, span := otel.Tracer().Start(r.Context(), "v3_connectorsConfigs") + defer span.End() + + configs := backend.ConnectorsConfigs() + + err := json.NewEncoder(w).Encode(api.BaseResponse[plugins.Configs]{ + Data: &configs, + }) + if err != nil { + otel.RecordError(span, err) + api.InternalServerError(w, r, err) + return + } + } +} diff --git a/components/payments/internal/api/v3/handler_connectors_install.go b/components/payments/internal/api/v3/handler_connectors_install.go new file mode 100644 index 0000000000..19c6b9fb98 --- /dev/null +++ b/components/payments/internal/api/v3/handler_connectors_install.go @@ -0,0 +1,35 @@ +package v3 + +import ( + "io" + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/otel" +) + +func connectorsInstall(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_connectorsInstall") + defer span.End() + + config, err := io.ReadAll(r.Body) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrMissingOrInvalidBody, err) + return + } + + provider := connector(r) + + connectorID, err := backend.ConnectorsInstall(ctx, provider, config) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.Created(w, connectorID.String()) + } +} diff --git a/components/payments/internal/api/v3/handler_connectors_list.go b/components/payments/internal/api/v3/handler_connectors_list.go new file mode 100644 index 0000000000..19690635d3 --- /dev/null +++ b/components/payments/internal/api/v3/handler_connectors_list.go @@ -0,0 +1,41 @@ +package v3 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/otel" + "github.com/formancehq/payments/internal/storage" +) + +func connectorsList(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_connectorsList") + defer span.End() + + query, err := bunpaginate.Extract[storage.ListConnectorsQuery](r, func() (*storage.ListConnectorsQuery, error) { + options, err := getPagination(r, storage.ConnectorQuery{}) + if err != nil { + return nil, err + } + return pointer.For(storage.NewListConnectorsQuery(*options)), nil + }) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + connectors, err := backend.ConnectorsList(ctx, *query) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.RenderCursor(w, *connectors) + } +} diff --git a/components/payments/internal/api/v3/handler_connectors_reset.go b/components/payments/internal/api/v3/handler_connectors_reset.go new file mode 100644 index 0000000000..ef895f3ce0 --- /dev/null +++ b/components/payments/internal/api/v3/handler_connectors_reset.go @@ -0,0 +1,32 @@ +package v3 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" +) + +func connectorsReset(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_connectorsReset") + defer span.End() + + connectorID, err := models.ConnectorIDFromString(connectorID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + if err := backend.ConnectorsReset(ctx, connectorID); err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.NoContent(w) + } +} diff --git a/components/payments/internal/api/v3/handler_connectors_uninstall.go b/components/payments/internal/api/v3/handler_connectors_uninstall.go new file mode 100644 index 0000000000..5d567af6b4 --- /dev/null +++ b/components/payments/internal/api/v3/handler_connectors_uninstall.go @@ -0,0 +1,32 @@ +package v3 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" +) + +func connectorsUninstall(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_connectorsUninstall") + defer span.End() + + connectorID, err := models.ConnectorIDFromString(connectorID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + if err := backend.ConnectorsUninstall(ctx, connectorID); err != nil { + otel.RecordError(span, err) + api.InternalServerError(w, r, err) + return + } + + api.NoContent(w) + } +} diff --git a/components/payments/internal/api/v3/handler_connectors_webhooks.go b/components/payments/internal/api/v3/handler_connectors_webhooks.go new file mode 100644 index 0000000000..3143836a1d --- /dev/null +++ b/components/payments/internal/api/v3/handler_connectors_webhooks.go @@ -0,0 +1,54 @@ +package v3 + +import ( + "io" + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" + "github.com/google/uuid" +) + +func connectorsWebhooks(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_connectorsWebhooks") + defer span.End() + + connectorID, err := models.ConnectorIDFromString(connectorID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + body, err := io.ReadAll(r.Body) + if err != nil && err != io.EOF { + otel.RecordError(span, err) + api.BadRequest(w, ErrMissingOrInvalidBody, err) + return + } + + headers := r.Header + queryValues := r.URL.Query() + path := r.URL.Path + + webhook := models.Webhook{ + ID: uuid.New().String(), + ConnectorID: connectorID, + QueryValues: queryValues, + Headers: headers, + Body: body, + } + + err = backend.ConnectorsHandleWebhooks(ctx, path, webhook) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.RawOk(w, nil) + } +} diff --git a/components/payments/internal/api/v3/handler_payments_get.go b/components/payments/internal/api/v3/handler_payments_get.go new file mode 100644 index 0000000000..c659a57b4f --- /dev/null +++ b/components/payments/internal/api/v3/handler_payments_get.go @@ -0,0 +1,33 @@ +package v3 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" +) + +func paymentsGet(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_paymentsGet") + defer span.End() + + id, err := models.PaymentIDFromString(paymentID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + payment, err := backend.PaymentsGet(ctx, id) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.Ok(w, payment) + } +} diff --git a/components/payments/internal/api/v3/handler_payments_list.go b/components/payments/internal/api/v3/handler_payments_list.go new file mode 100644 index 0000000000..c8c82bedb2 --- /dev/null +++ b/components/payments/internal/api/v3/handler_payments_list.go @@ -0,0 +1,41 @@ +package v3 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/otel" + "github.com/formancehq/payments/internal/storage" +) + +func paymentsList(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_paymentsList") + defer span.End() + + query, err := bunpaginate.Extract[storage.ListPaymentsQuery](r, func() (*storage.ListPaymentsQuery, error) { + options, err := getPagination(r, storage.PaymentQuery{}) + if err != nil { + return nil, err + } + return pointer.For(storage.NewListPaymentsQuery(*options)), nil + }) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + cursor, err := backend.PaymentsList(ctx, *query) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.RenderCursor(w, *cursor) + } +} diff --git a/components/payments/internal/api/v3/handler_payments_update_metadata.go b/components/payments/internal/api/v3/handler_payments_update_metadata.go new file mode 100644 index 0000000000..c7ac44d461 --- /dev/null +++ b/components/payments/internal/api/v3/handler_payments_update_metadata.go @@ -0,0 +1,42 @@ +package v3 + +import ( + "encoding/json" + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" +) + +func paymentsUpdateMetadata(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_paymentsUpdateMetadata") + defer span.End() + + id, err := models.PaymentIDFromString(paymentID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + var metadata map[string]string + err = json.NewDecoder(r.Body).Decode(&metadata) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + err = backend.PaymentsUpdateMetadata(ctx, id, metadata) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.NoContent(w) + } +} diff --git a/components/payments/internal/api/v3/handler_pools_add_account.go b/components/payments/internal/api/v3/handler_pools_add_account.go new file mode 100644 index 0000000000..8eea2f753e --- /dev/null +++ b/components/payments/internal/api/v3/handler_pools_add_account.go @@ -0,0 +1,41 @@ +package v3 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" + "github.com/google/uuid" +) + +func poolsAddAccount(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_poolsAddAccount") + defer span.End() + + id, err := uuid.Parse(poolID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + accountID, err := models.AccountIDFromString(accountID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + err = backend.PoolsAddAccount(ctx, id, accountID) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.NoContent(w) + } +} diff --git a/components/payments/internal/api/v3/handler_pools_balances_at.go b/components/payments/internal/api/v3/handler_pools_balances_at.go new file mode 100644 index 0000000000..d96f6c62bb --- /dev/null +++ b/components/payments/internal/api/v3/handler_pools_balances_at.go @@ -0,0 +1,49 @@ +package v3 + +import ( + "net/http" + "time" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/otel" + "github.com/google/uuid" + "github.com/pkg/errors" +) + +func poolsBalancesAt(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_poolsBalancesAt") + defer span.End() + + id, err := uuid.Parse(poolID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + atTime := r.URL.Query().Get("at") + if atTime == "" { + otel.RecordError(span, errors.New("missing atTime")) + api.BadRequest(w, ErrValidation, errors.New("missing atTime")) + return + } + + at, err := time.Parse(time.RFC3339, atTime) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, errors.Wrap(err, "invalid atTime")) + return + } + + balances, err := backend.PoolsBalancesAt(ctx, id, at) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.Ok(w, balances) + } +} diff --git a/components/payments/internal/api/v3/handler_pools_create.go b/components/payments/internal/api/v3/handler_pools_create.go new file mode 100644 index 0000000000..e8573657cc --- /dev/null +++ b/components/payments/internal/api/v3/handler_pools_create.go @@ -0,0 +1,64 @@ +package v3 + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" + "github.com/google/uuid" +) + +type createPoolRequest struct { + Name string `json:"name"` + AccountIDs []string `json:"accountIDs"` +} + +func poolsCreate(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_poolsCreate") + defer span.End() + + var createPoolRequest createPoolRequest + err := json.NewDecoder(r.Body).Decode(&createPoolRequest) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrMissingOrInvalidBody, err) + return + } + + pool := models.Pool{ + ID: uuid.New(), + Name: createPoolRequest.Name, + CreatedAt: time.Now().UTC(), + } + + accounts := make([]models.PoolAccounts, len(createPoolRequest.AccountIDs)) + for i, accountID := range createPoolRequest.AccountIDs { + aID, err := models.AccountIDFromString(accountID) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + accounts[i] = models.PoolAccounts{ + PoolID: pool.ID, + AccountID: aID, + } + } + pool.PoolAccounts = accounts + + err = backend.PoolsCreate(ctx, pool) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.Created(w, pool.ID.String()) + } +} diff --git a/components/payments/internal/api/v3/handler_pools_delete.go b/components/payments/internal/api/v3/handler_pools_delete.go new file mode 100644 index 0000000000..507765d4d1 --- /dev/null +++ b/components/payments/internal/api/v3/handler_pools_delete.go @@ -0,0 +1,33 @@ +package v3 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/otel" + "github.com/google/uuid" +) + +func poolsDelete(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_poolsDelete") + defer span.End() + + id, err := uuid.Parse(poolID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + err = backend.PoolsDelete(ctx, id) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.NoContent(w) + } +} diff --git a/components/payments/internal/api/v3/handler_pools_get.go b/components/payments/internal/api/v3/handler_pools_get.go new file mode 100644 index 0000000000..5eb41e0a81 --- /dev/null +++ b/components/payments/internal/api/v3/handler_pools_get.go @@ -0,0 +1,33 @@ +package v3 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/otel" + "github.com/google/uuid" +) + +func poolsGet(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_poolsGet") + defer span.End() + + id, err := uuid.Parse(poolID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + pool, err := backend.PoolsGet(ctx, id) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.Ok(w, pool) + } +} diff --git a/components/payments/internal/api/v3/handler_pools_list.go b/components/payments/internal/api/v3/handler_pools_list.go new file mode 100644 index 0000000000..f0afa68280 --- /dev/null +++ b/components/payments/internal/api/v3/handler_pools_list.go @@ -0,0 +1,41 @@ +package v3 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/otel" + "github.com/formancehq/payments/internal/storage" +) + +func poolsList(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_poolsList") + defer span.End() + + query, err := bunpaginate.Extract[storage.ListPoolsQuery](r, func() (*storage.ListPoolsQuery, error) { + options, err := getPagination(r, storage.PoolQuery{}) + if err != nil { + return nil, err + } + return pointer.For(storage.NewListPoolsQuery(*options)), nil + }) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + cursor, err := backend.PoolsList(ctx, *query) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.RenderCursor(w, *cursor) + } +} diff --git a/components/payments/internal/api/v3/handler_pools_remove_account.go b/components/payments/internal/api/v3/handler_pools_remove_account.go new file mode 100644 index 0000000000..2692527366 --- /dev/null +++ b/components/payments/internal/api/v3/handler_pools_remove_account.go @@ -0,0 +1,41 @@ +package v3 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" + "github.com/google/uuid" +) + +func poolsRemoveAccount(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_poolsRemoveAccount") + defer span.End() + + id, err := uuid.Parse(poolID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + accountID, err := models.AccountIDFromString(accountID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + err = backend.PoolsRemoveAccount(ctx, id, accountID) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.NoContent(w) + } +} diff --git a/components/payments/internal/api/v3/handler_schedules_list.go b/components/payments/internal/api/v3/handler_schedules_list.go new file mode 100644 index 0000000000..21b4430d57 --- /dev/null +++ b/components/payments/internal/api/v3/handler_schedules_list.go @@ -0,0 +1,41 @@ +package v3 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/otel" + "github.com/formancehq/payments/internal/storage" +) + +func schedulesList(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_schedulesList") + defer span.End() + + query, err := bunpaginate.Extract[storage.ListSchedulesQuery](r, func() (*storage.ListSchedulesQuery, error) { + options, err := getPagination(r, storage.ScheduleQuery{}) + if err != nil { + return nil, err + } + return pointer.For(storage.NewListSchedulesQuery(*options)), nil + }) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + cursor, err := backend.SchedulesList(ctx, *query) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.RenderCursor(w, *cursor) + } +} diff --git a/components/payments/internal/api/v3/handler_workflows_instances_list.go b/components/payments/internal/api/v3/handler_workflows_instances_list.go new file mode 100644 index 0000000000..7990e32da8 --- /dev/null +++ b/components/payments/internal/api/v3/handler_workflows_instances_list.go @@ -0,0 +1,61 @@ +package v3 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/go-libs/query" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" + "github.com/formancehq/payments/internal/storage" +) + +func workflowsInstancesList(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_workflowsInstancesList") + defer span.End() + + connectorID, err := models.ConnectorIDFromString(connectorID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + scheduleID := scheduleID(r) + + query, err := bunpaginate.Extract[storage.ListInstancesQuery](r, func() (*storage.ListInstancesQuery, error) { + pageSize, err := bunpaginate.GetPageSize(r) + if err != nil { + return nil, err + } + + options := pointer.For(bunpaginate.NewPaginatedQueryOptions(storage.InstanceQuery{}).WithPageSize(pageSize)) + options = pointer.For(options.WithQueryBuilder( + query.And( + query.Match("connector_id", connectorID), + query.Match("schedule_id", scheduleID), + ), + )) + + return pointer.For(storage.NewListInstancesQuery(*options)), nil + }) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + cursor, err := backend.WorkflowsInstancesList(ctx, *query) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.RenderCursor(w, *cursor) + } +} diff --git a/components/payments/internal/api/v3/module.go b/components/payments/internal/api/v3/module.go new file mode 100644 index 0000000000..83a095e380 --- /dev/null +++ b/components/payments/internal/api/v3/module.go @@ -0,0 +1,15 @@ +package v3 + +import ( + "github.com/formancehq/payments/internal/api" + "go.uber.org/fx" +) + +func NewModule() fx.Option { + return fx.Options( + fx.Supply(fx.Annotate(api.Version{ + Version: 3, + Builder: newRouter, + }, api.TagVersion())), + ) +} diff --git a/components/payments/internal/api/v3/router.go b/components/payments/internal/api/v3/router.go new file mode 100644 index 0000000000..3214a4280e --- /dev/null +++ b/components/payments/internal/api/v3/router.go @@ -0,0 +1,129 @@ +package v3 + +import ( + "net/http" + + "github.com/formancehq/go-libs/auth" + "github.com/formancehq/go-libs/service" + "github.com/formancehq/payments/internal/api/backend" + "github.com/go-chi/chi/v5" +) + +func newRouter(backend backend.Backend, a auth.Authenticator, debug bool) *chi.Mux { + r := chi.NewRouter() + + r.Group(func(r chi.Router) { + r.Use(service.OTLPMiddleware("payments", debug)) + + // Public routes + r.Group(func(r chi.Router) { + r.Handle("/connectors/webhooks/{connectorID}/*", connectorsWebhooks(backend)) + }) + + // Authenticated routes + r.Group(func(r chi.Router) { + r.Use(auth.Middleware(a)) + + // Accounts + r.Route("/accounts", func(r chi.Router) { + r.Get("/", accountsList(backend)) + + r.Route("/{accountID}", func(r chi.Router) { + r.Get("/", accountsGet(backend)) + r.Get("/balances", accountsBalances(backend)) + // TODO(polo): add create account handler + }) + }) + + // Bank Accounts + r.Route("/bank-accounts", func(r chi.Router) { + r.Post("/", bankAccountsCreate(backend)) + r.Get("/", bankAccountsList(backend)) + + r.Route("/{bankAccountID}", func(r chi.Router) { + r.Get("/", bankAccountsGet(backend)) + r.Patch("/metadata", bankAccountsUpdateMetadata(backend)) + r.Post("/forward", bankAccountsForwardToConnector(backend)) + }) + }) + + // Payments + r.Route("/payments", func(r chi.Router) { + r.Get("/", paymentsList(backend)) + + r.Route("/{paymentID}", func(r chi.Router) { + r.Get("/", paymentsGet(backend)) + r.Patch("/metadata", paymentsUpdateMetadata(backend)) + // TODO(polo): add create payment handler + }) + }) + + // Pools + r.Route("/pools", func(r chi.Router) { + r.Post("/", poolsCreate(backend)) + r.Get("/", poolsList(backend)) + + r.Route("/{poolID}", func(r chi.Router) { + r.Get("/", poolsGet(backend)) + r.Delete("/", poolsDelete(backend)) + r.Get("/balances", poolsBalancesAt(backend)) + + r.Route("/accounts/{accountID}", func(r chi.Router) { + r.Post("/", poolsAddAccount(backend)) + r.Delete("/", poolsRemoveAccount(backend)) + }) + }) + }) + + // Connectors + r.Route("/connectors", func(r chi.Router) { + r.Get("/", connectorsList(backend)) + r.Post("/install/{connector}", connectorsInstall(backend)) + + r.Get("/configs", connectorsConfigs(backend)) + + r.Route("/{connectorID}", func(r chi.Router) { + r.Delete("/", connectorsUninstall(backend)) + r.Get("/config", connectorsConfig(backend)) + r.Post("/reset", connectorsReset(backend)) + + r.Get("/schedules", schedulesList(backend)) + r.Route("/schedules/{scheduleID}", func(r chi.Router) { + r.Get("/instances", workflowsInstancesList(backend)) + }) + // TODO(polo): add update config handler + }) + }) + }) + }) + + return r +} + +func connector(r *http.Request) string { + return chi.URLParam(r, "connector") +} + +func connectorID(r *http.Request) string { + return chi.URLParam(r, "connectorID") +} + +func accountID(r *http.Request) string { + return chi.URLParam(r, "accountID") +} + +func paymentID(r *http.Request) string { + return chi.URLParam(r, "paymentID") +} + +func poolID(r *http.Request) string { + return chi.URLParam(r, "poolID") +} + +func bankAccountID(r *http.Request) string { + return chi.URLParam(r, "bankAccountID") +} + +func scheduleID(r *http.Request) string { + return chi.URLParam(r, "scheduleID") +} diff --git a/components/payments/internal/api/v3/utils.go b/components/payments/internal/api/v3/utils.go new file mode 100644 index 0000000000..4db8476c13 --- /dev/null +++ b/components/payments/internal/api/v3/utils.go @@ -0,0 +1,38 @@ +package v3 + +import ( + "io" + "net/http" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/go-libs/query" +) + +func getQueryBuilder(r *http.Request) (query.Builder, error) { + data, err := io.ReadAll(r.Body) + if err != nil { + return nil, err + } + + if len(data) > 0 { + return query.ParseJSON(string(data)) + } else { + // In order to be backward compatible + return query.ParseJSON(r.URL.Query().Get("query")) + } +} + +func getPagination[T any](r *http.Request, options T) (*bunpaginate.PaginatedQueryOptions[T], error) { + qb, err := getQueryBuilder(r) + if err != nil { + return nil, err + } + + pageSize, err := bunpaginate.GetPageSize(r) + if err != nil { + return nil, err + } + + return pointer.For(bunpaginate.NewPaginatedQueryOptions(options).WithQueryBuilder(qb).WithPageSize(pageSize)), nil +} diff --git a/components/payments/internal/connectors/engine/activities/activity.go b/components/payments/internal/connectors/engine/activities/activity.go new file mode 100644 index 0000000000..7a1ead27d0 --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/activity.go @@ -0,0 +1,214 @@ +package activities + +import ( + "errors" + + temporalworker "github.com/formancehq/go-libs/temporal" + "github.com/formancehq/payments/internal/connectors/engine/plugins" + "github.com/formancehq/payments/internal/events" + "github.com/formancehq/payments/internal/storage" + "go.temporal.io/sdk/temporal" + "go.temporal.io/sdk/workflow" +) + +type Activities struct { + storage storage.Storage + events *events.Events + + plugins plugins.Plugins +} + +func (a Activities) DefinitionSet() temporalworker.DefinitionSet { + return temporalworker.NewDefinitionSet(). + Append(temporalworker.Definition{ + Name: "PluginInstallConnector", + Func: a.PluginInstallConnector, + }). + Append(temporalworker.Definition{ + Name: "PluginUninstallConnector", + Func: a.PluginUninstallConnector, + }). + Append(temporalworker.Definition{ + Name: "PluginFetchNextAccounts", + Func: a.PluginFetchNextAccounts, + }). + Append(temporalworker.Definition{ + Name: "PluginFetchNextBalances", + Func: a.PluginFetchNextBalances, + }). + Append(temporalworker.Definition{ + Name: "PluginFetchNextExternalAccounts", + Func: a.PluginFetchNextExternalAccounts, + }). + Append(temporalworker.Definition{ + Name: "PluginFetchNextPayments", + Func: a.PluginFetchNextPayments, + }). + Append(temporalworker.Definition{ + Name: "PluginFetchNextOthers", + Func: a.PluginFetchNextOthers, + }). + Append(temporalworker.Definition{ + Name: "PluginCreateBankAccount", + Func: a.PluginCreateBankAccount, + }). + Append(temporalworker.Definition{ + Name: "PluginCreateWebhooks", + Func: a.PluginCreateWebhooks, + }). + Append(temporalworker.Definition{ + Name: "PluginTranslateWebhook", + Func: a.PluginTranslateWebhook, + }). + Append(temporalworker.Definition{ + Name: "StorageAccountsStore", + Func: a.StorageAccountsStore, + }). + Append(temporalworker.Definition{ + Name: "StorageAccountsDelete", + Func: a.StorageAccountsDelete, + }). + Append(temporalworker.Definition{ + Name: "StoragePaymentsStore", + Func: a.StoragePaymentsStore, + }). + Append(temporalworker.Definition{ + Name: "StoragePaymentsDelete", + Func: a.StoragePaymentsDelete, + }). + Append(temporalworker.Definition{ + Name: "StorageStatesGet", + Func: a.StorageStatesGet, + }). + Append(temporalworker.Definition{ + Name: "StorageStatesStore", + Func: a.StorageStatesStore, + }). + Append(temporalworker.Definition{ + Name: "StorageStatesDelete", + Func: a.StorageStatesDelete, + }). + Append(temporalworker.Definition{ + Name: "StorageTasksTreeStore", + Func: a.StorageTasksTreeStore, + }). + Append(temporalworker.Definition{ + Name: "StorageTasksTreeDelete", + Func: a.StorageTasksTreeDelete, + }). + Append(temporalworker.Definition{ + Name: "StorageConnectorsStore", + Func: a.StorageConnectorsStore, + }). + Append(temporalworker.Definition{ + Name: "StorageConnectorsDelete", + Func: a.StorageConnectorsDelete, + }). + Append(temporalworker.Definition{ + Name: "StorageSchedulesStore", + Func: a.StorageSchedulesStore, + }). + Append(temporalworker.Definition{ + Name: "StorageSchedulesList", + Func: a.StorageSchedulesList, + }). + Append(temporalworker.Definition{ + Name: "StorageSchedulesDelete", + Func: a.StorageSchedulesDelete, + }). + Append(temporalworker.Definition{ + Name: "StorageInstancesStore", + Func: a.StorageInstancesStore, + }). + Append(temporalworker.Definition{ + Name: "StorageInstancesUpdate", + Func: a.StorageInstancesUpdate, + }). + Append(temporalworker.Definition{ + Name: "StorageInstancesDelete", + Func: a.StorageInstancesDelete, + }). + Append(temporalworker.Definition{ + Name: "StorageBankAccountsDeleteRelatedAccounts", + Func: a.StorageBankAccountsDeleteRelatedAccounts, + }). + Append(temporalworker.Definition{ + Name: "StorageBankAccountsAddRelatedAccount", + Func: a.StorageBankAccountsAddRelatedAccount, + }). + Append(temporalworker.Definition{ + Name: "StorageBankAccountsGet", + Func: a.StorageBankAccountsGet, + }). + Append(temporalworker.Definition{ + Name: "StorageBalancesDelete", + Func: a.StorageBalancesDelete, + }). + Append(temporalworker.Definition{ + Name: "StorageBalancesStore", + Func: a.StorageBalancesStore, + }). + Append(temporalworker.Definition{ + Name: "StorageWebhooksConfigsStore", + Func: a.StorageWebhooksConfigsStore, + }). + Append(temporalworker.Definition{ + Name: "StorageWebhooksConfigsDelete", + Func: a.StorageWebhooksConfigsDelete, + }). + Append(temporalworker.Definition{ + Name: "StorageWebhooksStore", + Func: a.StorageWebhooksStore, + }). + Append(temporalworker.Definition{ + Name: "StorageWebhooksDelete", + Func: a.StorageWebhooksDelete, + }). + Append(temporalworker.Definition{ + Name: "EventsSendAccount", + Func: a.EventsSendAccount, + }). + Append(temporalworker.Definition{ + Name: "EventsSendBalance", + Func: a.EventsSendBalance, + }). + Append(temporalworker.Definition{ + Name: "EventsSendBankAccount", + Func: a.EventsSendBankAccount, + }). + Append(temporalworker.Definition{ + Name: "EventsSendConnectorReset", + Func: a.EventsSendConnectorReset, + }). + Append(temporalworker.Definition{ + Name: "EventsSendPayment", + Func: a.EventsSendPayment, + }). + Append(temporalworker.Definition{ + Name: "EventsSendPoolCreation", + Func: a.EventsSendPoolCreation, + }). + Append(temporalworker.Definition{ + Name: "EventsSendPoolDeletion", + Func: a.EventsSendPoolDeletion, + }) +} + +func New(storage storage.Storage, events *events.Events, plugins plugins.Plugins) Activities { + return Activities{ + storage: storage, + plugins: plugins, + events: events, + } +} + +func executeActivity(ctx workflow.Context, activity any, ret any, args ...any) error { + if err := workflow.ExecuteActivity(ctx, activity, args...).Get(ctx, ret); err != nil { + var timeoutError *temporal.TimeoutError + if errors.As(err, &timeoutError) { + return errors.New(timeoutError.Message()) + } + return err + } + return nil +} diff --git a/components/payments/internal/connectors/engine/activities/events_send_account.go b/components/payments/internal/connectors/engine/activities/events_send_account.go new file mode 100644 index 0000000000..51aac0797e --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/events_send_account.go @@ -0,0 +1,18 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) EventsSendAccount(ctx context.Context, account models.Account) error { + return a.events.Publish(ctx, a.events.NewEventSavedAccounts(account)) +} + +var EventsSendAccountActivity = Activities{}.EventsSendAccount + +func EventsSendAccount(ctx workflow.Context, account models.Account) error { + return executeActivity(ctx, EventsSendAccountActivity, nil, account) +} diff --git a/components/payments/internal/connectors/engine/activities/events_send_balance.go b/components/payments/internal/connectors/engine/activities/events_send_balance.go new file mode 100644 index 0000000000..d41aad2a8f --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/events_send_balance.go @@ -0,0 +1,18 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) EventsSendBalance(ctx context.Context, balance models.Balance) error { + return a.events.Publish(ctx, a.events.NewEventSavedBalances(balance)) +} + +var EventsSendBalanceActivity = Activities{}.EventsSendBalance + +func EventsSendBalance(ctx workflow.Context, balance models.Balance) error { + return executeActivity(ctx, EventsSendBalanceActivity, nil, balance) +} diff --git a/components/payments/internal/connectors/engine/activities/events_send_bank_account.go b/components/payments/internal/connectors/engine/activities/events_send_bank_account.go new file mode 100644 index 0000000000..b311769155 --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/events_send_bank_account.go @@ -0,0 +1,18 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) EventsSendBankAccount(ctx context.Context, bankAccount models.BankAccount) error { + return a.events.Publish(ctx, a.events.NewEventSavedBankAccounts(bankAccount)) +} + +var EventsSendBankAccountActivity = Activities{}.EventsSendBankAccount + +func EventsSendBankAccount(ctx workflow.Context, bankAccount models.BankAccount) error { + return executeActivity(ctx, EventsSendBankAccountActivity, nil, bankAccount) +} diff --git a/components/payments/internal/connectors/engine/activities/events_send_connector_reset.go b/components/payments/internal/connectors/engine/activities/events_send_connector_reset.go new file mode 100644 index 0000000000..134744f576 --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/events_send_connector_reset.go @@ -0,0 +1,18 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) EventsSendConnectorReset(ctx context.Context, connectorID models.ConnectorID) error { + return a.events.Publish(ctx, a.events.NewEventResetConnector(connectorID)) +} + +var EventsSendConnectorResetActivity = Activities{}.EventsSendConnectorReset + +func EventsSendConnectorReset(ctx workflow.Context, connectorID models.ConnectorID) error { + return executeActivity(ctx, EventsSendConnectorResetActivity, nil, connectorID) +} diff --git a/components/payments/internal/connectors/engine/activities/events_send_payment.go b/components/payments/internal/connectors/engine/activities/events_send_payment.go new file mode 100644 index 0000000000..80ab61d137 --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/events_send_payment.go @@ -0,0 +1,26 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +type EventsSendPaymentRequest struct { + Payment models.Payment + Adjustment models.PaymentAdjustment +} + +func (a Activities) EventsSendPayment(ctx context.Context, req EventsSendPaymentRequest) error { + return a.events.Publish(ctx, a.events.NewEventSavedPayments(req.Payment, req.Adjustment)) +} + +var EventsSendPaymentActivity = Activities{}.EventsSendPayment + +func EventsSendPayment(ctx workflow.Context, payment models.Payment, adjustment models.PaymentAdjustment) error { + return executeActivity(ctx, EventsSendPaymentActivity, nil, EventsSendPaymentRequest{ + Payment: payment, + Adjustment: adjustment, + }) +} diff --git a/components/payments/internal/connectors/engine/activities/events_send_pool_creation.go b/components/payments/internal/connectors/engine/activities/events_send_pool_creation.go new file mode 100644 index 0000000000..990244f766 --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/events_send_pool_creation.go @@ -0,0 +1,18 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) EventsSendPoolCreation(ctx context.Context, pool models.Pool) error { + return a.events.Publish(ctx, a.events.NewEventSavedPool(pool)) +} + +var EventsSendPoolCreationActivity = Activities{}.EventsSendPoolCreation + +func EventsSendPoolCreation(ctx workflow.Context, pool models.Pool) error { + return executeActivity(ctx, EventsSendPoolCreationActivity, nil, pool) +} diff --git a/components/payments/internal/connectors/engine/activities/events_send_pool_deletion.go b/components/payments/internal/connectors/engine/activities/events_send_pool_deletion.go new file mode 100644 index 0000000000..62f784a321 --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/events_send_pool_deletion.go @@ -0,0 +1,18 @@ +package activities + +import ( + "context" + + "github.com/google/uuid" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) EventsSendPoolDeletion(ctx context.Context, id uuid.UUID) error { + return a.events.Publish(ctx, a.events.NewEventDeletePool(id)) +} + +var EventsSendPoolDeletionActivity = Activities{}.EventsSendPoolDeletion + +func EventsSendPoolDeletion(ctx workflow.Context, id uuid.UUID) error { + return executeActivity(ctx, EventsSendPoolDeletionActivity, nil, id) +} diff --git a/components/payments/internal/connectors/engine/activities/plugin_create_bank_account.go b/components/payments/internal/connectors/engine/activities/plugin_create_bank_account.go new file mode 100644 index 0000000000..0fdadd4bc1 --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/plugin_create_bank_account.go @@ -0,0 +1,40 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +type CreateBankAccountRequest struct { + ConnectorID models.ConnectorID + Req models.CreateBankAccountRequest +} + +func (a Activities) PluginCreateBankAccount(ctx context.Context, request CreateBankAccountRequest) (*models.CreateBankAccountResponse, error) { + plugin, err := a.plugins.Get(request.ConnectorID) + if err != nil { + return nil, err + } + + resp, err := plugin.CreateBankAccount(ctx, request.Req) + if err != nil { + // TODO(polo): temporal errors + return nil, err + } + return &resp, nil +} + +var PluginCreateBankAccountActivity = Activities{}.PluginCreateBankAccount + +func PluginCreateBankAccount(ctx workflow.Context, connectorID models.ConnectorID, request models.CreateBankAccountRequest) (*models.CreateBankAccountResponse, error) { + ret := models.CreateBankAccountResponse{} + if err := executeActivity(ctx, PluginCreateBankAccountActivity, &ret, CreateBankAccountRequest{ + ConnectorID: connectorID, + Req: request, + }); err != nil { + return nil, err + } + return &ret, nil +} diff --git a/components/payments/internal/connectors/engine/activities/plugin_create_webhooks.go b/components/payments/internal/connectors/engine/activities/plugin_create_webhooks.go new file mode 100644 index 0000000000..6786e47958 --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/plugin_create_webhooks.go @@ -0,0 +1,40 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +type CreateWebhooksRequest struct { + ConnectorID models.ConnectorID + Req models.CreateWebhooksRequest +} + +func (a Activities) PluginCreateWebhooks(ctx context.Context, request CreateWebhooksRequest) (*models.CreateWebhooksResponse, error) { + plugin, err := a.plugins.Get(request.ConnectorID) + if err != nil { + return nil, err + } + + resp, err := plugin.CreateWebhooks(ctx, request.Req) + if err != nil { + // TODO(polo): temporal errors + return nil, err + } + return &resp, nil +} + +var PluginCreateWebhooksActivity = Activities{}.PluginCreateWebhooks + +func PluginCreateWebhooks(ctx workflow.Context, connectorID models.ConnectorID, request models.CreateWebhooksRequest) (*models.CreateWebhooksResponse, error) { + ret := models.CreateWebhooksResponse{} + if err := executeActivity(ctx, PluginCreateWebhooksActivity, &ret, CreateWebhooksRequest{ + ConnectorID: connectorID, + Req: request, + }); err != nil { + return nil, err + } + return &ret, nil +} diff --git a/components/payments/internal/connectors/engine/activities/plugin_fetch_next_accounts.go b/components/payments/internal/connectors/engine/activities/plugin_fetch_next_accounts.go new file mode 100644 index 0000000000..55ff033aab --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/plugin_fetch_next_accounts.go @@ -0,0 +1,46 @@ +package activities + +import ( + "context" + "encoding/json" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +type FetchNextAccountsRequest struct { + ConnectorID models.ConnectorID + Req models.FetchNextAccountsRequest +} + +func (a Activities) PluginFetchNextAccounts(ctx context.Context, request FetchNextAccountsRequest) (*models.FetchNextAccountsResponse, error) { + plugin, err := a.plugins.Get(request.ConnectorID) + if err != nil { + return nil, err + } + + resp, err := plugin.FetchNextAccounts(ctx, request.Req) + if err != nil { + // TODO(polo): temporal errors + return nil, err + } + return &resp, nil +} + +var PluginFetchNextAccountsActivity = Activities{}.PluginFetchNextAccounts + +func PluginFetchNextAccounts(ctx workflow.Context, connectorID models.ConnectorID, fromPayload, state json.RawMessage, pageSize int) (*models.FetchNextAccountsResponse, error) { + ret := models.FetchNextAccountsResponse{} + if err := executeActivity(ctx, PluginFetchNextAccountsActivity, &ret, FetchNextAccountsRequest{ + ConnectorID: connectorID, + Req: models.FetchNextAccountsRequest{ + FromPayload: fromPayload, + State: state, + PageSize: pageSize, + }, + }, + ); err != nil { + return nil, err + } + return &ret, nil +} diff --git a/components/payments/internal/connectors/engine/activities/plugin_fetch_next_balances.go b/components/payments/internal/connectors/engine/activities/plugin_fetch_next_balances.go new file mode 100644 index 0000000000..2640190f81 --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/plugin_fetch_next_balances.go @@ -0,0 +1,46 @@ +package activities + +import ( + "context" + "encoding/json" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +type FetchNextBalancesRequest struct { + ConnectorID models.ConnectorID + Req models.FetchNextBalancesRequest +} + +func (a Activities) PluginFetchNextBalances(ctx context.Context, request FetchNextBalancesRequest) (*models.FetchNextBalancesResponse, error) { + plugin, err := a.plugins.Get(request.ConnectorID) + if err != nil { + return nil, err + } + + resp, err := plugin.FetchNextBalances(ctx, request.Req) + if err != nil { + // TODO(polo): temporal errors + return nil, err + } + return &resp, nil +} + +var PluginFetchNextBalancesActivity = Activities{}.PluginFetchNextBalances + +func PluginFetchNextBalances(ctx workflow.Context, connectorID models.ConnectorID, fromPayload, state json.RawMessage, pageSize int) (*models.FetchNextBalancesResponse, error) { + ret := models.FetchNextBalancesResponse{} + if err := executeActivity(ctx, PluginFetchNextBalancesActivity, &ret, FetchNextBalancesRequest{ + ConnectorID: connectorID, + Req: models.FetchNextBalancesRequest{ + FromPayload: fromPayload, + State: state, + PageSize: pageSize, + }, + }, + ); err != nil { + return nil, err + } + return &ret, nil +} diff --git a/components/payments/internal/connectors/engine/activities/plugin_fetch_next_external_accounts.go b/components/payments/internal/connectors/engine/activities/plugin_fetch_next_external_accounts.go new file mode 100644 index 0000000000..91b48c4277 --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/plugin_fetch_next_external_accounts.go @@ -0,0 +1,46 @@ +package activities + +import ( + "context" + "encoding/json" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +type FetchNextExternalAccountsRequest struct { + ConnectorID models.ConnectorID + Req models.FetchNextExternalAccountsRequest +} + +func (a Activities) PluginFetchNextExternalAccounts(ctx context.Context, request FetchNextExternalAccountsRequest) (*models.FetchNextExternalAccountsResponse, error) { + plugin, err := a.plugins.Get(request.ConnectorID) + if err != nil { + return nil, err + } + + resp, err := plugin.FetchNextExternalAccounts(ctx, request.Req) + if err != nil { + // TODO(polo): temporal errors + return nil, err + } + + return &resp, nil +} + +var PluginFetchNextExternalAccountsActivity = Activities{}.PluginFetchNextExternalAccounts + +func PluginFetchNextExternalAccounts(ctx workflow.Context, connectorID models.ConnectorID, fromPayload, state json.RawMessage, pageSize int) (*models.FetchNextExternalAccountsResponse, error) { + ret := models.FetchNextExternalAccountsResponse{} + if err := executeActivity(ctx, PluginFetchNextExternalAccountsActivity, &ret, FetchNextExternalAccountsRequest{ + ConnectorID: connectorID, + Req: models.FetchNextExternalAccountsRequest{ + FromPayload: fromPayload, + State: state, + PageSize: pageSize, + }, + }); err != nil { + return nil, err + } + return &ret, nil +} diff --git a/components/payments/internal/connectors/engine/activities/plugin_fetch_next_others.go b/components/payments/internal/connectors/engine/activities/plugin_fetch_next_others.go new file mode 100644 index 0000000000..6be44c65a6 --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/plugin_fetch_next_others.go @@ -0,0 +1,47 @@ +package activities + +import ( + "context" + "encoding/json" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +type FetchNextOthersRequest struct { + ConnectorID models.ConnectorID + Req models.FetchNextOthersRequest +} + +func (a Activities) PluginFetchNextOthers(ctx context.Context, request FetchNextOthersRequest) (*models.FetchNextOthersResponse, error) { + plugin, err := a.plugins.Get(request.ConnectorID) + if err != nil { + return nil, err + } + + resp, err := plugin.FetchNextOthers(ctx, request.Req) + if err != nil { + // TODO(polo): temporal errors + return nil, err + } + + return &resp, nil +} + +var PluginFetchNextOthersActivity = Activities{}.PluginFetchNextOthers + +func PluginFetchNextOthers(ctx workflow.Context, connectorID models.ConnectorID, name string, fromPayload, state json.RawMessage, pageSize int) (*models.FetchNextOthersResponse, error) { + ret := models.FetchNextOthersResponse{} + if err := executeActivity(ctx, PluginFetchNextOthersActivity, &ret, FetchNextOthersRequest{ + ConnectorID: connectorID, + Req: models.FetchNextOthersRequest{ + Name: name, + FromPayload: fromPayload, + State: state, + PageSize: pageSize, + }, + }); err != nil { + return nil, err + } + return &ret, nil +} diff --git a/components/payments/internal/connectors/engine/activities/plugin_fetch_next_payments.go b/components/payments/internal/connectors/engine/activities/plugin_fetch_next_payments.go new file mode 100644 index 0000000000..e87c8813f3 --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/plugin_fetch_next_payments.go @@ -0,0 +1,46 @@ +package activities + +import ( + "context" + "encoding/json" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +type FetchNextPaymentsRequest struct { + ConnectorID models.ConnectorID + Req models.FetchNextPaymentsRequest +} + +func (a Activities) PluginFetchNextPayments(ctx context.Context, request FetchNextPaymentsRequest) (*models.FetchNextPaymentsResponse, error) { + plugin, err := a.plugins.Get(request.ConnectorID) + if err != nil { + return nil, err + } + + resp, err := plugin.FetchNextPayments(ctx, request.Req) + if err != nil { + // TODO(polo): temporal errors + return nil, err + } + + return &resp, nil +} + +var PluginFetchNextPaymentsActivity = Activities{}.PluginFetchNextPayments + +func PluginFetchNextPayments(ctx workflow.Context, connectorID models.ConnectorID, fromPayload, state json.RawMessage, pageSize int) (*models.FetchNextPaymentsResponse, error) { + ret := models.FetchNextPaymentsResponse{} + if err := executeActivity(ctx, PluginFetchNextPaymentsActivity, &ret, FetchNextPaymentsRequest{ + ConnectorID: connectorID, + Req: models.FetchNextPaymentsRequest{ + FromPayload: fromPayload, + State: state, + PageSize: pageSize, + }, + }); err != nil { + return nil, err + } + return &ret, nil +} diff --git a/components/payments/internal/connectors/engine/activities/plugin_install_connector.go b/components/payments/internal/connectors/engine/activities/plugin_install_connector.go new file mode 100644 index 0000000000..ebb6f72344 --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/plugin_install_connector.go @@ -0,0 +1,44 @@ +package activities + +import ( + "context" + "encoding/json" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +type InstallConnectorRequest struct { + ConnectorID models.ConnectorID + Req models.InstallRequest +} + +func (a Activities) PluginInstallConnector(ctx context.Context, request InstallConnectorRequest) (*models.InstallResponse, error) { + plugin, err := a.plugins.Get(request.ConnectorID) + if err != nil { + return nil, err + } + + resp, err := plugin.Install(ctx, request.Req) + if err != nil { + // TODO(polo): temporal errors + return nil, err + } + + return &resp, err +} + +var PluginInstallConnectorActivity = Activities{}.PluginInstallConnector + +func PluginInstallConnector(ctx workflow.Context, connectorID models.ConnectorID, config json.RawMessage) (*models.InstallResponse, error) { + ret := models.InstallResponse{} + if err := executeActivity(ctx, PluginInstallConnectorActivity, &ret, InstallConnectorRequest{ + ConnectorID: connectorID, + Req: models.InstallRequest{ + Config: config, + }, + }); err != nil { + return nil, err + } + return &ret, nil +} diff --git a/components/payments/internal/connectors/engine/activities/plugin_translate_webhook.go b/components/payments/internal/connectors/engine/activities/plugin_translate_webhook.go new file mode 100644 index 0000000000..f1a6225269 --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/plugin_translate_webhook.go @@ -0,0 +1,40 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +type TranslateWebhookRequest struct { + ConnectorID models.ConnectorID + Req models.TranslateWebhookRequest +} + +func (a Activities) PluginTranslateWebhook(ctx context.Context, request TranslateWebhookRequest) (*models.TranslateWebhookResponse, error) { + plugin, err := a.plugins.Get(request.ConnectorID) + if err != nil { + return nil, err + } + + resp, err := plugin.TranslateWebhook(ctx, request.Req) + if err != nil { + // TODO(polo): temporal errors + return nil, err + } + return &resp, nil +} + +var PluginTranslateWebhookActivity = Activities{}.PluginTranslateWebhook + +func PluginTranslateWebhook(ctx workflow.Context, connectorID models.ConnectorID, request models.TranslateWebhookRequest) (*models.TranslateWebhookResponse, error) { + ret := models.TranslateWebhookResponse{} + if err := executeActivity(ctx, PluginTranslateWebhookActivity, &ret, TranslateWebhookRequest{ + ConnectorID: connectorID, + Req: request, + }); err != nil { + return nil, err + } + return &ret, nil +} diff --git a/components/payments/internal/connectors/engine/activities/plugin_uninstall_connector.go b/components/payments/internal/connectors/engine/activities/plugin_uninstall_connector.go new file mode 100644 index 0000000000..b21ed0c03a --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/plugin_uninstall_connector.go @@ -0,0 +1,39 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +type UninstallConnectorRequest struct { + ConnectorID models.ConnectorID +} + +func (a Activities) PluginUninstallConnector(ctx context.Context, request UninstallConnectorRequest) (*models.UninstallResponse, error) { + plugin, err := a.plugins.Get(request.ConnectorID) + if err != nil { + return nil, err + } + + resp, err := plugin.Uninstall(ctx, models.UninstallRequest{}) + if err != nil { + // TODO(polo): temporal errors + return nil, err + } + + return &resp, err +} + +var PluginUninstallConnectorActivity = Activities{}.PluginUninstallConnector + +func PluginUninstallConnector(ctx workflow.Context, connectorID models.ConnectorID) (*models.UninstallResponse, error) { + ret := models.UninstallResponse{} + if err := executeActivity(ctx, PluginUninstallConnectorActivity, &ret, UninstallConnectorRequest{ + ConnectorID: connectorID, + }); err != nil { + return nil, err + } + return &ret, nil +} diff --git a/components/payments/internal/connectors/engine/activities/storage_accounts_delete.go b/components/payments/internal/connectors/engine/activities/storage_accounts_delete.go new file mode 100644 index 0000000000..0f38522c4b --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/storage_accounts_delete.go @@ -0,0 +1,18 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) StorageAccountsDelete(ctx context.Context, connectorID models.ConnectorID) error { + return a.storage.AccountsDeleteFromConnectorID(ctx, connectorID) +} + +var StorageAccountsDeleteActivity = Activities{}.StorageAccountsDelete + +func StorageAccountsDelete(ctx workflow.Context, connectorID models.ConnectorID) error { + return executeActivity(ctx, StorageAccountsDeleteActivity, nil, connectorID) +} diff --git a/components/payments/internal/connectors/engine/activities/storage_accounts_store.go b/components/payments/internal/connectors/engine/activities/storage_accounts_store.go new file mode 100644 index 0000000000..bf72f4d851 --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/storage_accounts_store.go @@ -0,0 +1,18 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) StorageAccountsStore(ctx context.Context, accounts []models.Account) error { + return a.storage.AccountsUpsert(ctx, accounts) +} + +var StorageAccountsStoreActivity = Activities{}.StorageAccountsStore + +func StorageAccountsStore(ctx workflow.Context, accounts []models.Account) error { + return executeActivity(ctx, StorageAccountsStoreActivity, nil, accounts) +} diff --git a/components/payments/internal/connectors/engine/activities/storage_balances_delete.go b/components/payments/internal/connectors/engine/activities/storage_balances_delete.go new file mode 100644 index 0000000000..5d2158326a --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/storage_balances_delete.go @@ -0,0 +1,18 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) StorageBalancesDelete(ctx context.Context, connectorID models.ConnectorID) error { + return a.storage.BalancesDeleteForConnectorID(ctx, connectorID) +} + +var StorageBalancesDeleteActivity = Activities{}.StorageBalancesDelete + +func StorageBalancesDelete(ctx workflow.Context, connectorID models.ConnectorID) error { + return executeActivity(ctx, StorageBalancesDeleteActivity, nil, connectorID) +} diff --git a/components/payments/internal/connectors/engine/activities/storage_balances_store.go b/components/payments/internal/connectors/engine/activities/storage_balances_store.go new file mode 100644 index 0000000000..e5c9830177 --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/storage_balances_store.go @@ -0,0 +1,18 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) StorageBalancesStore(ctx context.Context, balances []models.Balance) error { + return a.storage.BalancesUpsert(ctx, balances) +} + +var StorageBalancesStoreActivity = Activities{}.StorageBalancesStore + +func StorageBalancesStore(ctx workflow.Context, balances []models.Balance) error { + return executeActivity(ctx, StorageBalancesStoreActivity, nil, balances) +} diff --git a/components/payments/internal/connectors/engine/activities/storage_bank_accounts_add_related_account.go b/components/payments/internal/connectors/engine/activities/storage_bank_accounts_add_related_account.go new file mode 100644 index 0000000000..6e24ddf8e2 --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/storage_bank_accounts_add_related_account.go @@ -0,0 +1,18 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) StorageBankAccountsAddRelatedAccount(ctx context.Context, relatedAccount models.BankAccountRelatedAccount) error { + return a.storage.BankAccountsAddRelatedAccount(ctx, relatedAccount) +} + +var StorageBankAccountsAddRelatedAccountActivity = Activities{}.StorageBankAccountsAddRelatedAccount + +func StorageBankAccountsAddRelatedAccount(ctx workflow.Context, relatedAccount models.BankAccountRelatedAccount) error { + return executeActivity(ctx, StorageBankAccountsAddRelatedAccountActivity, nil, relatedAccount) +} diff --git a/components/payments/internal/connectors/engine/activities/storage_bank_accounts_delete_related_accounts.go b/components/payments/internal/connectors/engine/activities/storage_bank_accounts_delete_related_accounts.go new file mode 100644 index 0000000000..3a19be0cdb --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/storage_bank_accounts_delete_related_accounts.go @@ -0,0 +1,18 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) StorageBankAccountsDeleteRelatedAccounts(ctx context.Context, connectorID models.ConnectorID) error { + return a.storage.BankAccountsDeleteRelatedAccountFromConnectorID(ctx, connectorID) +} + +var StorageBankAccountsDeleteRelatedAccountsActivity = Activities{}.StorageBankAccountsDeleteRelatedAccounts + +func StorageBankAccountsDeleteRelatedAccounts(ctx workflow.Context, connectorID models.ConnectorID) error { + return executeActivity(ctx, StorageBankAccountsDeleteRelatedAccountsActivity, nil, connectorID) +} diff --git a/components/payments/internal/connectors/engine/activities/storage_bank_accounts_get.go b/components/payments/internal/connectors/engine/activities/storage_bank_accounts_get.go new file mode 100644 index 0000000000..4de9c592ea --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/storage_bank_accounts_get.go @@ -0,0 +1,21 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "github.com/google/uuid" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) StorageBankAccountsGet(ctx context.Context, id uuid.UUID, expand bool) (*models.BankAccount, error) { + return a.storage.BankAccountsGet(ctx, id, expand) +} + +var StorageBankAccountsGetActivity = Activities{}.StorageBankAccountsGet + +func StorageBankAccountsGet(ctx workflow.Context, id uuid.UUID, expand bool) (*models.BankAccount, error) { + var result models.BankAccount + err := executeActivity(ctx, StorageBankAccountsGetActivity, &result, id, expand) + return &result, err +} diff --git a/components/payments/internal/connectors/engine/activities/storage_connectors_delete.go b/components/payments/internal/connectors/engine/activities/storage_connectors_delete.go new file mode 100644 index 0000000000..763934a51e --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/storage_connectors_delete.go @@ -0,0 +1,18 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) StorageConnectorsDelete(ctx context.Context, connectorID models.ConnectorID) error { + return a.storage.ConnectorsUninstall(ctx, connectorID) +} + +var StorageConnectorsDeleteActivity = Activities{}.StorageConnectorsDelete + +func StorageConnectorsDelete(ctx workflow.Context, connectorID models.ConnectorID) error { + return executeActivity(ctx, StorageConnectorsDeleteActivity, nil, connectorID) +} diff --git a/components/payments/internal/connectors/engine/activities/storage_connectors_store.go b/components/payments/internal/connectors/engine/activities/storage_connectors_store.go new file mode 100644 index 0000000000..61181e079f --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/storage_connectors_store.go @@ -0,0 +1,18 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) StorageConnectorsStore(ctx context.Context, connector models.Connector) error { + return a.storage.ConnectorsInstall(ctx, connector) +} + +var StorageConnectorsStoreActivity = Activities{}.StorageConnectorsStore + +func StorageConnectorsStore(ctx workflow.Context, connector models.Connector) error { + return executeActivity(ctx, StorageConnectorsStoreActivity, nil, connector) +} diff --git a/components/payments/internal/connectors/engine/activities/storage_instances_delete.go b/components/payments/internal/connectors/engine/activities/storage_instances_delete.go new file mode 100644 index 0000000000..3f779dfe4e --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/storage_instances_delete.go @@ -0,0 +1,18 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) StorageInstancesDelete(ctx context.Context, connectorID models.ConnectorID) error { + return a.storage.InstancesDeleteFromConnectorID(ctx, connectorID) +} + +var StorageInstancesDeleteActivity = Activities{}.StorageInstancesDelete + +func StorageInstancesDelete(ctx workflow.Context, connectorID models.ConnectorID) error { + return executeActivity(ctx, StorageInstancesDeleteActivity, nil, connectorID) +} diff --git a/components/payments/internal/connectors/engine/activities/storage_instances_store.go b/components/payments/internal/connectors/engine/activities/storage_instances_store.go new file mode 100644 index 0000000000..b0b83559fe --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/storage_instances_store.go @@ -0,0 +1,18 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) StorageInstancesStore(ctx context.Context, instance models.Instance) error { + return a.storage.InstancesUpsert(ctx, instance) +} + +var StorageInstancesStoreActivity = Activities{}.StorageInstancesStore + +func StorageInstancesStore(ctx workflow.Context, instance models.Instance) error { + return executeActivity(ctx, StorageInstancesStoreActivity, nil, instance) +} diff --git a/components/payments/internal/connectors/engine/activities/storage_instances_update.go b/components/payments/internal/connectors/engine/activities/storage_instances_update.go new file mode 100644 index 0000000000..ebbd8912e9 --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/storage_instances_update.go @@ -0,0 +1,18 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) StorageInstancesUpdate(ctx context.Context, instance models.Instance) error { + return a.storage.InstancesUpdate(ctx, instance) +} + +var StorageInstancesUpdateActivity = Activities{}.StorageInstancesUpdate + +func StorageInstancesUpdate(ctx workflow.Context, instance models.Instance) error { + return executeActivity(ctx, StorageInstancesUpdateActivity, nil, instance) +} diff --git a/components/payments/internal/connectors/engine/activities/storage_payments_delete.go b/components/payments/internal/connectors/engine/activities/storage_payments_delete.go new file mode 100644 index 0000000000..900b8a0a53 --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/storage_payments_delete.go @@ -0,0 +1,18 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) StoragePaymentsDelete(ctx context.Context, connectorID models.ConnectorID) error { + return a.storage.PaymentsDeleteFromConnectorID(ctx, connectorID) +} + +var StoragePaymentsDeleteActivity = Activities{}.StoragePaymentsDelete + +func StoragePaymentsDelete(ctx workflow.Context, connectorID models.ConnectorID) error { + return executeActivity(ctx, StoragePaymentsDeleteActivity, nil, connectorID) +} diff --git a/components/payments/internal/connectors/engine/activities/storage_payments_store.go b/components/payments/internal/connectors/engine/activities/storage_payments_store.go new file mode 100644 index 0000000000..a3862d1a92 --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/storage_payments_store.go @@ -0,0 +1,18 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) StoragePaymentsStore(ctx context.Context, payments []models.Payment) error { + return a.storage.PaymentsUpsert(ctx, payments) +} + +var StoragePaymentsStoreActivity = Activities{}.StoragePaymentsStore + +func StoragePaymentsStore(ctx workflow.Context, payments []models.Payment) error { + return executeActivity(ctx, StoragePaymentsStoreActivity, nil, payments) +} diff --git a/components/payments/internal/connectors/engine/activities/storage_schedules_delete.go b/components/payments/internal/connectors/engine/activities/storage_schedules_delete.go new file mode 100644 index 0000000000..8a7c2e94ad --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/storage_schedules_delete.go @@ -0,0 +1,18 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) StorageSchedulesDelete(ctx context.Context, connectorID models.ConnectorID) error { + return a.storage.SchedulesDeleteFromConnectorID(ctx, connectorID) +} + +var StorageSchedulesDeleteActivity = Activities{}.StorageSchedulesDelete + +func StorageSchedulesDelete(ctx workflow.Context, connectorID models.ConnectorID) error { + return executeActivity(ctx, StorageSchedulesDeleteActivity, nil, connectorID) +} diff --git a/components/payments/internal/connectors/engine/activities/storage_schedules_fetch.go b/components/payments/internal/connectors/engine/activities/storage_schedules_fetch.go new file mode 100644 index 0000000000..f8fa6dc719 --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/storage_schedules_fetch.go @@ -0,0 +1,24 @@ +package activities + +import ( + "context" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/storage" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) StorageSchedulesList(ctx context.Context, query storage.ListSchedulesQuery) (*bunpaginate.Cursor[models.Schedule], error) { + return a.storage.SchedulesList(ctx, query) +} + +var StorageSchedulesListActivity = Activities{}.StorageSchedulesList + +func StorageSchedulesList(ctx workflow.Context, query storage.ListSchedulesQuery) (*bunpaginate.Cursor[models.Schedule], error) { + ret := bunpaginate.Cursor[models.Schedule]{} + if err := executeActivity(ctx, StorageSchedulesListActivity, &ret, query); err != nil { + return nil, err + } + return &ret, nil +} diff --git a/components/payments/internal/connectors/engine/activities/storage_schedules_store.go b/components/payments/internal/connectors/engine/activities/storage_schedules_store.go new file mode 100644 index 0000000000..c460b691c0 --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/storage_schedules_store.go @@ -0,0 +1,18 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) StorageSchedulesStore(ctx context.Context, schedule models.Schedule) error { + return a.storage.SchedulesUpsert(ctx, schedule) +} + +var StorageSchedulesStoreActivity = Activities{}.StorageSchedulesStore + +func StorageSchedulesStore(ctx workflow.Context, schedule models.Schedule) error { + return executeActivity(ctx, StorageSchedulesStoreActivity, nil, schedule) +} diff --git a/components/payments/internal/connectors/engine/activities/storage_states_delete.go b/components/payments/internal/connectors/engine/activities/storage_states_delete.go new file mode 100644 index 0000000000..a1870378f1 --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/storage_states_delete.go @@ -0,0 +1,18 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) StorageStatesDelete(ctx context.Context, connectorID models.ConnectorID) error { + return a.storage.StatesDeleteFromConnectorID(ctx, connectorID) +} + +var StorageStatesDeleteActivity = Activities{}.StorageStatesDelete + +func StorageStatesDelete(ctx workflow.Context, connectorID models.ConnectorID) error { + return executeActivity(ctx, StorageStatesDeleteActivity, nil, connectorID) +} diff --git a/components/payments/internal/connectors/engine/activities/storage_states_get.go b/components/payments/internal/connectors/engine/activities/storage_states_get.go new file mode 100644 index 0000000000..8fb0064521 --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/storage_states_get.go @@ -0,0 +1,34 @@ +package activities + +import ( + "context" + "errors" + + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/storage" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) StorageStatesGet(ctx context.Context, id models.StateID) (*models.State, error) { + resp, err := a.storage.StatesGet(ctx, id) + if err != nil { + if errors.Is(err, storage.ErrNotFound) { + return &models.State{ + ID: id, + ConnectorID: id.ConnectorID, + State: nil, + }, nil + } + } + return &resp, nil +} + +var StorageStatesGetActivity = Activities{}.StorageStatesGet + +func StorageStatesGet(ctx workflow.Context, id models.StateID) (*models.State, error) { + ret := models.State{} + if err := executeActivity(ctx, StorageStatesGetActivity, &ret, id); err != nil { + return nil, err + } + return &ret, nil +} diff --git a/components/payments/internal/connectors/engine/activities/storage_states_store.go b/components/payments/internal/connectors/engine/activities/storage_states_store.go new file mode 100644 index 0000000000..fe5b462e81 --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/storage_states_store.go @@ -0,0 +1,18 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) StorageStatesStore(ctx context.Context, state models.State) error { + return a.storage.StatesUpsert(ctx, state) +} + +var StorageStatesStoreActivity = Activities{}.StorageStatesStore + +func StorageStatesStore(ctx workflow.Context, state models.State) error { + return executeActivity(ctx, StorageStatesStoreActivity, nil, state) +} diff --git a/components/payments/internal/connectors/engine/activities/storage_tasks_tree_delete.go b/components/payments/internal/connectors/engine/activities/storage_tasks_tree_delete.go new file mode 100644 index 0000000000..0d0e355539 --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/storage_tasks_tree_delete.go @@ -0,0 +1,18 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) StorageTasksTreeDelete(ctx context.Context, connectorID models.ConnectorID) error { + return a.storage.TasksDeleteFromConnectorID(ctx, connectorID) +} + +var StorageTasksTreeDeleteActivity = Activities{}.StorageTasksTreeDelete + +func StorageTasksTreeDelete(ctx workflow.Context, connectorID models.ConnectorID) error { + return executeActivity(ctx, StorageTasksTreeDeleteActivity, nil, connectorID) +} diff --git a/components/payments/internal/connectors/engine/activities/storage_tasks_tree_store.go b/components/payments/internal/connectors/engine/activities/storage_tasks_tree_store.go new file mode 100644 index 0000000000..100b5ac6f3 --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/storage_tasks_tree_store.go @@ -0,0 +1,26 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +type TasksTreeStoreRequest struct { + ConnectorID models.ConnectorID + Workflow models.Tasks +} + +func (a Activities) StorageTasksTreeStore(ctx context.Context, request TasksTreeStoreRequest) error { + return a.storage.TasksUpsert(ctx, request.ConnectorID, request.Workflow) +} + +var StorageTasksTreeStoreActivity = Activities{}.StorageTasksTreeStore + +func StorageTasksTreeStore(ctx workflow.Context, connectorID models.ConnectorID, workflow models.Tasks) error { + return executeActivity(ctx, StorageTasksTreeStoreActivity, nil, TasksTreeStoreRequest{ + ConnectorID: connectorID, + Workflow: workflow, + }) +} diff --git a/components/payments/internal/connectors/engine/activities/storage_webhooks_configs_delete.go b/components/payments/internal/connectors/engine/activities/storage_webhooks_configs_delete.go new file mode 100644 index 0000000000..d5d6151929 --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/storage_webhooks_configs_delete.go @@ -0,0 +1,18 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) StorageWebhooksConfigsDelete(ctx context.Context, connectorID models.ConnectorID) error { + return a.storage.WebhooksConfigsDeleteFromConnectorID(ctx, connectorID) +} + +var StorageWebhooksConfigsDeleteActivity = Activities{}.StorageWebhooksConfigsDelete + +func StorageWebhooksConfigsDelete(ctx workflow.Context, connectorID models.ConnectorID) error { + return executeActivity(ctx, StorageWebhooksConfigsDeleteActivity, nil, connectorID) +} diff --git a/components/payments/internal/connectors/engine/activities/storage_webhooks_configs_store.go b/components/payments/internal/connectors/engine/activities/storage_webhooks_configs_store.go new file mode 100644 index 0000000000..2b06f465ea --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/storage_webhooks_configs_store.go @@ -0,0 +1,18 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) StorageWebhooksConfigsStore(ctx context.Context, configs []models.WebhookConfig) error { + return a.storage.WebhooksConfigsUpsert(ctx, configs) +} + +var StorageWebhooksConfigsStoreActivity = Activities{}.StorageWebhooksConfigsStore + +func StorageWebhooksConfigsStore(ctx workflow.Context, configs []models.WebhookConfig) error { + return executeActivity(ctx, StorageWebhooksConfigsStoreActivity, nil, configs) +} diff --git a/components/payments/internal/connectors/engine/activities/storage_webhooks_delete.go b/components/payments/internal/connectors/engine/activities/storage_webhooks_delete.go new file mode 100644 index 0000000000..f1713f589d --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/storage_webhooks_delete.go @@ -0,0 +1,18 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) StorageWebhooksDelete(ctx context.Context, connectorID models.ConnectorID) error { + return a.storage.WebhooksDeleteFromConnectorID(ctx, connectorID) +} + +var StorageWebhooksDeleteActivity = Activities{}.StorageWebhooksDelete + +func StorageWebhooksDelete(ctx workflow.Context, connectorID models.ConnectorID) error { + return executeActivity(ctx, StorageWebhooksDeleteActivity, nil, connectorID) +} diff --git a/components/payments/internal/connectors/engine/activities/storage_webhooks_store.go b/components/payments/internal/connectors/engine/activities/storage_webhooks_store.go new file mode 100644 index 0000000000..b6652ccbe5 --- /dev/null +++ b/components/payments/internal/connectors/engine/activities/storage_webhooks_store.go @@ -0,0 +1,18 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) StorageWebhooksStore(ctx context.Context, webhook models.Webhook) error { + return a.storage.WebhooksInsert(ctx, webhook) +} + +var StorageWebhooksStoreActivity = Activities{}.StorageWebhooksStore + +func StorageWebhooksStore(ctx workflow.Context, webhook models.Webhook) error { + return executeActivity(ctx, StorageWebhooksStoreActivity, nil, webhook) +} diff --git a/components/payments/internal/connectors/engine/engine.go b/components/payments/internal/connectors/engine/engine.go new file mode 100644 index 0000000000..79bac35e4f --- /dev/null +++ b/components/payments/internal/connectors/engine/engine.go @@ -0,0 +1,494 @@ +package engine + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/contextutil" + "github.com/formancehq/payments/internal/connectors/engine/plugins" + "github.com/formancehq/payments/internal/connectors/engine/webhooks" + "github.com/formancehq/payments/internal/connectors/engine/workflow" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/storage" + "github.com/google/uuid" + "github.com/pkg/errors" + "go.temporal.io/api/enums/v1" + "go.temporal.io/sdk/client" +) + +type Engine interface { + InstallConnector(ctx context.Context, provider string, rawConfig json.RawMessage) (models.ConnectorID, error) + UninstallConnector(ctx context.Context, connectorID models.ConnectorID) error + ResetConnector(ctx context.Context, connectorID models.ConnectorID) error + ForwardBankAccount(ctx context.Context, bankAccountID uuid.UUID, connectorID models.ConnectorID) (*models.BankAccount, error) + HandleWebhook(ctx context.Context, urlPath string, webhook models.Webhook) error + CreatePool(ctx context.Context, pool models.Pool) error + AddAccountToPool(ctx context.Context, id uuid.UUID, accountID models.AccountID) error + RemoveAccountFromPool(ctx context.Context, id uuid.UUID, accountID models.AccountID) error + DeletePool(ctx context.Context, poolID uuid.UUID) error + + OnStart(ctx context.Context) error +} + +type engine struct { + temporalClient client.Client + + workers *Workers + plugins plugins.Plugins + storage storage.Storage + webhooks webhooks.Webhooks + + stack string +} + +func New(temporalClient client.Client, workers *Workers, plugins plugins.Plugins, storage storage.Storage, webhooks webhooks.Webhooks, stack string) Engine { + return &engine{ + temporalClient: temporalClient, + workers: workers, + plugins: plugins, + storage: storage, + webhooks: webhooks, + } +} + +func (e *engine) InstallConnector(ctx context.Context, provider string, rawConfig json.RawMessage) (models.ConnectorID, error) { + config := models.DefaultConfig() + if err := json.Unmarshal(rawConfig, &config); err != nil { + return models.ConnectorID{}, err + } + + if err := config.Validate(); err != nil { + return models.ConnectorID{}, errors.Wrap(ErrValidation, err.Error()) + } + + connector := models.Connector{ + ID: models.ConnectorID{ + Reference: uuid.New(), + Provider: provider, + }, + Name: config.Name, + CreatedAt: time.Now().UTC(), + Provider: provider, + Config: rawConfig, + } + + if err := e.storage.ConnectorsInstall(ctx, connector); err != nil { + return models.ConnectorID{}, err + } + + err := e.plugins.RegisterPlugin(connector.ID) + if err != nil { + return models.ConnectorID{}, handlePluginError(err) + } + + err = e.workers.AddWorker(connector.ID.String()) + if err != nil { + return models.ConnectorID{}, err + } + + // Launch the workflow + run, err := e.temporalClient.ExecuteWorkflow( + ctx, + client.StartWorkflowOptions{ + ID: fmt.Sprintf("install-%s", connector.ID.String()), + TaskQueue: connector.ID.String(), + WorkflowIDReusePolicy: enums.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE, + WorkflowExecutionErrorWhenAlreadyStarted: false, + SearchAttributes: map[string]interface{}{ + workflow.SearchAttributeStack: e.stack, + }, + }, + workflow.RunInstallConnector, + workflow.InstallConnector{ + ConnectorID: connector.ID, + RawConfig: rawConfig, + Config: config, + }, + ) + if err != nil { + return models.ConnectorID{}, err + } + + // Wait for installation to complete + if err := run.Get(ctx, nil); err != nil { + return models.ConnectorID{}, err + } + + return connector.ID, nil +} + +func (e *engine) UninstallConnector(ctx context.Context, connectorID models.ConnectorID) error { + run, err := e.temporalClient.ExecuteWorkflow( + ctx, + client.StartWorkflowOptions{ + ID: fmt.Sprintf("uninstall-%s", connectorID.String()), + TaskQueue: connectorID.String(), + WorkflowIDReusePolicy: enums.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY, + WorkflowExecutionErrorWhenAlreadyStarted: false, + SearchAttributes: map[string]interface{}{ + workflow.SearchAttributeStack: e.stack, + }, + }, + workflow.RunUninstallConnector, + workflow.UninstallConnector{ + ConnectorID: connectorID, + }, + ) + if err != nil { + return err + } + + // Wait for uninstallation to complete + if err := run.Get(ctx, nil); err != nil { + return err + } + + if err := e.workers.RemoveWorker(connectorID.String()); err != nil { + return err + } + + if err := e.plugins.UnregisterPlugin(connectorID); err != nil { + return handlePluginError(err) + } + + return nil +} + +func (e *engine) ResetConnector(ctx context.Context, connectorID models.ConnectorID) error { + connector, err := e.storage.ConnectorsGet(ctx, connectorID) + if err != nil { + return err + } + + // Detached the context to avoid being in a weird state if request is + // cancelled in the middle of the operation. + ctx, _ = contextutil.Detached(ctx) + if err := e.UninstallConnector(ctx, connectorID); err != nil { + return err + } + + _, err = e.InstallConnector(ctx, connectorID.Provider, connector.Config) + if err != nil { + return err + } + + run, err := e.temporalClient.ExecuteWorkflow( + ctx, + client.StartWorkflowOptions{ + ID: fmt.Sprintf("reset-%s", connectorID.String()), + TaskQueue: connectorID.String(), + WorkflowIDReusePolicy: enums.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY, + SearchAttributes: map[string]interface{}{ + workflow.SearchAttributeStack: e.stack, + }, + }, + workflow.RunSendEvents, + workflow.SendEvents{ + ConnectorReset: &connectorID, + }, + ) + if err != nil { + return err + } + + if err := run.Get(ctx, nil); err != nil { + return err + } + + return nil +} + +func (e *engine) ForwardBankAccount(ctx context.Context, bankAccountID uuid.UUID, connectorID models.ConnectorID) (*models.BankAccount, error) { + run, err := e.temporalClient.ExecuteWorkflow( + ctx, + client.StartWorkflowOptions{ + ID: fmt.Sprintf("create-bank-account-%s-%s", connectorID.String(), bankAccountID.String()), + TaskQueue: connectorID.String(), + WorkflowIDReusePolicy: enums.WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE, + WorkflowExecutionErrorWhenAlreadyStarted: false, + SearchAttributes: map[string]interface{}{ + workflow.SearchAttributeStack: e.stack, + }, + }, + workflow.RunCreateBankAccount, + workflow.CreateBankAccount{ + ConnectorID: connectorID, + BankAccountID: bankAccountID, + }, + ) + if err != nil { + return nil, err + } + + var bankAccount models.BankAccount + // Wait for bank account creation to complete + if err := run.Get(ctx, &bankAccount); err != nil { + return nil, err + } + + return &bankAccount, nil +} + +func (e *engine) HandleWebhook(ctx context.Context, urlPath string, webhook models.Webhook) error { + configs, err := e.webhooks.GetConfigs(webhook.ConnectorID, urlPath) + if err != nil { + return err + } + + var config *models.WebhookConfig + for _, c := range configs { + if !strings.Contains(urlPath, c.URLPath) { + continue + } + + config = &c + break + } + + if config == nil { + return errors.New("webhook config not found") + } + + ctx, _ = contextutil.Detached(ctx) + if _, err := e.temporalClient.ExecuteWorkflow( + ctx, + client.StartWorkflowOptions{ + ID: fmt.Sprintf("webhook-%s-%s", webhook.ConnectorID.String(), webhook.ID), + TaskQueue: webhook.ConnectorID.String(), + WorkflowIDReusePolicy: enums.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE, + WorkflowExecutionErrorWhenAlreadyStarted: false, + SearchAttributes: map[string]interface{}{ + workflow.SearchAttributeStack: e.stack, + }, + }, + workflow.RunHandleWebhooks, + workflow.HandleWebhooks{ + ConnectorID: webhook.ConnectorID, + WebhookConfig: *config, + Webhook: webhook, + }, + ); err != nil { + return err + } + + return nil +} + +func (e *engine) CreatePool(ctx context.Context, pool models.Pool) error { + if err := e.storage.PoolsUpsert(ctx, pool); err != nil { + return err + } + + ctx, _ = contextutil.Detached(ctx) + run, err := e.temporalClient.ExecuteWorkflow( + ctx, + client.StartWorkflowOptions{ + ID: fmt.Sprintf("pools-creation-%s", pool.IdempotencyKey()), + TaskQueue: defaultWorkerName, + WorkflowIDReusePolicy: enums.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE, + WorkflowExecutionErrorWhenAlreadyStarted: false, + SearchAttributes: map[string]interface{}{ + workflow.SearchAttributeStack: e.stack, + }, + }, + workflow.RunSendEvents, + workflow.SendEvents{ + PoolsCreation: &pool, + }, + ) + if err != nil { + return err + } + + if err := run.Get(ctx, nil); err != nil { + return err + } + + return nil +} + +func (e *engine) AddAccountToPool(ctx context.Context, id uuid.UUID, accountID models.AccountID) error { + if err := e.storage.PoolsAddAccount(ctx, id, accountID); err != nil { + return err + } + + ctx, _ = contextutil.Detached(ctx) + pool, err := e.storage.PoolsGet(ctx, id) + if err != nil { + return err + } + + run, err := e.temporalClient.ExecuteWorkflow( + ctx, + client.StartWorkflowOptions{ + ID: fmt.Sprintf("pools-add-account-%s", pool.IdempotencyKey()), + TaskQueue: defaultWorkerName, + WorkflowIDReusePolicy: enums.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE, + WorkflowExecutionErrorWhenAlreadyStarted: false, + SearchAttributes: map[string]interface{}{ + workflow.SearchAttributeStack: e.stack, + }, + }, + workflow.RunSendEvents, + workflow.SendEvents{ + PoolsCreation: pool, + }, + ) + if err != nil { + return err + } + + if err := run.Get(ctx, nil); err != nil { + return err + } + + return nil +} + +func (e *engine) RemoveAccountFromPool(ctx context.Context, id uuid.UUID, accountID models.AccountID) error { + if err := e.storage.PoolsRemoveAccount(ctx, id, accountID); err != nil { + return err + } + + ctx, _ = contextutil.Detached(ctx) + pool, err := e.storage.PoolsGet(ctx, id) + if err != nil { + return err + } + + run, err := e.temporalClient.ExecuteWorkflow( + ctx, + client.StartWorkflowOptions{ + ID: fmt.Sprintf("pools-remove-account-%s", pool.IdempotencyKey()), + TaskQueue: defaultWorkerName, + WorkflowIDReusePolicy: enums.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE, + WorkflowExecutionErrorWhenAlreadyStarted: false, + SearchAttributes: map[string]interface{}{ + workflow.SearchAttributeStack: e.stack, + }, + }, + workflow.RunSendEvents, + workflow.SendEvents{ + PoolsCreation: pool, + }, + ) + if err != nil { + return err + } + + if err := run.Get(ctx, nil); err != nil { + return err + } + + return nil +} + +func (e *engine) DeletePool(ctx context.Context, poolID uuid.UUID) error { + if err := e.storage.PoolsDelete(ctx, poolID); err != nil { + return err + } + ctx, _ = contextutil.Detached(ctx) + run, err := e.temporalClient.ExecuteWorkflow( + ctx, + client.StartWorkflowOptions{ + ID: fmt.Sprintf("pools-deletion-%s", poolID.String()), + TaskQueue: defaultWorkerName, + WorkflowIDReusePolicy: enums.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE, + WorkflowExecutionErrorWhenAlreadyStarted: false, + SearchAttributes: map[string]interface{}{ + workflow.SearchAttributeStack: e.stack, + }, + }, + workflow.RunSendEvents, + workflow.SendEvents{ + PoolsDeletion: &poolID, + }, + ) + if err != nil { + return err + } + + if err := run.Get(ctx, nil); err != nil { + return err + } + + return nil +} + +func (e *engine) OnStart(ctx context.Context) error { + query := storage.NewListConnectorsQuery( + bunpaginate.NewPaginatedQueryOptions(storage.ConnectorQuery{}). + WithPageSize(100), + ) + + for { + connectors, err := e.storage.ConnectorsList(ctx, query) + if err != nil { + return err + } + + for _, connector := range connectors.Data { + if err := e.onStartPlugin(ctx, connector); err != nil { + return err + } + } + + if !connectors.HasMore { + break + } + + err = bunpaginate.UnmarshalCursor(connectors.Next, &query) + if err != nil { + return err + } + } + return nil +} + +func (e *engine) onStartPlugin(ctx context.Context, connector models.Connector) error { + err := e.plugins.RegisterPlugin(connector.ID) + if err != nil { + return err + } + + err = e.workers.AddWorker(connector.ID.String()) + if err != nil { + return err + } + + config := models.DefaultConfig() + if err := json.Unmarshal(connector.Config, &config); err != nil { + return err + } + + // Launch the workflow + _, err = e.temporalClient.ExecuteWorkflow( + ctx, + client.StartWorkflowOptions{ + ID: fmt.Sprintf("install-%s", connector.ID.String()), + TaskQueue: connector.ID.String(), + WorkflowIDReusePolicy: enums.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE, + WorkflowExecutionErrorWhenAlreadyStarted: false, + SearchAttributes: map[string]interface{}{ + workflow.SearchAttributeStack: e.stack, + }, + }, + workflow.RunInstallConnector, + workflow.InstallConnector{ + ConnectorID: connector.ID, + RawConfig: connector.Config, + Config: config, + }, + ) + if err != nil { + return err + } + + return nil +} + +var _ Engine = &engine{} diff --git a/components/payments/internal/connectors/engine/errors.go b/components/payments/internal/connectors/engine/errors.go new file mode 100644 index 0000000000..a9bff8ce17 --- /dev/null +++ b/components/payments/internal/connectors/engine/errors.go @@ -0,0 +1,24 @@ +package engine + +import ( + "github.com/formancehq/payments/internal/connectors/engine/plugins" + "github.com/pkg/errors" +) + +var ( + ErrValidation = errors.New("validation error") + ErrNotFound = errors.New("not found") +) + +func handlePluginError(err error) error { + if err == nil { + return nil + } + + switch { + case errors.Is(err, plugins.ErrNotFound): + return errors.Wrap(ErrNotFound, err.Error()) + default: + return err + } +} diff --git a/components/payments/internal/connectors/engine/module.go b/components/payments/internal/connectors/engine/module.go new file mode 100644 index 0000000000..a18a092d78 --- /dev/null +++ b/components/payments/internal/connectors/engine/module.go @@ -0,0 +1,78 @@ +package engine + +import ( + "context" + + "github.com/ThreeDotsLabs/watermill/message" + "github.com/formancehq/go-libs/logging" + "github.com/formancehq/go-libs/temporal" + "github.com/formancehq/payments/internal/connectors/engine/activities" + "github.com/formancehq/payments/internal/connectors/engine/plugins" + "github.com/formancehq/payments/internal/connectors/engine/webhooks" + "github.com/formancehq/payments/internal/connectors/engine/workflow" + "github.com/formancehq/payments/internal/events" + "github.com/formancehq/payments/internal/storage" + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/worker" + "go.uber.org/fx" +) + +func Module(pluginPath map[string]string, stack, stackURL string) fx.Option { + ret := []fx.Option{ + fx.Supply(worker.Options{}), + fx.Provide(func( + temporalClient client.Client, + workers *Workers, + plugins plugins.Plugins, + storage storage.Storage, + webhooks webhooks.Webhooks, + ) Engine { + return New(temporalClient, workers, plugins, storage, webhooks, stack) + }), + fx.Provide(func(publisher message.Publisher) *events.Events { + return events.New(publisher, stackURL) + }), + fx.Provide(func() plugins.Plugins { + return plugins.New(pluginPath) + }), + fx.Provide(func() webhooks.Webhooks { + return webhooks.New() + }), + fx.Provide(func(temporalClient client.Client, plugins plugins.Plugins, webhooks webhooks.Webhooks) workflow.Workflow { + return workflow.New(temporalClient, plugins, webhooks, stack) + }), + fx.Provide(func(storage storage.Storage, events *events.Events, plugins plugins.Plugins) activities.Activities { + return activities.New(storage, events, plugins) + }), + fx.Provide( + fx.Annotate(func( + logger logging.Logger, + temporalClient client.Client, + workflows, + activities []temporal.DefinitionSet, + options worker.Options, + ) *Workers { + return NewWorkers(logger, temporalClient, workflows, activities, options) + }, fx.ParamTags(``, ``, `group:"workflows"`, `group:"activities"`, ``)), + ), + fx.Invoke(func(lc fx.Lifecycle, engine Engine, workers *Workers) { + lc.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + return engine.OnStart(ctx) + }, + OnStop: func(ctx context.Context) error { + workers.Close() + return nil + }, + }) + }), + fx.Provide(fx.Annotate(func(a activities.Activities) temporal.DefinitionSet { + return a.DefinitionSet() + }, fx.ResultTags(`group:"activities"`))), + fx.Provide(fx.Annotate(func(workflow workflow.Workflow) temporal.DefinitionSet { + return workflow.DefinitionSet() + }, fx.ResultTags(`group:"workflows"`))), + } + + return fx.Options(ret...) +} diff --git a/components/payments/internal/connectors/engine/plugins/impl.go b/components/payments/internal/connectors/engine/plugins/impl.go new file mode 100644 index 0000000000..d9b47805b4 --- /dev/null +++ b/components/payments/internal/connectors/engine/plugins/impl.go @@ -0,0 +1,252 @@ +package plugins + +import ( + "context" + "errors" + + "github.com/formancehq/payments/internal/connectors/grpc" + "github.com/formancehq/payments/internal/connectors/grpc/proto/services" + "github.com/formancehq/payments/internal/models" +) + +type impl struct { + pluginClient grpc.PSP +} + +func (i *impl) Install(ctx context.Context, req models.InstallRequest) (models.InstallResponse, error) { + resp, err := i.pluginClient.Install(ctx, &services.InstallRequest{ + Config: req.Config, + }) + if err != nil { + return models.InstallResponse{}, err + } + + capabilities := make([]models.Capability, 0, len(resp.Capabilities)) + for _, capability := range resp.Capabilities { + capabilities = append(capabilities, models.Capability(capability)) + } + + webhooksConfigs := make([]models.PSPWebhookConfig, 0, len(resp.WebhooksConfigs)) + for _, webhook := range resp.WebhooksConfigs { + webhooksConfigs = append(webhooksConfigs, models.PSPWebhookConfig{ + Name: webhook.Name, + URLPath: webhook.UrlPath, + }) + } + + return models.InstallResponse{ + Capabilities: capabilities, + Workflow: grpc.TranslateProtoWorkflow(resp.Workflow), + WebhooksConfigs: webhooksConfigs, + }, nil +} + +func (i *impl) Uninstall(ctx context.Context, req models.UninstallRequest) (models.UninstallResponse, error) { + _, err := i.pluginClient.Uninstall(ctx, &services.UninstallRequest{ + ConnectorId: req.ConnectorID, + }) + if err != nil { + return models.UninstallResponse{}, err + } + + return models.UninstallResponse{}, nil +} + +func (i *impl) FetchNextAccounts(ctx context.Context, req models.FetchNextAccountsRequest) (models.FetchNextAccountsResponse, error) { + resp, err := i.pluginClient.FetchNextAccounts(ctx, &services.FetchNextAccountsRequest{ + FromPayload: req.FromPayload, + State: req.State, + PageSize: int64(req.PageSize), + }) + if err != nil { + return models.FetchNextAccountsResponse{}, err + } + + accounts := make([]models.PSPAccount, 0, len(resp.Accounts)) + for _, account := range resp.Accounts { + accounts = append(accounts, grpc.TranslateProtoAccount(account)) + } + + return models.FetchNextAccountsResponse{ + Accounts: accounts, + NewState: resp.NewState, + HasMore: resp.HasMore, + }, nil +} + +func (i *impl) FetchNextBalances(ctx context.Context, req models.FetchNextBalancesRequest) (models.FetchNextBalancesResponse, error) { + resp, err := i.pluginClient.FetchNextBalances(ctx, &services.FetchNextBalancesRequest{ + FromPayload: req.FromPayload, + State: req.State, + PageSize: int64(req.PageSize), + }) + if err != nil { + return models.FetchNextBalancesResponse{}, err + } + + balances := make([]models.PSPBalance, 0, len(resp.Balances)) + for _, balance := range resp.Balances { + b, err := grpc.TranslateProtoBalance(balance) + if err != nil { + return models.FetchNextBalancesResponse{}, err + } + balances = append(balances, b) + } + + return models.FetchNextBalancesResponse{ + Balances: balances, + NewState: resp.NewState, + HasMore: resp.HasMore, + }, nil +} + +func (i *impl) FetchNextExternalAccounts(ctx context.Context, req models.FetchNextExternalAccountsRequest) (models.FetchNextExternalAccountsResponse, error) { + resp, err := i.pluginClient.FetchNextExternalAccounts(ctx, &services.FetchNextExternalAccountsRequest{ + FromPayload: req.FromPayload, + State: req.State, + PageSize: int64(req.PageSize), + }) + if err != nil { + return models.FetchNextExternalAccountsResponse{}, err + } + + externalAccounts := make([]models.PSPAccount, 0, len(resp.Accounts)) + for _, account := range resp.Accounts { + externalAccounts = append(externalAccounts, grpc.TranslateProtoAccount(account)) + } + + return models.FetchNextExternalAccountsResponse{ + ExternalAccounts: externalAccounts, + NewState: resp.NewState, + HasMore: resp.HasMore, + }, nil +} + +func (i *impl) FetchNextPayments(ctx context.Context, req models.FetchNextPaymentsRequest) (models.FetchNextPaymentsResponse, error) { + resp, err := i.pluginClient.FetchNextPayments(ctx, &services.FetchNextPaymentsRequest{ + FromPayload: req.FromPayload, + State: req.State, + PageSize: int64(req.PageSize), + }) + if err != nil { + return models.FetchNextPaymentsResponse{}, err + } + + payments := make([]models.PSPPayment, 0, len(resp.Payments)) + for _, payment := range resp.Payments { + p, err := grpc.TranslateProtoPayment(payment) + if err != nil { + return models.FetchNextPaymentsResponse{}, err + } + payments = append(payments, p) + } + + return models.FetchNextPaymentsResponse{ + Payments: payments, + NewState: resp.NewState, + HasMore: resp.HasMore, + }, nil +} + +func (i *impl) FetchNextOthers(ctx context.Context, req models.FetchNextOthersRequest) (models.FetchNextOthersResponse, error) { + resp, err := i.pluginClient.FetchNextOthers(ctx, &services.FetchNextOthersRequest{ + Name: req.Name, + FromPayload: req.FromPayload, + State: req.State, + PageSize: int64(req.PageSize), + }) + if err != nil { + return models.FetchNextOthersResponse{}, err + } + + others := make([]models.PSPOther, 0, len(resp.Others)) + for _, other := range resp.Others { + others = append(others, models.PSPOther{ + ID: other.Id, + Other: other.Other, + }) + } + + return models.FetchNextOthersResponse{ + Others: others, + NewState: resp.NewState, + HasMore: resp.HasMore, + }, nil +} + +func (i *impl) CreateBankAccount(ctx context.Context, req models.CreateBankAccountRequest) (models.CreateBankAccountResponse, error) { + resp, err := i.pluginClient.CreateBankAccount(ctx, &services.CreateBankAccountRequest{ + BankAccount: grpc.TranslateBankAccount(req.BankAccount), + }) + if err != nil { + return models.CreateBankAccountResponse{}, err + } + + return models.CreateBankAccountResponse{ + RelatedAccount: grpc.TranslateProtoAccount(resp.RelatedAccount), + }, nil +} + +func (i *impl) CreateWebhooks(ctx context.Context, req models.CreateWebhooksRequest) (models.CreateWebhooksResponse, error) { + resp, err := i.pluginClient.CreateWebhooks(ctx, &services.CreateWebhooksRequest{ + ConnectorId: req.ConnectorID, + FromPayload: req.FromPayload, + }) + if err != nil { + return models.CreateWebhooksResponse{}, err + } + + others := make([]models.PSPOther, 0, len(resp.Others)) + for _, other := range resp.Others { + others = append(others, models.PSPOther{ + ID: other.Id, + Other: other.Other, + }) + } + + return models.CreateWebhooksResponse{ + Others: others, + }, nil +} + +func (i *impl) TranslateWebhook(ctx context.Context, req models.TranslateWebhookRequest) (models.TranslateWebhookResponse, error) { + resp, err := i.pluginClient.TranslateWebhook(ctx, &services.TranslateWebhookRequest{ + Name: req.Name, + Webhook: grpc.TranslateWebhook(req.Webhook), + }) + if err != nil { + return models.TranslateWebhookResponse{}, err + } + + responses := make([]models.WebhookResponse, 0, len(resp.Responses)) + for _, response := range resp.Responses { + r := models.WebhookResponse{ + IdempotencyKey: response.IdempotencyKey, + } + + switch v := response.Translated.(type) { + case *services.TranslateWebhookResponse_Response_Payment: + p, err := grpc.TranslateProtoPayment(v.Payment) + if err != nil { + return models.TranslateWebhookResponse{}, err + } + r.Payment = &p + case *services.TranslateWebhookResponse_Response_Account: + a := grpc.TranslateProtoAccount(v.Account) + r.Account = &a + case *services.TranslateWebhookResponse_Response_ExternalAccount: + a := grpc.TranslateProtoAccount(v.ExternalAccount) + r.ExternalAccount = &a + default: + return models.TranslateWebhookResponse{}, errors.New("unknown translated webhook type") + } + + responses = append(responses, r) + } + + return models.TranslateWebhookResponse{ + Responses: responses, + }, nil +} + +var _ models.Plugin = &impl{} diff --git a/components/payments/internal/connectors/engine/plugins/plugin.go b/components/payments/internal/connectors/engine/plugins/plugin.go new file mode 100644 index 0000000000..f4af6bebe0 --- /dev/null +++ b/components/payments/internal/connectors/engine/plugins/plugin.go @@ -0,0 +1,133 @@ +package plugins + +import ( + "fmt" + "os" + "os/exec" + "sync" + + "github.com/formancehq/payments/internal/connectors/grpc" + "github.com/formancehq/payments/internal/models" + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-plugin" + "github.com/pkg/errors" +) + +var ( + ErrNotFound = errors.New("plugin not found") +) + +type Plugins interface { + RegisterPlugin(connectorID models.ConnectorID) error + UnregisterPlugin(connectorID models.ConnectorID) error + Get(connectorID models.ConnectorID) (models.Plugin, error) +} + +// Will start, hold, manage and stop plugins +type plugins struct { + pluginsPath map[string]string + + plugins map[string]pluginInformation + rwMutex sync.RWMutex +} + +type pluginInformation struct { + client *plugin.Client +} + +func New(pluginsPath map[string]string) *plugins { + return &plugins{ + pluginsPath: pluginsPath, + plugins: make(map[string]pluginInformation), + } +} + +func (p *plugins) RegisterPlugin(connectorID models.ConnectorID) error { + p.rwMutex.Lock() + defer p.rwMutex.Unlock() + + // Check if plugin is already installed + _, ok := p.plugins[connectorID.String()] + if ok { + return nil + } + + pluginPath, ok := p.pluginsPath[connectorID.Provider] + if !ok { + return errors.Wrap(ErrNotFound, "plugin path not found") + } + + pc := plugin.NewClient(&plugin.ClientConfig{ + HandshakeConfig: grpc.Handshake, + Plugins: grpc.PluginMap, + Cmd: exec.Command("sh", "-c", pluginPath), + AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC}, + Logger: hclog.New(&hclog.LoggerOptions{ + Name: fmt.Sprintf("%s-%s", connectorID.Provider, connectorID.String()), + Output: os.Stdout, + Level: hclog.Debug, + }), + }) + + p.plugins[connectorID.String()] = pluginInformation{ + client: pc, + } + + return nil +} + +func (p *plugins) UnregisterPlugin(connectorID models.ConnectorID) error { + p.rwMutex.Lock() + defer p.rwMutex.Unlock() + + pluginInfo, ok := p.plugins[connectorID.String()] + if !ok { + // Nothing to do`` + return nil + } + + // Close the connection + pluginInfo.client.Kill() + + delete(p.plugins, connectorID.String()) + + return nil +} + +func (p *plugins) Get(connectorID models.ConnectorID) (models.Plugin, error) { + p.rwMutex.RLock() + defer p.rwMutex.RUnlock() + + pluginInfo, ok := p.plugins[connectorID.String()] + if !ok { + return nil, errors.New("plugin not found") + } + + return getPlugin(pluginInfo.client) +} + +func getPlugin(client *plugin.Client) (models.Plugin, error) { + // Connect via RPC + conn, err := client.Client() + if err != nil { + return nil, errors.Wrap(err, "failed to connect to plugin") + } + + raw, err := conn.Dispense("psp") + if err != nil { + return nil, errors.Wrap(err, "failed to dispense plugin") + } + + plugin, ok := raw.(grpc.PSP) + if !ok { + return nil, errors.New("failed to cast plugin") + } + + impl := &impl{ + pluginClient: plugin, + } + + return impl, nil +} + +var _ Plugins = &plugins{} diff --git a/components/payments/internal/connectors/engine/search_attributes.go b/components/payments/internal/connectors/engine/search_attributes.go new file mode 100644 index 0000000000..977eba0866 --- /dev/null +++ b/components/payments/internal/connectors/engine/search_attributes.go @@ -0,0 +1,14 @@ +package engine + +import ( + "github.com/formancehq/payments/internal/connectors/engine/workflow" + "go.temporal.io/api/enums/v1" +) + +var ( + SearchAttributes = map[string]enums.IndexedValueType{ + workflow.SearchAttributeWorkflowID: enums.INDEXED_VALUE_TYPE_KEYWORD, + workflow.SearchAttributeScheduleID: enums.INDEXED_VALUE_TYPE_KEYWORD, + workflow.SearchAttributeStack: enums.INDEXED_VALUE_TYPE_KEYWORD, + } +) diff --git a/components/payments/internal/connectors/engine/tracer.go b/components/payments/internal/connectors/engine/tracer.go new file mode 100644 index 0000000000..2aae55db72 --- /dev/null +++ b/components/payments/internal/connectors/engine/tracer.go @@ -0,0 +1,5 @@ +package engine + +import "go.opentelemetry.io/otel" + +var Tracer = otel.Tracer("connectors") diff --git a/components/payments/internal/connectors/engine/webhooks/webhooks.go b/components/payments/internal/connectors/engine/webhooks/webhooks.go new file mode 100644 index 0000000000..94be9e35cf --- /dev/null +++ b/components/payments/internal/connectors/engine/webhooks/webhooks.go @@ -0,0 +1,53 @@ +package webhooks + +import ( + "errors" + "sync" + + "github.com/formancehq/payments/internal/models" +) + +type Webhooks interface { + RegisterWebhooks(connectorID models.ConnectorID, webhooks []models.WebhookConfig) + UnregisterWebhooks(connectorID models.ConnectorID) + GetConfigs(connectorID models.ConnectorID, urlPath string) ([]models.WebhookConfig, error) +} + +type webhooks struct { + registeredWebhooksConfigs map[string][]models.WebhookConfig + rwMutex sync.RWMutex +} + +func New() *webhooks { + return &webhooks{ + registeredWebhooksConfigs: make(map[string][]models.WebhookConfig), + } +} + +func (w *webhooks) RegisterWebhooks(connectorID models.ConnectorID, webhooks []models.WebhookConfig) { + w.rwMutex.Lock() + defer w.rwMutex.Unlock() + + w.registeredWebhooksConfigs[connectorID.String()] = webhooks +} + +func (w *webhooks) UnregisterWebhooks(connectorID models.ConnectorID) { + w.rwMutex.Lock() + defer w.rwMutex.Unlock() + + delete(w.registeredWebhooksConfigs, connectorID.String()) +} + +func (w *webhooks) GetConfigs(connectorID models.ConnectorID, urlPath string) ([]models.WebhookConfig, error) { + w.rwMutex.RLock() + defer w.rwMutex.RUnlock() + + webhooksConfigs, ok := w.registeredWebhooksConfigs[connectorID.String()] + if !ok { + return nil, errors.New("connector not found") + } + + return webhooksConfigs, nil +} + +var _ Webhooks = &webhooks{} diff --git a/components/payments/internal/connectors/engine/workers.go b/components/payments/internal/connectors/engine/workers.go new file mode 100644 index 0000000000..156b8e6bac --- /dev/null +++ b/components/payments/internal/connectors/engine/workers.go @@ -0,0 +1,123 @@ +package engine + +import ( + "sync" + + "github.com/formancehq/go-libs/logging" + "github.com/formancehq/go-libs/temporal" + "go.temporal.io/sdk/activity" + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/worker" + temporalworkflow "go.temporal.io/sdk/workflow" +) + +const ( + defaultWorkerName = "default" +) + +type Workers struct { + logger logging.Logger + + temporalClient client.Client + + workers map[string]Worker + rwMutex sync.RWMutex + + workflows []temporal.DefinitionSet + activities []temporal.DefinitionSet + + options worker.Options +} + +type Worker struct { + worker worker.Worker +} + +func NewWorkers(logger logging.Logger, temporalClient client.Client, workflows, activities []temporal.DefinitionSet, options worker.Options) *Workers { + workers := &Workers{ + logger: logger, + temporalClient: temporalClient, + workers: make(map[string]Worker), + workflows: workflows, + activities: activities, + options: options, + } + + // For all operation outside of connectors handlers + workers.AddWorker(defaultWorkerName) + + return workers +} + +// Close is called when app is terminated +func (w *Workers) Close() { + w.rwMutex.Lock() + defer w.rwMutex.Unlock() + + for _, worker := range w.workers { + worker.worker.Stop() + } +} + +// Installing a new connector lauches a new worker +// A default one is instantiated when the workers struct is created +func (w *Workers) AddWorker(name string) error { + w.rwMutex.Lock() + defer w.rwMutex.Unlock() + + if _, ok := w.workers[name]; ok { + return nil + } + + worker := worker.New(w.temporalClient, name, w.options) + + for _, set := range w.workflows { + for _, workflow := range set { + worker.RegisterWorkflowWithOptions(workflow.Func, temporalworkflow.RegisterOptions{ + Name: workflow.Name, + }) + } + } + + for _, set := range w.activities { + for _, act := range set { + worker.RegisterActivityWithOptions(act.Func, activity.RegisterOptions{ + Name: act.Name, + }) + } + } + + go func() { + err := worker.Run(nil) + if err != nil { + w.logger.Errorf("worker loop stopped: %v", err) + } + }() + + w.workers[name] = Worker{ + worker: worker, + } + + w.logger.Infof("worker for connector %s started", name) + + return nil +} + +// Uninstalling a connector stops the worker +func (w *Workers) RemoveWorker(name string) error { + w.rwMutex.Lock() + defer w.rwMutex.Unlock() + + worker, ok := w.workers[name] + if !ok { + return nil + } + + worker.worker.Stop() + + delete(w.workers, name) + + w.logger.Infof("worker for connector %s removed", name) + + return nil +} diff --git a/components/payments/internal/connectors/engine/workflow/context.go b/components/payments/internal/connectors/engine/workflow/context.go new file mode 100644 index 0000000000..a39a422253 --- /dev/null +++ b/components/payments/internal/connectors/engine/workflow/context.go @@ -0,0 +1,26 @@ +package workflow + +import ( + "time" + + "go.temporal.io/sdk/temporal" + "go.temporal.io/sdk/workflow" +) + +const ( + ErrorCodeValidation = "VALIDATION" +) + +func infiniteRetryContext(ctx workflow.Context) workflow.Context { + return workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ + StartToCloseTimeout: 60 * time.Second, + RetryPolicy: &temporal.RetryPolicy{ + InitialInterval: time.Second, + BackoffCoefficient: 2, + MaximumInterval: 100 * time.Second, + NonRetryableErrorTypes: []string{ + ErrorCodeValidation, + }, + }, + }) +} diff --git a/components/payments/internal/connectors/engine/workflow/create_bank_account.go b/components/payments/internal/connectors/engine/workflow/create_bank_account.go new file mode 100644 index 0000000000..9ab9cc611e --- /dev/null +++ b/components/payments/internal/connectors/engine/workflow/create_bank_account.go @@ -0,0 +1,97 @@ +package workflow + +import ( + "github.com/formancehq/payments/internal/connectors/engine/activities" + "github.com/formancehq/payments/internal/models" + "github.com/google/uuid" + "go.temporal.io/api/enums/v1" + "go.temporal.io/sdk/workflow" +) + +type CreateBankAccount struct { + ConnectorID models.ConnectorID + BankAccountID uuid.UUID +} + +func (w Workflow) runCreateBankAccount( + ctx workflow.Context, + createBankAccount CreateBankAccount, +) (*models.BankAccount, error) { + bankAccount, err := activities.StorageBankAccountsGet( + infiniteRetryContext(ctx), + createBankAccount.BankAccountID, + true, + ) + if err != nil { + return nil, err + } + + createBAResponse, err := activities.PluginCreateBankAccount( + infiniteRetryContext(ctx), + createBankAccount.ConnectorID, + models.CreateBankAccountRequest{ + BankAccount: *bankAccount, + }, + ) + if err != nil { + return nil, err + } + + account := models.FromPSPAccount( + createBAResponse.RelatedAccount, + models.ACCOUNT_TYPE_EXTERNAL, + createBankAccount.ConnectorID, + ) + + err = activities.StorageAccountsStore( + infiniteRetryContext(ctx), + []models.Account{account}, + ) + if err != nil { + return nil, err + } + + relatedAccount := models.BankAccountRelatedAccount{ + BankAccountID: createBankAccount.BankAccountID, + AccountID: account.ID, + ConnectorID: createBankAccount.ConnectorID, + CreatedAt: createBAResponse.RelatedAccount.CreatedAt, + } + + err = activities.StorageBankAccountsAddRelatedAccount( + infiniteRetryContext(ctx), + relatedAccount, + ) + if err != nil { + return nil, err + } + + bankAccount.RelatedAccounts = append(bankAccount.RelatedAccounts, relatedAccount) + + if err := workflow.ExecuteChildWorkflow( + workflow.WithChildOptions( + ctx, + workflow.ChildWorkflowOptions{ + TaskQueue: relatedAccount.ConnectorID.String(), + ParentClosePolicy: enums.PARENT_CLOSE_POLICY_ABANDON, + SearchAttributes: map[string]interface{}{ + SearchAttributeStack: w.stack, + }, + }, + ), + RunSendEvents, + SendEvents{ + BankAccount: bankAccount, + }, + ).Get(ctx, nil); err != nil { + return nil, err + } + + return bankAccount, nil +} + +var RunCreateBankAccount any + +func init() { + RunCreateBankAccount = Workflow{}.runCreateBankAccount +} diff --git a/components/payments/internal/connectors/engine/workflow/create_webhooks.go b/components/payments/internal/connectors/engine/workflow/create_webhooks.go new file mode 100644 index 0000000000..ec3b73cbec --- /dev/null +++ b/components/payments/internal/connectors/engine/workflow/create_webhooks.go @@ -0,0 +1,78 @@ +package workflow + +import ( + "github.com/formancehq/payments/internal/connectors/engine/activities" + "github.com/formancehq/payments/internal/models" + "github.com/pkg/errors" + "go.temporal.io/api/enums/v1" + "go.temporal.io/sdk/workflow" +) + +type CreateWebhooks struct { + ConnectorID models.ConnectorID + Config models.Config + FromPayload *FromPayload +} + +func (w Workflow) runCreateWebhooks( + ctx workflow.Context, + createWebhooks CreateWebhooks, + nextTasks []models.TaskTree, +) error { + if err := w.createInstance(ctx, createWebhooks.ConnectorID); err != nil { + return errors.Wrap(err, "creating instance") + } + err := w.createWebhooks(ctx, createWebhooks, nextTasks) + return w.terminateInstance(ctx, createWebhooks.ConnectorID, err) +} + +func (w Workflow) createWebhooks( + ctx workflow.Context, + createWebhooks CreateWebhooks, + nextTasks []models.TaskTree, +) error { + resp, err := activities.PluginCreateWebhooks( + infiniteRetryContext(ctx), + createWebhooks.ConnectorID, + models.CreateWebhooksRequest{ + ConnectorID: createWebhooks.ConnectorID.String(), + FromPayload: createWebhooks.FromPayload.GetPayload(), + }, + ) + if err != nil { + return errors.Wrap(err, "failed to create webhooks") + } + + for _, other := range resp.Others { + if err := workflow.ExecuteChildWorkflow( + workflow.WithChildOptions( + ctx, + workflow.ChildWorkflowOptions{ + TaskQueue: createWebhooks.ConnectorID.String(), + ParentClosePolicy: enums.PARENT_CLOSE_POLICY_ABANDON, + SearchAttributes: map[string]interface{}{ + SearchAttributeStack: w.stack, + }, + }, + ), + Run, + createWebhooks.Config, + createWebhooks.ConnectorID, + &FromPayload{ + ID: other.ID, + Payload: other.Other, + }, + nextTasks, + ).Get(ctx, nil); err != nil { + return errors.Wrap(err, "running next workflow") + } + } + + return nil +} + +var RunCreateWebhooks any + +func init() { + RunCreateWebhooks = Workflow{}.runCreateWebhooks +} diff --git a/components/payments/internal/connectors/engine/workflow/fetch_accounts.go b/components/payments/internal/connectors/engine/workflow/fetch_accounts.go new file mode 100644 index 0000000000..a78c630b70 --- /dev/null +++ b/components/payments/internal/connectors/engine/workflow/fetch_accounts.go @@ -0,0 +1,174 @@ +package workflow + +import ( + "encoding/json" + "fmt" + + "github.com/formancehq/payments/internal/connectors/engine/activities" + "github.com/formancehq/payments/internal/models" + "github.com/pkg/errors" + "go.temporal.io/api/enums/v1" + "go.temporal.io/sdk/workflow" +) + +type FetchNextAccounts struct { + Config models.Config `json:"config"` + ConnectorID models.ConnectorID `json:"connectorID"` + FromPayload *FromPayload `json:"fromPayload"` +} + +func (w Workflow) runFetchNextAccounts( + ctx workflow.Context, + fetchNextAccount FetchNextAccounts, + nextTasks []models.TaskTree, +) error { + if err := w.createInstance(ctx, fetchNextAccount.ConnectorID); err != nil { + return errors.Wrap(err, "creating instance") + } + err := w.fetchAccounts(ctx, fetchNextAccount, nextTasks) + return w.terminateInstance(ctx, fetchNextAccount.ConnectorID, err) +} + +func (w Workflow) fetchAccounts( + ctx workflow.Context, + fetchNextAccount FetchNextAccounts, + nextTasks []models.TaskTree, +) error { + stateReference := models.CAPABILITY_FETCH_ACCOUNTS.String() + if fetchNextAccount.FromPayload != nil { + stateReference = fmt.Sprintf("%s-%s", models.CAPABILITY_FETCH_ACCOUNTS.String(), fetchNextAccount.FromPayload.ID) + } + + stateID := models.StateID{ + Reference: stateReference, + ConnectorID: fetchNextAccount.ConnectorID, + } + state, err := activities.StorageStatesGet(infiniteRetryContext(ctx), stateID) + if err != nil { + return fmt.Errorf("retrieving state %s: %v", stateID.String(), err) + } + + hasMore := true + for hasMore { + accountsResponse, err := activities.PluginFetchNextAccounts( + infiniteRetryContext(ctx), + fetchNextAccount.ConnectorID, + fetchNextAccount.FromPayload.GetPayload(), + state.State, + fetchNextAccount.Config.PageSize, + ) + if err != nil { + return errors.Wrap(err, "fetching next accounts") + } + + accounts := models.FromPSPAccounts( + accountsResponse.Accounts, + models.ACCOUNT_TYPE_INTERNAL, + fetchNextAccount.ConnectorID, + ) + + if len(accountsResponse.Accounts) > 0 { + err = activities.StorageAccountsStore( + infiniteRetryContext(ctx), + accounts, + ) + if err != nil { + return errors.Wrap(err, "storing next accounts") + } + } + + wg := workflow.NewWaitGroup(ctx) + errChan := make(chan error, len(accountsResponse.Accounts)*2) + for _, account := range accounts { + acc := account + + wg.Add(1) + workflow.Go(ctx, func(ctx workflow.Context) { + defer wg.Done() + + if err := workflow.ExecuteChildWorkflow( + workflow.WithChildOptions( + ctx, + workflow.ChildWorkflowOptions{ + TaskQueue: fetchNextAccount.ConnectorID.String(), + ParentClosePolicy: enums.PARENT_CLOSE_POLICY_ABANDON, + SearchAttributes: map[string]interface{}{ + SearchAttributeStack: w.stack, + }, + }, + ), + RunSendEvents, + SendEvents{ + Account: &acc, + }, + ).Get(ctx, nil); err != nil { + errChan <- errors.Wrap(err, "sending events") + } + }) + } + + for _, account := range accountsResponse.Accounts { + acc := account + + wg.Add(1) + workflow.Go(ctx, func(ctx workflow.Context) { + defer wg.Done() + + payload, err := json.Marshal(acc) + if err != nil { + errChan <- errors.Wrap(err, "marshalling account") + } + + if err := workflow.ExecuteChildWorkflow( + workflow.WithChildOptions( + ctx, + workflow.ChildWorkflowOptions{ + TaskQueue: fetchNextAccount.ConnectorID.String(), + ParentClosePolicy: enums.PARENT_CLOSE_POLICY_ABANDON, + SearchAttributes: map[string]interface{}{ + SearchAttributeStack: w.stack, + }, + }, + ), + Run, + fetchNextAccount.Config, + fetchNextAccount.ConnectorID, + &FromPayload{ + ID: acc.Reference, + Payload: payload, + }, + nextTasks, + ).Get(ctx, nil); err != nil { + errChan <- errors.Wrap(err, "running next workflow") + } + }) + } + + wg.Wait(ctx) + close(errChan) + for err := range errChan { + if err != nil { + return err + } + } + + state.State = accountsResponse.NewState + err = activities.StorageStatesStore( + infiniteRetryContext(ctx), + *state, + ) + if err != nil { + return errors.Wrap(err, "storing state") + } + + hasMore = accountsResponse.HasMore + } + + return nil +} + +var RunFetchNextAccounts any + +func init() { + RunFetchNextAccounts = Workflow{}.runFetchNextAccounts +} diff --git a/components/payments/internal/connectors/engine/workflow/fetch_balances.go b/components/payments/internal/connectors/engine/workflow/fetch_balances.go new file mode 100644 index 0000000000..786e4bac7f --- /dev/null +++ b/components/payments/internal/connectors/engine/workflow/fetch_balances.go @@ -0,0 +1,172 @@ +package workflow + +import ( + "encoding/json" + "fmt" + + "github.com/formancehq/payments/internal/connectors/engine/activities" + "github.com/formancehq/payments/internal/models" + "github.com/pkg/errors" + "go.temporal.io/api/enums/v1" + "go.temporal.io/sdk/workflow" +) + +type FetchNextBalances struct { + Config models.Config `json:"config"` + ConnectorID models.ConnectorID `json:"connectorID"` + FromPayload *FromPayload `json:"fromPayload"` +} + +func (w Workflow) runFetchNextBalances( + ctx workflow.Context, + fetchNextBalances FetchNextBalances, + nextTasks []models.TaskTree, +) error { + if err := w.createInstance(ctx, fetchNextBalances.ConnectorID); err != nil { + return errors.Wrap(err, "creating instance") + } + err := w.fetchBalances(ctx, fetchNextBalances, nextTasks) + return w.terminateInstance(ctx, fetchNextBalances.ConnectorID, err) +} + +func (w Workflow) fetchBalances( + ctx workflow.Context, + fetchNextBalances FetchNextBalances, + nextTasks []models.TaskTree, +) error { + stateReference := models.CAPABILITY_FETCH_BALANCES.String() + if fetchNextBalances.FromPayload != nil { + stateReference = fmt.Sprintf("%s-%s", models.CAPABILITY_FETCH_BALANCES.String(), fetchNextBalances.FromPayload.ID) + } + + stateID := models.StateID{ + Reference: stateReference, + ConnectorID: fetchNextBalances.ConnectorID, + } + state, err := activities.StorageStatesGet(infiniteRetryContext(ctx), stateID) + if err != nil { + return fmt.Errorf("retrieving state %s: %v", stateID.String(), err) + } + + hasMore := true + for hasMore { + balancesResponse, err := activities.PluginFetchNextBalances( + infiniteRetryContext(ctx), + fetchNextBalances.ConnectorID, + fetchNextBalances.FromPayload.GetPayload(), + state.State, + fetchNextBalances.Config.PageSize, + ) + if err != nil { + return errors.Wrap(err, "fetching next accounts") + } + + balances := models.FromPSPBalances( + balancesResponse.Balances, + fetchNextBalances.ConnectorID, + ) + if len(balancesResponse.Balances) > 0 { + err = activities.StorageBalancesStore( + infiniteRetryContext(ctx), + balances, + ) + if err != nil { + return errors.Wrap(err, "storing next accounts") + } + } + + wg := workflow.NewWaitGroup(ctx) + errChan := make(chan error, len(balancesResponse.Balances)*2) + for _, balance := range balances { + b := balance + + wg.Add(1) + workflow.Go(ctx, func(ctx workflow.Context) { + defer wg.Done() + + if err := workflow.ExecuteChildWorkflow( + workflow.WithChildOptions( + ctx, + workflow.ChildWorkflowOptions{ + TaskQueue: fetchNextBalances.ConnectorID.String(), + ParentClosePolicy: enums.PARENT_CLOSE_POLICY_ABANDON, + SearchAttributes: map[string]interface{}{ + SearchAttributeStack: w.stack, + }, + }, + ), + RunSendEvents, + SendEvents{ + Balance: &b, + }, + ).Get(ctx, nil); err != nil { + errChan <- errors.Wrap(err, "sending events") + } + }) + } + + for _, balance := range balancesResponse.Balances { + b := balance + + wg.Add(1) + workflow.Go(ctx, func(ctx workflow.Context) { + defer wg.Done() + + payload, err := json.Marshal(b) + if err != nil { + errChan <- errors.Wrap(err, "marshalling account") + } + + if err := workflow.ExecuteChildWorkflow( + workflow.WithChildOptions( + ctx, + workflow.ChildWorkflowOptions{ + TaskQueue: fetchNextBalances.ConnectorID.String(), + ParentClosePolicy: enums.PARENT_CLOSE_POLICY_ABANDON, + SearchAttributes: map[string]interface{}{ + SearchAttributeStack: w.stack, + }, + }, + ), + Run, + fetchNextBalances.Config, + fetchNextBalances.ConnectorID, + &FromPayload{ + ID: fmt.Sprintf("%s-balances", b.AccountReference), + Payload: payload, + }, + nextTasks, + ).Get(ctx, nil); err != nil { + errChan <- errors.Wrap(err, "running next workflow") + } + }) + } + + wg.Wait(ctx) + close(errChan) + for err := range errChan { + if err != nil { + return err + } + } + + state.State = balancesResponse.NewState + err = activities.StorageStatesStore( + infiniteRetryContext(ctx), + *state, + ) + if err != nil { + return errors.Wrap(err, "storing state") + } + + hasMore = balancesResponse.HasMore + } + + return nil +} + +var RunFetchNextBalances any + +func init() { + RunFetchNextBalances = Workflow{}.runFetchNextBalances +} diff --git a/components/payments/internal/connectors/engine/workflow/fetch_external_accounts.go b/components/payments/internal/connectors/engine/workflow/fetch_external_accounts.go new file mode 100644 index 0000000000..7758056cc4 --- /dev/null +++ b/components/payments/internal/connectors/engine/workflow/fetch_external_accounts.go @@ -0,0 +1,173 @@ +package workflow + +import ( + "encoding/json" + "fmt" + + "github.com/formancehq/payments/internal/connectors/engine/activities" + "github.com/formancehq/payments/internal/models" + "github.com/pkg/errors" + "go.temporal.io/api/enums/v1" + "go.temporal.io/sdk/workflow" +) + +type FetchNextExternalAccounts struct { + Config models.Config `json:"config"` + ConnectorID models.ConnectorID `json:"connectorID"` + FromPayload *FromPayload `json:"fromPayload"` +} + +func (w Workflow) runFetchNextExternalAccounts( + ctx workflow.Context, + fetchNextExternalAccount FetchNextExternalAccounts, + nextTasks []models.TaskTree, +) error { + if err := w.createInstance(ctx, fetchNextExternalAccount.ConnectorID); err != nil { + return errors.Wrap(err, "creating instance") + } + err := w.fetchExternalAccounts(ctx, fetchNextExternalAccount, nextTasks) + return w.terminateInstance(ctx, fetchNextExternalAccount.ConnectorID, err) +} + +func (w Workflow) fetchExternalAccounts( + ctx workflow.Context, + fetchNextExternalAccount FetchNextExternalAccounts, + nextTasks []models.TaskTree, +) error { + stateReference := models.CAPABILITY_FETCH_EXTERNAL_ACCOUNTS.String() + if fetchNextExternalAccount.FromPayload != nil { + stateReference = fmt.Sprintf("%s-%s", models.CAPABILITY_FETCH_EXTERNAL_ACCOUNTS.String(), fetchNextExternalAccount.FromPayload.ID) + } + + stateID := models.StateID{ + Reference: stateReference, + ConnectorID: fetchNextExternalAccount.ConnectorID, + } + state, err := activities.StorageStatesGet(infiniteRetryContext(ctx), stateID) + if err != nil { + return fmt.Errorf("retrieving state %s: %v", stateID.String(), err) + } + + hasMore := true + for hasMore { + externalAccountsResponse, err := activities.PluginFetchNextExternalAccounts( + infiniteRetryContext(ctx), + fetchNextExternalAccount.ConnectorID, + fetchNextExternalAccount.FromPayload.GetPayload(), + state.State, + fetchNextExternalAccount.Config.PageSize, + ) + if err != nil { + return errors.Wrap(err, "fetching next accounts") + } + + accounts := models.FromPSPAccounts( + externalAccountsResponse.ExternalAccounts, + models.ACCOUNT_TYPE_EXTERNAL, + fetchNextExternalAccount.ConnectorID, + ) + + if len(externalAccountsResponse.ExternalAccounts) > 0 { + err = activities.StorageAccountsStore( + infiniteRetryContext(ctx), + accounts, + ) + if err != nil { + return errors.Wrap(err, "storing next accounts") + } + } + + wg := workflow.NewWaitGroup(ctx) + errChan := make(chan error, len(externalAccountsResponse.ExternalAccounts)*2) + for _, externalAccount := range accounts { + acc := externalAccount + wg.Add(1) + workflow.Go(ctx, func(ctx workflow.Context) { + defer wg.Done() + + if err := workflow.ExecuteChildWorkflow( + workflow.WithChildOptions( + ctx, + workflow.ChildWorkflowOptions{ + TaskQueue: fetchNextExternalAccount.ConnectorID.String(), + ParentClosePolicy: enums.PARENT_CLOSE_POLICY_ABANDON, + SearchAttributes: map[string]interface{}{ + SearchAttributeStack: w.stack, + }, + }, + ), + RunSendEvents, + SendEvents{ + Account: &acc, + }, + ).Get(ctx, nil); err != nil { + errChan <- errors.Wrap(err, "sending events") + } + }) + } + + for _, externalAccount := range externalAccountsResponse.ExternalAccounts { + acc := externalAccount + + wg.Add(1) + workflow.Go(ctx, func(ctx workflow.Context) { + defer wg.Done() + + payload, err := json.Marshal(acc) + if err != nil { + errChan <- errors.Wrap(err, "marshalling external account") + } + + if err := workflow.ExecuteChildWorkflow( + workflow.WithChildOptions( + ctx, + workflow.ChildWorkflowOptions{ + TaskQueue: fetchNextExternalAccount.ConnectorID.String(), + ParentClosePolicy: enums.PARENT_CLOSE_POLICY_ABANDON, + SearchAttributes: map[string]interface{}{ + SearchAttributeStack: w.stack, + }, + }, + ), + Run, + fetchNextExternalAccount.Config, + fetchNextExternalAccount.ConnectorID, + &FromPayload{ + ID: acc.Reference, + Payload: payload, + }, + nextTasks, + ).Get(ctx, nil); err != nil { + errChan <- errors.Wrap(err, "running next workflow") + } + }) + } + + wg.Wait(ctx) + close(errChan) + for err := range errChan { + if err != nil { + return err + } + } + + state.State = externalAccountsResponse.NewState + err = activities.StorageStatesStore( + infiniteRetryContext(ctx), + *state, + ) + if err != nil { + return errors.Wrap(err, "storing state") + } + + hasMore = externalAccountsResponse.HasMore + } + + return nil +} + +var RunFetchNextExternalAccounts any + +func init() { + RunFetchNextExternalAccounts = Workflow{}.runFetchNextExternalAccounts +} diff --git a/components/payments/internal/connectors/engine/workflow/fetch_others.go b/components/payments/internal/connectors/engine/workflow/fetch_others.go new file mode 100644 index 0000000000..8d520ac2bd --- /dev/null +++ b/components/payments/internal/connectors/engine/workflow/fetch_others.go @@ -0,0 +1,126 @@ +package workflow + +import ( + "fmt" + + "github.com/formancehq/payments/internal/connectors/engine/activities" + "github.com/formancehq/payments/internal/models" + "github.com/pkg/errors" + "go.temporal.io/api/enums/v1" + "go.temporal.io/sdk/workflow" +) + +type FetchNextOthers struct { + Config models.Config `json:"config"` + ConnectorID models.ConnectorID `json:"connectorID"` + Name string `json:"name"` + FromPayload *FromPayload `json:"fromPayload"` +} + +func (w Workflow) runFetchNextOthers( + ctx workflow.Context, + fetchNextOthers FetchNextOthers, + nextTasks []models.TaskTree, +) error { + if err := w.createInstance(ctx, fetchNextOthers.ConnectorID); err != nil { + return errors.Wrap(err, "creating instance") + } + err := w.fetchNextOthers(ctx, fetchNextOthers, nextTasks) + return w.terminateInstance(ctx, fetchNextOthers.ConnectorID, err) +} + +func (w Workflow) fetchNextOthers( + ctx workflow.Context, + fetchNextOthers FetchNextOthers, + nextTasks []models.TaskTree, +) error { + stateReference := models.CAPABILITY_FETCH_OTHERS.String() + if fetchNextOthers.FromPayload != nil { + stateReference = fmt.Sprintf("%s-%s", models.CAPABILITY_FETCH_OTHERS.String(), fetchNextOthers.FromPayload.ID) + } + + stateID := models.StateID{ + Reference: stateReference, + ConnectorID: fetchNextOthers.ConnectorID, + } + state, err := activities.StorageStatesGet(infiniteRetryContext(ctx), stateID) + if err != nil { + return fmt.Errorf("retrieving state %s: %v", stateID.String(), err) + } + + hasMore := true + for hasMore { + othersResponse, err := activities.PluginFetchNextOthers( + infiniteRetryContext(ctx), + fetchNextOthers.ConnectorID, + fetchNextOthers.Name, + fetchNextOthers.FromPayload.GetPayload(), + state.State, + fetchNextOthers.Config.PageSize, + ) + if err != nil { + return errors.Wrap(err, "fetching next others") + } + + wg := workflow.NewWaitGroup(ctx) + errChan := make(chan error, len(othersResponse.Others)) + for _, other := range othersResponse.Others { + o := other + + wg.Add(1) + workflow.Go(ctx, func(ctx workflow.Context) { + defer wg.Done() + + if err := workflow.ExecuteChildWorkflow( + workflow.WithChildOptions( + ctx, + workflow.ChildWorkflowOptions{ + TaskQueue: fetchNextOthers.ConnectorID.String(), + ParentClosePolicy: enums.PARENT_CLOSE_POLICY_ABANDON, + SearchAttributes: map[string]interface{}{ + SearchAttributeStack: w.stack, + }, + }, + ), + Run, + fetchNextOthers.Config, + fetchNextOthers.ConnectorID, + &FromPayload{ + ID: o.ID, + Payload: o.Other, + }, + nextTasks, + ).Get(ctx, nil); err != nil { + errChan <- errors.Wrap(err, "running next workflow") + } + }) + } + + wg.Wait(ctx) + close(errChan) + for err := range errChan { + if err != nil { + return err + } + } + + state.State = othersResponse.NewState + err = activities.StorageStatesStore( + infiniteRetryContext(ctx), + *state, + ) + if err != nil { + return errors.Wrap(err, "storing state") + } + + hasMore = othersResponse.HasMore + } + + return nil +} + +var RunFetchNextOthers any + +func init() { + RunFetchNextOthers = Workflow{}.runFetchNextOthers +} diff --git a/components/payments/internal/connectors/engine/workflow/fetch_payments.go b/components/payments/internal/connectors/engine/workflow/fetch_payments.go new file mode 100644 index 0000000000..1c089dbaad --- /dev/null +++ b/components/payments/internal/connectors/engine/workflow/fetch_payments.go @@ -0,0 +1,173 @@ +package workflow + +import ( + "encoding/json" + "fmt" + + "github.com/formancehq/payments/internal/connectors/engine/activities" + "github.com/formancehq/payments/internal/models" + "github.com/pkg/errors" + "go.temporal.io/api/enums/v1" + "go.temporal.io/sdk/workflow" +) + +type FetchNextPayments struct { + Config models.Config `json:"config"` + ConnectorID models.ConnectorID `json:"connectorID"` + FromPayload *FromPayload `json:"fromPayload"` +} + +func (w Workflow) runFetchNextPayments( + ctx workflow.Context, + fetchNextPayments FetchNextPayments, + nextTasks []models.TaskTree, +) error { + if err := w.createInstance(ctx, fetchNextPayments.ConnectorID); err != nil { + return errors.Wrap(err, "creating instance") + } + err := w.fetchNextPayments(ctx, fetchNextPayments, nextTasks) + return w.terminateInstance(ctx, fetchNextPayments.ConnectorID, err) +} + +func (w Workflow) fetchNextPayments( + ctx workflow.Context, + fetchNextPayments FetchNextPayments, + nextTasks []models.TaskTree, +) error { + stateReference := models.CAPABILITY_FETCH_PAYMENTS.String() + if fetchNextPayments.FromPayload != nil { + stateReference = fmt.Sprintf("%s-%s", models.CAPABILITY_FETCH_PAYMENTS.String(), fetchNextPayments.FromPayload.ID) + } + + stateID := models.StateID{ + Reference: stateReference, + ConnectorID: fetchNextPayments.ConnectorID, + } + state, err := activities.StorageStatesGet(infiniteRetryContext(ctx), stateID) + if err != nil { + return fmt.Errorf("retrieving state %s: %v", stateID.String(), err) + } + + hasMore := true + for hasMore { + paymentsResponse, err := activities.PluginFetchNextPayments( + infiniteRetryContext(ctx), + fetchNextPayments.ConnectorID, + fetchNextPayments.FromPayload.GetPayload(), + state.State, + fetchNextPayments.Config.PageSize, + ) + if err != nil { + return errors.Wrap(err, "fetching next payments") + } + + payments := models.FromPSPPayments( + paymentsResponse.Payments, + fetchNextPayments.ConnectorID, + ) + + if len(paymentsResponse.Payments) > 0 { + err = activities.StoragePaymentsStore( + infiniteRetryContext(ctx), + payments, + ) + if err != nil { + return errors.Wrap(err, "storing next accounts") + } + } + + wg := workflow.NewWaitGroup(ctx) + errChan := make(chan error, len(paymentsResponse.Payments)*2) + for _, payment := range payments { + p := payment + + wg.Add(1) + workflow.Go(ctx, func(ctx workflow.Context) { + defer wg.Done() + + if err := workflow.ExecuteChildWorkflow( + workflow.WithChildOptions( + ctx, + workflow.ChildWorkflowOptions{ + TaskQueue: fetchNextPayments.ConnectorID.String(), + ParentClosePolicy: enums.PARENT_CLOSE_POLICY_ABANDON, + SearchAttributes: map[string]interface{}{ + SearchAttributeStack: w.stack, + }, + }, + ), + RunSendEvents, + SendEvents{ + Payment: &p, + }, + ).Get(ctx, nil); err != nil { + errChan <- errors.Wrap(err, "sending events") + } + }) + } + + for _, payment := range paymentsResponse.Payments { + p := payment + + wg.Add(1) + workflow.Go(ctx, func(ctx workflow.Context) { + defer wg.Done() + + payload, err := json.Marshal(p) + if err != nil { + errChan <- errors.Wrap(err, "marshalling payment") + } + + if err := workflow.ExecuteChildWorkflow( + workflow.WithChildOptions( + ctx, + workflow.ChildWorkflowOptions{ + TaskQueue: fetchNextPayments.ConnectorID.String(), + ParentClosePolicy: enums.PARENT_CLOSE_POLICY_ABANDON, + SearchAttributes: map[string]interface{}{ + SearchAttributeStack: w.stack, + }, + }, + ), + Run, + fetchNextPayments.Config, + fetchNextPayments.ConnectorID, + &FromPayload{ + ID: p.Reference, + Payload: payload, + }, + nextTasks, + ).Get(ctx, nil); err != nil { + errChan <- errors.Wrap(err, "running next workflow") + } + }) + } + + wg.Wait(ctx) + close(errChan) + for err := range errChan { + if err != nil { + return err + } + } + + state.State = paymentsResponse.NewState + err = activities.StorageStatesStore( + infiniteRetryContext(ctx), + *state, + ) + if err != nil { + return errors.Wrap(err, "storing state") + } + + hasMore = paymentsResponse.HasMore + } + + return nil +} + +var RunFetchNextPayments any + +func init() { + RunFetchNextPayments = Workflow{}.runFetchNextPayments +} diff --git a/components/payments/internal/connectors/engine/workflow/handle_webhooks.go b/components/payments/internal/connectors/engine/workflow/handle_webhooks.go new file mode 100644 index 0000000000..7c715e9500 --- /dev/null +++ b/components/payments/internal/connectors/engine/workflow/handle_webhooks.go @@ -0,0 +1,143 @@ +package workflow + +import ( + "fmt" + + "github.com/formancehq/payments/internal/connectors/engine/activities" + "github.com/formancehq/payments/internal/models" + "github.com/pkg/errors" + "go.temporal.io/api/enums/v1" + "go.temporal.io/sdk/temporal" + "go.temporal.io/sdk/workflow" +) + +type HandleWebhooks struct { + ConnectorID models.ConnectorID + WebhookConfig models.WebhookConfig + Webhook models.Webhook +} + +func (w Workflow) runHandleWebhooks( + ctx workflow.Context, + handleWebhooks HandleWebhooks, +) error { + err := activities.StorageWebhooksStore(infiniteRetryContext(ctx), handleWebhooks.Webhook) + if err != nil { + return errors.Wrap(err, "failed to store webhook") + } + + resp, err := activities.PluginTranslateWebhook( + infiniteRetryContext(ctx), + handleWebhooks.ConnectorID, + models.TranslateWebhookRequest{ + Name: handleWebhooks.WebhookConfig.Name, + Webhook: models.PSPWebhook{ + QueryValues: handleWebhooks.Webhook.QueryValues, + Headers: handleWebhooks.Webhook.Headers, + Body: handleWebhooks.Webhook.Body, + }, + }, + ) + if err != nil { + return errors.Wrap(err, "failed to translate webhook") + } + + for _, response := range resp.Responses { + if err := workflow.ExecuteChildWorkflow( + workflow.WithChildOptions( + ctx, + workflow.ChildWorkflowOptions{ + WorkflowID: fmt.Sprintf("store-webhook-%s-%s", handleWebhooks.ConnectorID.String(), response.IdempotencyKey), + TaskQueue: handleWebhooks.ConnectorID.String(), + ParentClosePolicy: enums.PARENT_CLOSE_POLICY_ABANDON, + WorkflowIDReusePolicy: enums.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY, + SearchAttributes: map[string]interface{}{ + SearchAttributeStack: w.stack, + }, + }, + ), + RunStoreWebhookTranslation, + StoreWebhookTranslation{ + ConnectorID: handleWebhooks.ConnectorID, + Account: response.Account, + ExternalAccount: response.ExternalAccount, + Payment: response.Payment, + }, + ).Get(ctx, nil); err != nil { + applicationError := &temporal.ApplicationError{} + if errors.As(err, &applicationError) { + if applicationError.Type() != "ChildWorkflowExecutionAlreadyStartedError" { + return err + } + } else { + return errors.Wrap(err, "running store workflow") + } + } + } + + return nil +} + +var RunHandleWebhooks any + +type StoreWebhookTranslation struct { + ConnectorID models.ConnectorID + Account *models.PSPAccount + ExternalAccount *models.PSPAccount + Payment *models.PSPPayment +} + +func (w Workflow) runStoreWebhookTranslation( + ctx workflow.Context, + storeWebhookTranslation StoreWebhookTranslation, +) error { + if storeWebhookTranslation.Account != nil { + err := activities.StorageAccountsStore( + infiniteRetryContext(ctx), + models.FromPSPAccounts( + []models.PSPAccount{*storeWebhookTranslation.Account}, + models.ACCOUNT_TYPE_INTERNAL, + storeWebhookTranslation.ConnectorID, + ), + ) + if err != nil { + return errors.Wrap(err, "storing next accounts") + } + } + + if storeWebhookTranslation.ExternalAccount != nil { + err := activities.StorageAccountsStore( + infiniteRetryContext(ctx), + models.FromPSPAccounts( + []models.PSPAccount{*storeWebhookTranslation.ExternalAccount}, + models.ACCOUNT_TYPE_EXTERNAL, + storeWebhookTranslation.ConnectorID, + ), + ) + if err != nil { + return errors.Wrap(err, "storing next accounts") + } + } + + if storeWebhookTranslation.Payment != nil { + err := activities.StoragePaymentsStore( + infiniteRetryContext(ctx), + models.FromPSPPayments( + []models.PSPPayment{*storeWebhookTranslation.Payment}, + storeWebhookTranslation.ConnectorID, + ), + ) + if err != nil { + return errors.Wrap(err, "storing next accounts") + } + } + + return nil +} + +var RunStoreWebhookTranslation any + +func init() { + RunHandleWebhooks = Workflow{}.runHandleWebhooks + RunStoreWebhookTranslation = Workflow{}.runStoreWebhookTranslation +} diff --git a/components/payments/internal/connectors/engine/workflow/install_connector.go b/components/payments/internal/connectors/engine/workflow/install_connector.go new file mode 100644 index 0000000000..4444040a92 --- /dev/null +++ b/components/payments/internal/connectors/engine/workflow/install_connector.go @@ -0,0 +1,97 @@ +package workflow + +import ( + "encoding/json" + "fmt" + + "github.com/formancehq/payments/internal/connectors/engine/activities" + "github.com/formancehq/payments/internal/models" + "github.com/pkg/errors" + "go.temporal.io/api/enums/v1" + "go.temporal.io/sdk/temporal" + "go.temporal.io/sdk/workflow" +) + +type InstallConnector struct { + ConnectorID models.ConnectorID + Config models.Config + RawConfig json.RawMessage +} + +func (w Workflow) runInstallConnector( + ctx workflow.Context, + installConnector InstallConnector, +) error { + // Second step: install the connector via the plugin and get the list of + // capabilities and the workflow of polling data + installResponse, err := activities.PluginInstallConnector( + infiniteRetryContext(ctx), + installConnector.ConnectorID, + installConnector.RawConfig, + ) + if err != nil { + return errors.Wrap(err, "failed to install connector") + } + + // Third step: store the workflow of the connector + err = activities.StorageTasksTreeStore(infiniteRetryContext(ctx), installConnector.ConnectorID, installResponse.Workflow) + if err != nil { + return errors.Wrap(err, "failed to store tasks tree") + } + + if len(installResponse.WebhooksConfigs) > 0 { + configs := make([]models.WebhookConfig, 0, len(installResponse.WebhooksConfigs)) + for _, webhookConfig := range installResponse.WebhooksConfigs { + configs = append(configs, models.WebhookConfig{ + Name: webhookConfig.Name, + ConnectorID: installConnector.ConnectorID, + URLPath: webhookConfig.URLPath, + }) + } + // TODO(polo): store the capabilities + err = activities.StorageWebhooksConfigsStore(infiniteRetryContext(ctx), configs) + if err != nil { + return errors.Wrap(err, "failed to store webhooks configs") + } + + w.webhooks.RegisterWebhooks(installConnector.ConnectorID, configs) + } + + // Fourth step: launch the workflow tree + if err := workflow.ExecuteChildWorkflow( + workflow.WithChildOptions( + ctx, + workflow.ChildWorkflowOptions{ + WorkflowID: fmt.Sprintf("run-tasks-%s", installConnector.ConnectorID.String()), + WorkflowIDReusePolicy: enums.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY, + TaskQueue: installConnector.ConnectorID.String(), + ParentClosePolicy: enums.PARENT_CLOSE_POLICY_ABANDON, + SearchAttributes: map[string]interface{}{ + SearchAttributeStack: w.stack, + }, + }, + ), + Run, + installConnector.Config, + installConnector.ConnectorID, + nil, + []models.TaskTree(installResponse.Workflow), + ).Get(ctx, nil); err != nil { + applicationError := &temporal.ApplicationError{} + if errors.As(err, &applicationError) { + if applicationError.Type() != "ChildWorkflowExecutionAlreadyStartedError" { + return err + } + } else { + return errors.Wrap(err, "running next workflow") + } + } + + return nil +} + +var RunInstallConnector any + +func init() { + RunInstallConnector = Workflow{}.runInstallConnector +} diff --git a/components/payments/internal/connectors/engine/workflow/instances.go b/components/payments/internal/connectors/engine/workflow/instances.go new file mode 100644 index 0000000000..d08da52cea --- /dev/null +++ b/components/payments/internal/connectors/engine/workflow/instances.go @@ -0,0 +1,97 @@ +package workflow + +import ( + "encoding/json" + + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/connectors/engine/activities" + "github.com/formancehq/payments/internal/models" + "github.com/pkg/errors" + "go.temporal.io/sdk/workflow" +) + +var ( + errNotFromSchedule = errors.New("not from schedule") +) + +func (w Workflow) createInstance( + ctx workflow.Context, + connectorID models.ConnectorID, +) error { + info := workflow.GetInfo(ctx) + + scheduleID, err := getPaymentScheduleID(info) + if err != nil { + if errors.Is(err, errNotFromSchedule) { + return nil + } + return err + } + + instance := models.Instance{ + ID: info.WorkflowExecution.ID, + ScheduleID: scheduleID, + ConnectorID: connectorID, + CreatedAt: workflow.Now(ctx).UTC(), + UpdatedAt: workflow.Now(ctx).UTC(), + Terminated: false, + } + + return activities.StorageInstancesStore(infiniteRetryContext(ctx), instance) +} + +func (w Workflow) terminateInstance( + ctx workflow.Context, + connectorID models.ConnectorID, + terminateError error, +) error { + info := workflow.GetInfo(ctx) + + scheduleID, err := getPaymentScheduleID(info) + if err != nil { + if errors.Is(err, errNotFromSchedule) { + return nil + } + return err + } + + var errMessage *string + if terminateError != nil { + errMessage = pointer.For(terminateError.Error()) + } + + now := workflow.Now(ctx).UTC() + + instance := models.Instance{ + ID: info.WorkflowExecution.ID, + ScheduleID: scheduleID, + ConnectorID: connectorID, + UpdatedAt: now, + Terminated: true, + TerminatedAt: &now, + Error: errMessage, + } + + return activities.StorageInstancesUpdate(infiniteRetryContext(ctx), instance) +} + +func getPaymentScheduleID( + info *workflow.Info, +) (string, error) { + attributes := info.SearchAttributes.GetIndexedFields() + if attributes == nil { + return "", errNotFromSchedule + } + + v, ok := attributes[SearchAttributeScheduleID] + if !ok || v == nil { + return "", errNotFromSchedule + } + + var scheduleID string + if err := json.Unmarshal(v.Data, &scheduleID); err != nil { + return "", errors.Wrap(err, "unmarshalling schedule ID") + } + + return scheduleID, nil +} diff --git a/components/payments/internal/connectors/engine/workflow/plugin_workflow.go b/components/payments/internal/connectors/engine/workflow/plugin_workflow.go new file mode 100644 index 0000000000..e9ff6d7106 --- /dev/null +++ b/components/payments/internal/connectors/engine/workflow/plugin_workflow.go @@ -0,0 +1,179 @@ +package workflow + +import ( + "context" + "fmt" + + "github.com/formancehq/payments/internal/connectors/engine/activities" + "github.com/formancehq/payments/internal/models" + "github.com/pkg/errors" + "go.temporal.io/api/enums/v1" + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/temporal" + "go.temporal.io/sdk/workflow" +) + +func (w Workflow) run( + ctx workflow.Context, + config models.Config, + connectorID models.ConnectorID, + fromPayload *FromPayload, + taskTree []models.TaskTree, +) error { + var nextWorkflow interface{} + var request interface{} + var capability models.Capability + for _, task := range taskTree { + switch task.TaskType { + case models.TASK_FETCH_ACCOUNTS: + req := FetchNextAccounts{ + Config: config, + ConnectorID: connectorID, + FromPayload: fromPayload, + } + + nextWorkflow = RunFetchNextAccounts + request = req + capability = models.CAPABILITY_FETCH_ACCOUNTS + + case models.TASK_FETCH_EXTERNAL_ACCOUNTS: + req := FetchNextExternalAccounts{ + Config: config, + ConnectorID: connectorID, + FromPayload: fromPayload, + } + + nextWorkflow = RunFetchNextExternalAccounts + request = req + capability = models.CAPABILITY_FETCH_EXTERNAL_ACCOUNTS + + case models.TASK_FETCH_OTHERS: + req := FetchNextOthers{ + Config: config, + ConnectorID: connectorID, + Name: task.Name, + FromPayload: fromPayload, + } + + nextWorkflow = RunFetchNextOthers + request = req + capability = models.CAPABILITY_FETCH_OTHERS + + case models.TASK_FETCH_PAYMENTS: + req := FetchNextPayments{ + Config: config, + ConnectorID: connectorID, + FromPayload: fromPayload, + } + + nextWorkflow = RunFetchNextPayments + request = req + capability = models.CAPABILITY_FETCH_PAYMENTS + + case models.TASK_FETCH_BALANCES: + req := FetchNextBalances{ + Config: config, + ConnectorID: connectorID, + FromPayload: fromPayload, + } + + nextWorkflow = RunFetchNextBalances + request = req + capability = models.CAPABILITY_FETCH_BALANCES + + case models.TASK_CREATE_WEBHOOKS: + req := CreateWebhooks{ + Config: config, + ConnectorID: connectorID, + FromPayload: fromPayload, + } + + nextWorkflow = RunCreateWebhooks + request = req + capability = models.CAPABILITY_WEBHOOKS + + default: + return fmt.Errorf("unknown task type: %v", task.TaskType) + } + + if task.Periodically { + // Schedule next workflow every polling duration + // TODO(polo): context + var scheduleID string + if fromPayload == nil { + scheduleID = fmt.Sprintf("%s-%s", connectorID.String(), capability.String()) + } else { + scheduleID = fmt.Sprintf("%s-%s-%s", connectorID.String(), capability.String(), fromPayload.ID) + } + scheduleHandle, err := w.temporalClient.ScheduleClient().Create(context.Background(), client.ScheduleOptions{ + ID: scheduleID, + Spec: client.ScheduleSpec{ + Intervals: []client.ScheduleIntervalSpec{ + { + Every: config.PollingPeriod, + }, + }, + }, + Action: &client.ScheduleWorkflowAction{ + Workflow: nextWorkflow, + Args: []interface{}{ + request, + task.NextTasks, + }, + TaskQueue: connectorID.String(), + // Search attributes are used to query workflows + TypedSearchAttributes: temporal.NewSearchAttributes( + temporal.NewSearchAttributeKeyKeyword(SearchAttributeScheduleID).ValueSet(scheduleID), + temporal.NewSearchAttributeKeyKeyword(SearchAttributeStack).ValueSet(w.stack), + ), + }, + Overlap: enums.SCHEDULE_OVERLAP_POLICY_SKIP, + TriggerImmediately: true, + SearchAttributes: map[string]any{ + SearchAttributeScheduleID: scheduleID, + SearchAttributeStack: w.stack, + }, + }) + if err != nil { + return err + } + + err = activities.StorageSchedulesStore( + infiniteRetryContext(ctx), + models.Schedule{ + ID: scheduleHandle.GetID(), + ConnectorID: connectorID, + CreatedAt: workflow.Now(ctx).UTC(), + }) + if err != nil { + return err + } + } else { + // Run next workflow immediately + if err := workflow.ExecuteChildWorkflow( + workflow.WithChildOptions( + ctx, + workflow.ChildWorkflowOptions{ + TaskQueue: connectorID.String(), + ParentClosePolicy: enums.PARENT_CLOSE_POLICY_ABANDON, + SearchAttributes: map[string]interface{}{ + SearchAttributeStack: w.stack, + }, + }, + ), + nextWorkflow, + request, + task.NextTasks, + ).GetChildWorkflowExecution().Get(ctx, nil); err != nil { + return errors.Wrap(err, "running next workflow") + } + } + } + return nil +} + +var Run any + +func init() { + Run = Workflow{}.run +} diff --git a/components/payments/internal/connectors/engine/workflow/send_events.go b/components/payments/internal/connectors/engine/workflow/send_events.go new file mode 100644 index 0000000000..56032a1a8c --- /dev/null +++ b/components/payments/internal/connectors/engine/workflow/send_events.go @@ -0,0 +1,104 @@ +package workflow + +import ( + "github.com/formancehq/payments/internal/connectors/engine/activities" + "github.com/formancehq/payments/internal/models" + "github.com/google/uuid" + "go.temporal.io/sdk/workflow" +) + +type SendEvents struct { + Account *models.Account + Balance *models.Balance + BankAccount *models.BankAccount + Payment *models.Payment + ConnectorReset *models.ConnectorID + PoolsCreation *models.Pool + PoolsDeletion *uuid.UUID +} + +func (w Workflow) runSendEvents( + ctx workflow.Context, + sendEvents SendEvents, +) error { + if sendEvents.Account != nil { + err := activities.EventsSendAccount( + infiniteRetryContext(ctx), + *sendEvents.Account, + ) + if err != nil { + return err + } + } + + if sendEvents.Balance != nil { + err := activities.EventsSendBalance( + infiniteRetryContext(ctx), + *sendEvents.Balance, + ) + if err != nil { + return err + } + } + + if sendEvents.BankAccount != nil { + err := activities.EventsSendBankAccount( + infiniteRetryContext(ctx), + *sendEvents.BankAccount, + ) + if err != nil { + return err + } + } + + if sendEvents.Payment != nil { + for _, adjustment := range sendEvents.Payment.Adjustments { + err := activities.EventsSendPayment( + infiniteRetryContext(ctx), + *sendEvents.Payment, + adjustment, + ) + if err != nil { + return err + } + } + } + + if sendEvents.ConnectorReset != nil { + err := activities.EventsSendConnectorReset( + infiniteRetryContext(ctx), + *sendEvents.ConnectorReset, + ) + if err != nil { + return err + } + } + + if sendEvents.PoolsCreation != nil { + err := activities.EventsSendPoolCreation( + infiniteRetryContext(ctx), + *sendEvents.PoolsCreation, + ) + if err != nil { + return err + } + } + + if sendEvents.PoolsDeletion != nil { + err := activities.EventsSendPoolDeletion( + infiniteRetryContext(ctx), + *sendEvents.PoolsDeletion, + ) + if err != nil { + return err + } + } + + return nil +} + +var RunSendEvents any + +func init() { + RunSendEvents = Workflow{}.runSendEvents +} diff --git a/components/payments/internal/connectors/engine/workflow/terminate_schedules.go b/components/payments/internal/connectors/engine/workflow/terminate_schedules.go new file mode 100644 index 0000000000..317edd9750 --- /dev/null +++ b/components/payments/internal/connectors/engine/workflow/terminate_schedules.go @@ -0,0 +1,72 @@ +package workflow + +import ( + "context" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/query" + "github.com/formancehq/payments/internal/connectors/engine/activities" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/storage" + "go.temporal.io/sdk/workflow" +) + +type TerminateSchedules struct { + ConnectorID models.ConnectorID +} + +func (w Workflow) runTerminateSchedules( + ctx workflow.Context, + terminateSchedules TerminateSchedules, +) error { + query := storage.NewListSchedulesQuery( + bunpaginate.NewPaginatedQueryOptions(storage.ScheduleQuery{}). + WithPageSize(100). + WithQueryBuilder( + query.Match("connector_id", terminateSchedules.ConnectorID.String()), + ), + ) + for { + schedules, err := activities.StorageSchedulesList(infiniteRetryContext(ctx), query) + if err != nil { + return err + } + + wg := workflow.NewWaitGroup(ctx) + + for _, schedule := range schedules.Data { + s := schedule + wg.Add(1) + workflow.Go(ctx, func(ctx workflow.Context) { + defer wg.Done() + // TODO(polo): context.Background() ? + scheduleHandler := w.temporalClient.ScheduleClient().GetHandle(context.Background(), s.ID) + if err := scheduleHandler.Delete(context.Background()); err != nil { + // TODO(polo): log error but continue + _ = err + } + }) + + // TODO(polo): delete workflow execution ? + } + + wg.Wait(ctx) + + if !schedules.HasMore { + break + } + + err = bunpaginate.UnmarshalCursor(schedules.Next, &query) + if err != nil { + return err + } + } + + return nil +} + +var RunTerminateSchedules any + +func init() { + RunTerminateSchedules = Workflow{}.runTerminateSchedules +} diff --git a/components/payments/internal/connectors/engine/workflow/uninstall_connector.go b/components/payments/internal/connectors/engine/workflow/uninstall_connector.go new file mode 100644 index 0000000000..88125f0280 --- /dev/null +++ b/components/payments/internal/connectors/engine/workflow/uninstall_connector.go @@ -0,0 +1,132 @@ +package workflow + +import ( + "github.com/formancehq/payments/internal/connectors/engine/activities" + "github.com/formancehq/payments/internal/models" + "github.com/pkg/errors" + "go.temporal.io/api/enums/v1" + "go.temporal.io/sdk/workflow" +) + +type UninstallConnector struct { + ConnectorID models.ConnectorID +} + +func (w Workflow) runUninstallConnector( + ctx workflow.Context, + uninstallConnector UninstallConnector, +) error { + if err := workflow.ExecuteChildWorkflow( + workflow.WithChildOptions( + ctx, + workflow.ChildWorkflowOptions{ + TaskQueue: uninstallConnector.ConnectorID.String(), + ParentClosePolicy: enums.PARENT_CLOSE_POLICY_ABANDON, + SearchAttributes: map[string]interface{}{ + SearchAttributeStack: w.stack, + }, + }, + ), + RunTerminateSchedules, + TerminateSchedules{ + ConnectorID: uninstallConnector.ConnectorID, + }, + ).Get(ctx, nil); err != nil { + return errors.Wrap(err, "terminate schedules") + } + + wg := workflow.NewWaitGroup(ctx) + errChan := make(chan error, 16) + + wg.Add(1) + workflow.Go(ctx, func(ctx workflow.Context) { + defer wg.Done() + _, err := activities.PluginUninstallConnector(infiniteRetryContext(ctx), uninstallConnector.ConnectorID) + errChan <- err + }) + + wg.Add(1) + workflow.Go(ctx, func(ctx workflow.Context) { + defer wg.Done() + err := activities.StorageSchedulesDelete(infiniteRetryContext(ctx), uninstallConnector.ConnectorID) + errChan <- err + }) + + wg.Add(1) + workflow.Go(ctx, func(ctx workflow.Context) { + defer wg.Done() + err := activities.StorageInstancesDelete(infiniteRetryContext(ctx), uninstallConnector.ConnectorID) + errChan <- err + }) + + wg.Add(1) + workflow.Go(ctx, func(ctx workflow.Context) { + defer wg.Done() + err := activities.StorageTasksTreeDelete(infiniteRetryContext(ctx), uninstallConnector.ConnectorID) + errChan <- err + }) + + wg.Add(1) + workflow.Go(ctx, func(ctx workflow.Context) { + defer wg.Done() + err := activities.StorageBankAccountsDeleteRelatedAccounts(infiniteRetryContext(ctx), uninstallConnector.ConnectorID) + errChan <- err + }) + + wg.Add(1) + workflow.Go(ctx, func(ctx workflow.Context) { + defer wg.Done() + err := activities.StorageAccountsDelete(infiniteRetryContext(ctx), uninstallConnector.ConnectorID) + errChan <- err + }) + + wg.Add(1) + workflow.Go(ctx, func(ctx workflow.Context) { + defer wg.Done() + err := activities.StoragePaymentsDelete(infiniteRetryContext(ctx), uninstallConnector.ConnectorID) + errChan <- err + }) + + wg.Add(1) + workflow.Go(ctx, func(ctx workflow.Context) { + defer wg.Done() + err := activities.StorageStatesDelete(infiniteRetryContext(ctx), uninstallConnector.ConnectorID) + errChan <- err + }) + + wg.Add(1) + workflow.Go(ctx, func(ctx workflow.Context) { + defer wg.Done() + err := activities.StorageWebhooksConfigsDelete(infiniteRetryContext(ctx), uninstallConnector.ConnectorID) + errChan <- err + }) + + wg.Add(1) + workflow.Go(ctx, func(ctx workflow.Context) { + defer wg.Done() + err := activities.StorageWebhooksDelete(infiniteRetryContext(ctx), uninstallConnector.ConnectorID) + errChan <- err + }) + + wg.Wait(ctx) + close(errChan) + + for err := range errChan { + if err != nil { + return err + } + } + + err := activities.StorageConnectorsDelete(infiniteRetryContext(ctx), uninstallConnector.ConnectorID) + if err != nil { + return err + } + + return nil +} + +var RunUninstallConnector any + +func init() { + RunUninstallConnector = Workflow{}.runUninstallConnector +} diff --git a/components/payments/internal/connectors/engine/workflow/workflow.go b/components/payments/internal/connectors/engine/workflow/workflow.go new file mode 100644 index 0000000000..6c0efe3219 --- /dev/null +++ b/components/payments/internal/connectors/engine/workflow/workflow.go @@ -0,0 +1,106 @@ +package workflow + +import ( + "encoding/json" + + temporalworker "github.com/formancehq/go-libs/temporal" + "github.com/formancehq/payments/internal/connectors/engine/plugins" + "github.com/formancehq/payments/internal/connectors/engine/webhooks" + "go.temporal.io/sdk/client" +) + +const ( + SearchAttributeWorkflowID = "PaymentWorkflowID" + SearchAttributeScheduleID = "PaymentScheduleID" + SearchAttributeStack = "Stack" +) + +type FromPayload struct { + ID string `json:"id"` + Payload json.RawMessage `json:"payload"` +} + +func (f *FromPayload) GetPayload() json.RawMessage { + if f == nil { + return nil + } + return f.Payload +} + +type Workflow struct { + temporalClient client.Client + + plugins plugins.Plugins + webhooks webhooks.Webhooks + + stack string +} + +func New(temporalClient client.Client, plugins plugins.Plugins, webhooks webhooks.Webhooks, stack string) Workflow { + return Workflow{ + temporalClient: temporalClient, + plugins: plugins, + webhooks: webhooks, + stack: stack, + } +} + +func (w Workflow) DefinitionSet() temporalworker.DefinitionSet { + return temporalworker.NewDefinitionSet(). + Append(temporalworker.Definition{ + Name: "FetchAccounts", + Func: w.runFetchNextAccounts, + }). + Append(temporalworker.Definition{ + Name: "FetchBalances", + Func: w.runFetchNextBalances, + }). + Append(temporalworker.Definition{ + Name: "FetchExternalAccounts", + Func: w.runFetchNextExternalAccounts, + }). + Append(temporalworker.Definition{ + Name: "FetchOthers", + Func: w.runFetchNextOthers, + }). + Append(temporalworker.Definition{ + Name: "FetchPayments", + Func: w.runFetchNextPayments, + }). + Append(temporalworker.Definition{ + Name: "TerminateSchedules", + Func: w.runTerminateSchedules, + }). + Append(temporalworker.Definition{ + Name: "InstallConnector", + Func: w.runInstallConnector, + }). + Append(temporalworker.Definition{ + Name: "UninstallConnector", + Func: w.runUninstallConnector, + }). + Append(temporalworker.Definition{ + Name: "CreateBankAccount", + Func: w.runCreateBankAccount, + }). + Append(temporalworker.Definition{ + Name: "Run", + Func: w.run, + }). + Append(temporalworker.Definition{ + Name: "RunCreateWebhooks", + Func: w.runCreateWebhooks, + }). + Append(temporalworker.Definition{ + Name: "RunHandleWebhooks", + Func: w.runHandleWebhooks, + }). + Append(temporalworker.Definition{ + Name: "RunStoreWebhookTranslation", + Func: w.runStoreWebhookTranslation, + }). + Append(temporalworker.Definition{ + Name: "RunSendEvents", + Func: w.runSendEvents, + }) +} diff --git a/components/payments/internal/connectors/grpc/grpc_client.go b/components/payments/internal/connectors/grpc/grpc_client.go new file mode 100644 index 0000000000..26fc2ee127 --- /dev/null +++ b/components/payments/internal/connectors/grpc/grpc_client.go @@ -0,0 +1,53 @@ +package grpc + +import ( + "context" + + "github.com/formancehq/payments/internal/connectors/grpc/proto/services" +) + +type GRPCClient struct { + client services.PluginClient +} + +func (c *GRPCClient) Install(ctx context.Context, req *services.InstallRequest) (*services.InstallResponse, error) { + return c.client.Install(ctx, req) +} + +func (c *GRPCClient) Uninstall(ctx context.Context, req *services.UninstallRequest) (*services.UninstallResponse, error) { + return c.client.Uninstall(ctx, req) +} + +func (c *GRPCClient) FetchNextAccounts(ctx context.Context, req *services.FetchNextAccountsRequest) (*services.FetchNextAccountsResponse, error) { + return c.client.FetchNextAccounts(ctx, req) +} + +func (c *GRPCClient) FetchNextPayments(ctx context.Context, req *services.FetchNextPaymentsRequest) (*services.FetchNextPaymentsResponse, error) { + return c.client.FetchNextPayments(ctx, req) +} + +func (c *GRPCClient) FetchNextExternalAccounts(ctx context.Context, req *services.FetchNextExternalAccountsRequest) (*services.FetchNextExternalAccountsResponse, error) { + return c.client.FetchNextExternalAccounts(ctx, req) +} + +func (c *GRPCClient) FetchNextBalances(ctx context.Context, req *services.FetchNextBalancesRequest) (*services.FetchNextBalancesResponse, error) { + return c.client.FetchNextBalances(ctx, req) +} + +func (c *GRPCClient) FetchNextOthers(ctx context.Context, req *services.FetchNextOthersRequest) (*services.FetchNextOthersResponse, error) { + return c.client.FetchNextOthers(ctx, req) +} + +func (c *GRPCClient) CreateBankAccount(ctx context.Context, req *services.CreateBankAccountRequest) (*services.CreateBankAccountResponse, error) { + return c.client.CreateBankAccount(ctx, req) +} + +func (c *GRPCClient) CreateWebhooks(ctx context.Context, req *services.CreateWebhooksRequest) (*services.CreateWebhooksResponse, error) { + return c.client.CreateWebhooks(ctx, req) +} + +func (c *GRPCClient) TranslateWebhook(ctx context.Context, req *services.TranslateWebhookRequest) (*services.TranslateWebhookResponse, error) { + return c.client.TranslateWebhook(ctx, req) +} + +var _ PSP = &GRPCClient{} diff --git a/components/payments/internal/connectors/grpc/grpc_server.go b/components/payments/internal/connectors/grpc/grpc_server.go new file mode 100644 index 0000000000..13e05ac4e9 --- /dev/null +++ b/components/payments/internal/connectors/grpc/grpc_server.go @@ -0,0 +1,55 @@ +package grpc + +import ( + "context" + + "github.com/formancehq/payments/internal/connectors/grpc/proto/services" +) + +var _ services.PluginServer = &GRPCServer{} + +type GRPCServer struct { + services.UnimplementedPluginServer + // This is the real implementation + Impl PSP +} + +func (s *GRPCServer) Install(ctx context.Context, req *services.InstallRequest) (*services.InstallResponse, error) { + return s.Impl.Install(ctx, req) +} + +func (s *GRPCServer) Uninstall(ctx context.Context, req *services.UninstallRequest) (*services.UninstallResponse, error) { + return s.Impl.Uninstall(ctx, req) +} + +func (s *GRPCServer) FetchNextAccounts(ctx context.Context, req *services.FetchNextAccountsRequest) (*services.FetchNextAccountsResponse, error) { + return s.Impl.FetchNextAccounts(ctx, req) +} + +func (s *GRPCServer) FetchNextExternalAccounts(ctx context.Context, req *services.FetchNextExternalAccountsRequest) (*services.FetchNextExternalAccountsResponse, error) { + return s.Impl.FetchNextExternalAccounts(ctx, req) +} + +func (s *GRPCServer) FetchNextPayments(ctx context.Context, req *services.FetchNextPaymentsRequest) (*services.FetchNextPaymentsResponse, error) { + return s.Impl.FetchNextPayments(ctx, req) +} + +func (s *GRPCServer) FetchNextBalances(ctx context.Context, req *services.FetchNextBalancesRequest) (*services.FetchNextBalancesResponse, error) { + return s.Impl.FetchNextBalances(ctx, req) +} + +func (s *GRPCServer) FetchNextOthers(ctx context.Context, req *services.FetchNextOthersRequest) (*services.FetchNextOthersResponse, error) { + return s.Impl.FetchNextOthers(ctx, req) +} + +func (s *GRPCServer) CreateBankAccount(ctx context.Context, req *services.CreateBankAccountRequest) (*services.CreateBankAccountResponse, error) { + return s.Impl.CreateBankAccount(ctx, req) +} + +func (s *GRPCServer) CreateWebhooks(ctx context.Context, req *services.CreateWebhooksRequest) (*services.CreateWebhooksResponse, error) { + return s.Impl.CreateWebhooks(ctx, req) +} + +func (s *GRPCServer) TranslateWebhook(ctx context.Context, req *services.TranslateWebhookRequest) (*services.TranslateWebhookResponse, error) { + return s.Impl.TranslateWebhook(ctx, req) +} diff --git a/components/payments/internal/connectors/grpc/interfaces.go b/components/payments/internal/connectors/grpc/interfaces.go new file mode 100644 index 0000000000..5f51e72c7f --- /dev/null +++ b/components/payments/internal/connectors/grpc/interfaces.go @@ -0,0 +1,57 @@ +package grpc + +import ( + "context" + "os" + + "github.com/formancehq/payments/internal/connectors/grpc/proto/services" + "github.com/hashicorp/go-plugin" + "google.golang.org/grpc" +) + +type PSP interface { + Install(ctx context.Context, in *services.InstallRequest) (*services.InstallResponse, error) + Uninstall(ctx context.Context, in *services.UninstallRequest) (*services.UninstallResponse, error) + + FetchNextOthers(ctx context.Context, in *services.FetchNextOthersRequest) (*services.FetchNextOthersResponse, error) + FetchNextPayments(ctx context.Context, in *services.FetchNextPaymentsRequest) (*services.FetchNextPaymentsResponse, error) + FetchNextAccounts(ctx context.Context, in *services.FetchNextAccountsRequest) (*services.FetchNextAccountsResponse, error) + FetchNextBalances(ctx context.Context, in *services.FetchNextBalancesRequest) (*services.FetchNextBalancesResponse, error) + FetchNextExternalAccounts(ctx context.Context, in *services.FetchNextExternalAccountsRequest) (*services.FetchNextExternalAccountsResponse, error) + + CreateBankAccount(ctx context.Context, in *services.CreateBankAccountRequest) (*services.CreateBankAccountResponse, error) + + CreateWebhooks(ctx context.Context, in *services.CreateWebhooksRequest) (*services.CreateWebhooksResponse, error) + TranslateWebhook(ctx context.Context, in *services.TranslateWebhookRequest) (*services.TranslateWebhookResponse, error) +} + +type PSPGRPCPlugin struct { + // GRPCPlugin must still implement the Plugin interface + plugin.Plugin + // Concrete implementation, written in Go. This is only used for plugins + // that are written in Go. + Impl PSP +} + +func (p *PSPGRPCPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error { + services.RegisterPluginServer(s, &GRPCServer{Impl: p.Impl}) + return nil +} + +func (p *PSPGRPCPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { + return &GRPCClient{client: services.NewPluginClient(c)}, nil +} + +var PluginMap = map[string]plugin.Plugin{ + "psp": &PSPGRPCPlugin{}, +} + +var _ plugin.GRPCPlugin = &PSPGRPCPlugin{} + +// Handshake is a common handshake that is shared by plugin and host. +var Handshake = plugin.HandshakeConfig{ + // This isn't required when using VersionedPlugins + ProtocolVersion: 1, + MagicCookieKey: "PLUGIN_KEY", + MagicCookieValue: os.Getenv("PLUGIN_MAGIC_COOKIE"), +} diff --git a/components/payments/internal/connectors/grpc/proto/account.pb.go b/components/payments/internal/connectors/grpc/proto/account.pb.go new file mode 100644 index 0000000000..cc28d052f1 --- /dev/null +++ b/components/payments/internal/connectors/grpc/proto/account.pb.go @@ -0,0 +1,243 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.30.0 +// protoc v3.21.12 +// source: account.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + wrapperspb "google.golang.org/protobuf/types/known/wrapperspb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Represents an internal account in Formance +type Account struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // PSP reference of the account + // Example for stripe: acc_xxxxxxxx + Reference string `protobuf:"bytes,1,opt,name=reference,proto3" json:"reference,omitempty"` + // Human readable name of the account (if existing) + Name *wrapperspb.StringValue `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + // Account's creation date + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + SyncedAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=synced_at,json=syncedAt,proto3" json:"synced_at,omitempty"` + // If provided, the default asset of the account. + // Example: USD/2 + DefaultAsset *wrapperspb.StringValue `protobuf:"bytes,5,opt,name=default_asset,json=defaultAsset,proto3" json:"default_asset,omitempty"` + // Additional metadata + Metadata map[string]string `protobuf:"bytes,6,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + // The PSP raw message + Raw []byte `protobuf:"bytes,7,opt,name=raw,proto3" json:"raw,omitempty"` +} + +func (x *Account) Reset() { + *x = Account{} + if protoimpl.UnsafeEnabled { + mi := &file_account_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Account) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Account) ProtoMessage() {} + +func (x *Account) ProtoReflect() protoreflect.Message { + mi := &file_account_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Account.ProtoReflect.Descriptor instead. +func (*Account) Descriptor() ([]byte, []int) { + return file_account_proto_rawDescGZIP(), []int{0} +} + +func (x *Account) GetReference() string { + if x != nil { + return x.Reference + } + return "" +} + +func (x *Account) GetName() *wrapperspb.StringValue { + if x != nil { + return x.Name + } + return nil +} + +func (x *Account) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *Account) GetSyncedAt() *timestamppb.Timestamp { + if x != nil { + return x.SyncedAt + } + return nil +} + +func (x *Account) GetDefaultAsset() *wrapperspb.StringValue { + if x != nil { + return x.DefaultAsset + } + return nil +} + +func (x *Account) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *Account) GetRaw() []byte { + if x != nil { + return x.Raw + } + return nil +} + +var File_account_proto protoreflect.FileDescriptor + +var file_account_proto_rawDesc = []byte{ + 0x0a, 0x0d, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x27, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, + 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, + 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, + 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xbb, 0x03, 0x0a, 0x07, 0x41, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, + 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, + 0x6e, 0x63, 0x65, 0x12, 0x30, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, + 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, + 0x12, 0x37, 0x0a, 0x09, 0x73, 0x79, 0x6e, 0x63, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x08, 0x73, 0x79, 0x6e, 0x63, 0x65, 0x64, 0x41, 0x74, 0x12, 0x41, 0x0a, 0x0d, 0x64, 0x65, 0x66, + 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0c, + 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x12, 0x5a, 0x0a, 0x08, + 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3e, + 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, + 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, + 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, + 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x61, 0x77, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x72, 0x61, 0x77, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, + 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x3f, 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x68, 0x71, + 0x2f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2f, 0x67, 0x72, + 0x70, 0x63, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_account_proto_rawDescOnce sync.Once + file_account_proto_rawDescData = file_account_proto_rawDesc +) + +func file_account_proto_rawDescGZIP() []byte { + file_account_proto_rawDescOnce.Do(func() { + file_account_proto_rawDescData = protoimpl.X.CompressGZIP(file_account_proto_rawDescData) + }) + return file_account_proto_rawDescData +} + +var file_account_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_account_proto_goTypes = []interface{}{ + (*Account)(nil), // 0: formance.payments.connectors.grpc.proto.Account + nil, // 1: formance.payments.connectors.grpc.proto.Account.MetadataEntry + (*wrapperspb.StringValue)(nil), // 2: google.protobuf.StringValue + (*timestamppb.Timestamp)(nil), // 3: google.protobuf.Timestamp +} +var file_account_proto_depIdxs = []int32{ + 2, // 0: formance.payments.connectors.grpc.proto.Account.name:type_name -> google.protobuf.StringValue + 3, // 1: formance.payments.connectors.grpc.proto.Account.created_at:type_name -> google.protobuf.Timestamp + 3, // 2: formance.payments.connectors.grpc.proto.Account.synced_at:type_name -> google.protobuf.Timestamp + 2, // 3: formance.payments.connectors.grpc.proto.Account.default_asset:type_name -> google.protobuf.StringValue + 1, // 4: formance.payments.connectors.grpc.proto.Account.metadata:type_name -> formance.payments.connectors.grpc.proto.Account.MetadataEntry + 5, // [5:5] is the sub-list for method output_type + 5, // [5:5] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_account_proto_init() } +func file_account_proto_init() { + if File_account_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_account_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Account); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_account_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_account_proto_goTypes, + DependencyIndexes: file_account_proto_depIdxs, + MessageInfos: file_account_proto_msgTypes, + }.Build() + File_account_proto = out.File + file_account_proto_rawDesc = nil + file_account_proto_goTypes = nil + file_account_proto_depIdxs = nil +} diff --git a/components/payments/internal/connectors/grpc/proto/account.proto b/components/payments/internal/connectors/grpc/proto/account.proto new file mode 100644 index 0000000000..2fb0f00756 --- /dev/null +++ b/components/payments/internal/connectors/grpc/proto/account.proto @@ -0,0 +1,30 @@ +syntax = "proto3"; +package formance.payments.connectors.grpc.proto; +option go_package = "github.com/formancehq/payments/internal/connectors/grpc/proto"; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/wrappers.proto"; + +// Represents an internal account in Formance +message Account { + // PSP reference of the account + // Example for stripe: acc_xxxxxxxx + string reference = 1; + + // Human readable name of the account (if existing) + google.protobuf.StringValue name = 2; + + // Account's creation date + google.protobuf.Timestamp created_at = 3; + google.protobuf.Timestamp synced_at = 4; + + // If provided, the default asset of the account. + // Example: USD/2 + google.protobuf.StringValue default_asset = 5; + + // Additional metadata + map metadata = 6; + + // The PSP raw message + bytes raw = 7; +} \ No newline at end of file diff --git a/components/payments/internal/connectors/grpc/proto/balance.pb.go b/components/payments/internal/connectors/grpc/proto/balance.pb.go new file mode 100644 index 0000000000..bb7f123da1 --- /dev/null +++ b/components/payments/internal/connectors/grpc/proto/balance.pb.go @@ -0,0 +1,182 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.30.0 +// protoc v3.21.12 +// source: balance.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Balance struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + AccountReference string `protobuf:"bytes,1,opt,name=account_reference,json=accountReference,proto3" json:"account_reference,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + Balance *Monetary `protobuf:"bytes,3,opt,name=balance,proto3" json:"balance,omitempty"` +} + +func (x *Balance) Reset() { + *x = Balance{} + if protoimpl.UnsafeEnabled { + mi := &file_balance_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Balance) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Balance) ProtoMessage() {} + +func (x *Balance) ProtoReflect() protoreflect.Message { + mi := &file_balance_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Balance.ProtoReflect.Descriptor instead. +func (*Balance) Descriptor() ([]byte, []int) { + return file_balance_proto_rawDescGZIP(), []int{0} +} + +func (x *Balance) GetAccountReference() string { + if x != nil { + return x.AccountReference + } + return "" +} + +func (x *Balance) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *Balance) GetBalance() *Monetary { + if x != nil { + return x.Balance + } + return nil +} + +var File_balance_proto protoreflect.FileDescriptor + +var file_balance_proto_rawDesc = []byte{ + 0x0a, 0x0d, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x27, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, + 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, + 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0e, 0x6d, 0x6f, 0x6e, 0x65, 0x74, + 0x61, 0x72, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xbe, 0x01, 0x0a, 0x07, 0x42, 0x61, + 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x2b, 0x0a, 0x11, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x5f, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x10, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, + 0x63, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x4b, 0x0a, + 0x07, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x31, + 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, + 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, + 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4d, 0x6f, 0x6e, 0x65, 0x74, 0x61, 0x72, + 0x79, 0x52, 0x07, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x42, 0x3f, 0x5a, 0x3d, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, + 0x65, 0x68, 0x71, 0x2f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, + 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, +} + +var ( + file_balance_proto_rawDescOnce sync.Once + file_balance_proto_rawDescData = file_balance_proto_rawDesc +) + +func file_balance_proto_rawDescGZIP() []byte { + file_balance_proto_rawDescOnce.Do(func() { + file_balance_proto_rawDescData = protoimpl.X.CompressGZIP(file_balance_proto_rawDescData) + }) + return file_balance_proto_rawDescData +} + +var file_balance_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_balance_proto_goTypes = []interface{}{ + (*Balance)(nil), // 0: formance.payments.connectors.grpc.proto.Balance + (*timestamppb.Timestamp)(nil), // 1: google.protobuf.Timestamp + (*Monetary)(nil), // 2: formance.payments.connectors.grpc.proto.Monetary +} +var file_balance_proto_depIdxs = []int32{ + 1, // 0: formance.payments.connectors.grpc.proto.Balance.created_at:type_name -> google.protobuf.Timestamp + 2, // 1: formance.payments.connectors.grpc.proto.Balance.balance:type_name -> formance.payments.connectors.grpc.proto.Monetary + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_balance_proto_init() } +func file_balance_proto_init() { + if File_balance_proto != nil { + return + } + file_monetary_proto_init() + if !protoimpl.UnsafeEnabled { + file_balance_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Balance); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_balance_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_balance_proto_goTypes, + DependencyIndexes: file_balance_proto_depIdxs, + MessageInfos: file_balance_proto_msgTypes, + }.Build() + File_balance_proto = out.File + file_balance_proto_rawDesc = nil + file_balance_proto_goTypes = nil + file_balance_proto_depIdxs = nil +} diff --git a/components/payments/internal/connectors/grpc/proto/balance.proto b/components/payments/internal/connectors/grpc/proto/balance.proto new file mode 100644 index 0000000000..e37273c4cd --- /dev/null +++ b/components/payments/internal/connectors/grpc/proto/balance.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; +package formance.payments.connectors.grpc.proto; +option go_package = "github.com/formancehq/payments/internal/connectors/grpc/proto"; + +import "google/protobuf/timestamp.proto"; + +import "monetary.proto"; + +message Balance { + string account_reference = 1; + + google.protobuf.Timestamp created_at = 2; + + formance.payments.connectors.grpc.proto.Monetary balance = 3; +} \ No newline at end of file diff --git a/components/payments/internal/connectors/grpc/proto/bank_account.pb.go b/components/payments/internal/connectors/grpc/proto/bank_account.pb.go new file mode 100644 index 0000000000..25fca418af --- /dev/null +++ b/components/payments/internal/connectors/grpc/proto/bank_account.pb.go @@ -0,0 +1,257 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.30.0 +// protoc v3.21.12 +// source: bank_account.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + wrapperspb "google.golang.org/protobuf/types/known/wrapperspb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Represents a bank account created on the formance platform +type BankAccount struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // ID of the bank account. Must be an uuid. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // Bank Account's creation date + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + // Bank Account's name + Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` + // Optional: Account number of the bank account + AccountNumber *wrapperspb.StringValue `protobuf:"bytes,4,opt,name=account_number,json=accountNumber,proto3" json:"account_number,omitempty"` + // Optional: iban of the bank account + Iban *wrapperspb.StringValue `protobuf:"bytes,5,opt,name=iban,proto3" json:"iban,omitempty"` + // Optional: swift code of the bank account + SwiftBicCode *wrapperspb.StringValue `protobuf:"bytes,6,opt,name=swift_bic_code,json=swiftBicCode,proto3" json:"swift_bic_code,omitempty"` + // Optional: country of the bank account + Country *wrapperspb.StringValue `protobuf:"bytes,7,opt,name=country,proto3" json:"country,omitempty"` + // Additional metadata + Metadata map[string]string `protobuf:"bytes,8,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *BankAccount) Reset() { + *x = BankAccount{} + if protoimpl.UnsafeEnabled { + mi := &file_bank_account_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *BankAccount) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BankAccount) ProtoMessage() {} + +func (x *BankAccount) ProtoReflect() protoreflect.Message { + mi := &file_bank_account_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BankAccount.ProtoReflect.Descriptor instead. +func (*BankAccount) Descriptor() ([]byte, []int) { + return file_bank_account_proto_rawDescGZIP(), []int{0} +} + +func (x *BankAccount) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *BankAccount) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *BankAccount) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *BankAccount) GetAccountNumber() *wrapperspb.StringValue { + if x != nil { + return x.AccountNumber + } + return nil +} + +func (x *BankAccount) GetIban() *wrapperspb.StringValue { + if x != nil { + return x.Iban + } + return nil +} + +func (x *BankAccount) GetSwiftBicCode() *wrapperspb.StringValue { + if x != nil { + return x.SwiftBicCode + } + return nil +} + +func (x *BankAccount) GetCountry() *wrapperspb.StringValue { + if x != nil { + return x.Country + } + return nil +} + +func (x *BankAccount) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +var File_bank_account_proto protoreflect.FileDescriptor + +var file_bank_account_proto_rawDesc = []byte{ + 0x0a, 0x12, 0x62, 0x61, 0x6e, 0x6b, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x27, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, + 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, + 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, + 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xfc, + 0x03, 0x0a, 0x0b, 0x42, 0x61, 0x6e, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x0e, + 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x39, + 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, + 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x43, 0x0a, + 0x0e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x52, 0x0d, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x4e, 0x75, 0x6d, 0x62, + 0x65, 0x72, 0x12, 0x30, 0x0a, 0x04, 0x69, 0x62, 0x61, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x04, + 0x69, 0x62, 0x61, 0x6e, 0x12, 0x42, 0x0a, 0x0e, 0x73, 0x77, 0x69, 0x66, 0x74, 0x5f, 0x62, 0x69, + 0x63, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, + 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0c, 0x73, 0x77, 0x69, 0x66, + 0x74, 0x42, 0x69, 0x63, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x36, 0x0a, 0x07, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x72, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, + 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, + 0x12, 0x5e, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x08, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x42, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x42, 0x61, 0x6e, + 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x3f, 0x5a, + 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x6f, 0x72, 0x6d, + 0x61, 0x6e, 0x63, 0x65, 0x68, 0x71, 0x2f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2f, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x6f, 0x72, 0x73, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_bank_account_proto_rawDescOnce sync.Once + file_bank_account_proto_rawDescData = file_bank_account_proto_rawDesc +) + +func file_bank_account_proto_rawDescGZIP() []byte { + file_bank_account_proto_rawDescOnce.Do(func() { + file_bank_account_proto_rawDescData = protoimpl.X.CompressGZIP(file_bank_account_proto_rawDescData) + }) + return file_bank_account_proto_rawDescData +} + +var file_bank_account_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_bank_account_proto_goTypes = []interface{}{ + (*BankAccount)(nil), // 0: formance.payments.connectors.grpc.proto.BankAccount + nil, // 1: formance.payments.connectors.grpc.proto.BankAccount.MetadataEntry + (*timestamppb.Timestamp)(nil), // 2: google.protobuf.Timestamp + (*wrapperspb.StringValue)(nil), // 3: google.protobuf.StringValue +} +var file_bank_account_proto_depIdxs = []int32{ + 2, // 0: formance.payments.connectors.grpc.proto.BankAccount.created_at:type_name -> google.protobuf.Timestamp + 3, // 1: formance.payments.connectors.grpc.proto.BankAccount.account_number:type_name -> google.protobuf.StringValue + 3, // 2: formance.payments.connectors.grpc.proto.BankAccount.iban:type_name -> google.protobuf.StringValue + 3, // 3: formance.payments.connectors.grpc.proto.BankAccount.swift_bic_code:type_name -> google.protobuf.StringValue + 3, // 4: formance.payments.connectors.grpc.proto.BankAccount.country:type_name -> google.protobuf.StringValue + 1, // 5: formance.payments.connectors.grpc.proto.BankAccount.metadata:type_name -> formance.payments.connectors.grpc.proto.BankAccount.MetadataEntry + 6, // [6:6] is the sub-list for method output_type + 6, // [6:6] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_bank_account_proto_init() } +func file_bank_account_proto_init() { + if File_bank_account_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_bank_account_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*BankAccount); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_bank_account_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_bank_account_proto_goTypes, + DependencyIndexes: file_bank_account_proto_depIdxs, + MessageInfos: file_bank_account_proto_msgTypes, + }.Build() + File_bank_account_proto = out.File + file_bank_account_proto_rawDesc = nil + file_bank_account_proto_goTypes = nil + file_bank_account_proto_depIdxs = nil +} diff --git a/components/payments/internal/connectors/grpc/proto/bank_account.proto b/components/payments/internal/connectors/grpc/proto/bank_account.proto new file mode 100644 index 0000000000..b4ecb19dfc --- /dev/null +++ b/components/payments/internal/connectors/grpc/proto/bank_account.proto @@ -0,0 +1,30 @@ +syntax = "proto3"; +package formance.payments.connectors.grpc.proto; +option go_package = "github.com/formancehq/payments/internal/connectors/grpc/proto"; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/wrappers.proto"; + +// Represents a bank account created on the formance platform +message BankAccount { + // ID of the bank account. Must be an uuid. + string id = 1; + + // Bank Account's creation date + google.protobuf.Timestamp created_at = 2; + + // Bank Account's name + string name = 3; + + // Optional: Account number of the bank account + google.protobuf.StringValue account_number = 4; + // Optional: iban of the bank account + google.protobuf.StringValue iban = 5; + // Optional: swift code of the bank account + google.protobuf.StringValue swift_bic_code = 6; + // Optional: country of the bank account + google.protobuf.StringValue country = 7; + + // Additional metadata + map metadata = 8; +} \ No newline at end of file diff --git a/components/payments/internal/connectors/grpc/proto/capability.pb.go b/components/payments/internal/connectors/grpc/proto/capability.pb.go new file mode 100644 index 0000000000..e678412db3 --- /dev/null +++ b/components/payments/internal/connectors/grpc/proto/capability.pb.go @@ -0,0 +1,169 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.30.0 +// protoc v3.21.12 +// source: capability.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Capability int32 + +const ( + Capability_CAPABILITY_UNKNOWN Capability = 0 + Capability_CAPABILITY_FETCH_ACCOUNTS Capability = 1 + Capability_CAPABILITY_FETCH_BALANCES Capability = 2 + Capability_CAPABILITY_FETCH_EXTERNAL_ACCOUNTS Capability = 3 + Capability_CAPABILITY_FETCH_PAYMENTS Capability = 4 + Capability_CAPABILITY_FETCH_OTHERS Capability = 5 + Capability_CAPABILITY_WEBHOOKS Capability = 6 + Capability_CAPABILITY_CREATION_BANK_ACCOUNT Capability = 7 + Capability_CAPABILITY_CREATION_PAYMENT Capability = 8 +) + +// Enum value maps for Capability. +var ( + Capability_name = map[int32]string{ + 0: "CAPABILITY_UNKNOWN", + 1: "CAPABILITY_FETCH_ACCOUNTS", + 2: "CAPABILITY_FETCH_BALANCES", + 3: "CAPABILITY_FETCH_EXTERNAL_ACCOUNTS", + 4: "CAPABILITY_FETCH_PAYMENTS", + 5: "CAPABILITY_FETCH_OTHERS", + 6: "CAPABILITY_WEBHOOKS", + 7: "CAPABILITY_CREATION_BANK_ACCOUNT", + 8: "CAPABILITY_CREATION_PAYMENT", + } + Capability_value = map[string]int32{ + "CAPABILITY_UNKNOWN": 0, + "CAPABILITY_FETCH_ACCOUNTS": 1, + "CAPABILITY_FETCH_BALANCES": 2, + "CAPABILITY_FETCH_EXTERNAL_ACCOUNTS": 3, + "CAPABILITY_FETCH_PAYMENTS": 4, + "CAPABILITY_FETCH_OTHERS": 5, + "CAPABILITY_WEBHOOKS": 6, + "CAPABILITY_CREATION_BANK_ACCOUNT": 7, + "CAPABILITY_CREATION_PAYMENT": 8, + } +) + +func (x Capability) Enum() *Capability { + p := new(Capability) + *p = x + return p +} + +func (x Capability) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Capability) Descriptor() protoreflect.EnumDescriptor { + return file_capability_proto_enumTypes[0].Descriptor() +} + +func (Capability) Type() protoreflect.EnumType { + return &file_capability_proto_enumTypes[0] +} + +func (x Capability) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Capability.Descriptor instead. +func (Capability) EnumDescriptor() ([]byte, []int) { + return file_capability_proto_rawDescGZIP(), []int{0} +} + +var File_capability_proto protoreflect.FileDescriptor + +var file_capability_proto_rawDesc = []byte{ + 0x0a, 0x10, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x12, 0x27, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, + 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2a, 0xa6, 0x02, 0x0a, 0x0a, + 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x12, 0x16, 0x0a, 0x12, 0x43, 0x41, + 0x50, 0x41, 0x42, 0x49, 0x4c, 0x49, 0x54, 0x59, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, + 0x10, 0x00, 0x12, 0x1d, 0x0a, 0x19, 0x43, 0x41, 0x50, 0x41, 0x42, 0x49, 0x4c, 0x49, 0x54, 0x59, + 0x5f, 0x46, 0x45, 0x54, 0x43, 0x48, 0x5f, 0x41, 0x43, 0x43, 0x4f, 0x55, 0x4e, 0x54, 0x53, 0x10, + 0x01, 0x12, 0x1d, 0x0a, 0x19, 0x43, 0x41, 0x50, 0x41, 0x42, 0x49, 0x4c, 0x49, 0x54, 0x59, 0x5f, + 0x46, 0x45, 0x54, 0x43, 0x48, 0x5f, 0x42, 0x41, 0x4c, 0x41, 0x4e, 0x43, 0x45, 0x53, 0x10, 0x02, + 0x12, 0x26, 0x0a, 0x22, 0x43, 0x41, 0x50, 0x41, 0x42, 0x49, 0x4c, 0x49, 0x54, 0x59, 0x5f, 0x46, + 0x45, 0x54, 0x43, 0x48, 0x5f, 0x45, 0x58, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x5f, 0x41, 0x43, + 0x43, 0x4f, 0x55, 0x4e, 0x54, 0x53, 0x10, 0x03, 0x12, 0x1d, 0x0a, 0x19, 0x43, 0x41, 0x50, 0x41, + 0x42, 0x49, 0x4c, 0x49, 0x54, 0x59, 0x5f, 0x46, 0x45, 0x54, 0x43, 0x48, 0x5f, 0x50, 0x41, 0x59, + 0x4d, 0x45, 0x4e, 0x54, 0x53, 0x10, 0x04, 0x12, 0x1b, 0x0a, 0x17, 0x43, 0x41, 0x50, 0x41, 0x42, + 0x49, 0x4c, 0x49, 0x54, 0x59, 0x5f, 0x46, 0x45, 0x54, 0x43, 0x48, 0x5f, 0x4f, 0x54, 0x48, 0x45, + 0x52, 0x53, 0x10, 0x05, 0x12, 0x17, 0x0a, 0x13, 0x43, 0x41, 0x50, 0x41, 0x42, 0x49, 0x4c, 0x49, + 0x54, 0x59, 0x5f, 0x57, 0x45, 0x42, 0x48, 0x4f, 0x4f, 0x4b, 0x53, 0x10, 0x06, 0x12, 0x24, 0x0a, + 0x20, 0x43, 0x41, 0x50, 0x41, 0x42, 0x49, 0x4c, 0x49, 0x54, 0x59, 0x5f, 0x43, 0x52, 0x45, 0x41, + 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x42, 0x41, 0x4e, 0x4b, 0x5f, 0x41, 0x43, 0x43, 0x4f, 0x55, 0x4e, + 0x54, 0x10, 0x07, 0x12, 0x1f, 0x0a, 0x1b, 0x43, 0x41, 0x50, 0x41, 0x42, 0x49, 0x4c, 0x49, 0x54, + 0x59, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x50, 0x41, 0x59, 0x4d, 0x45, + 0x4e, 0x54, 0x10, 0x08, 0x42, 0x3f, 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x68, 0x71, 0x2f, 0x70, 0x61, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, + 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_capability_proto_rawDescOnce sync.Once + file_capability_proto_rawDescData = file_capability_proto_rawDesc +) + +func file_capability_proto_rawDescGZIP() []byte { + file_capability_proto_rawDescOnce.Do(func() { + file_capability_proto_rawDescData = protoimpl.X.CompressGZIP(file_capability_proto_rawDescData) + }) + return file_capability_proto_rawDescData +} + +var file_capability_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_capability_proto_goTypes = []interface{}{ + (Capability)(0), // 0: formance.payments.connectors.grpc.proto.Capability +} +var file_capability_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_capability_proto_init() } +func file_capability_proto_init() { + if File_capability_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_capability_proto_rawDesc, + NumEnums: 1, + NumMessages: 0, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_capability_proto_goTypes, + DependencyIndexes: file_capability_proto_depIdxs, + EnumInfos: file_capability_proto_enumTypes, + }.Build() + File_capability_proto = out.File + file_capability_proto_rawDesc = nil + file_capability_proto_goTypes = nil + file_capability_proto_depIdxs = nil +} diff --git a/components/payments/internal/connectors/grpc/proto/capability.proto b/components/payments/internal/connectors/grpc/proto/capability.proto new file mode 100644 index 0000000000..4b9a0e2747 --- /dev/null +++ b/components/payments/internal/connectors/grpc/proto/capability.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; +package formance.payments.connectors.grpc.proto; +option go_package = "github.com/formancehq/payments/internal/connectors/grpc/proto"; + +enum Capability { + CAPABILITY_UNKNOWN = 0; + + CAPABILITY_FETCH_ACCOUNTS = 1; + CAPABILITY_FETCH_BALANCES = 2; + CAPABILITY_FETCH_EXTERNAL_ACCOUNTS = 3; + CAPABILITY_FETCH_PAYMENTS = 4; + CAPABILITY_FETCH_OTHERS = 5; + + CAPABILITY_WEBHOOKS = 6; + + CAPABILITY_CREATION_BANK_ACCOUNT = 7; + CAPABILITY_CREATION_PAYMENT = 8; +} \ No newline at end of file diff --git a/components/payments/internal/connectors/grpc/proto/monetary.pb.go b/components/payments/internal/connectors/grpc/proto/monetary.pb.go new file mode 100644 index 0000000000..570a6cda64 --- /dev/null +++ b/components/payments/internal/connectors/grpc/proto/monetary.pb.go @@ -0,0 +1,158 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.30.0 +// protoc v3.21.12 +// source: monetary.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Monetary struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Asset string `protobuf:"bytes,1,opt,name=asset,proto3" json:"asset,omitempty"` + // Protobuf doesn't have a type for big integers. Therefore, + // we need to convert them as bytes. + Amount []byte `protobuf:"bytes,2,opt,name=amount,proto3" json:"amount,omitempty"` +} + +func (x *Monetary) Reset() { + *x = Monetary{} + if protoimpl.UnsafeEnabled { + mi := &file_monetary_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Monetary) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Monetary) ProtoMessage() {} + +func (x *Monetary) ProtoReflect() protoreflect.Message { + mi := &file_monetary_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Monetary.ProtoReflect.Descriptor instead. +func (*Monetary) Descriptor() ([]byte, []int) { + return file_monetary_proto_rawDescGZIP(), []int{0} +} + +func (x *Monetary) GetAsset() string { + if x != nil { + return x.Asset + } + return "" +} + +func (x *Monetary) GetAmount() []byte { + if x != nil { + return x.Amount + } + return nil +} + +var File_monetary_proto protoreflect.FileDescriptor + +var file_monetary_proto_rawDesc = []byte{ + 0x0a, 0x0e, 0x6d, 0x6f, 0x6e, 0x65, 0x74, 0x61, 0x72, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x12, 0x27, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, + 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, + 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x38, 0x0a, 0x08, 0x4d, 0x6f, 0x6e, + 0x65, 0x74, 0x61, 0x72, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x73, 0x73, 0x65, 0x74, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x61, 0x73, 0x73, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x61, + 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x61, 0x6d, 0x6f, + 0x75, 0x6e, 0x74, 0x42, 0x3f, 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x68, 0x71, 0x2f, 0x70, 0x61, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x63, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_monetary_proto_rawDescOnce sync.Once + file_monetary_proto_rawDescData = file_monetary_proto_rawDesc +) + +func file_monetary_proto_rawDescGZIP() []byte { + file_monetary_proto_rawDescOnce.Do(func() { + file_monetary_proto_rawDescData = protoimpl.X.CompressGZIP(file_monetary_proto_rawDescData) + }) + return file_monetary_proto_rawDescData +} + +var file_monetary_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_monetary_proto_goTypes = []interface{}{ + (*Monetary)(nil), // 0: formance.payments.connectors.grpc.proto.Monetary +} +var file_monetary_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_monetary_proto_init() } +func file_monetary_proto_init() { + if File_monetary_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_monetary_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Monetary); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_monetary_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_monetary_proto_goTypes, + DependencyIndexes: file_monetary_proto_depIdxs, + MessageInfos: file_monetary_proto_msgTypes, + }.Build() + File_monetary_proto = out.File + file_monetary_proto_rawDesc = nil + file_monetary_proto_goTypes = nil + file_monetary_proto_depIdxs = nil +} diff --git a/components/payments/internal/connectors/grpc/proto/monetary.proto b/components/payments/internal/connectors/grpc/proto/monetary.proto new file mode 100644 index 0000000000..412cef0936 --- /dev/null +++ b/components/payments/internal/connectors/grpc/proto/monetary.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; +package formance.payments.connectors.grpc.proto; +option go_package = "github.com/formancehq/payments/internal/connectors/grpc/proto"; + +message Monetary { + string asset = 1; + + // Protobuf doesn't have a type for big integers. Therefore, + // we need to convert them as bytes. + bytes amount = 2; +} \ No newline at end of file diff --git a/components/payments/internal/connectors/grpc/proto/other.pb.go b/components/payments/internal/connectors/grpc/proto/other.pb.go new file mode 100644 index 0000000000..c8799c95ba --- /dev/null +++ b/components/payments/internal/connectors/grpc/proto/other.pb.go @@ -0,0 +1,155 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.30.0 +// protoc v3.21.12 +// source: other.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Other struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Other []byte `protobuf:"bytes,2,opt,name=other,proto3" json:"other,omitempty"` +} + +func (x *Other) Reset() { + *x = Other{} + if protoimpl.UnsafeEnabled { + mi := &file_other_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Other) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Other) ProtoMessage() {} + +func (x *Other) ProtoReflect() protoreflect.Message { + mi := &file_other_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Other.ProtoReflect.Descriptor instead. +func (*Other) Descriptor() ([]byte, []int) { + return file_other_proto_rawDescGZIP(), []int{0} +} + +func (x *Other) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Other) GetOther() []byte { + if x != nil { + return x.Other + } + return nil +} + +var File_other_proto protoreflect.FileDescriptor + +var file_other_proto_rawDesc = []byte{ + 0x0a, 0x0b, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x27, 0x66, + 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, + 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x2d, 0x0a, 0x05, 0x4f, 0x74, 0x68, 0x65, 0x72, 0x12, + 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, + 0x14, 0x0a, 0x05, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, + 0x6f, 0x74, 0x68, 0x65, 0x72, 0x42, 0x3f, 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x68, 0x71, 0x2f, 0x70, + 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2f, 0x67, 0x72, 0x70, 0x63, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_other_proto_rawDescOnce sync.Once + file_other_proto_rawDescData = file_other_proto_rawDesc +) + +func file_other_proto_rawDescGZIP() []byte { + file_other_proto_rawDescOnce.Do(func() { + file_other_proto_rawDescData = protoimpl.X.CompressGZIP(file_other_proto_rawDescData) + }) + return file_other_proto_rawDescData +} + +var file_other_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_other_proto_goTypes = []interface{}{ + (*Other)(nil), // 0: formance.payments.connectors.grpc.proto.Other +} +var file_other_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_other_proto_init() } +func file_other_proto_init() { + if File_other_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_other_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Other); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_other_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_other_proto_goTypes, + DependencyIndexes: file_other_proto_depIdxs, + MessageInfos: file_other_proto_msgTypes, + }.Build() + File_other_proto = out.File + file_other_proto_rawDesc = nil + file_other_proto_goTypes = nil + file_other_proto_depIdxs = nil +} diff --git a/components/payments/internal/connectors/grpc/proto/other.proto b/components/payments/internal/connectors/grpc/proto/other.proto new file mode 100644 index 0000000000..81288de440 --- /dev/null +++ b/components/payments/internal/connectors/grpc/proto/other.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; +package formance.payments.connectors.grpc.proto; +option go_package = "github.com/formancehq/payments/internal/connectors/grpc/proto"; + +message Other { + string id = 1; + bytes other = 2; +} \ No newline at end of file diff --git a/components/payments/internal/connectors/grpc/proto/payment.pb.go b/components/payments/internal/connectors/grpc/proto/payment.pb.go new file mode 100644 index 0000000000..2ab306ebc8 --- /dev/null +++ b/components/payments/internal/connectors/grpc/proto/payment.pb.go @@ -0,0 +1,637 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.30.0 +// protoc v3.21.12 +// source: payment.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + wrapperspb "google.golang.org/protobuf/types/known/wrapperspb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type PaymentType int32 + +const ( + PaymentType_PAYMENT_TYPE_UNKNOWN PaymentType = 0 + PaymentType_PAYMENT_TYPE_PAYIN PaymentType = 1 + PaymentType_PAYMENT_TYPE_PAYOUT PaymentType = 2 + PaymentType_PAYMENT_TYPE_TRANSFER PaymentType = 3 + PaymentType_PAYMENT_TYPE_OTHER PaymentType = 100 +) + +// Enum value maps for PaymentType. +var ( + PaymentType_name = map[int32]string{ + 0: "PAYMENT_TYPE_UNKNOWN", + 1: "PAYMENT_TYPE_PAYIN", + 2: "PAYMENT_TYPE_PAYOUT", + 3: "PAYMENT_TYPE_TRANSFER", + 100: "PAYMENT_TYPE_OTHER", + } + PaymentType_value = map[string]int32{ + "PAYMENT_TYPE_UNKNOWN": 0, + "PAYMENT_TYPE_PAYIN": 1, + "PAYMENT_TYPE_PAYOUT": 2, + "PAYMENT_TYPE_TRANSFER": 3, + "PAYMENT_TYPE_OTHER": 100, + } +) + +func (x PaymentType) Enum() *PaymentType { + p := new(PaymentType) + *p = x + return p +} + +func (x PaymentType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (PaymentType) Descriptor() protoreflect.EnumDescriptor { + return file_payment_proto_enumTypes[0].Descriptor() +} + +func (PaymentType) Type() protoreflect.EnumType { + return &file_payment_proto_enumTypes[0] +} + +func (x PaymentType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use PaymentType.Descriptor instead. +func (PaymentType) EnumDescriptor() ([]byte, []int) { + return file_payment_proto_rawDescGZIP(), []int{0} +} + +type PaymentScheme int32 + +const ( + PaymentScheme_PAYMENT_SCHEME_UNKNOWN PaymentScheme = 0 + // Scheme cards + PaymentScheme_PAYMENT_SCHEME_CARD_VISA PaymentScheme = 1 + PaymentScheme_PAYMENT_SCHEME_CARD_MASTERCARD PaymentScheme = 2 + PaymentScheme_PAYMENT_SCHEME_CARD_AMEX PaymentScheme = 3 + PaymentScheme_PAYMENT_SCHEME_CARD_DINERS PaymentScheme = 4 + PaymentScheme_PAYMENT_SCHEME_CARD_DISCOVER PaymentScheme = 5 + PaymentScheme_PAYMENT_SCHEME_CARD_JCB PaymentScheme = 6 + PaymentScheme_PAYMENT_SCHEME_CARD_UNION_PAY PaymentScheme = 7 + PaymentScheme_PAYMENT_SCHEME_CARD_ALIPAY PaymentScheme = 8 + PaymentScheme_PAYMENT_SCHEME_CARD_CUP PaymentScheme = 9 + PaymentScheme_PAYMENT_SCHEME_SEPA_DEBIT PaymentScheme = 10 + PaymentScheme_PAYMENT_SCHEME_SEPA_CREDIT PaymentScheme = 11 + PaymentScheme_PAYMENT_SCHEME_SEPA PaymentScheme = 12 + PaymentScheme_PAYMENT_SCHEME_GOOGLE_PAY PaymentScheme = 13 + PaymentScheme_PAYMENT_SCHEME_APPLE_PAY PaymentScheme = 14 + PaymentScheme_PAYMENT_SCHEME_DOKU PaymentScheme = 15 + PaymentScheme_PAYMENT_SCHEME_DRAGON_PAY PaymentScheme = 16 + PaymentScheme_PAYMENT_SCHEME_MAESTRO PaymentScheme = 17 + PaymentScheme_PAYMENT_SCHEME_MOL_PAY PaymentScheme = 18 + PaymentScheme_PAYMENT_SCHEME_A2A PaymentScheme = 19 + PaymentScheme_PAYMENT_SCHEME_ACH_DEBIT PaymentScheme = 20 + PaymentScheme_PAYMENT_SCHEME_ACH PaymentScheme = 21 + PaymentScheme_PAYMENT_SCHEME_RTP PaymentScheme = 22 + PaymentScheme_PAYMENT_SCHEME_OTHER PaymentScheme = 100 +) + +// Enum value maps for PaymentScheme. +var ( + PaymentScheme_name = map[int32]string{ + 0: "PAYMENT_SCHEME_UNKNOWN", + 1: "PAYMENT_SCHEME_CARD_VISA", + 2: "PAYMENT_SCHEME_CARD_MASTERCARD", + 3: "PAYMENT_SCHEME_CARD_AMEX", + 4: "PAYMENT_SCHEME_CARD_DINERS", + 5: "PAYMENT_SCHEME_CARD_DISCOVER", + 6: "PAYMENT_SCHEME_CARD_JCB", + 7: "PAYMENT_SCHEME_CARD_UNION_PAY", + 8: "PAYMENT_SCHEME_CARD_ALIPAY", + 9: "PAYMENT_SCHEME_CARD_CUP", + 10: "PAYMENT_SCHEME_SEPA_DEBIT", + 11: "PAYMENT_SCHEME_SEPA_CREDIT", + 12: "PAYMENT_SCHEME_SEPA", + 13: "PAYMENT_SCHEME_GOOGLE_PAY", + 14: "PAYMENT_SCHEME_APPLE_PAY", + 15: "PAYMENT_SCHEME_DOKU", + 16: "PAYMENT_SCHEME_DRAGON_PAY", + 17: "PAYMENT_SCHEME_MAESTRO", + 18: "PAYMENT_SCHEME_MOL_PAY", + 19: "PAYMENT_SCHEME_A2A", + 20: "PAYMENT_SCHEME_ACH_DEBIT", + 21: "PAYMENT_SCHEME_ACH", + 22: "PAYMENT_SCHEME_RTP", + 100: "PAYMENT_SCHEME_OTHER", + } + PaymentScheme_value = map[string]int32{ + "PAYMENT_SCHEME_UNKNOWN": 0, + "PAYMENT_SCHEME_CARD_VISA": 1, + "PAYMENT_SCHEME_CARD_MASTERCARD": 2, + "PAYMENT_SCHEME_CARD_AMEX": 3, + "PAYMENT_SCHEME_CARD_DINERS": 4, + "PAYMENT_SCHEME_CARD_DISCOVER": 5, + "PAYMENT_SCHEME_CARD_JCB": 6, + "PAYMENT_SCHEME_CARD_UNION_PAY": 7, + "PAYMENT_SCHEME_CARD_ALIPAY": 8, + "PAYMENT_SCHEME_CARD_CUP": 9, + "PAYMENT_SCHEME_SEPA_DEBIT": 10, + "PAYMENT_SCHEME_SEPA_CREDIT": 11, + "PAYMENT_SCHEME_SEPA": 12, + "PAYMENT_SCHEME_GOOGLE_PAY": 13, + "PAYMENT_SCHEME_APPLE_PAY": 14, + "PAYMENT_SCHEME_DOKU": 15, + "PAYMENT_SCHEME_DRAGON_PAY": 16, + "PAYMENT_SCHEME_MAESTRO": 17, + "PAYMENT_SCHEME_MOL_PAY": 18, + "PAYMENT_SCHEME_A2A": 19, + "PAYMENT_SCHEME_ACH_DEBIT": 20, + "PAYMENT_SCHEME_ACH": 21, + "PAYMENT_SCHEME_RTP": 22, + "PAYMENT_SCHEME_OTHER": 100, + } +) + +func (x PaymentScheme) Enum() *PaymentScheme { + p := new(PaymentScheme) + *p = x + return p +} + +func (x PaymentScheme) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (PaymentScheme) Descriptor() protoreflect.EnumDescriptor { + return file_payment_proto_enumTypes[1].Descriptor() +} + +func (PaymentScheme) Type() protoreflect.EnumType { + return &file_payment_proto_enumTypes[1] +} + +func (x PaymentScheme) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use PaymentScheme.Descriptor instead. +func (PaymentScheme) EnumDescriptor() ([]byte, []int) { + return file_payment_proto_rawDescGZIP(), []int{1} +} + +type PaymentStatus int32 + +const ( + PaymentStatus_PAYMENT_STATUS_UNKNOWN PaymentStatus = 0 + PaymentStatus_PAYMENT_STATUS_PENDING PaymentStatus = 1 + PaymentStatus_PAYMENT_STATUS_SUCCEEDED PaymentStatus = 2 + PaymentStatus_PAYMENT_STATUS_CANCELLED PaymentStatus = 3 + PaymentStatus_PAYMENT_STATUS_FAILED PaymentStatus = 4 + PaymentStatus_PAYMENT_STATUS_EXPIRED PaymentStatus = 5 + PaymentStatus_PAYMENT_STATUS_REFUNDED PaymentStatus = 6 + PaymentStatus_PAYMENT_STATUS_REFUNDED_FAILURE PaymentStatus = 7 + PaymentStatus_PAYMENT_STATUS_DISPUTE PaymentStatus = 8 + PaymentStatus_PAYMENT_STATUS_DISPUTE_WON PaymentStatus = 9 + PaymentStatus_PAYMENT_STATUS_DISPUTE_LOST PaymentStatus = 10 + PaymentStatus_PAYMENT_STATUS_OTHER PaymentStatus = 100 +) + +// Enum value maps for PaymentStatus. +var ( + PaymentStatus_name = map[int32]string{ + 0: "PAYMENT_STATUS_UNKNOWN", + 1: "PAYMENT_STATUS_PENDING", + 2: "PAYMENT_STATUS_SUCCEEDED", + 3: "PAYMENT_STATUS_CANCELLED", + 4: "PAYMENT_STATUS_FAILED", + 5: "PAYMENT_STATUS_EXPIRED", + 6: "PAYMENT_STATUS_REFUNDED", + 7: "PAYMENT_STATUS_REFUNDED_FAILURE", + 8: "PAYMENT_STATUS_DISPUTE", + 9: "PAYMENT_STATUS_DISPUTE_WON", + 10: "PAYMENT_STATUS_DISPUTE_LOST", + 100: "PAYMENT_STATUS_OTHER", + } + PaymentStatus_value = map[string]int32{ + "PAYMENT_STATUS_UNKNOWN": 0, + "PAYMENT_STATUS_PENDING": 1, + "PAYMENT_STATUS_SUCCEEDED": 2, + "PAYMENT_STATUS_CANCELLED": 3, + "PAYMENT_STATUS_FAILED": 4, + "PAYMENT_STATUS_EXPIRED": 5, + "PAYMENT_STATUS_REFUNDED": 6, + "PAYMENT_STATUS_REFUNDED_FAILURE": 7, + "PAYMENT_STATUS_DISPUTE": 8, + "PAYMENT_STATUS_DISPUTE_WON": 9, + "PAYMENT_STATUS_DISPUTE_LOST": 10, + "PAYMENT_STATUS_OTHER": 100, + } +) + +func (x PaymentStatus) Enum() *PaymentStatus { + p := new(PaymentStatus) + *p = x + return p +} + +func (x PaymentStatus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (PaymentStatus) Descriptor() protoreflect.EnumDescriptor { + return file_payment_proto_enumTypes[2].Descriptor() +} + +func (PaymentStatus) Type() protoreflect.EnumType { + return &file_payment_proto_enumTypes[2] +} + +func (x PaymentStatus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use PaymentStatus.Descriptor instead. +func (PaymentStatus) EnumDescriptor() ([]byte, []int) { + return file_payment_proto_rawDescGZIP(), []int{2} +} + +// Represent a payment/transaction in Formance platform +type Payment struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // PSP reference of the payment + Reference string `protobuf:"bytes,1,opt,name=reference,proto3" json:"reference,omitempty"` + // Payment's creation date + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + SyncedAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=synced_at,json=syncedAt,proto3" json:"synced_at,omitempty"` + // Payment Type, can be payin, payout, transfer or other + PaymentType PaymentType `protobuf:"varint,5,opt,name=payment_type,json=paymentType,proto3,enum=formance.payments.connectors.grpc.proto.PaymentType" json:"payment_type,omitempty"` + // amount + Amount *Monetary `protobuf:"bytes,6,opt,name=amount,proto3" json:"amount,omitempty"` + // Payment scheme, for example when you pay via visa card etc... + Scheme PaymentScheme `protobuf:"varint,7,opt,name=scheme,proto3,enum=formance.payments.connectors.grpc.proto.PaymentScheme" json:"scheme,omitempty"` + // Payment status, for example pending, succeeded, failed etc... + Status PaymentStatus `protobuf:"varint,8,opt,name=status,proto3,enum=formance.payments.connectors.grpc.proto.PaymentStatus" json:"status,omitempty"` + // Nullable string, we can have or not a source account id depending + // on the payment type + SourceAccountReference *wrapperspb.StringValue `protobuf:"bytes,9,opt,name=source_account_reference,json=sourceAccountReference,proto3" json:"source_account_reference,omitempty"` + // Nullable string, we can have or not a destination account id depending + // on the payment type + DestinationAccountReference *wrapperspb.StringValue `protobuf:"bytes,10,opt,name=destination_account_reference,json=destinationAccountReference,proto3" json:"destination_account_reference,omitempty"` + // Additional metadata + Metadata map[string]string `protobuf:"bytes,11,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + // The PSP raw message + Raw []byte `protobuf:"bytes,12,opt,name=raw,proto3" json:"raw,omitempty"` +} + +func (x *Payment) Reset() { + *x = Payment{} + if protoimpl.UnsafeEnabled { + mi := &file_payment_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Payment) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Payment) ProtoMessage() {} + +func (x *Payment) ProtoReflect() protoreflect.Message { + mi := &file_payment_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Payment.ProtoReflect.Descriptor instead. +func (*Payment) Descriptor() ([]byte, []int) { + return file_payment_proto_rawDescGZIP(), []int{0} +} + +func (x *Payment) GetReference() string { + if x != nil { + return x.Reference + } + return "" +} + +func (x *Payment) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *Payment) GetSyncedAt() *timestamppb.Timestamp { + if x != nil { + return x.SyncedAt + } + return nil +} + +func (x *Payment) GetPaymentType() PaymentType { + if x != nil { + return x.PaymentType + } + return PaymentType_PAYMENT_TYPE_UNKNOWN +} + +func (x *Payment) GetAmount() *Monetary { + if x != nil { + return x.Amount + } + return nil +} + +func (x *Payment) GetScheme() PaymentScheme { + if x != nil { + return x.Scheme + } + return PaymentScheme_PAYMENT_SCHEME_UNKNOWN +} + +func (x *Payment) GetStatus() PaymentStatus { + if x != nil { + return x.Status + } + return PaymentStatus_PAYMENT_STATUS_UNKNOWN +} + +func (x *Payment) GetSourceAccountReference() *wrapperspb.StringValue { + if x != nil { + return x.SourceAccountReference + } + return nil +} + +func (x *Payment) GetDestinationAccountReference() *wrapperspb.StringValue { + if x != nil { + return x.DestinationAccountReference + } + return nil +} + +func (x *Payment) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *Payment) GetRaw() []byte { + if x != nil { + return x.Raw + } + return nil +} + +var File_payment_proto protoreflect.FileDescriptor + +var file_payment_proto_rawDesc = []byte{ + 0x0a, 0x0d, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x27, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, + 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, + 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, + 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0e, 0x6d, 0x6f, 0x6e, 0x65, 0x74, + 0x61, 0x72, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xc4, 0x06, 0x0a, 0x07, 0x50, 0x61, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, + 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, + 0x6e, 0x63, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x37, + 0x0a, 0x09, 0x73, 0x79, 0x6e, 0x63, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x08, 0x73, + 0x79, 0x6e, 0x63, 0x65, 0x64, 0x41, 0x74, 0x12, 0x57, 0x0a, 0x0c, 0x70, 0x61, 0x79, 0x6d, 0x65, + 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x34, 0x2e, + 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, + 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, + 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x54, + 0x79, 0x70, 0x65, 0x52, 0x0b, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, + 0x12, 0x49, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x31, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, + 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, + 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4d, 0x6f, 0x6e, 0x65, 0x74, + 0x61, 0x72, 0x79, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x4e, 0x0a, 0x06, 0x73, + 0x63, 0x68, 0x65, 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x36, 0x2e, 0x66, 0x6f, + 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, + 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x68, + 0x65, 0x6d, 0x65, 0x52, 0x06, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x12, 0x4e, 0x0a, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x36, 0x2e, 0x66, 0x6f, + 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, + 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x56, 0x0a, 0x18, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x72, 0x65, + 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x16, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, + 0x6e, 0x63, 0x65, 0x12, 0x60, 0x0a, 0x1d, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x72, 0x65, 0x66, 0x65, 0x72, + 0x65, 0x6e, 0x63, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, + 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x1b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x66, 0x65, + 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x5a, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3e, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, + 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x61, 0x77, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, + 0x72, 0x61, 0x77, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, + 0x2a, 0x8b, 0x01, 0x0a, 0x0b, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, + 0x12, 0x18, 0x0a, 0x14, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, + 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x16, 0x0a, 0x12, 0x50, 0x41, + 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x50, 0x41, 0x59, 0x49, 0x4e, + 0x10, 0x01, 0x12, 0x17, 0x0a, 0x13, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x54, 0x59, + 0x50, 0x45, 0x5f, 0x50, 0x41, 0x59, 0x4f, 0x55, 0x54, 0x10, 0x02, 0x12, 0x19, 0x0a, 0x15, 0x50, + 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x54, 0x52, 0x41, 0x4e, + 0x53, 0x46, 0x45, 0x52, 0x10, 0x03, 0x12, 0x16, 0x0a, 0x12, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, + 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4f, 0x54, 0x48, 0x45, 0x52, 0x10, 0x64, 0x2a, 0xcf, + 0x05, 0x0a, 0x0d, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x65, + 0x12, 0x1a, 0x0a, 0x16, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x43, 0x48, 0x45, + 0x4d, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, + 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x43, 0x48, 0x45, 0x4d, 0x45, 0x5f, 0x43, + 0x41, 0x52, 0x44, 0x5f, 0x56, 0x49, 0x53, 0x41, 0x10, 0x01, 0x12, 0x22, 0x0a, 0x1e, 0x50, 0x41, + 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x43, 0x48, 0x45, 0x4d, 0x45, 0x5f, 0x43, 0x41, 0x52, + 0x44, 0x5f, 0x4d, 0x41, 0x53, 0x54, 0x45, 0x52, 0x43, 0x41, 0x52, 0x44, 0x10, 0x02, 0x12, 0x1c, + 0x0a, 0x18, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x43, 0x48, 0x45, 0x4d, 0x45, + 0x5f, 0x43, 0x41, 0x52, 0x44, 0x5f, 0x41, 0x4d, 0x45, 0x58, 0x10, 0x03, 0x12, 0x1e, 0x0a, 0x1a, + 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x43, 0x48, 0x45, 0x4d, 0x45, 0x5f, 0x43, + 0x41, 0x52, 0x44, 0x5f, 0x44, 0x49, 0x4e, 0x45, 0x52, 0x53, 0x10, 0x04, 0x12, 0x20, 0x0a, 0x1c, + 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x43, 0x48, 0x45, 0x4d, 0x45, 0x5f, 0x43, + 0x41, 0x52, 0x44, 0x5f, 0x44, 0x49, 0x53, 0x43, 0x4f, 0x56, 0x45, 0x52, 0x10, 0x05, 0x12, 0x1b, + 0x0a, 0x17, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x43, 0x48, 0x45, 0x4d, 0x45, + 0x5f, 0x43, 0x41, 0x52, 0x44, 0x5f, 0x4a, 0x43, 0x42, 0x10, 0x06, 0x12, 0x21, 0x0a, 0x1d, 0x50, + 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x43, 0x48, 0x45, 0x4d, 0x45, 0x5f, 0x43, 0x41, + 0x52, 0x44, 0x5f, 0x55, 0x4e, 0x49, 0x4f, 0x4e, 0x5f, 0x50, 0x41, 0x59, 0x10, 0x07, 0x12, 0x1e, + 0x0a, 0x1a, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x43, 0x48, 0x45, 0x4d, 0x45, + 0x5f, 0x43, 0x41, 0x52, 0x44, 0x5f, 0x41, 0x4c, 0x49, 0x50, 0x41, 0x59, 0x10, 0x08, 0x12, 0x1b, + 0x0a, 0x17, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x43, 0x48, 0x45, 0x4d, 0x45, + 0x5f, 0x43, 0x41, 0x52, 0x44, 0x5f, 0x43, 0x55, 0x50, 0x10, 0x09, 0x12, 0x1d, 0x0a, 0x19, 0x50, + 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x43, 0x48, 0x45, 0x4d, 0x45, 0x5f, 0x53, 0x45, + 0x50, 0x41, 0x5f, 0x44, 0x45, 0x42, 0x49, 0x54, 0x10, 0x0a, 0x12, 0x1e, 0x0a, 0x1a, 0x50, 0x41, + 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x43, 0x48, 0x45, 0x4d, 0x45, 0x5f, 0x53, 0x45, 0x50, + 0x41, 0x5f, 0x43, 0x52, 0x45, 0x44, 0x49, 0x54, 0x10, 0x0b, 0x12, 0x17, 0x0a, 0x13, 0x50, 0x41, + 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x43, 0x48, 0x45, 0x4d, 0x45, 0x5f, 0x53, 0x45, 0x50, + 0x41, 0x10, 0x0c, 0x12, 0x1d, 0x0a, 0x19, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, + 0x43, 0x48, 0x45, 0x4d, 0x45, 0x5f, 0x47, 0x4f, 0x4f, 0x47, 0x4c, 0x45, 0x5f, 0x50, 0x41, 0x59, + 0x10, 0x0d, 0x12, 0x1c, 0x0a, 0x18, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x43, + 0x48, 0x45, 0x4d, 0x45, 0x5f, 0x41, 0x50, 0x50, 0x4c, 0x45, 0x5f, 0x50, 0x41, 0x59, 0x10, 0x0e, + 0x12, 0x17, 0x0a, 0x13, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x43, 0x48, 0x45, + 0x4d, 0x45, 0x5f, 0x44, 0x4f, 0x4b, 0x55, 0x10, 0x0f, 0x12, 0x1d, 0x0a, 0x19, 0x50, 0x41, 0x59, + 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x43, 0x48, 0x45, 0x4d, 0x45, 0x5f, 0x44, 0x52, 0x41, 0x47, + 0x4f, 0x4e, 0x5f, 0x50, 0x41, 0x59, 0x10, 0x10, 0x12, 0x1a, 0x0a, 0x16, 0x50, 0x41, 0x59, 0x4d, + 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x43, 0x48, 0x45, 0x4d, 0x45, 0x5f, 0x4d, 0x41, 0x45, 0x53, 0x54, + 0x52, 0x4f, 0x10, 0x11, 0x12, 0x1a, 0x0a, 0x16, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, + 0x53, 0x43, 0x48, 0x45, 0x4d, 0x45, 0x5f, 0x4d, 0x4f, 0x4c, 0x5f, 0x50, 0x41, 0x59, 0x10, 0x12, + 0x12, 0x16, 0x0a, 0x12, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x43, 0x48, 0x45, + 0x4d, 0x45, 0x5f, 0x41, 0x32, 0x41, 0x10, 0x13, 0x12, 0x1c, 0x0a, 0x18, 0x50, 0x41, 0x59, 0x4d, + 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x43, 0x48, 0x45, 0x4d, 0x45, 0x5f, 0x41, 0x43, 0x48, 0x5f, 0x44, + 0x45, 0x42, 0x49, 0x54, 0x10, 0x14, 0x12, 0x16, 0x0a, 0x12, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, + 0x54, 0x5f, 0x53, 0x43, 0x48, 0x45, 0x4d, 0x45, 0x5f, 0x41, 0x43, 0x48, 0x10, 0x15, 0x12, 0x16, + 0x0a, 0x12, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x43, 0x48, 0x45, 0x4d, 0x45, + 0x5f, 0x52, 0x54, 0x50, 0x10, 0x16, 0x12, 0x18, 0x0a, 0x14, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, + 0x54, 0x5f, 0x53, 0x43, 0x48, 0x45, 0x4d, 0x45, 0x5f, 0x4f, 0x54, 0x48, 0x45, 0x52, 0x10, 0x64, + 0x2a, 0xf3, 0x02, 0x0a, 0x0d, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x12, 0x1a, 0x0a, 0x16, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x54, + 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x1a, + 0x0a, 0x16, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, + 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x1c, 0x0a, 0x18, 0x50, 0x41, + 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x55, 0x43, + 0x43, 0x45, 0x45, 0x44, 0x45, 0x44, 0x10, 0x02, 0x12, 0x1c, 0x0a, 0x18, 0x50, 0x41, 0x59, 0x4d, + 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, + 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x03, 0x12, 0x19, 0x0a, 0x15, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, + 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, + 0x04, 0x12, 0x1a, 0x0a, 0x16, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x54, 0x41, + 0x54, 0x55, 0x53, 0x5f, 0x45, 0x58, 0x50, 0x49, 0x52, 0x45, 0x44, 0x10, 0x05, 0x12, 0x1b, 0x0a, + 0x17, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, + 0x52, 0x45, 0x46, 0x55, 0x4e, 0x44, 0x45, 0x44, 0x10, 0x06, 0x12, 0x23, 0x0a, 0x1f, 0x50, 0x41, + 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x52, 0x45, 0x46, + 0x55, 0x4e, 0x44, 0x45, 0x44, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x10, 0x07, 0x12, + 0x1a, 0x0a, 0x16, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, + 0x53, 0x5f, 0x44, 0x49, 0x53, 0x50, 0x55, 0x54, 0x45, 0x10, 0x08, 0x12, 0x1e, 0x0a, 0x1a, 0x50, + 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x44, 0x49, + 0x53, 0x50, 0x55, 0x54, 0x45, 0x5f, 0x57, 0x4f, 0x4e, 0x10, 0x09, 0x12, 0x1f, 0x0a, 0x1b, 0x50, + 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x44, 0x49, + 0x53, 0x50, 0x55, 0x54, 0x45, 0x5f, 0x4c, 0x4f, 0x53, 0x54, 0x10, 0x0a, 0x12, 0x18, 0x0a, 0x14, + 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x4f, + 0x54, 0x48, 0x45, 0x52, 0x10, 0x64, 0x42, 0x3f, 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x68, 0x71, 0x2f, + 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2f, 0x67, 0x72, 0x70, + 0x63, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_payment_proto_rawDescOnce sync.Once + file_payment_proto_rawDescData = file_payment_proto_rawDesc +) + +func file_payment_proto_rawDescGZIP() []byte { + file_payment_proto_rawDescOnce.Do(func() { + file_payment_proto_rawDescData = protoimpl.X.CompressGZIP(file_payment_proto_rawDescData) + }) + return file_payment_proto_rawDescData +} + +var file_payment_proto_enumTypes = make([]protoimpl.EnumInfo, 3) +var file_payment_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_payment_proto_goTypes = []interface{}{ + (PaymentType)(0), // 0: formance.payments.connectors.grpc.proto.PaymentType + (PaymentScheme)(0), // 1: formance.payments.connectors.grpc.proto.PaymentScheme + (PaymentStatus)(0), // 2: formance.payments.connectors.grpc.proto.PaymentStatus + (*Payment)(nil), // 3: formance.payments.connectors.grpc.proto.Payment + nil, // 4: formance.payments.connectors.grpc.proto.Payment.MetadataEntry + (*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp + (*Monetary)(nil), // 6: formance.payments.connectors.grpc.proto.Monetary + (*wrapperspb.StringValue)(nil), // 7: google.protobuf.StringValue +} +var file_payment_proto_depIdxs = []int32{ + 5, // 0: formance.payments.connectors.grpc.proto.Payment.created_at:type_name -> google.protobuf.Timestamp + 5, // 1: formance.payments.connectors.grpc.proto.Payment.synced_at:type_name -> google.protobuf.Timestamp + 0, // 2: formance.payments.connectors.grpc.proto.Payment.payment_type:type_name -> formance.payments.connectors.grpc.proto.PaymentType + 6, // 3: formance.payments.connectors.grpc.proto.Payment.amount:type_name -> formance.payments.connectors.grpc.proto.Monetary + 1, // 4: formance.payments.connectors.grpc.proto.Payment.scheme:type_name -> formance.payments.connectors.grpc.proto.PaymentScheme + 2, // 5: formance.payments.connectors.grpc.proto.Payment.status:type_name -> formance.payments.connectors.grpc.proto.PaymentStatus + 7, // 6: formance.payments.connectors.grpc.proto.Payment.source_account_reference:type_name -> google.protobuf.StringValue + 7, // 7: formance.payments.connectors.grpc.proto.Payment.destination_account_reference:type_name -> google.protobuf.StringValue + 4, // 8: formance.payments.connectors.grpc.proto.Payment.metadata:type_name -> formance.payments.connectors.grpc.proto.Payment.MetadataEntry + 9, // [9:9] is the sub-list for method output_type + 9, // [9:9] is the sub-list for method input_type + 9, // [9:9] is the sub-list for extension type_name + 9, // [9:9] is the sub-list for extension extendee + 0, // [0:9] is the sub-list for field type_name +} + +func init() { file_payment_proto_init() } +func file_payment_proto_init() { + if File_payment_proto != nil { + return + } + file_monetary_proto_init() + if !protoimpl.UnsafeEnabled { + file_payment_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Payment); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_payment_proto_rawDesc, + NumEnums: 3, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_payment_proto_goTypes, + DependencyIndexes: file_payment_proto_depIdxs, + EnumInfos: file_payment_proto_enumTypes, + MessageInfos: file_payment_proto_msgTypes, + }.Build() + File_payment_proto = out.File + file_payment_proto_rawDesc = nil + file_payment_proto_goTypes = nil + file_payment_proto_depIdxs = nil +} diff --git a/components/payments/internal/connectors/grpc/proto/payment.proto b/components/payments/internal/connectors/grpc/proto/payment.proto new file mode 100644 index 0000000000..dfd099e1ce --- /dev/null +++ b/components/payments/internal/connectors/grpc/proto/payment.proto @@ -0,0 +1,107 @@ +syntax = "proto3"; +package formance.payments.connectors.grpc.proto; +option go_package = "github.com/formancehq/payments/internal/connectors/grpc/proto"; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/wrappers.proto"; + +import "monetary.proto"; + +enum PaymentType { + PAYMENT_TYPE_UNKNOWN = 0; + PAYMENT_TYPE_PAYIN = 1; + PAYMENT_TYPE_PAYOUT = 2; + PAYMENT_TYPE_TRANSFER = 3; + PAYMENT_TYPE_OTHER = 100; +} + +enum PaymentScheme { + PAYMENT_SCHEME_UNKNOWN = 0; + + // Scheme cards + PAYMENT_SCHEME_CARD_VISA = 1; + PAYMENT_SCHEME_CARD_MASTERCARD = 2; + PAYMENT_SCHEME_CARD_AMEX = 3; + PAYMENT_SCHEME_CARD_DINERS = 4; + PAYMENT_SCHEME_CARD_DISCOVER = 5; + PAYMENT_SCHEME_CARD_JCB = 6; + PAYMENT_SCHEME_CARD_UNION_PAY = 7; + PAYMENT_SCHEME_CARD_ALIPAY = 8; + PAYMENT_SCHEME_CARD_CUP = 9; + + PAYMENT_SCHEME_SEPA_DEBIT = 10; + PAYMENT_SCHEME_SEPA_CREDIT = 11; + PAYMENT_SCHEME_SEPA = 12; + + PAYMENT_SCHEME_GOOGLE_PAY = 13; + PAYMENT_SCHEME_APPLE_PAY = 14; + + PAYMENT_SCHEME_DOKU = 15; + PAYMENT_SCHEME_DRAGON_PAY = 16; + PAYMENT_SCHEME_MAESTRO = 17; + PAYMENT_SCHEME_MOL_PAY = 18; + PAYMENT_SCHEME_A2A = 19; + PAYMENT_SCHEME_ACH_DEBIT = 20; + PAYMENT_SCHEME_ACH = 21; + PAYMENT_SCHEME_RTP = 22; + + + PAYMENT_SCHEME_OTHER = 100; +} + +enum PaymentStatus { + PAYMENT_STATUS_UNKNOWN = 0; + + PAYMENT_STATUS_PENDING = 1; + + PAYMENT_STATUS_SUCCEEDED = 2; + + PAYMENT_STATUS_CANCELLED = 3; + PAYMENT_STATUS_FAILED = 4; + PAYMENT_STATUS_EXPIRED = 5; + + PAYMENT_STATUS_REFUNDED = 6; + PAYMENT_STATUS_REFUNDED_FAILURE = 7; + + PAYMENT_STATUS_DISPUTE = 8; + PAYMENT_STATUS_DISPUTE_WON = 9; + PAYMENT_STATUS_DISPUTE_LOST = 10; + + PAYMENT_STATUS_OTHER = 100; +} + +// Represent a payment/transaction in Formance platform +message Payment { + // PSP reference of the payment + string reference = 1; + + // Payment's creation date + google.protobuf.Timestamp created_at = 2; + google.protobuf.Timestamp synced_at = 4; + + // Payment Type, can be payin, payout, transfer or other + PaymentType payment_type = 5; + + // amount + formance.payments.connectors.grpc.proto.Monetary amount = 6; + + // Payment scheme, for example when you pay via visa card etc... + PaymentScheme scheme = 7; + + // Payment status, for example pending, succeeded, failed etc... + PaymentStatus status = 8; + + // Nullable string, we can have or not a source account id depending + // on the payment type + google.protobuf.StringValue source_account_reference = 9; + + // Nullable string, we can have or not a destination account id depending + // on the payment type + google.protobuf.StringValue destination_account_reference = 10; + + // Additional metadata + map metadata = 11; + + // The PSP raw message + bytes raw = 12; +} \ No newline at end of file diff --git a/components/payments/internal/connectors/grpc/proto/services/plugin.pb.go b/components/payments/internal/connectors/grpc/proto/services/plugin.pb.go new file mode 100644 index 0000000000..fb5d1acc55 --- /dev/null +++ b/components/payments/internal/connectors/grpc/proto/services/plugin.pb.go @@ -0,0 +1,1936 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.30.0 +// protoc v3.21.12 +// source: services/plugin.proto + +package services + +import ( + proto "github.com/formancehq/payments/internal/connectors/grpc/proto" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type InstallRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Config []byte `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` +} + +func (x *InstallRequest) Reset() { + *x = InstallRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_services_plugin_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *InstallRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InstallRequest) ProtoMessage() {} + +func (x *InstallRequest) ProtoReflect() protoreflect.Message { + mi := &file_services_plugin_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InstallRequest.ProtoReflect.Descriptor instead. +func (*InstallRequest) Descriptor() ([]byte, []int) { + return file_services_plugin_proto_rawDescGZIP(), []int{0} +} + +func (x *InstallRequest) GetConfig() []byte { + if x != nil { + return x.Config + } + return nil +} + +type InstallResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Capabilities []proto.Capability `protobuf:"varint,1,rep,packed,name=capabilities,proto3,enum=formance.payments.connectors.grpc.proto.Capability" json:"capabilities,omitempty"` + Workflow *proto.Workflow `protobuf:"bytes,2,opt,name=workflow,proto3" json:"workflow,omitempty"` + WebhooksConfigs []*proto.WebhookConfig `protobuf:"bytes,3,rep,name=webhooks_configs,json=webhooksConfigs,proto3" json:"webhooks_configs,omitempty"` +} + +func (x *InstallResponse) Reset() { + *x = InstallResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_services_plugin_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *InstallResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InstallResponse) ProtoMessage() {} + +func (x *InstallResponse) ProtoReflect() protoreflect.Message { + mi := &file_services_plugin_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InstallResponse.ProtoReflect.Descriptor instead. +func (*InstallResponse) Descriptor() ([]byte, []int) { + return file_services_plugin_proto_rawDescGZIP(), []int{1} +} + +func (x *InstallResponse) GetCapabilities() []proto.Capability { + if x != nil { + return x.Capabilities + } + return nil +} + +func (x *InstallResponse) GetWorkflow() *proto.Workflow { + if x != nil { + return x.Workflow + } + return nil +} + +func (x *InstallResponse) GetWebhooksConfigs() []*proto.WebhookConfig { + if x != nil { + return x.WebhooksConfigs + } + return nil +} + +type UninstallRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ConnectorId string `protobuf:"bytes,1,opt,name=connector_id,json=connectorId,proto3" json:"connector_id,omitempty"` +} + +func (x *UninstallRequest) Reset() { + *x = UninstallRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_services_plugin_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UninstallRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UninstallRequest) ProtoMessage() {} + +func (x *UninstallRequest) ProtoReflect() protoreflect.Message { + mi := &file_services_plugin_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UninstallRequest.ProtoReflect.Descriptor instead. +func (*UninstallRequest) Descriptor() ([]byte, []int) { + return file_services_plugin_proto_rawDescGZIP(), []int{2} +} + +func (x *UninstallRequest) GetConnectorId() string { + if x != nil { + return x.ConnectorId + } + return "" +} + +type UninstallResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *UninstallResponse) Reset() { + *x = UninstallResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_services_plugin_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UninstallResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UninstallResponse) ProtoMessage() {} + +func (x *UninstallResponse) ProtoReflect() protoreflect.Message { + mi := &file_services_plugin_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UninstallResponse.ProtoReflect.Descriptor instead. +func (*UninstallResponse) Descriptor() ([]byte, []int) { + return file_services_plugin_proto_rawDescGZIP(), []int{3} +} + +type FetchNextOthersRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + FromPayload []byte `protobuf:"bytes,2,opt,name=from_payload,json=fromPayload,proto3" json:"from_payload,omitempty"` + State []byte `protobuf:"bytes,3,opt,name=state,proto3" json:"state,omitempty"` + PageSize int64 `protobuf:"varint,4,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` +} + +func (x *FetchNextOthersRequest) Reset() { + *x = FetchNextOthersRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_services_plugin_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FetchNextOthersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FetchNextOthersRequest) ProtoMessage() {} + +func (x *FetchNextOthersRequest) ProtoReflect() protoreflect.Message { + mi := &file_services_plugin_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FetchNextOthersRequest.ProtoReflect.Descriptor instead. +func (*FetchNextOthersRequest) Descriptor() ([]byte, []int) { + return file_services_plugin_proto_rawDescGZIP(), []int{4} +} + +func (x *FetchNextOthersRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *FetchNextOthersRequest) GetFromPayload() []byte { + if x != nil { + return x.FromPayload + } + return nil +} + +func (x *FetchNextOthersRequest) GetState() []byte { + if x != nil { + return x.State + } + return nil +} + +func (x *FetchNextOthersRequest) GetPageSize() int64 { + if x != nil { + return x.PageSize + } + return 0 +} + +type FetchNextOthersResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Others []*proto.Other `protobuf:"bytes,1,rep,name=others,proto3" json:"others,omitempty"` + NewState []byte `protobuf:"bytes,2,opt,name=new_state,json=newState,proto3" json:"new_state,omitempty"` + HasMore bool `protobuf:"varint,3,opt,name=has_more,json=hasMore,proto3" json:"has_more,omitempty"` +} + +func (x *FetchNextOthersResponse) Reset() { + *x = FetchNextOthersResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_services_plugin_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FetchNextOthersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FetchNextOthersResponse) ProtoMessage() {} + +func (x *FetchNextOthersResponse) ProtoReflect() protoreflect.Message { + mi := &file_services_plugin_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FetchNextOthersResponse.ProtoReflect.Descriptor instead. +func (*FetchNextOthersResponse) Descriptor() ([]byte, []int) { + return file_services_plugin_proto_rawDescGZIP(), []int{5} +} + +func (x *FetchNextOthersResponse) GetOthers() []*proto.Other { + if x != nil { + return x.Others + } + return nil +} + +func (x *FetchNextOthersResponse) GetNewState() []byte { + if x != nil { + return x.NewState + } + return nil +} + +func (x *FetchNextOthersResponse) GetHasMore() bool { + if x != nil { + return x.HasMore + } + return false +} + +type FetchNextPaymentsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + FromPayload []byte `protobuf:"bytes,1,opt,name=from_payload,json=fromPayload,proto3" json:"from_payload,omitempty"` + State []byte `protobuf:"bytes,2,opt,name=state,proto3" json:"state,omitempty"` + PageSize int64 `protobuf:"varint,3,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` +} + +func (x *FetchNextPaymentsRequest) Reset() { + *x = FetchNextPaymentsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_services_plugin_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FetchNextPaymentsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FetchNextPaymentsRequest) ProtoMessage() {} + +func (x *FetchNextPaymentsRequest) ProtoReflect() protoreflect.Message { + mi := &file_services_plugin_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FetchNextPaymentsRequest.ProtoReflect.Descriptor instead. +func (*FetchNextPaymentsRequest) Descriptor() ([]byte, []int) { + return file_services_plugin_proto_rawDescGZIP(), []int{6} +} + +func (x *FetchNextPaymentsRequest) GetFromPayload() []byte { + if x != nil { + return x.FromPayload + } + return nil +} + +func (x *FetchNextPaymentsRequest) GetState() []byte { + if x != nil { + return x.State + } + return nil +} + +func (x *FetchNextPaymentsRequest) GetPageSize() int64 { + if x != nil { + return x.PageSize + } + return 0 +} + +type FetchNextPaymentsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Payments []*proto.Payment `protobuf:"bytes,1,rep,name=payments,proto3" json:"payments,omitempty"` + NewState []byte `protobuf:"bytes,2,opt,name=new_state,json=newState,proto3" json:"new_state,omitempty"` + HasMore bool `protobuf:"varint,3,opt,name=has_more,json=hasMore,proto3" json:"has_more,omitempty"` +} + +func (x *FetchNextPaymentsResponse) Reset() { + *x = FetchNextPaymentsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_services_plugin_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FetchNextPaymentsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FetchNextPaymentsResponse) ProtoMessage() {} + +func (x *FetchNextPaymentsResponse) ProtoReflect() protoreflect.Message { + mi := &file_services_plugin_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FetchNextPaymentsResponse.ProtoReflect.Descriptor instead. +func (*FetchNextPaymentsResponse) Descriptor() ([]byte, []int) { + return file_services_plugin_proto_rawDescGZIP(), []int{7} +} + +func (x *FetchNextPaymentsResponse) GetPayments() []*proto.Payment { + if x != nil { + return x.Payments + } + return nil +} + +func (x *FetchNextPaymentsResponse) GetNewState() []byte { + if x != nil { + return x.NewState + } + return nil +} + +func (x *FetchNextPaymentsResponse) GetHasMore() bool { + if x != nil { + return x.HasMore + } + return false +} + +type FetchNextAccountsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + FromPayload []byte `protobuf:"bytes,1,opt,name=from_payload,json=fromPayload,proto3" json:"from_payload,omitempty"` + State []byte `protobuf:"bytes,2,opt,name=state,proto3" json:"state,omitempty"` + PageSize int64 `protobuf:"varint,3,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` +} + +func (x *FetchNextAccountsRequest) Reset() { + *x = FetchNextAccountsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_services_plugin_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FetchNextAccountsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FetchNextAccountsRequest) ProtoMessage() {} + +func (x *FetchNextAccountsRequest) ProtoReflect() protoreflect.Message { + mi := &file_services_plugin_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FetchNextAccountsRequest.ProtoReflect.Descriptor instead. +func (*FetchNextAccountsRequest) Descriptor() ([]byte, []int) { + return file_services_plugin_proto_rawDescGZIP(), []int{8} +} + +func (x *FetchNextAccountsRequest) GetFromPayload() []byte { + if x != nil { + return x.FromPayload + } + return nil +} + +func (x *FetchNextAccountsRequest) GetState() []byte { + if x != nil { + return x.State + } + return nil +} + +func (x *FetchNextAccountsRequest) GetPageSize() int64 { + if x != nil { + return x.PageSize + } + return 0 +} + +type FetchNextAccountsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Accounts []*proto.Account `protobuf:"bytes,1,rep,name=accounts,proto3" json:"accounts,omitempty"` + NewState []byte `protobuf:"bytes,2,opt,name=new_state,json=newState,proto3" json:"new_state,omitempty"` + HasMore bool `protobuf:"varint,3,opt,name=has_more,json=hasMore,proto3" json:"has_more,omitempty"` +} + +func (x *FetchNextAccountsResponse) Reset() { + *x = FetchNextAccountsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_services_plugin_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FetchNextAccountsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FetchNextAccountsResponse) ProtoMessage() {} + +func (x *FetchNextAccountsResponse) ProtoReflect() protoreflect.Message { + mi := &file_services_plugin_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FetchNextAccountsResponse.ProtoReflect.Descriptor instead. +func (*FetchNextAccountsResponse) Descriptor() ([]byte, []int) { + return file_services_plugin_proto_rawDescGZIP(), []int{9} +} + +func (x *FetchNextAccountsResponse) GetAccounts() []*proto.Account { + if x != nil { + return x.Accounts + } + return nil +} + +func (x *FetchNextAccountsResponse) GetNewState() []byte { + if x != nil { + return x.NewState + } + return nil +} + +func (x *FetchNextAccountsResponse) GetHasMore() bool { + if x != nil { + return x.HasMore + } + return false +} + +type FetchNextExternalAccountsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + FromPayload []byte `protobuf:"bytes,1,opt,name=from_payload,json=fromPayload,proto3" json:"from_payload,omitempty"` + State []byte `protobuf:"bytes,2,opt,name=state,proto3" json:"state,omitempty"` + PageSize int64 `protobuf:"varint,3,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` +} + +func (x *FetchNextExternalAccountsRequest) Reset() { + *x = FetchNextExternalAccountsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_services_plugin_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FetchNextExternalAccountsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FetchNextExternalAccountsRequest) ProtoMessage() {} + +func (x *FetchNextExternalAccountsRequest) ProtoReflect() protoreflect.Message { + mi := &file_services_plugin_proto_msgTypes[10] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FetchNextExternalAccountsRequest.ProtoReflect.Descriptor instead. +func (*FetchNextExternalAccountsRequest) Descriptor() ([]byte, []int) { + return file_services_plugin_proto_rawDescGZIP(), []int{10} +} + +func (x *FetchNextExternalAccountsRequest) GetFromPayload() []byte { + if x != nil { + return x.FromPayload + } + return nil +} + +func (x *FetchNextExternalAccountsRequest) GetState() []byte { + if x != nil { + return x.State + } + return nil +} + +func (x *FetchNextExternalAccountsRequest) GetPageSize() int64 { + if x != nil { + return x.PageSize + } + return 0 +} + +type FetchNextExternalAccountsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Accounts []*proto.Account `protobuf:"bytes,1,rep,name=accounts,proto3" json:"accounts,omitempty"` + NewState []byte `protobuf:"bytes,2,opt,name=new_state,json=newState,proto3" json:"new_state,omitempty"` + HasMore bool `protobuf:"varint,3,opt,name=has_more,json=hasMore,proto3" json:"has_more,omitempty"` +} + +func (x *FetchNextExternalAccountsResponse) Reset() { + *x = FetchNextExternalAccountsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_services_plugin_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FetchNextExternalAccountsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FetchNextExternalAccountsResponse) ProtoMessage() {} + +func (x *FetchNextExternalAccountsResponse) ProtoReflect() protoreflect.Message { + mi := &file_services_plugin_proto_msgTypes[11] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FetchNextExternalAccountsResponse.ProtoReflect.Descriptor instead. +func (*FetchNextExternalAccountsResponse) Descriptor() ([]byte, []int) { + return file_services_plugin_proto_rawDescGZIP(), []int{11} +} + +func (x *FetchNextExternalAccountsResponse) GetAccounts() []*proto.Account { + if x != nil { + return x.Accounts + } + return nil +} + +func (x *FetchNextExternalAccountsResponse) GetNewState() []byte { + if x != nil { + return x.NewState + } + return nil +} + +func (x *FetchNextExternalAccountsResponse) GetHasMore() bool { + if x != nil { + return x.HasMore + } + return false +} + +type FetchNextBalancesRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + FromPayload []byte `protobuf:"bytes,1,opt,name=from_payload,json=fromPayload,proto3" json:"from_payload,omitempty"` + State []byte `protobuf:"bytes,2,opt,name=state,proto3" json:"state,omitempty"` + PageSize int64 `protobuf:"varint,3,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` +} + +func (x *FetchNextBalancesRequest) Reset() { + *x = FetchNextBalancesRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_services_plugin_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FetchNextBalancesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FetchNextBalancesRequest) ProtoMessage() {} + +func (x *FetchNextBalancesRequest) ProtoReflect() protoreflect.Message { + mi := &file_services_plugin_proto_msgTypes[12] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FetchNextBalancesRequest.ProtoReflect.Descriptor instead. +func (*FetchNextBalancesRequest) Descriptor() ([]byte, []int) { + return file_services_plugin_proto_rawDescGZIP(), []int{12} +} + +func (x *FetchNextBalancesRequest) GetFromPayload() []byte { + if x != nil { + return x.FromPayload + } + return nil +} + +func (x *FetchNextBalancesRequest) GetState() []byte { + if x != nil { + return x.State + } + return nil +} + +func (x *FetchNextBalancesRequest) GetPageSize() int64 { + if x != nil { + return x.PageSize + } + return 0 +} + +type FetchNextBalancesResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Balances []*proto.Balance `protobuf:"bytes,1,rep,name=balances,proto3" json:"balances,omitempty"` + NewState []byte `protobuf:"bytes,2,opt,name=new_state,json=newState,proto3" json:"new_state,omitempty"` + HasMore bool `protobuf:"varint,3,opt,name=has_more,json=hasMore,proto3" json:"has_more,omitempty"` +} + +func (x *FetchNextBalancesResponse) Reset() { + *x = FetchNextBalancesResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_services_plugin_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FetchNextBalancesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FetchNextBalancesResponse) ProtoMessage() {} + +func (x *FetchNextBalancesResponse) ProtoReflect() protoreflect.Message { + mi := &file_services_plugin_proto_msgTypes[13] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FetchNextBalancesResponse.ProtoReflect.Descriptor instead. +func (*FetchNextBalancesResponse) Descriptor() ([]byte, []int) { + return file_services_plugin_proto_rawDescGZIP(), []int{13} +} + +func (x *FetchNextBalancesResponse) GetBalances() []*proto.Balance { + if x != nil { + return x.Balances + } + return nil +} + +func (x *FetchNextBalancesResponse) GetNewState() []byte { + if x != nil { + return x.NewState + } + return nil +} + +func (x *FetchNextBalancesResponse) GetHasMore() bool { + if x != nil { + return x.HasMore + } + return false +} + +type CreateBankAccountRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + BankAccount *proto.BankAccount `protobuf:"bytes,1,opt,name=bank_account,json=bankAccount,proto3" json:"bank_account,omitempty"` +} + +func (x *CreateBankAccountRequest) Reset() { + *x = CreateBankAccountRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_services_plugin_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateBankAccountRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateBankAccountRequest) ProtoMessage() {} + +func (x *CreateBankAccountRequest) ProtoReflect() protoreflect.Message { + mi := &file_services_plugin_proto_msgTypes[14] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateBankAccountRequest.ProtoReflect.Descriptor instead. +func (*CreateBankAccountRequest) Descriptor() ([]byte, []int) { + return file_services_plugin_proto_rawDescGZIP(), []int{14} +} + +func (x *CreateBankAccountRequest) GetBankAccount() *proto.BankAccount { + if x != nil { + return x.BankAccount + } + return nil +} + +type CreateBankAccountResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RelatedAccount *proto.Account `protobuf:"bytes,1,opt,name=related_account,json=relatedAccount,proto3" json:"related_account,omitempty"` +} + +func (x *CreateBankAccountResponse) Reset() { + *x = CreateBankAccountResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_services_plugin_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateBankAccountResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateBankAccountResponse) ProtoMessage() {} + +func (x *CreateBankAccountResponse) ProtoReflect() protoreflect.Message { + mi := &file_services_plugin_proto_msgTypes[15] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateBankAccountResponse.ProtoReflect.Descriptor instead. +func (*CreateBankAccountResponse) Descriptor() ([]byte, []int) { + return file_services_plugin_proto_rawDescGZIP(), []int{15} +} + +func (x *CreateBankAccountResponse) GetRelatedAccount() *proto.Account { + if x != nil { + return x.RelatedAccount + } + return nil +} + +type TranslateWebhookRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Webhook *proto.Webhook `protobuf:"bytes,2,opt,name=webhook,proto3" json:"webhook,omitempty"` +} + +func (x *TranslateWebhookRequest) Reset() { + *x = TranslateWebhookRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_services_plugin_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TranslateWebhookRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TranslateWebhookRequest) ProtoMessage() {} + +func (x *TranslateWebhookRequest) ProtoReflect() protoreflect.Message { + mi := &file_services_plugin_proto_msgTypes[16] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TranslateWebhookRequest.ProtoReflect.Descriptor instead. +func (*TranslateWebhookRequest) Descriptor() ([]byte, []int) { + return file_services_plugin_proto_rawDescGZIP(), []int{16} +} + +func (x *TranslateWebhookRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *TranslateWebhookRequest) GetWebhook() *proto.Webhook { + if x != nil { + return x.Webhook + } + return nil +} + +type TranslateWebhookResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Responses []*TranslateWebhookResponse_Response `protobuf:"bytes,1,rep,name=responses,proto3" json:"responses,omitempty"` +} + +func (x *TranslateWebhookResponse) Reset() { + *x = TranslateWebhookResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_services_plugin_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TranslateWebhookResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TranslateWebhookResponse) ProtoMessage() {} + +func (x *TranslateWebhookResponse) ProtoReflect() protoreflect.Message { + mi := &file_services_plugin_proto_msgTypes[17] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TranslateWebhookResponse.ProtoReflect.Descriptor instead. +func (*TranslateWebhookResponse) Descriptor() ([]byte, []int) { + return file_services_plugin_proto_rawDescGZIP(), []int{17} +} + +func (x *TranslateWebhookResponse) GetResponses() []*TranslateWebhookResponse_Response { + if x != nil { + return x.Responses + } + return nil +} + +type CreateWebhooksRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + FromPayload []byte `protobuf:"bytes,1,opt,name=from_payload,json=fromPayload,proto3" json:"from_payload,omitempty"` + ConnectorId string `protobuf:"bytes,2,opt,name=connector_id,json=connectorId,proto3" json:"connector_id,omitempty"` +} + +func (x *CreateWebhooksRequest) Reset() { + *x = CreateWebhooksRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_services_plugin_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateWebhooksRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateWebhooksRequest) ProtoMessage() {} + +func (x *CreateWebhooksRequest) ProtoReflect() protoreflect.Message { + mi := &file_services_plugin_proto_msgTypes[18] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateWebhooksRequest.ProtoReflect.Descriptor instead. +func (*CreateWebhooksRequest) Descriptor() ([]byte, []int) { + return file_services_plugin_proto_rawDescGZIP(), []int{18} +} + +func (x *CreateWebhooksRequest) GetFromPayload() []byte { + if x != nil { + return x.FromPayload + } + return nil +} + +func (x *CreateWebhooksRequest) GetConnectorId() string { + if x != nil { + return x.ConnectorId + } + return "" +} + +type CreateWebhooksResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Others []*proto.Other `protobuf:"bytes,1,rep,name=others,proto3" json:"others,omitempty"` +} + +func (x *CreateWebhooksResponse) Reset() { + *x = CreateWebhooksResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_services_plugin_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateWebhooksResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateWebhooksResponse) ProtoMessage() {} + +func (x *CreateWebhooksResponse) ProtoReflect() protoreflect.Message { + mi := &file_services_plugin_proto_msgTypes[19] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateWebhooksResponse.ProtoReflect.Descriptor instead. +func (*CreateWebhooksResponse) Descriptor() ([]byte, []int) { + return file_services_plugin_proto_rawDescGZIP(), []int{19} +} + +func (x *CreateWebhooksResponse) GetOthers() []*proto.Other { + if x != nil { + return x.Others + } + return nil +} + +type TranslateWebhookResponse_Response struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + IdempotencyKey string `protobuf:"bytes,1,opt,name=idempotency_key,json=idempotencyKey,proto3" json:"idempotency_key,omitempty"` + // Types that are assignable to Translated: + // + // *TranslateWebhookResponse_Response_Account + // *TranslateWebhookResponse_Response_ExternalAccount + // *TranslateWebhookResponse_Response_Payment + // *TranslateWebhookResponse_Response_Balance + Translated isTranslateWebhookResponse_Response_Translated `protobuf_oneof:"translated"` +} + +func (x *TranslateWebhookResponse_Response) Reset() { + *x = TranslateWebhookResponse_Response{} + if protoimpl.UnsafeEnabled { + mi := &file_services_plugin_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TranslateWebhookResponse_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TranslateWebhookResponse_Response) ProtoMessage() {} + +func (x *TranslateWebhookResponse_Response) ProtoReflect() protoreflect.Message { + mi := &file_services_plugin_proto_msgTypes[20] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TranslateWebhookResponse_Response.ProtoReflect.Descriptor instead. +func (*TranslateWebhookResponse_Response) Descriptor() ([]byte, []int) { + return file_services_plugin_proto_rawDescGZIP(), []int{17, 0} +} + +func (x *TranslateWebhookResponse_Response) GetIdempotencyKey() string { + if x != nil { + return x.IdempotencyKey + } + return "" +} + +func (m *TranslateWebhookResponse_Response) GetTranslated() isTranslateWebhookResponse_Response_Translated { + if m != nil { + return m.Translated + } + return nil +} + +func (x *TranslateWebhookResponse_Response) GetAccount() *proto.Account { + if x, ok := x.GetTranslated().(*TranslateWebhookResponse_Response_Account); ok { + return x.Account + } + return nil +} + +func (x *TranslateWebhookResponse_Response) GetExternalAccount() *proto.Account { + if x, ok := x.GetTranslated().(*TranslateWebhookResponse_Response_ExternalAccount); ok { + return x.ExternalAccount + } + return nil +} + +func (x *TranslateWebhookResponse_Response) GetPayment() *proto.Payment { + if x, ok := x.GetTranslated().(*TranslateWebhookResponse_Response_Payment); ok { + return x.Payment + } + return nil +} + +func (x *TranslateWebhookResponse_Response) GetBalance() *proto.Balance { + if x, ok := x.GetTranslated().(*TranslateWebhookResponse_Response_Balance); ok { + return x.Balance + } + return nil +} + +type isTranslateWebhookResponse_Response_Translated interface { + isTranslateWebhookResponse_Response_Translated() +} + +type TranslateWebhookResponse_Response_Account struct { + Account *proto.Account `protobuf:"bytes,10,opt,name=account,proto3,oneof"` +} + +type TranslateWebhookResponse_Response_ExternalAccount struct { + ExternalAccount *proto.Account `protobuf:"bytes,11,opt,name=external_account,json=externalAccount,proto3,oneof"` +} + +type TranslateWebhookResponse_Response_Payment struct { + Payment *proto.Payment `protobuf:"bytes,12,opt,name=payment,proto3,oneof"` +} + +type TranslateWebhookResponse_Response_Balance struct { + Balance *proto.Balance `protobuf:"bytes,13,opt,name=balance,proto3,oneof"` +} + +func (*TranslateWebhookResponse_Response_Account) isTranslateWebhookResponse_Response_Translated() {} + +func (*TranslateWebhookResponse_Response_ExternalAccount) isTranslateWebhookResponse_Response_Translated() { +} + +func (*TranslateWebhookResponse_Response_Payment) isTranslateWebhookResponse_Response_Translated() {} + +func (*TranslateWebhookResponse_Response_Balance) isTranslateWebhookResponse_Response_Translated() {} + +var File_services_plugin_proto protoreflect.FileDescriptor + +var file_services_plugin_proto_rawDesc = []byte{ + 0x0a, 0x15, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, + 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x1a, 0x0d, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0d, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x12, 0x62, 0x61, 0x6e, 0x6b, 0x5f, 0x61, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x10, 0x63, 0x61, 0x70, 0x61, + 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0b, 0x6f, 0x74, + 0x68, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0d, 0x70, 0x61, 0x79, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0d, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, + 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, + 0x77, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x28, 0x0a, 0x0e, 0x49, 0x6e, 0x73, 0x74, 0x61, + 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x22, 0x9c, 0x02, 0x0a, 0x0f, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x0c, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, + 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x33, 0x2e, 0x66, 0x6f, + 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, + 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, + 0x52, 0x0c, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x4d, + 0x0a, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x31, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, + 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, + 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x66, + 0x6c, 0x6f, 0x77, 0x52, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x12, 0x61, 0x0a, + 0x10, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, + 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, + 0x0f, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, + 0x22, 0x35, 0x0a, 0x10, 0x55, 0x6e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, + 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x22, 0x13, 0x0a, 0x11, 0x55, 0x6e, 0x69, 0x6e, 0x73, + 0x74, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x82, 0x01, 0x0a, + 0x16, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x4f, 0x74, 0x68, 0x65, 0x72, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x66, + 0x72, 0x6f, 0x6d, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x0b, 0x66, 0x72, 0x6f, 0x6d, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x14, + 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, + 0x74, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, + 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, + 0x65, 0x22, 0x99, 0x01, 0x0a, 0x17, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x4f, + 0x74, 0x68, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, + 0x06, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2e, 0x2e, + 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, + 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, + 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4f, 0x74, 0x68, 0x65, 0x72, 0x52, 0x06, 0x6f, + 0x74, 0x68, 0x65, 0x72, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x65, 0x77, 0x5f, 0x73, 0x74, 0x61, + 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x6e, 0x65, 0x77, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x68, 0x61, 0x73, 0x5f, 0x6d, 0x6f, 0x72, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x68, 0x61, 0x73, 0x4d, 0x6f, 0x72, 0x65, 0x22, 0x70, 0x0a, + 0x18, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, + 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x66, 0x72, 0x6f, + 0x6d, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x0b, 0x66, 0x72, 0x6f, 0x6d, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x14, 0x0a, 0x05, + 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, + 0x74, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x22, + 0xa1, 0x01, 0x0a, 0x19, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, + 0x08, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x30, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, + 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, + 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, + 0x74, 0x52, 0x08, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6e, + 0x65, 0x77, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, + 0x6e, 0x65, 0x77, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x68, 0x61, 0x73, 0x5f, + 0x6d, 0x6f, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x68, 0x61, 0x73, 0x4d, + 0x6f, 0x72, 0x65, 0x22, 0x70, 0x0a, 0x18, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, + 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x21, 0x0a, 0x0c, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x66, 0x72, 0x6f, 0x6d, 0x50, 0x61, 0x79, 0x6c, 0x6f, + 0x61, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, + 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x70, 0x61, 0x67, + 0x65, 0x53, 0x69, 0x7a, 0x65, 0x22, 0xa1, 0x01, 0x0a, 0x19, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, + 0x65, 0x78, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x08, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, + 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x08, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x65, 0x77, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x6e, 0x65, 0x77, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, + 0x0a, 0x08, 0x68, 0x61, 0x73, 0x5f, 0x6d, 0x6f, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x07, 0x68, 0x61, 0x73, 0x4d, 0x6f, 0x72, 0x65, 0x22, 0x78, 0x0a, 0x20, 0x46, 0x65, 0x74, + 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, + 0x0c, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x66, 0x72, 0x6f, 0x6d, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, + 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, + 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, + 0x69, 0x7a, 0x65, 0x22, 0xa9, 0x01, 0x0a, 0x21, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, + 0x74, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x08, 0x61, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x66, 0x6f, + 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, + 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x08, 0x61, + 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x65, 0x77, 0x5f, 0x73, + 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x6e, 0x65, 0x77, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x68, 0x61, 0x73, 0x5f, 0x6d, 0x6f, 0x72, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x68, 0x61, 0x73, 0x4d, 0x6f, 0x72, 0x65, 0x22, + 0x70, 0x0a, 0x18, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x42, 0x61, 0x6c, 0x61, + 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x66, + 0x72, 0x6f, 0x6d, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x0b, 0x66, 0x72, 0x6f, 0x6d, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x14, + 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, + 0x74, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, + 0x65, 0x22, 0xa1, 0x01, 0x0a, 0x19, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x42, + 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x4c, 0x0a, 0x08, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x30, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, + 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x42, 0x61, 0x6c, 0x61, + 0x6e, 0x63, 0x65, 0x52, 0x08, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x1b, 0x0a, + 0x09, 0x6e, 0x65, 0x77, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x08, 0x6e, 0x65, 0x77, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x68, 0x61, + 0x73, 0x5f, 0x6d, 0x6f, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x68, 0x61, + 0x73, 0x4d, 0x6f, 0x72, 0x65, 0x22, 0x73, 0x0a, 0x18, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x42, + 0x61, 0x6e, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x57, 0x0a, 0x0c, 0x62, 0x61, 0x6e, 0x6b, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, + 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x42, 0x61, 0x6e, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x0b, 0x62, + 0x61, 0x6e, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x76, 0x0a, 0x19, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x42, 0x61, 0x6e, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x59, 0x0a, 0x0f, 0x72, 0x65, 0x6c, 0x61, 0x74, + 0x65, 0x64, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x30, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, + 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, + 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x52, 0x0e, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x63, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x22, 0x79, 0x0a, 0x17, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x57, + 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x12, 0x4a, 0x0a, 0x07, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x57, 0x65, 0x62, + 0x68, 0x6f, 0x6f, 0x6b, 0x52, 0x07, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x22, 0x89, 0x04, + 0x0a, 0x18, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x68, 0x6f, + 0x6f, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x60, 0x0a, 0x09, 0x72, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x42, 0x2e, + 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, + 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, + 0x54, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x1a, 0x8a, 0x03, 0x0a, + 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x64, 0x65, + 0x6d, 0x70, 0x6f, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0e, 0x69, 0x64, 0x65, 0x6d, 0x70, 0x6f, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x4b, + 0x65, 0x79, 0x12, 0x4c, 0x0a, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, + 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, + 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x12, 0x5d, 0x0a, 0x10, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x66, 0x6f, 0x72, + 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x0f, + 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, + 0x4c, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x30, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, + 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, + 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, + 0x6e, 0x74, 0x48, 0x00, 0x52, 0x07, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x4c, 0x0a, + 0x07, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, + 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, + 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, + 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, + 0x48, 0x00, 0x52, 0x07, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x74, + 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x22, 0x5d, 0x0a, 0x15, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, + 0x61, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x66, 0x72, 0x6f, 0x6d, 0x50, 0x61, + 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x22, 0x60, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x46, 0x0a, 0x06, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4f, 0x74, 0x68, + 0x65, 0x72, 0x52, 0x06, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x73, 0x32, 0xec, 0x0a, 0x0a, 0x06, 0x50, + 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12, 0x6e, 0x0a, 0x07, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, + 0x12, 0x2f, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, + 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x73, 0x2e, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x30, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x73, 0x2e, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x74, 0x0a, 0x09, 0x55, 0x6e, 0x69, 0x6e, 0x73, 0x74, 0x61, + 0x6c, 0x6c, 0x12, 0x31, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x73, 0x2e, 0x55, 0x6e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x32, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, + 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x55, 0x6e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, + 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x86, 0x01, 0x0a, 0x0f, + 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x4f, 0x74, 0x68, 0x65, 0x72, 0x73, 0x12, + 0x37, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, + 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x73, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x4f, 0x74, 0x68, 0x65, 0x72, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x38, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, + 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, + 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, + 0x4e, 0x65, 0x78, 0x74, 0x4f, 0x74, 0x68, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x12, 0x8c, 0x01, 0x0a, 0x11, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, + 0x78, 0x74, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x39, 0x2e, 0x66, 0x6f, 0x72, + 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, + 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x46, 0x65, 0x74, + 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3a, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, + 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, + 0x74, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x8c, 0x01, 0x0a, 0x11, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, + 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, 0x39, 0x2e, 0x66, 0x6f, 0x72, 0x6d, + 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, + 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x46, 0x65, 0x74, 0x63, + 0x68, 0x4e, 0x65, 0x78, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3a, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, + 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, + 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x12, 0xa4, 0x01, 0x0a, 0x19, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, + 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, + 0x12, 0x41, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, + 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x73, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x45, 0x78, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x42, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, + 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x45, + 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x8c, 0x01, 0x0a, 0x11, 0x46, 0x65, + 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x12, + 0x39, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, + 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x73, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, + 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3a, 0x2e, 0x66, 0x6f, 0x72, + 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, + 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x46, 0x65, 0x74, + 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x8c, 0x01, 0x0a, 0x11, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x42, 0x61, 0x6e, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x39, + 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, + 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, + 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x42, 0x61, 0x6e, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3a, 0x2e, 0x66, 0x6f, 0x72, 0x6d, + 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, + 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x42, 0x61, 0x6e, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x83, 0x01, 0x0a, 0x0e, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x12, 0x36, 0x2e, 0x66, 0x6f, 0x72, + 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, + 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x37, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x68, 0x6f, + 0x6f, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x89, 0x01, + 0x0a, 0x10, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x68, 0x6f, + 0x6f, 0x6b, 0x12, 0x38, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x73, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x57, 0x65, + 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x39, 0x2e, 0x66, + 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, + 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x54, + 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x48, 0x5a, 0x46, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, + 0x68, 0x71, 0x2f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2f, + 0x67, 0x72, 0x70, 0x63, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_services_plugin_proto_rawDescOnce sync.Once + file_services_plugin_proto_rawDescData = file_services_plugin_proto_rawDesc +) + +func file_services_plugin_proto_rawDescGZIP() []byte { + file_services_plugin_proto_rawDescOnce.Do(func() { + file_services_plugin_proto_rawDescData = protoimpl.X.CompressGZIP(file_services_plugin_proto_rawDescData) + }) + return file_services_plugin_proto_rawDescData +} + +var file_services_plugin_proto_msgTypes = make([]protoimpl.MessageInfo, 21) +var file_services_plugin_proto_goTypes = []interface{}{ + (*InstallRequest)(nil), // 0: formance.payments.grpc.services.InstallRequest + (*InstallResponse)(nil), // 1: formance.payments.grpc.services.InstallResponse + (*UninstallRequest)(nil), // 2: formance.payments.grpc.services.UninstallRequest + (*UninstallResponse)(nil), // 3: formance.payments.grpc.services.UninstallResponse + (*FetchNextOthersRequest)(nil), // 4: formance.payments.grpc.services.FetchNextOthersRequest + (*FetchNextOthersResponse)(nil), // 5: formance.payments.grpc.services.FetchNextOthersResponse + (*FetchNextPaymentsRequest)(nil), // 6: formance.payments.grpc.services.FetchNextPaymentsRequest + (*FetchNextPaymentsResponse)(nil), // 7: formance.payments.grpc.services.FetchNextPaymentsResponse + (*FetchNextAccountsRequest)(nil), // 8: formance.payments.grpc.services.FetchNextAccountsRequest + (*FetchNextAccountsResponse)(nil), // 9: formance.payments.grpc.services.FetchNextAccountsResponse + (*FetchNextExternalAccountsRequest)(nil), // 10: formance.payments.grpc.services.FetchNextExternalAccountsRequest + (*FetchNextExternalAccountsResponse)(nil), // 11: formance.payments.grpc.services.FetchNextExternalAccountsResponse + (*FetchNextBalancesRequest)(nil), // 12: formance.payments.grpc.services.FetchNextBalancesRequest + (*FetchNextBalancesResponse)(nil), // 13: formance.payments.grpc.services.FetchNextBalancesResponse + (*CreateBankAccountRequest)(nil), // 14: formance.payments.grpc.services.CreateBankAccountRequest + (*CreateBankAccountResponse)(nil), // 15: formance.payments.grpc.services.CreateBankAccountResponse + (*TranslateWebhookRequest)(nil), // 16: formance.payments.grpc.services.TranslateWebhookRequest + (*TranslateWebhookResponse)(nil), // 17: formance.payments.grpc.services.TranslateWebhookResponse + (*CreateWebhooksRequest)(nil), // 18: formance.payments.grpc.services.CreateWebhooksRequest + (*CreateWebhooksResponse)(nil), // 19: formance.payments.grpc.services.CreateWebhooksResponse + (*TranslateWebhookResponse_Response)(nil), // 20: formance.payments.grpc.services.TranslateWebhookResponse.Response + (proto.Capability)(0), // 21: formance.payments.connectors.grpc.proto.Capability + (*proto.Workflow)(nil), // 22: formance.payments.connectors.grpc.proto.Workflow + (*proto.WebhookConfig)(nil), // 23: formance.payments.connectors.grpc.proto.WebhookConfig + (*proto.Other)(nil), // 24: formance.payments.connectors.grpc.proto.Other + (*proto.Payment)(nil), // 25: formance.payments.connectors.grpc.proto.Payment + (*proto.Account)(nil), // 26: formance.payments.connectors.grpc.proto.Account + (*proto.Balance)(nil), // 27: formance.payments.connectors.grpc.proto.Balance + (*proto.BankAccount)(nil), // 28: formance.payments.connectors.grpc.proto.BankAccount + (*proto.Webhook)(nil), // 29: formance.payments.connectors.grpc.proto.Webhook +} +var file_services_plugin_proto_depIdxs = []int32{ + 21, // 0: formance.payments.grpc.services.InstallResponse.capabilities:type_name -> formance.payments.connectors.grpc.proto.Capability + 22, // 1: formance.payments.grpc.services.InstallResponse.workflow:type_name -> formance.payments.connectors.grpc.proto.Workflow + 23, // 2: formance.payments.grpc.services.InstallResponse.webhooks_configs:type_name -> formance.payments.connectors.grpc.proto.WebhookConfig + 24, // 3: formance.payments.grpc.services.FetchNextOthersResponse.others:type_name -> formance.payments.connectors.grpc.proto.Other + 25, // 4: formance.payments.grpc.services.FetchNextPaymentsResponse.payments:type_name -> formance.payments.connectors.grpc.proto.Payment + 26, // 5: formance.payments.grpc.services.FetchNextAccountsResponse.accounts:type_name -> formance.payments.connectors.grpc.proto.Account + 26, // 6: formance.payments.grpc.services.FetchNextExternalAccountsResponse.accounts:type_name -> formance.payments.connectors.grpc.proto.Account + 27, // 7: formance.payments.grpc.services.FetchNextBalancesResponse.balances:type_name -> formance.payments.connectors.grpc.proto.Balance + 28, // 8: formance.payments.grpc.services.CreateBankAccountRequest.bank_account:type_name -> formance.payments.connectors.grpc.proto.BankAccount + 26, // 9: formance.payments.grpc.services.CreateBankAccountResponse.related_account:type_name -> formance.payments.connectors.grpc.proto.Account + 29, // 10: formance.payments.grpc.services.TranslateWebhookRequest.webhook:type_name -> formance.payments.connectors.grpc.proto.Webhook + 20, // 11: formance.payments.grpc.services.TranslateWebhookResponse.responses:type_name -> formance.payments.grpc.services.TranslateWebhookResponse.Response + 24, // 12: formance.payments.grpc.services.CreateWebhooksResponse.others:type_name -> formance.payments.connectors.grpc.proto.Other + 26, // 13: formance.payments.grpc.services.TranslateWebhookResponse.Response.account:type_name -> formance.payments.connectors.grpc.proto.Account + 26, // 14: formance.payments.grpc.services.TranslateWebhookResponse.Response.external_account:type_name -> formance.payments.connectors.grpc.proto.Account + 25, // 15: formance.payments.grpc.services.TranslateWebhookResponse.Response.payment:type_name -> formance.payments.connectors.grpc.proto.Payment + 27, // 16: formance.payments.grpc.services.TranslateWebhookResponse.Response.balance:type_name -> formance.payments.connectors.grpc.proto.Balance + 0, // 17: formance.payments.grpc.services.Plugin.Install:input_type -> formance.payments.grpc.services.InstallRequest + 2, // 18: formance.payments.grpc.services.Plugin.Uninstall:input_type -> formance.payments.grpc.services.UninstallRequest + 4, // 19: formance.payments.grpc.services.Plugin.FetchNextOthers:input_type -> formance.payments.grpc.services.FetchNextOthersRequest + 6, // 20: formance.payments.grpc.services.Plugin.FetchNextPayments:input_type -> formance.payments.grpc.services.FetchNextPaymentsRequest + 8, // 21: formance.payments.grpc.services.Plugin.FetchNextAccounts:input_type -> formance.payments.grpc.services.FetchNextAccountsRequest + 10, // 22: formance.payments.grpc.services.Plugin.FetchNextExternalAccounts:input_type -> formance.payments.grpc.services.FetchNextExternalAccountsRequest + 12, // 23: formance.payments.grpc.services.Plugin.FetchNextBalances:input_type -> formance.payments.grpc.services.FetchNextBalancesRequest + 14, // 24: formance.payments.grpc.services.Plugin.CreateBankAccount:input_type -> formance.payments.grpc.services.CreateBankAccountRequest + 18, // 25: formance.payments.grpc.services.Plugin.CreateWebhooks:input_type -> formance.payments.grpc.services.CreateWebhooksRequest + 16, // 26: formance.payments.grpc.services.Plugin.TranslateWebhook:input_type -> formance.payments.grpc.services.TranslateWebhookRequest + 1, // 27: formance.payments.grpc.services.Plugin.Install:output_type -> formance.payments.grpc.services.InstallResponse + 3, // 28: formance.payments.grpc.services.Plugin.Uninstall:output_type -> formance.payments.grpc.services.UninstallResponse + 5, // 29: formance.payments.grpc.services.Plugin.FetchNextOthers:output_type -> formance.payments.grpc.services.FetchNextOthersResponse + 7, // 30: formance.payments.grpc.services.Plugin.FetchNextPayments:output_type -> formance.payments.grpc.services.FetchNextPaymentsResponse + 9, // 31: formance.payments.grpc.services.Plugin.FetchNextAccounts:output_type -> formance.payments.grpc.services.FetchNextAccountsResponse + 11, // 32: formance.payments.grpc.services.Plugin.FetchNextExternalAccounts:output_type -> formance.payments.grpc.services.FetchNextExternalAccountsResponse + 13, // 33: formance.payments.grpc.services.Plugin.FetchNextBalances:output_type -> formance.payments.grpc.services.FetchNextBalancesResponse + 15, // 34: formance.payments.grpc.services.Plugin.CreateBankAccount:output_type -> formance.payments.grpc.services.CreateBankAccountResponse + 19, // 35: formance.payments.grpc.services.Plugin.CreateWebhooks:output_type -> formance.payments.grpc.services.CreateWebhooksResponse + 17, // 36: formance.payments.grpc.services.Plugin.TranslateWebhook:output_type -> formance.payments.grpc.services.TranslateWebhookResponse + 27, // [27:37] is the sub-list for method output_type + 17, // [17:27] is the sub-list for method input_type + 17, // [17:17] is the sub-list for extension type_name + 17, // [17:17] is the sub-list for extension extendee + 0, // [0:17] is the sub-list for field type_name +} + +func init() { file_services_plugin_proto_init() } +func file_services_plugin_proto_init() { + if File_services_plugin_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_services_plugin_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*InstallRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_services_plugin_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*InstallResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_services_plugin_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UninstallRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_services_plugin_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UninstallResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_services_plugin_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FetchNextOthersRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_services_plugin_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FetchNextOthersResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_services_plugin_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FetchNextPaymentsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_services_plugin_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FetchNextPaymentsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_services_plugin_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FetchNextAccountsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_services_plugin_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FetchNextAccountsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_services_plugin_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FetchNextExternalAccountsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_services_plugin_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FetchNextExternalAccountsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_services_plugin_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FetchNextBalancesRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_services_plugin_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FetchNextBalancesResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_services_plugin_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateBankAccountRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_services_plugin_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateBankAccountResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_services_plugin_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TranslateWebhookRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_services_plugin_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TranslateWebhookResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_services_plugin_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateWebhooksRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_services_plugin_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateWebhooksResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_services_plugin_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TranslateWebhookResponse_Response); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_services_plugin_proto_msgTypes[20].OneofWrappers = []interface{}{ + (*TranslateWebhookResponse_Response_Account)(nil), + (*TranslateWebhookResponse_Response_ExternalAccount)(nil), + (*TranslateWebhookResponse_Response_Payment)(nil), + (*TranslateWebhookResponse_Response_Balance)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_services_plugin_proto_rawDesc, + NumEnums: 0, + NumMessages: 21, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_services_plugin_proto_goTypes, + DependencyIndexes: file_services_plugin_proto_depIdxs, + MessageInfos: file_services_plugin_proto_msgTypes, + }.Build() + File_services_plugin_proto = out.File + file_services_plugin_proto_rawDesc = nil + file_services_plugin_proto_goTypes = nil + file_services_plugin_proto_depIdxs = nil +} diff --git a/components/payments/internal/connectors/grpc/proto/services/plugin.proto b/components/payments/internal/connectors/grpc/proto/services/plugin.proto new file mode 100644 index 0000000000..745e3f9f68 --- /dev/null +++ b/components/payments/internal/connectors/grpc/proto/services/plugin.proto @@ -0,0 +1,137 @@ +syntax = "proto3"; +package formance.payments.grpc.services; +option go_package = "github.com/formancehq/payments/internal/connectors/grpc/proto/services"; + +import "account.proto"; +import "balance.proto"; +import "bank_account.proto"; +import "capability.proto"; +import "other.proto"; +import "payment.proto"; +import "webhook.proto"; +import "workflow.proto"; + +message InstallRequest { + bytes config = 1; +} + +message InstallResponse { + repeated formance.payments.connectors.grpc.proto.Capability capabilities = 1; + formance.payments.connectors.grpc.proto.Workflow workflow = 2; + repeated formance.payments.connectors.grpc.proto.WebhookConfig webhooks_configs = 3; +} + +message UninstallRequest { + string connector_id = 1; +} + +message UninstallResponse {} + +message FetchNextOthersRequest { + string name = 1; + bytes from_payload = 2; + bytes state = 3; + int64 page_size = 4; +} +message FetchNextOthersResponse { + repeated formance.payments.connectors.grpc.proto.Other others = 1; + bytes new_state = 2; + bool has_more = 3; +} + +message FetchNextPaymentsRequest { + bytes from_payload = 1; + bytes state = 2; + int64 page_size = 3; +} +message FetchNextPaymentsResponse { + repeated formance.payments.connectors.grpc.proto.Payment payments = 1; + bytes new_state = 2; + bool has_more = 3; +} + +message FetchNextAccountsRequest { + bytes from_payload = 1; + bytes state = 2; + int64 page_size = 3; +} +message FetchNextAccountsResponse { + repeated formance.payments.connectors.grpc.proto.Account accounts = 1; + bytes new_state = 2; + bool has_more = 3; +} + +message FetchNextExternalAccountsRequest { + bytes from_payload = 1; + bytes state = 2; + int64 page_size = 3; +} + +message FetchNextExternalAccountsResponse { + repeated formance.payments.connectors.grpc.proto.Account accounts = 1; + bytes new_state = 2; + bool has_more = 3; +} + +message FetchNextBalancesRequest { + bytes from_payload = 1; + bytes state = 2; + int64 page_size = 3; +} + +message FetchNextBalancesResponse { + repeated formance.payments.connectors.grpc.proto.Balance balances = 1; + bytes new_state = 2; + bool has_more = 3; +} + +message CreateBankAccountRequest { + formance.payments.connectors.grpc.proto.BankAccount bank_account = 1; +} + +message CreateBankAccountResponse { + formance.payments.connectors.grpc.proto.Account related_account = 1; +} + +message TranslateWebhookRequest { + string name = 1; + formance.payments.connectors.grpc.proto.Webhook webhook = 2; +} + +message TranslateWebhookResponse { + message Response { + string idempotency_key = 1; + + oneof translated { + formance.payments.connectors.grpc.proto.Account account = 10; + formance.payments.connectors.grpc.proto.Account external_account = 11; + formance.payments.connectors.grpc.proto.Payment payment = 12; + formance.payments.connectors.grpc.proto.Balance balance = 13; + } + } + + repeated Response responses = 1; +} + +message CreateWebhooksRequest { + bytes from_payload = 1; + string connector_id = 2; +} + +message CreateWebhooksResponse { + repeated formance.payments.connectors.grpc.proto.Other others = 1; +} + +service Plugin { + rpc Install(InstallRequest) returns (InstallResponse) {} + rpc Uninstall(UninstallRequest) returns (UninstallResponse) {} + + rpc FetchNextOthers(FetchNextOthersRequest) returns (FetchNextOthersResponse) {} + rpc FetchNextPayments(FetchNextPaymentsRequest) returns (FetchNextPaymentsResponse) {} + rpc FetchNextAccounts(FetchNextAccountsRequest) returns (FetchNextAccountsResponse) {} + rpc FetchNextExternalAccounts(FetchNextExternalAccountsRequest) returns (FetchNextExternalAccountsResponse) {} + rpc FetchNextBalances(FetchNextBalancesRequest) returns (FetchNextBalancesResponse) {} + rpc CreateBankAccount(CreateBankAccountRequest) returns (CreateBankAccountResponse) {} + rpc CreateWebhooks(CreateWebhooksRequest) returns (CreateWebhooksResponse) {} + rpc TranslateWebhook(TranslateWebhookRequest) returns (TranslateWebhookResponse) {} +} \ No newline at end of file diff --git a/components/payments/internal/connectors/grpc/proto/services/plugin_grpc.pb.go b/components/payments/internal/connectors/grpc/proto/services/plugin_grpc.pb.go new file mode 100644 index 0000000000..abe79e06d3 --- /dev/null +++ b/components/payments/internal/connectors/grpc/proto/services/plugin_grpc.pb.go @@ -0,0 +1,425 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package services + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// PluginClient is the client API for Plugin service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type PluginClient interface { + Install(ctx context.Context, in *InstallRequest, opts ...grpc.CallOption) (*InstallResponse, error) + Uninstall(ctx context.Context, in *UninstallRequest, opts ...grpc.CallOption) (*UninstallResponse, error) + FetchNextOthers(ctx context.Context, in *FetchNextOthersRequest, opts ...grpc.CallOption) (*FetchNextOthersResponse, error) + FetchNextPayments(ctx context.Context, in *FetchNextPaymentsRequest, opts ...grpc.CallOption) (*FetchNextPaymentsResponse, error) + FetchNextAccounts(ctx context.Context, in *FetchNextAccountsRequest, opts ...grpc.CallOption) (*FetchNextAccountsResponse, error) + FetchNextExternalAccounts(ctx context.Context, in *FetchNextExternalAccountsRequest, opts ...grpc.CallOption) (*FetchNextExternalAccountsResponse, error) + FetchNextBalances(ctx context.Context, in *FetchNextBalancesRequest, opts ...grpc.CallOption) (*FetchNextBalancesResponse, error) + CreateBankAccount(ctx context.Context, in *CreateBankAccountRequest, opts ...grpc.CallOption) (*CreateBankAccountResponse, error) + CreateWebhooks(ctx context.Context, in *CreateWebhooksRequest, opts ...grpc.CallOption) (*CreateWebhooksResponse, error) + TranslateWebhook(ctx context.Context, in *TranslateWebhookRequest, opts ...grpc.CallOption) (*TranslateWebhookResponse, error) +} + +type pluginClient struct { + cc grpc.ClientConnInterface +} + +func NewPluginClient(cc grpc.ClientConnInterface) PluginClient { + return &pluginClient{cc} +} + +func (c *pluginClient) Install(ctx context.Context, in *InstallRequest, opts ...grpc.CallOption) (*InstallResponse, error) { + out := new(InstallResponse) + err := c.cc.Invoke(ctx, "/formance.payments.grpc.services.Plugin/Install", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *pluginClient) Uninstall(ctx context.Context, in *UninstallRequest, opts ...grpc.CallOption) (*UninstallResponse, error) { + out := new(UninstallResponse) + err := c.cc.Invoke(ctx, "/formance.payments.grpc.services.Plugin/Uninstall", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *pluginClient) FetchNextOthers(ctx context.Context, in *FetchNextOthersRequest, opts ...grpc.CallOption) (*FetchNextOthersResponse, error) { + out := new(FetchNextOthersResponse) + err := c.cc.Invoke(ctx, "/formance.payments.grpc.services.Plugin/FetchNextOthers", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *pluginClient) FetchNextPayments(ctx context.Context, in *FetchNextPaymentsRequest, opts ...grpc.CallOption) (*FetchNextPaymentsResponse, error) { + out := new(FetchNextPaymentsResponse) + err := c.cc.Invoke(ctx, "/formance.payments.grpc.services.Plugin/FetchNextPayments", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *pluginClient) FetchNextAccounts(ctx context.Context, in *FetchNextAccountsRequest, opts ...grpc.CallOption) (*FetchNextAccountsResponse, error) { + out := new(FetchNextAccountsResponse) + err := c.cc.Invoke(ctx, "/formance.payments.grpc.services.Plugin/FetchNextAccounts", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *pluginClient) FetchNextExternalAccounts(ctx context.Context, in *FetchNextExternalAccountsRequest, opts ...grpc.CallOption) (*FetchNextExternalAccountsResponse, error) { + out := new(FetchNextExternalAccountsResponse) + err := c.cc.Invoke(ctx, "/formance.payments.grpc.services.Plugin/FetchNextExternalAccounts", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *pluginClient) FetchNextBalances(ctx context.Context, in *FetchNextBalancesRequest, opts ...grpc.CallOption) (*FetchNextBalancesResponse, error) { + out := new(FetchNextBalancesResponse) + err := c.cc.Invoke(ctx, "/formance.payments.grpc.services.Plugin/FetchNextBalances", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *pluginClient) CreateBankAccount(ctx context.Context, in *CreateBankAccountRequest, opts ...grpc.CallOption) (*CreateBankAccountResponse, error) { + out := new(CreateBankAccountResponse) + err := c.cc.Invoke(ctx, "/formance.payments.grpc.services.Plugin/CreateBankAccount", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *pluginClient) CreateWebhooks(ctx context.Context, in *CreateWebhooksRequest, opts ...grpc.CallOption) (*CreateWebhooksResponse, error) { + out := new(CreateWebhooksResponse) + err := c.cc.Invoke(ctx, "/formance.payments.grpc.services.Plugin/CreateWebhooks", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *pluginClient) TranslateWebhook(ctx context.Context, in *TranslateWebhookRequest, opts ...grpc.CallOption) (*TranslateWebhookResponse, error) { + out := new(TranslateWebhookResponse) + err := c.cc.Invoke(ctx, "/formance.payments.grpc.services.Plugin/TranslateWebhook", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// PluginServer is the server API for Plugin service. +// All implementations must embed UnimplementedPluginServer +// for forward compatibility +type PluginServer interface { + Install(context.Context, *InstallRequest) (*InstallResponse, error) + Uninstall(context.Context, *UninstallRequest) (*UninstallResponse, error) + FetchNextOthers(context.Context, *FetchNextOthersRequest) (*FetchNextOthersResponse, error) + FetchNextPayments(context.Context, *FetchNextPaymentsRequest) (*FetchNextPaymentsResponse, error) + FetchNextAccounts(context.Context, *FetchNextAccountsRequest) (*FetchNextAccountsResponse, error) + FetchNextExternalAccounts(context.Context, *FetchNextExternalAccountsRequest) (*FetchNextExternalAccountsResponse, error) + FetchNextBalances(context.Context, *FetchNextBalancesRequest) (*FetchNextBalancesResponse, error) + CreateBankAccount(context.Context, *CreateBankAccountRequest) (*CreateBankAccountResponse, error) + CreateWebhooks(context.Context, *CreateWebhooksRequest) (*CreateWebhooksResponse, error) + TranslateWebhook(context.Context, *TranslateWebhookRequest) (*TranslateWebhookResponse, error) + mustEmbedUnimplementedPluginServer() +} + +// UnimplementedPluginServer must be embedded to have forward compatible implementations. +type UnimplementedPluginServer struct { +} + +func (UnimplementedPluginServer) Install(context.Context, *InstallRequest) (*InstallResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Install not implemented") +} +func (UnimplementedPluginServer) Uninstall(context.Context, *UninstallRequest) (*UninstallResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Uninstall not implemented") +} +func (UnimplementedPluginServer) FetchNextOthers(context.Context, *FetchNextOthersRequest) (*FetchNextOthersResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method FetchNextOthers not implemented") +} +func (UnimplementedPluginServer) FetchNextPayments(context.Context, *FetchNextPaymentsRequest) (*FetchNextPaymentsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method FetchNextPayments not implemented") +} +func (UnimplementedPluginServer) FetchNextAccounts(context.Context, *FetchNextAccountsRequest) (*FetchNextAccountsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method FetchNextAccounts not implemented") +} +func (UnimplementedPluginServer) FetchNextExternalAccounts(context.Context, *FetchNextExternalAccountsRequest) (*FetchNextExternalAccountsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method FetchNextExternalAccounts not implemented") +} +func (UnimplementedPluginServer) FetchNextBalances(context.Context, *FetchNextBalancesRequest) (*FetchNextBalancesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method FetchNextBalances not implemented") +} +func (UnimplementedPluginServer) CreateBankAccount(context.Context, *CreateBankAccountRequest) (*CreateBankAccountResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateBankAccount not implemented") +} +func (UnimplementedPluginServer) CreateWebhooks(context.Context, *CreateWebhooksRequest) (*CreateWebhooksResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateWebhooks not implemented") +} +func (UnimplementedPluginServer) TranslateWebhook(context.Context, *TranslateWebhookRequest) (*TranslateWebhookResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method TranslateWebhook not implemented") +} +func (UnimplementedPluginServer) mustEmbedUnimplementedPluginServer() {} + +// UnsafePluginServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to PluginServer will +// result in compilation errors. +type UnsafePluginServer interface { + mustEmbedUnimplementedPluginServer() +} + +func RegisterPluginServer(s grpc.ServiceRegistrar, srv PluginServer) { + s.RegisterService(&Plugin_ServiceDesc, srv) +} + +func _Plugin_Install_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(InstallRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PluginServer).Install(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/formance.payments.grpc.services.Plugin/Install", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PluginServer).Install(ctx, req.(*InstallRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Plugin_Uninstall_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UninstallRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PluginServer).Uninstall(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/formance.payments.grpc.services.Plugin/Uninstall", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PluginServer).Uninstall(ctx, req.(*UninstallRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Plugin_FetchNextOthers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(FetchNextOthersRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PluginServer).FetchNextOthers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/formance.payments.grpc.services.Plugin/FetchNextOthers", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PluginServer).FetchNextOthers(ctx, req.(*FetchNextOthersRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Plugin_FetchNextPayments_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(FetchNextPaymentsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PluginServer).FetchNextPayments(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/formance.payments.grpc.services.Plugin/FetchNextPayments", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PluginServer).FetchNextPayments(ctx, req.(*FetchNextPaymentsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Plugin_FetchNextAccounts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(FetchNextAccountsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PluginServer).FetchNextAccounts(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/formance.payments.grpc.services.Plugin/FetchNextAccounts", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PluginServer).FetchNextAccounts(ctx, req.(*FetchNextAccountsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Plugin_FetchNextExternalAccounts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(FetchNextExternalAccountsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PluginServer).FetchNextExternalAccounts(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/formance.payments.grpc.services.Plugin/FetchNextExternalAccounts", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PluginServer).FetchNextExternalAccounts(ctx, req.(*FetchNextExternalAccountsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Plugin_FetchNextBalances_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(FetchNextBalancesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PluginServer).FetchNextBalances(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/formance.payments.grpc.services.Plugin/FetchNextBalances", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PluginServer).FetchNextBalances(ctx, req.(*FetchNextBalancesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Plugin_CreateBankAccount_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateBankAccountRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PluginServer).CreateBankAccount(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/formance.payments.grpc.services.Plugin/CreateBankAccount", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PluginServer).CreateBankAccount(ctx, req.(*CreateBankAccountRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Plugin_CreateWebhooks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateWebhooksRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PluginServer).CreateWebhooks(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/formance.payments.grpc.services.Plugin/CreateWebhooks", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PluginServer).CreateWebhooks(ctx, req.(*CreateWebhooksRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Plugin_TranslateWebhook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TranslateWebhookRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PluginServer).TranslateWebhook(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/formance.payments.grpc.services.Plugin/TranslateWebhook", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PluginServer).TranslateWebhook(ctx, req.(*TranslateWebhookRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Plugin_ServiceDesc is the grpc.ServiceDesc for Plugin service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Plugin_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "formance.payments.grpc.services.Plugin", + HandlerType: (*PluginServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Install", + Handler: _Plugin_Install_Handler, + }, + { + MethodName: "Uninstall", + Handler: _Plugin_Uninstall_Handler, + }, + { + MethodName: "FetchNextOthers", + Handler: _Plugin_FetchNextOthers_Handler, + }, + { + MethodName: "FetchNextPayments", + Handler: _Plugin_FetchNextPayments_Handler, + }, + { + MethodName: "FetchNextAccounts", + Handler: _Plugin_FetchNextAccounts_Handler, + }, + { + MethodName: "FetchNextExternalAccounts", + Handler: _Plugin_FetchNextExternalAccounts_Handler, + }, + { + MethodName: "FetchNextBalances", + Handler: _Plugin_FetchNextBalances_Handler, + }, + { + MethodName: "CreateBankAccount", + Handler: _Plugin_CreateBankAccount_Handler, + }, + { + MethodName: "CreateWebhooks", + Handler: _Plugin_CreateWebhooks_Handler, + }, + { + MethodName: "TranslateWebhook", + Handler: _Plugin_TranslateWebhook_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "services/plugin.proto", +} diff --git a/components/payments/internal/connectors/grpc/proto/webhook.pb.go b/components/payments/internal/connectors/grpc/proto/webhook.pb.go new file mode 100644 index 0000000000..86122d114c --- /dev/null +++ b/components/payments/internal/connectors/grpc/proto/webhook.pb.go @@ -0,0 +1,329 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.30.0 +// protoc v3.21.12 +// source: webhook.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type WebhookConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + UrlPath string `protobuf:"bytes,2,opt,name=url_path,json=urlPath,proto3" json:"url_path,omitempty"` +} + +func (x *WebhookConfig) Reset() { + *x = WebhookConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_webhook_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *WebhookConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WebhookConfig) ProtoMessage() {} + +func (x *WebhookConfig) ProtoReflect() protoreflect.Message { + mi := &file_webhook_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WebhookConfig.ProtoReflect.Descriptor instead. +func (*WebhookConfig) Descriptor() ([]byte, []int) { + return file_webhook_proto_rawDescGZIP(), []int{0} +} + +func (x *WebhookConfig) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *WebhookConfig) GetUrlPath() string { + if x != nil { + return x.UrlPath + } + return "" +} + +type Webhook struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Headers map[string]*Webhook_Values `protobuf:"bytes,1,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + QueryValues map[string]*Webhook_Values `protobuf:"bytes,2,rep,name=query_values,json=queryValues,proto3" json:"query_values,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Body []byte `protobuf:"bytes,3,opt,name=body,proto3" json:"body,omitempty"` +} + +func (x *Webhook) Reset() { + *x = Webhook{} + if protoimpl.UnsafeEnabled { + mi := &file_webhook_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Webhook) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Webhook) ProtoMessage() {} + +func (x *Webhook) ProtoReflect() protoreflect.Message { + mi := &file_webhook_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Webhook.ProtoReflect.Descriptor instead. +func (*Webhook) Descriptor() ([]byte, []int) { + return file_webhook_proto_rawDescGZIP(), []int{1} +} + +func (x *Webhook) GetHeaders() map[string]*Webhook_Values { + if x != nil { + return x.Headers + } + return nil +} + +func (x *Webhook) GetQueryValues() map[string]*Webhook_Values { + if x != nil { + return x.QueryValues + } + return nil +} + +func (x *Webhook) GetBody() []byte { + if x != nil { + return x.Body + } + return nil +} + +type Webhook_Values struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Values []string `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"` +} + +func (x *Webhook_Values) Reset() { + *x = Webhook_Values{} + if protoimpl.UnsafeEnabled { + mi := &file_webhook_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Webhook_Values) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Webhook_Values) ProtoMessage() {} + +func (x *Webhook_Values) ProtoReflect() protoreflect.Message { + mi := &file_webhook_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Webhook_Values.ProtoReflect.Descriptor instead. +func (*Webhook_Values) Descriptor() ([]byte, []int) { + return file_webhook_proto_rawDescGZIP(), []int{1, 0} +} + +func (x *Webhook_Values) GetValues() []string { + if x != nil { + return x.Values + } + return nil +} + +var File_webhook_proto protoreflect.FileDescriptor + +var file_webhook_proto_rawDesc = []byte{ + 0x0a, 0x0d, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x27, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, + 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, + 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x3e, 0x0a, 0x0d, 0x57, 0x65, 0x62, 0x68, + 0x6f, 0x6f, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x19, 0x0a, + 0x08, 0x75, 0x72, 0x6c, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x75, 0x72, 0x6c, 0x50, 0x61, 0x74, 0x68, 0x22, 0xec, 0x03, 0x0a, 0x07, 0x57, 0x65, 0x62, + 0x68, 0x6f, 0x6f, 0x6b, 0x12, 0x57, 0x0a, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3d, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, + 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x64, 0x0a, + 0x0c, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x41, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, + 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, + 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x57, 0x65, + 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x71, 0x75, 0x65, 0x72, 0x79, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x1a, 0x20, 0x0a, 0x06, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x73, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x1a, 0x73, 0x0a, 0x0c, 0x48, 0x65, 0x61, + 0x64, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x4d, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x66, 0x6f, 0x72, + 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x2e, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x77, + 0x0a, 0x10, 0x51, 0x75, 0x65, 0x72, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x4d, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, + 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, + 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x57, 0x65, + 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x3f, 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x68, 0x71, + 0x2f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2f, 0x67, 0x72, + 0x70, 0x63, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_webhook_proto_rawDescOnce sync.Once + file_webhook_proto_rawDescData = file_webhook_proto_rawDesc +) + +func file_webhook_proto_rawDescGZIP() []byte { + file_webhook_proto_rawDescOnce.Do(func() { + file_webhook_proto_rawDescData = protoimpl.X.CompressGZIP(file_webhook_proto_rawDescData) + }) + return file_webhook_proto_rawDescData +} + +var file_webhook_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_webhook_proto_goTypes = []interface{}{ + (*WebhookConfig)(nil), // 0: formance.payments.connectors.grpc.proto.WebhookConfig + (*Webhook)(nil), // 1: formance.payments.connectors.grpc.proto.Webhook + (*Webhook_Values)(nil), // 2: formance.payments.connectors.grpc.proto.Webhook.Values + nil, // 3: formance.payments.connectors.grpc.proto.Webhook.HeadersEntry + nil, // 4: formance.payments.connectors.grpc.proto.Webhook.QueryValuesEntry +} +var file_webhook_proto_depIdxs = []int32{ + 3, // 0: formance.payments.connectors.grpc.proto.Webhook.headers:type_name -> formance.payments.connectors.grpc.proto.Webhook.HeadersEntry + 4, // 1: formance.payments.connectors.grpc.proto.Webhook.query_values:type_name -> formance.payments.connectors.grpc.proto.Webhook.QueryValuesEntry + 2, // 2: formance.payments.connectors.grpc.proto.Webhook.HeadersEntry.value:type_name -> formance.payments.connectors.grpc.proto.Webhook.Values + 2, // 3: formance.payments.connectors.grpc.proto.Webhook.QueryValuesEntry.value:type_name -> formance.payments.connectors.grpc.proto.Webhook.Values + 4, // [4:4] is the sub-list for method output_type + 4, // [4:4] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_webhook_proto_init() } +func file_webhook_proto_init() { + if File_webhook_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_webhook_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*WebhookConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_webhook_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Webhook); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_webhook_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Webhook_Values); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_webhook_proto_rawDesc, + NumEnums: 0, + NumMessages: 5, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_webhook_proto_goTypes, + DependencyIndexes: file_webhook_proto_depIdxs, + MessageInfos: file_webhook_proto_msgTypes, + }.Build() + File_webhook_proto = out.File + file_webhook_proto_rawDesc = nil + file_webhook_proto_goTypes = nil + file_webhook_proto_depIdxs = nil +} diff --git a/components/payments/internal/connectors/grpc/proto/webhook.proto b/components/payments/internal/connectors/grpc/proto/webhook.proto new file mode 100644 index 0000000000..1e136051c1 --- /dev/null +++ b/components/payments/internal/connectors/grpc/proto/webhook.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; +package formance.payments.connectors.grpc.proto; +option go_package = "github.com/formancehq/payments/internal/connectors/grpc/proto"; + +message WebhookConfig { + string name = 1; + string url_path = 2; +} + +message Webhook { + message Values { + repeated string values = 1; + } + map headers = 1; + map query_values = 2; + bytes body = 3; +} \ No newline at end of file diff --git a/components/payments/internal/connectors/grpc/proto/workflow.pb.go b/components/payments/internal/connectors/grpc/proto/workflow.pb.go new file mode 100644 index 0000000000..4af3d01c07 --- /dev/null +++ b/components/payments/internal/connectors/grpc/proto/workflow.pb.go @@ -0,0 +1,704 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.30.0 +// protoc v3.21.12 +// source: workflow.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type TaskTree struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Periodically bool `protobuf:"varint,2,opt,name=periodically,proto3" json:"periodically,omitempty"` + NextTasks []*TaskTree `protobuf:"bytes,3,rep,name=next_tasks,json=nextTasks,proto3" json:"next_tasks,omitempty"` + // Types that are assignable to Task: + // + // *TaskTree_FetchAccounts_ + // *TaskTree_FetchBalances_ + // *TaskTree_FetchExternalAccounts_ + // *TaskTree_FetchPayments_ + // *TaskTree_FetchOthers_ + // *TaskTree_CreateWebhooks_ + Task isTaskTree_Task `protobuf_oneof:"task"` +} + +func (x *TaskTree) Reset() { + *x = TaskTree{} + if protoimpl.UnsafeEnabled { + mi := &file_workflow_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TaskTree) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TaskTree) ProtoMessage() {} + +func (x *TaskTree) ProtoReflect() protoreflect.Message { + mi := &file_workflow_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TaskTree.ProtoReflect.Descriptor instead. +func (*TaskTree) Descriptor() ([]byte, []int) { + return file_workflow_proto_rawDescGZIP(), []int{0} +} + +func (x *TaskTree) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *TaskTree) GetPeriodically() bool { + if x != nil { + return x.Periodically + } + return false +} + +func (x *TaskTree) GetNextTasks() []*TaskTree { + if x != nil { + return x.NextTasks + } + return nil +} + +func (m *TaskTree) GetTask() isTaskTree_Task { + if m != nil { + return m.Task + } + return nil +} + +func (x *TaskTree) GetFetchAccounts() *TaskTree_FetchAccounts { + if x, ok := x.GetTask().(*TaskTree_FetchAccounts_); ok { + return x.FetchAccounts + } + return nil +} + +func (x *TaskTree) GetFetchBalances() *TaskTree_FetchBalances { + if x, ok := x.GetTask().(*TaskTree_FetchBalances_); ok { + return x.FetchBalances + } + return nil +} + +func (x *TaskTree) GetFetchExternalAccounts() *TaskTree_FetchExternalAccounts { + if x, ok := x.GetTask().(*TaskTree_FetchExternalAccounts_); ok { + return x.FetchExternalAccounts + } + return nil +} + +func (x *TaskTree) GetFetchPayments() *TaskTree_FetchPayments { + if x, ok := x.GetTask().(*TaskTree_FetchPayments_); ok { + return x.FetchPayments + } + return nil +} + +func (x *TaskTree) GetFetchOthers() *TaskTree_FetchOthers { + if x, ok := x.GetTask().(*TaskTree_FetchOthers_); ok { + return x.FetchOthers + } + return nil +} + +func (x *TaskTree) GetCreateWebhooks() *TaskTree_CreateWebhooks { + if x, ok := x.GetTask().(*TaskTree_CreateWebhooks_); ok { + return x.CreateWebhooks + } + return nil +} + +type isTaskTree_Task interface { + isTaskTree_Task() +} + +type TaskTree_FetchAccounts_ struct { + FetchAccounts *TaskTree_FetchAccounts `protobuf:"bytes,10,opt,name=fetch_accounts,json=fetchAccounts,proto3,oneof"` +} + +type TaskTree_FetchBalances_ struct { + FetchBalances *TaskTree_FetchBalances `protobuf:"bytes,11,opt,name=fetch_balances,json=fetchBalances,proto3,oneof"` +} + +type TaskTree_FetchExternalAccounts_ struct { + FetchExternalAccounts *TaskTree_FetchExternalAccounts `protobuf:"bytes,12,opt,name=fetch_external_accounts,json=fetchExternalAccounts,proto3,oneof"` +} + +type TaskTree_FetchPayments_ struct { + FetchPayments *TaskTree_FetchPayments `protobuf:"bytes,13,opt,name=fetch_payments,json=fetchPayments,proto3,oneof"` +} + +type TaskTree_FetchOthers_ struct { + FetchOthers *TaskTree_FetchOthers `protobuf:"bytes,14,opt,name=fetch_others,json=fetchOthers,proto3,oneof"` +} + +type TaskTree_CreateWebhooks_ struct { + CreateWebhooks *TaskTree_CreateWebhooks `protobuf:"bytes,15,opt,name=create_webhooks,json=createWebhooks,proto3,oneof"` +} + +func (*TaskTree_FetchAccounts_) isTaskTree_Task() {} + +func (*TaskTree_FetchBalances_) isTaskTree_Task() {} + +func (*TaskTree_FetchExternalAccounts_) isTaskTree_Task() {} + +func (*TaskTree_FetchPayments_) isTaskTree_Task() {} + +func (*TaskTree_FetchOthers_) isTaskTree_Task() {} + +func (*TaskTree_CreateWebhooks_) isTaskTree_Task() {} + +type Workflow struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Tasks []*TaskTree `protobuf:"bytes,1,rep,name=tasks,proto3" json:"tasks,omitempty"` +} + +func (x *Workflow) Reset() { + *x = Workflow{} + if protoimpl.UnsafeEnabled { + mi := &file_workflow_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Workflow) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Workflow) ProtoMessage() {} + +func (x *Workflow) ProtoReflect() protoreflect.Message { + mi := &file_workflow_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Workflow.ProtoReflect.Descriptor instead. +func (*Workflow) Descriptor() ([]byte, []int) { + return file_workflow_proto_rawDescGZIP(), []int{1} +} + +func (x *Workflow) GetTasks() []*TaskTree { + if x != nil { + return x.Tasks + } + return nil +} + +type TaskTree_FetchAccounts struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *TaskTree_FetchAccounts) Reset() { + *x = TaskTree_FetchAccounts{} + if protoimpl.UnsafeEnabled { + mi := &file_workflow_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TaskTree_FetchAccounts) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TaskTree_FetchAccounts) ProtoMessage() {} + +func (x *TaskTree_FetchAccounts) ProtoReflect() protoreflect.Message { + mi := &file_workflow_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TaskTree_FetchAccounts.ProtoReflect.Descriptor instead. +func (*TaskTree_FetchAccounts) Descriptor() ([]byte, []int) { + return file_workflow_proto_rawDescGZIP(), []int{0, 0} +} + +type TaskTree_FetchBalances struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *TaskTree_FetchBalances) Reset() { + *x = TaskTree_FetchBalances{} + if protoimpl.UnsafeEnabled { + mi := &file_workflow_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TaskTree_FetchBalances) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TaskTree_FetchBalances) ProtoMessage() {} + +func (x *TaskTree_FetchBalances) ProtoReflect() protoreflect.Message { + mi := &file_workflow_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TaskTree_FetchBalances.ProtoReflect.Descriptor instead. +func (*TaskTree_FetchBalances) Descriptor() ([]byte, []int) { + return file_workflow_proto_rawDescGZIP(), []int{0, 1} +} + +type TaskTree_FetchExternalAccounts struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *TaskTree_FetchExternalAccounts) Reset() { + *x = TaskTree_FetchExternalAccounts{} + if protoimpl.UnsafeEnabled { + mi := &file_workflow_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TaskTree_FetchExternalAccounts) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TaskTree_FetchExternalAccounts) ProtoMessage() {} + +func (x *TaskTree_FetchExternalAccounts) ProtoReflect() protoreflect.Message { + mi := &file_workflow_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TaskTree_FetchExternalAccounts.ProtoReflect.Descriptor instead. +func (*TaskTree_FetchExternalAccounts) Descriptor() ([]byte, []int) { + return file_workflow_proto_rawDescGZIP(), []int{0, 2} +} + +type TaskTree_FetchPayments struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *TaskTree_FetchPayments) Reset() { + *x = TaskTree_FetchPayments{} + if protoimpl.UnsafeEnabled { + mi := &file_workflow_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TaskTree_FetchPayments) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TaskTree_FetchPayments) ProtoMessage() {} + +func (x *TaskTree_FetchPayments) ProtoReflect() protoreflect.Message { + mi := &file_workflow_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TaskTree_FetchPayments.ProtoReflect.Descriptor instead. +func (*TaskTree_FetchPayments) Descriptor() ([]byte, []int) { + return file_workflow_proto_rawDescGZIP(), []int{0, 3} +} + +type TaskTree_FetchOthers struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *TaskTree_FetchOthers) Reset() { + *x = TaskTree_FetchOthers{} + if protoimpl.UnsafeEnabled { + mi := &file_workflow_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TaskTree_FetchOthers) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TaskTree_FetchOthers) ProtoMessage() {} + +func (x *TaskTree_FetchOthers) ProtoReflect() protoreflect.Message { + mi := &file_workflow_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TaskTree_FetchOthers.ProtoReflect.Descriptor instead. +func (*TaskTree_FetchOthers) Descriptor() ([]byte, []int) { + return file_workflow_proto_rawDescGZIP(), []int{0, 4} +} + +type TaskTree_CreateWebhooks struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *TaskTree_CreateWebhooks) Reset() { + *x = TaskTree_CreateWebhooks{} + if protoimpl.UnsafeEnabled { + mi := &file_workflow_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TaskTree_CreateWebhooks) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TaskTree_CreateWebhooks) ProtoMessage() {} + +func (x *TaskTree_CreateWebhooks) ProtoReflect() protoreflect.Message { + mi := &file_workflow_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TaskTree_CreateWebhooks.ProtoReflect.Descriptor instead. +func (*TaskTree_CreateWebhooks) Descriptor() ([]byte, []int) { + return file_workflow_proto_rawDescGZIP(), []int{0, 5} +} + +var File_workflow_proto protoreflect.FileDescriptor + +var file_workflow_proto_rawDesc = []byte{ + 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x12, 0x27, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, + 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, + 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x9c, 0x07, 0x0a, 0x08, 0x54, 0x61, + 0x73, 0x6b, 0x54, 0x72, 0x65, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x70, 0x65, + 0x72, 0x69, 0x6f, 0x64, 0x69, 0x63, 0x61, 0x6c, 0x6c, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x0c, 0x70, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x69, 0x63, 0x61, 0x6c, 0x6c, 0x79, 0x12, 0x50, + 0x0a, 0x0a, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x74, 0x61, 0x73, 0x6b, 0x73, 0x18, 0x03, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x54, 0x61, 0x73, + 0x6b, 0x54, 0x72, 0x65, 0x65, 0x52, 0x09, 0x6e, 0x65, 0x78, 0x74, 0x54, 0x61, 0x73, 0x6b, 0x73, + 0x12, 0x68, 0x0a, 0x0e, 0x66, 0x65, 0x74, 0x63, 0x68, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3f, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, + 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x54, 0x72, 0x65, 0x65, 0x2e, 0x46, 0x65, 0x74, 0x63, + 0x68, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x48, 0x00, 0x52, 0x0d, 0x66, 0x65, 0x74, + 0x63, 0x68, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, 0x68, 0x0a, 0x0e, 0x66, 0x65, + 0x74, 0x63, 0x68, 0x5f, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x3f, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x54, 0x61, 0x73, + 0x6b, 0x54, 0x72, 0x65, 0x65, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x42, 0x61, 0x6c, 0x61, 0x6e, + 0x63, 0x65, 0x73, 0x48, 0x00, 0x52, 0x0d, 0x66, 0x65, 0x74, 0x63, 0x68, 0x42, 0x61, 0x6c, 0x61, + 0x6e, 0x63, 0x65, 0x73, 0x12, 0x81, 0x01, 0x0a, 0x17, 0x66, 0x65, 0x74, 0x63, 0x68, 0x5f, 0x65, + 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, + 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x47, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, + 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x54, 0x72, 0x65, 0x65, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x45, + 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x48, + 0x00, 0x52, 0x15, 0x66, 0x65, 0x74, 0x63, 0x68, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, 0x68, 0x0a, 0x0e, 0x66, 0x65, 0x74, 0x63, + 0x68, 0x5f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x3f, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, + 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, + 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x54, + 0x72, 0x65, 0x65, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, + 0x73, 0x48, 0x00, 0x52, 0x0d, 0x66, 0x65, 0x74, 0x63, 0x68, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, + 0x74, 0x73, 0x12, 0x62, 0x0a, 0x0c, 0x66, 0x65, 0x74, 0x63, 0x68, 0x5f, 0x6f, 0x74, 0x68, 0x65, + 0x72, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3d, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, + 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x54, 0x72, 0x65, 0x65, 0x2e, 0x46, 0x65, 0x74, 0x63, + 0x68, 0x4f, 0x74, 0x68, 0x65, 0x72, 0x73, 0x48, 0x00, 0x52, 0x0b, 0x66, 0x65, 0x74, 0x63, 0x68, + 0x4f, 0x74, 0x68, 0x65, 0x72, 0x73, 0x12, 0x6b, 0x0a, 0x0f, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x5f, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x40, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, + 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, + 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x54, 0x61, 0x73, 0x6b, 0x54, 0x72, + 0x65, 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, + 0x73, 0x48, 0x00, 0x52, 0x0e, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x68, 0x6f, + 0x6f, 0x6b, 0x73, 0x1a, 0x0f, 0x0a, 0x0d, 0x46, 0x65, 0x74, 0x63, 0x68, 0x41, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x73, 0x1a, 0x0f, 0x0a, 0x0d, 0x46, 0x65, 0x74, 0x63, 0x68, 0x42, 0x61, 0x6c, + 0x61, 0x6e, 0x63, 0x65, 0x73, 0x1a, 0x17, 0x0a, 0x15, 0x46, 0x65, 0x74, 0x63, 0x68, 0x45, 0x78, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x1a, 0x0f, + 0x0a, 0x0d, 0x46, 0x65, 0x74, 0x63, 0x68, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x1a, + 0x0d, 0x0a, 0x0b, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4f, 0x74, 0x68, 0x65, 0x72, 0x73, 0x1a, 0x10, + 0x0a, 0x0e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x73, + 0x42, 0x06, 0x0a, 0x04, 0x74, 0x61, 0x73, 0x6b, 0x22, 0x53, 0x0a, 0x08, 0x57, 0x6f, 0x72, 0x6b, + 0x66, 0x6c, 0x6f, 0x77, 0x12, 0x47, 0x0a, 0x05, 0x74, 0x61, 0x73, 0x6b, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, + 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, + 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x54, 0x61, + 0x73, 0x6b, 0x54, 0x72, 0x65, 0x65, 0x52, 0x05, 0x74, 0x61, 0x73, 0x6b, 0x73, 0x42, 0x3f, 0x5a, + 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x6f, 0x72, 0x6d, + 0x61, 0x6e, 0x63, 0x65, 0x68, 0x71, 0x2f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2f, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x6f, 0x72, 0x73, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_workflow_proto_rawDescOnce sync.Once + file_workflow_proto_rawDescData = file_workflow_proto_rawDesc +) + +func file_workflow_proto_rawDescGZIP() []byte { + file_workflow_proto_rawDescOnce.Do(func() { + file_workflow_proto_rawDescData = protoimpl.X.CompressGZIP(file_workflow_proto_rawDescData) + }) + return file_workflow_proto_rawDescData +} + +var file_workflow_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_workflow_proto_goTypes = []interface{}{ + (*TaskTree)(nil), // 0: formance.payments.connectors.grpc.proto.TaskTree + (*Workflow)(nil), // 1: formance.payments.connectors.grpc.proto.Workflow + (*TaskTree_FetchAccounts)(nil), // 2: formance.payments.connectors.grpc.proto.TaskTree.FetchAccounts + (*TaskTree_FetchBalances)(nil), // 3: formance.payments.connectors.grpc.proto.TaskTree.FetchBalances + (*TaskTree_FetchExternalAccounts)(nil), // 4: formance.payments.connectors.grpc.proto.TaskTree.FetchExternalAccounts + (*TaskTree_FetchPayments)(nil), // 5: formance.payments.connectors.grpc.proto.TaskTree.FetchPayments + (*TaskTree_FetchOthers)(nil), // 6: formance.payments.connectors.grpc.proto.TaskTree.FetchOthers + (*TaskTree_CreateWebhooks)(nil), // 7: formance.payments.connectors.grpc.proto.TaskTree.CreateWebhooks +} +var file_workflow_proto_depIdxs = []int32{ + 0, // 0: formance.payments.connectors.grpc.proto.TaskTree.next_tasks:type_name -> formance.payments.connectors.grpc.proto.TaskTree + 2, // 1: formance.payments.connectors.grpc.proto.TaskTree.fetch_accounts:type_name -> formance.payments.connectors.grpc.proto.TaskTree.FetchAccounts + 3, // 2: formance.payments.connectors.grpc.proto.TaskTree.fetch_balances:type_name -> formance.payments.connectors.grpc.proto.TaskTree.FetchBalances + 4, // 3: formance.payments.connectors.grpc.proto.TaskTree.fetch_external_accounts:type_name -> formance.payments.connectors.grpc.proto.TaskTree.FetchExternalAccounts + 5, // 4: formance.payments.connectors.grpc.proto.TaskTree.fetch_payments:type_name -> formance.payments.connectors.grpc.proto.TaskTree.FetchPayments + 6, // 5: formance.payments.connectors.grpc.proto.TaskTree.fetch_others:type_name -> formance.payments.connectors.grpc.proto.TaskTree.FetchOthers + 7, // 6: formance.payments.connectors.grpc.proto.TaskTree.create_webhooks:type_name -> formance.payments.connectors.grpc.proto.TaskTree.CreateWebhooks + 0, // 7: formance.payments.connectors.grpc.proto.Workflow.tasks:type_name -> formance.payments.connectors.grpc.proto.TaskTree + 8, // [8:8] is the sub-list for method output_type + 8, // [8:8] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name +} + +func init() { file_workflow_proto_init() } +func file_workflow_proto_init() { + if File_workflow_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_workflow_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TaskTree); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_workflow_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Workflow); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_workflow_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TaskTree_FetchAccounts); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_workflow_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TaskTree_FetchBalances); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_workflow_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TaskTree_FetchExternalAccounts); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_workflow_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TaskTree_FetchPayments); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_workflow_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TaskTree_FetchOthers); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_workflow_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TaskTree_CreateWebhooks); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_workflow_proto_msgTypes[0].OneofWrappers = []interface{}{ + (*TaskTree_FetchAccounts_)(nil), + (*TaskTree_FetchBalances_)(nil), + (*TaskTree_FetchExternalAccounts_)(nil), + (*TaskTree_FetchPayments_)(nil), + (*TaskTree_FetchOthers_)(nil), + (*TaskTree_CreateWebhooks_)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_workflow_proto_rawDesc, + NumEnums: 0, + NumMessages: 8, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_workflow_proto_goTypes, + DependencyIndexes: file_workflow_proto_depIdxs, + MessageInfos: file_workflow_proto_msgTypes, + }.Build() + File_workflow_proto = out.File + file_workflow_proto_rawDesc = nil + file_workflow_proto_goTypes = nil + file_workflow_proto_depIdxs = nil +} diff --git a/components/payments/internal/connectors/grpc/proto/workflow.proto b/components/payments/internal/connectors/grpc/proto/workflow.proto new file mode 100644 index 0000000000..1913f191cc --- /dev/null +++ b/components/payments/internal/connectors/grpc/proto/workflow.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; +package formance.payments.connectors.grpc.proto; +option go_package = "github.com/formancehq/payments/internal/connectors/grpc/proto"; + +message TaskTree { + message FetchAccounts {} + message FetchBalances {} + message FetchExternalAccounts {} + message FetchPayments {} + message FetchOthers {} + message CreateWebhooks {} + + string name = 1; + bool periodically = 2; + repeated TaskTree next_tasks = 3; + + oneof task { + FetchAccounts fetch_accounts = 10; + FetchBalances fetch_balances = 11; + FetchExternalAccounts fetch_external_accounts = 12; + FetchPayments fetch_payments = 13; + FetchOthers fetch_others = 14; + CreateWebhooks create_webhooks = 15; + } +} + +message Workflow { + repeated TaskTree tasks = 1; +} \ No newline at end of file diff --git a/components/payments/internal/connectors/grpc/translate.go b/components/payments/internal/connectors/grpc/translate.go new file mode 100644 index 0000000000..cf5085a816 --- /dev/null +++ b/components/payments/internal/connectors/grpc/translate.go @@ -0,0 +1,359 @@ +package grpc + +import ( + "errors" + "math/big" + "time" + + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/connectors/grpc/proto" + "github.com/formancehq/payments/internal/models" + "github.com/google/uuid" + "google.golang.org/protobuf/types/known/timestamppb" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +func TranslateAccount(account models.PSPAccount) *proto.Account { + return &proto.Account{ + Reference: account.Reference, + Name: func() *wrapperspb.StringValue { + if account.Name == nil { + return nil + } + + return wrapperspb.String(*account.Name) + }(), + CreatedAt: timestamppb.New(account.CreatedAt), + SyncedAt: timestamppb.New(time.Now().UTC()), + DefaultAsset: func() *wrapperspb.StringValue { + if account.DefaultAsset == nil { + return nil + } + + return wrapperspb.String(*account.DefaultAsset) + }(), + Metadata: account.Metadata, + Raw: account.Raw, + } +} + +func TranslateProtoAccount(account *proto.Account) models.PSPAccount { + return models.PSPAccount{ + Reference: account.Reference, + CreatedAt: account.CreatedAt.AsTime(), + Name: func() *string { + if account.Name == nil { + return nil + } + + return pointer.For(account.Name.GetValue()) + }(), + DefaultAsset: func() *string { + if account.DefaultAsset == nil { + return nil + } + + return pointer.For(account.DefaultAsset.GetValue()) + }(), + Metadata: account.Metadata, + Raw: account.Raw, + } +} + +func TranslateBankAccount(bankAccount models.BankAccount) *proto.BankAccount { + return &proto.BankAccount{ + Id: bankAccount.ID.String(), + CreatedAt: timestamppb.New(bankAccount.CreatedAt), + Name: bankAccount.Name, + AccountNumber: func() *wrapperspb.StringValue { + if bankAccount.AccountNumber == nil { + return nil + } + return wrapperspb.String(*bankAccount.AccountNumber) + }(), + Iban: func() *wrapperspb.StringValue { + if bankAccount.IBAN == nil { + return nil + } + return wrapperspb.String(*bankAccount.IBAN) + }(), + SwiftBicCode: func() *wrapperspb.StringValue { + if bankAccount.SwiftBicCode == nil { + return nil + } + return wrapperspb.String(*bankAccount.SwiftBicCode) + }(), + Country: func() *wrapperspb.StringValue { + if bankAccount.Country == nil { + return nil + } + return wrapperspb.String(*bankAccount.Country) + }(), + Metadata: bankAccount.Metadata, + } +} + +func TranslateProtoBankAccount(bankAccount *proto.BankAccount) models.BankAccount { + uuid, err := uuid.Parse(bankAccount.Id) + if err != nil { + panic(err) + } + + return models.BankAccount{ + ID: uuid, + CreatedAt: bankAccount.CreatedAt.AsTime(), + Name: bankAccount.Name, + AccountNumber: func() *string { + if bankAccount.AccountNumber == nil { + return nil + } + return pointer.For(bankAccount.AccountNumber.GetValue()) + }(), + IBAN: func() *string { + if bankAccount.Iban == nil { + return nil + } + return pointer.For(bankAccount.Iban.GetValue()) + }(), + SwiftBicCode: func() *string { + if bankAccount.SwiftBicCode == nil { + return nil + } + return pointer.For(bankAccount.SwiftBicCode.GetValue()) + }(), + Country: func() *string { + if bankAccount.Country == nil { + return nil + } + return pointer.For(bankAccount.Country.GetValue()) + }(), + Metadata: bankAccount.Metadata, + } +} + +func TranslateBalance(balance models.PSPBalance) *proto.Balance { + return &proto.Balance{ + AccountReference: balance.AccountReference, + CreatedAt: timestamppb.New(balance.CreatedAt), + Balance: &proto.Monetary{ + Asset: balance.Asset, + Amount: []byte(balance.Amount.Text(10)), + }, + } +} + +func TranslateProtoBalance(balance *proto.Balance) (models.PSPBalance, error) { + amount, ok := big.NewInt(0).SetString(string(balance.Balance.Amount), 10) + if !ok { + return models.PSPBalance{}, errors.New("failed to parse amount") + } + + return models.PSPBalance{ + AccountReference: balance.AccountReference, + CreatedAt: balance.CreatedAt.AsTime(), + Amount: amount, + Asset: balance.Balance.Asset, + }, nil +} + +func TranslatePayment(payment models.PSPPayment) *proto.Payment { + return &proto.Payment{ + Reference: payment.Reference, + CreatedAt: timestamppb.New(payment.CreatedAt), + SyncedAt: timestamppb.New(time.Now().UTC()), + PaymentType: proto.PaymentType(payment.Type), + Amount: &proto.Monetary{ + Asset: payment.Asset, + Amount: []byte(payment.Amount.Text(10)), + }, + Scheme: proto.PaymentScheme(payment.Scheme), + Status: proto.PaymentStatus(payment.Status), + SourceAccountReference: func() *wrapperspb.StringValue { + if payment.SourceAccountReference == nil { + return nil + } + + return wrapperspb.String(*payment.SourceAccountReference) + }(), + DestinationAccountReference: func() *wrapperspb.StringValue { + if payment.DestinationAccountReference == nil { + return nil + } + + return wrapperspb.String(*payment.DestinationAccountReference) + }(), + Metadata: payment.Metadata, + Raw: payment.Raw, + } +} + +func TranslateProtoPayment(payment *proto.Payment) (models.PSPPayment, error) { + amount, ok := big.NewInt(0).SetString(string(payment.Amount.Amount), 10) + if !ok { + return models.PSPPayment{}, errors.New("failed to parse amount") + } + return models.PSPPayment{ + Reference: payment.Reference, + CreatedAt: payment.CreatedAt.AsTime(), + Type: models.PaymentType(payment.PaymentType), + Amount: amount, + Asset: payment.Amount.Asset, + Scheme: models.PaymentScheme(payment.Scheme), + Status: models.PaymentStatus(payment.Status), + SourceAccountReference: func() *string { + if payment.SourceAccountReference == nil { + return nil + } + + return pointer.For(payment.SourceAccountReference.GetValue()) + }(), + DestinationAccountReference: func() *string { + if payment.DestinationAccountReference == nil { + return nil + } + + return pointer.For(payment.DestinationAccountReference.GetValue()) + }(), + Metadata: payment.Metadata, + Raw: payment.Raw, + }, nil +} + +func TranslateTask(taskTree models.TaskTree) *proto.TaskTree { + res := proto.TaskTree{ + NextTasks: []*proto.TaskTree{}, + Name: taskTree.Name, + Periodically: taskTree.Periodically, + Task: nil, + } + + switch taskTree.TaskType { + case models.TASK_FETCH_ACCOUNTS: + res.Task = &proto.TaskTree_FetchAccounts_{ + FetchAccounts: &proto.TaskTree_FetchAccounts{}, + } + case models.TASK_FETCH_EXTERNAL_ACCOUNTS: + res.Task = &proto.TaskTree_FetchExternalAccounts_{ + FetchExternalAccounts: &proto.TaskTree_FetchExternalAccounts{}, + } + case models.TASK_FETCH_PAYMENTS: + res.Task = &proto.TaskTree_FetchPayments_{ + FetchPayments: &proto.TaskTree_FetchPayments{}, + } + case models.TASK_FETCH_OTHERS: + res.Task = &proto.TaskTree_FetchOthers_{ + FetchOthers: &proto.TaskTree_FetchOthers{}, + } + case models.TASK_FETCH_BALANCES: + res.Task = &proto.TaskTree_FetchBalances_{ + FetchBalances: &proto.TaskTree_FetchBalances{}, + } + case models.TASK_CREATE_WEBHOOKS: + res.Task = &proto.TaskTree_CreateWebhooks_{ + CreateWebhooks: &proto.TaskTree_CreateWebhooks{}, + } + default: + // TODO(polo): better error handling + panic("unknown task type") + } + + for _, nextTask := range taskTree.NextTasks { + res.NextTasks = append(res.NextTasks, TranslateTask(nextTask)) + } + + return &res +} + +func TranslateProtoTask(task *proto.TaskTree) models.TaskTree { + res := models.TaskTree{ + TaskType: 0, + Name: task.Name, + Periodically: task.Periodically, + NextTasks: []models.TaskTree{}, + } + + switch task.Task.(type) { + case *proto.TaskTree_FetchAccounts_: + res.TaskType = models.TASK_FETCH_ACCOUNTS + res.TaskTreeFetchAccounts = &models.TaskTreeFetchAccounts{} + case *proto.TaskTree_FetchExternalAccounts_: + res.TaskType = models.TASK_FETCH_EXTERNAL_ACCOUNTS + res.TaskTreeFetchExternalAccounts = &models.TaskTreeFetchExternalAccounts{} + case *proto.TaskTree_FetchPayments_: + res.TaskType = models.TASK_FETCH_PAYMENTS + res.TaskTreeFetchPayments = &models.TaskTreeFetchPayments{} + case *proto.TaskTree_FetchOthers_: + res.TaskType = models.TASK_FETCH_OTHERS + res.TaskTreeFetchOther = &models.TaskTreeFetchOther{} + case *proto.TaskTree_FetchBalances_: + res.TaskType = models.TASK_FETCH_BALANCES + res.TaskTreeFetchBalances = &models.TaskTreeFetchBalances{} + case *proto.TaskTree_CreateWebhooks_: + res.TaskType = models.TASK_CREATE_WEBHOOKS + res.TaskTreeCreateWebhooks = &models.TaskTreeCreateWebhooks{} + default: + panic("unknown task type") + } + + for _, nextTask := range task.NextTasks { + res.NextTasks = append(res.NextTasks, TranslateProtoTask(nextTask)) + } + + return res +} + +func TranslateWorkflow(workflows models.Tasks) *proto.Workflow { + res := proto.Workflow{} + + for _, task := range workflows { + res.Tasks = append(res.Tasks, TranslateTask(task)) + } + + return &res +} + +func TranslateProtoWorkflow(workflow *proto.Workflow) models.Tasks { + res := models.Tasks{} + + for _, task := range workflow.Tasks { + res = append(res, TranslateProtoTask(task)) + } + + return res +} + +func TranslateWebhook(from models.PSPWebhook) *proto.Webhook { + headers := make(map[string]*proto.Webhook_Values) + for k, v := range from.Headers { + headers[k] = &proto.Webhook_Values{Values: v} + } + + queryValues := make(map[string]*proto.Webhook_Values) + for k, v := range from.QueryValues { + queryValues[k] = &proto.Webhook_Values{Values: v} + } + + return &proto.Webhook{ + Headers: headers, + QueryValues: queryValues, + Body: from.Body, + } +} + +func TranslateProtoWebhook(from *proto.Webhook) models.PSPWebhook { + headers := make(map[string][]string) + for k, v := range from.Headers { + headers[k] = v.Values + } + + queryValues := make(map[string][]string) + for k, v := range from.QueryValues { + queryValues[k] = v.Values + } + + return models.PSPWebhook{ + QueryValues: queryValues, + Headers: headers, + Body: from.Body, + } +} diff --git a/components/payments/internal/connectors/httpwrapper/client.go b/components/payments/internal/connectors/httpwrapper/client.go new file mode 100644 index 0000000000..137baac6f0 --- /dev/null +++ b/components/payments/internal/connectors/httpwrapper/client.go @@ -0,0 +1,108 @@ +package httpwrapper + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "time" + + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "golang.org/x/oauth2" +) + +var ( + ErrStatusCodeUnexpected = errors.New("unexpected status code") + + defaultHttpErrorCheckerFn = func(statusCode int) error { + if statusCode >= http.StatusBadRequest { + return ErrStatusCodeUnexpected + } + return nil + } +) + +// Client is a convenience wrapper that encapsulates common code related to interacting with HTTP endpoints +type Client interface { + // Do performs an HTTP request while handling errors and unmarshaling success and error responses into the provided interfaces + // expectedBody and errorBody should be pointers to structs + Do(req *http.Request, expectedBody, errorBody any) (statusCode int, err error) +} + +type client struct { + httpClient *http.Client + + httpErrorCheckerFn func(statusCode int) error +} + +func NewClient(config *Config) (Client, error) { + if config.Timeout == 0 { + config.Timeout = 10 * time.Second + } + if config.Transport != nil { + config.Transport = otelhttp.NewTransport(config.Transport) + } else { + config.Transport = http.DefaultTransport.(*http.Transport).Clone() + } + + httpClient := &http.Client{ + Timeout: config.Timeout, + Transport: config.Transport, + } + if config.OAuthConfig != nil { + // pass a pre-configured http client to oauth lib via the context + ctx := context.WithValue(context.Background(), oauth2.HTTPClient, httpClient) + httpClient = config.OAuthConfig.Client(ctx) + } + + if config.HttpErrorCheckerFn == nil { + config.HttpErrorCheckerFn = defaultHttpErrorCheckerFn + } + + return &client{ + httpErrorCheckerFn: config.HttpErrorCheckerFn, + httpClient: httpClient, + }, nil +} + +func (c *client) Do(req *http.Request, expectedBody, errorBody any) (int, error) { + resp, err := c.httpClient.Do(req) + if err != nil { + return 0, fmt.Errorf("failed to make request: %w", err) + } + + reqErr := c.httpErrorCheckerFn(resp.StatusCode) + // the caller doesn't care about the response body so we return early + if resp.Body == nil || (reqErr == nil && expectedBody == nil) || (reqErr != nil && errorBody == nil) { + return resp.StatusCode, reqErr + } + + defer func() { + err = resp.Body.Close() + if err != nil { + _ = err + // TODO(polo): log error + } + }() + + // TODO: reading everything into memory might not be optimal if we expect long responses + rawBody, err := io.ReadAll(resp.Body) + if err != nil { + return resp.StatusCode, fmt.Errorf("failed to read response body: %w", err) + } + + if reqErr != nil { + if err = json.Unmarshal(rawBody, errorBody); err != nil { + return resp.StatusCode, fmt.Errorf("failed to unmarshal error response with status %d: %w", resp.StatusCode, err) + } + return resp.StatusCode, reqErr + } + + // TODO: assuming json bodies for now, but may need to handle other body types + if err = json.Unmarshal(rawBody, expectedBody); err != nil { + return resp.StatusCode, fmt.Errorf("failed to unmarshal response with status %d: %w", resp.StatusCode, err) + } + return resp.StatusCode, nil +} diff --git a/components/payments/internal/connectors/httpwrapper/client_test.go b/components/payments/internal/connectors/httpwrapper/client_test.go new file mode 100644 index 0000000000..09c1aaffb2 --- /dev/null +++ b/components/payments/internal/connectors/httpwrapper/client_test.go @@ -0,0 +1,92 @@ +package httpwrapper_test + +import ( + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "testing" + "time" + + "github.com/formancehq/payments/internal/connectors/httpwrapper" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestClient(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Client Suite") +} + +type successRes struct { + ID string `json:"id"` +} + +type errorRes struct { + Code string `json:"code"` +} + +var _ = Describe("ClientWrapper", func() { + var ( + config *httpwrapper.Config + client httpwrapper.Client + server *httptest.Server + ) + + BeforeEach(func() { + config = &httpwrapper.Config{Timeout: 30 * time.Millisecond} + var err error + client, err = httpwrapper.NewClient(config) + Expect(err).To(BeNil()) + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + params, err := url.ParseQuery(r.URL.RawQuery) + Expect(err).To(BeNil()) + + code := params.Get("code") + statusCode, err := strconv.Atoi(code) + Expect(err).To(BeNil()) + if statusCode == http.StatusOK { + w.Write([]byte(`{"id":"someid"}`)) + return + } + + w.WriteHeader(statusCode) + w.Write([]byte(`{"code":"err123"}`)) + })) + }) + AfterEach(func() { + server.Close() + }) + + Context("making a request with default client settings", func() { + It("unmarshals successful responses when acceptable status code seen", func(ctx SpecContext) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, server.URL+"?code=200", http.NoBody) + Expect(err).To(BeNil()) + + res := &successRes{} + code, doErr := client.Do(req, res, nil) + Expect(code).To(Equal(http.StatusOK)) + Expect(doErr).To(BeNil()) + Expect(res.ID).To(Equal("someid")) + }) + It("unmarshals error responses when bad status code seen", func(ctx SpecContext) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, server.URL+"?code=500", http.NoBody) + Expect(err).To(BeNil()) + + res := &errorRes{} + code, doErr := client.Do(req, &successRes{}, res) + Expect(code).To(Equal(http.StatusInternalServerError)) + Expect(doErr).To(MatchError(httpwrapper.ErrStatusCodeUnexpected)) + Expect(res.Code).To(Equal("err123")) + }) + It("responds with error when HTTP request fails", func(ctx SpecContext) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "notaurl", http.NoBody) + Expect(err).To(BeNil()) + + res := &errorRes{} + code, doErr := client.Do(req, &successRes{}, res) + Expect(code).To(Equal(0)) + Expect(doErr).To(MatchError(ContainSubstring("failed to make request"))) + }) + }) +}) diff --git a/components/payments/internal/connectors/httpwrapper/config.go b/components/payments/internal/connectors/httpwrapper/config.go new file mode 100644 index 0000000000..43c497d1d2 --- /dev/null +++ b/components/payments/internal/connectors/httpwrapper/config.go @@ -0,0 +1,16 @@ +package httpwrapper + +import ( + "net/http" + "time" + + "golang.org/x/oauth2/clientcredentials" +) + +type Config struct { + HttpErrorCheckerFn func(code int) error + + Timeout time.Duration + Transport http.RoundTripper + OAuthConfig *clientcredentials.Config +} diff --git a/components/payments/internal/connectors/plugins/configs.go b/components/payments/internal/connectors/plugins/configs.go new file mode 100644 index 0000000000..794eca3b86 --- /dev/null +++ b/components/payments/internal/connectors/plugins/configs.go @@ -0,0 +1,79 @@ +package plugins + +import ( + _ "embed" + "encoding/json" + "errors" + "sync" +) + +//go:embed configs.json +var configsFile []byte + +type Type string + +const ( + TypeLongString Type = "long string" + TypeString Type = "string" + TypeDurationNs Type = "duration ns" + TypeDurationUnsignedInteger Type = "unsigned integer" + TypeBoolean Type = "boolean" +) + +type Configs map[string]Config +type Config map[string]Parameter +type Parameter struct { + DataType Type `json:"dataType"` + Required bool `json:"required"` + DefaultValue string `json:"defaultValue"` +} + +var ( + defaultParameters = map[string]Parameter{ + "pollingPeriod": { + DataType: "duration ns", + Required: false, + DefaultValue: "2m", + }, + "pageSize": { + DataType: "unsigned integer", + Required: false, + DefaultValue: "100", + }, + "name": { + DataType: "string", + Required: true, + }, + } + + configs Configs +) + +var once sync.Once + +func GetConfigs() Configs { + once.Do(func() { + if err := json.Unmarshal(configsFile, &configs); err != nil { + panic(err) + } + + for key := range configs { + for paramName, param := range defaultParameters { + if _, ok := configs[key][paramName]; !ok { + configs[key][paramName] = param + } + } + } + }) + + return configs +} + +func GetConfig(provider string) (Config, error) { + config, ok := configs[provider] + if !ok { + return nil, errors.New("config not found") + } + + return config, nil +} diff --git a/components/payments/cmd/connectors/internal/connectors/generic/client/generated/go.sum b/components/payments/internal/connectors/plugins/configs.json similarity index 100% rename from components/payments/cmd/connectors/internal/connectors/generic/client/generated/go.sum rename to components/payments/internal/connectors/plugins/configs.json diff --git a/components/payments/cmd/connectors/internal/connectors/currency/amount.go b/components/payments/internal/connectors/plugins/currency/amount.go similarity index 100% rename from components/payments/cmd/connectors/internal/connectors/currency/amount.go rename to components/payments/internal/connectors/plugins/currency/amount.go diff --git a/components/payments/cmd/connectors/internal/connectors/currency/amount_test.go b/components/payments/internal/connectors/plugins/currency/amount_test.go similarity index 100% rename from components/payments/cmd/connectors/internal/connectors/currency/amount_test.go rename to components/payments/internal/connectors/plugins/currency/amount_test.go diff --git a/components/payments/cmd/connectors/internal/connectors/currency/currency.go b/components/payments/internal/connectors/plugins/currency/currency.go similarity index 95% rename from components/payments/cmd/connectors/internal/connectors/currency/currency.go rename to components/payments/internal/connectors/plugins/currency/currency.go index 3ec4e7ce67..2523727c2a 100644 --- a/components/payments/cmd/connectors/internal/connectors/currency/currency.go +++ b/components/payments/internal/connectors/plugins/currency/currency.go @@ -4,8 +4,6 @@ import ( "errors" "fmt" "strings" - - "github.com/formancehq/payments/internal/models" ) var ( @@ -185,19 +183,19 @@ var ( } ) -func FormatAsset(currencies map[string]int, cur string) models.Asset { +func FormatAsset(currencies map[string]int, cur string) string { asset := strings.ToUpper(string(cur)) def, ok := currencies[asset] if !ok { - return models.Asset(asset) + return asset } if def == 0 { - return models.Asset(asset) + return asset } - return models.Asset(fmt.Sprintf("%s/%d", asset, def)) + return fmt.Sprintf("%s/%d", asset, def) } func GetPrecision(currencies map[string]int, cur string) (int, error) { @@ -211,8 +209,8 @@ func GetPrecision(currencies map[string]int, cur string) (int, error) { return def, nil } -func GetCurrencyAndPrecisionFromAsset(currencies map[string]int, asset models.Asset) (string, int, error) { - parts := strings.Split(asset.String(), "/") +func GetCurrencyAndPrecisionFromAsset(currencies map[string]int, asset string) (string, int, error) { + parts := strings.Split(asset, "/") if len(parts) != 2 { return "", 0, errors.New("invalid asset") } diff --git a/components/payments/internal/connectors/plugins/errors.go b/components/payments/internal/connectors/plugins/errors.go new file mode 100644 index 0000000000..94ebe530e9 --- /dev/null +++ b/components/payments/internal/connectors/plugins/errors.go @@ -0,0 +1,8 @@ +package plugins + +import "errors" + +var ( + ErrNotImplemented = errors.New("not implemented") + ErrNotYetInstalled = errors.New("not yet installed") +) diff --git a/components/payments/internal/connectors/plugins/grpc.go b/components/payments/internal/connectors/plugins/grpc.go new file mode 100644 index 0000000000..c70327257c --- /dev/null +++ b/components/payments/internal/connectors/plugins/grpc.go @@ -0,0 +1,323 @@ +package plugins + +import ( + "context" + "errors" + "os" + + "github.com/formancehq/payments/internal/connectors/grpc" + "github.com/formancehq/payments/internal/connectors/grpc/proto" + "github.com/formancehq/payments/internal/connectors/grpc/proto/services" + "github.com/formancehq/payments/internal/models" + "github.com/hashicorp/go-hclog" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type impl struct { + logger hclog.Logger + + plugin models.Plugin +} + +func NewGRPCImplem(plugin models.Plugin) *impl { + logger := hclog.New(&hclog.LoggerOptions{ + Level: hclog.Debug, + Output: os.Stderr, + }) + + return &impl{ + logger: logger, + plugin: plugin, + } +} + +func (i *impl) Install(ctx context.Context, req *services.InstallRequest) (*services.InstallResponse, error) { + i.logger.Info("installing...") + + resp, err := i.plugin.Install(ctx, models.InstallRequest{ + Config: req.Config, + }) + if err != nil { + i.logger.Error("install failed: ", err) + return nil, translateErrorToGRPC(err) + } + + capabilities := make([]proto.Capability, 0, len(resp.Capabilities)) + for _, capability := range resp.Capabilities { + capabilities = append(capabilities, proto.Capability(capability)) + } + + webhooksConfigs := make([]*proto.WebhookConfig, 0, len(resp.WebhooksConfigs)) + for _, webhook := range resp.WebhooksConfigs { + webhooksConfigs = append(webhooksConfigs, &proto.WebhookConfig{ + Name: webhook.Name, + UrlPath: webhook.URLPath, + }) + } + + i.logger.Info("installed!") + + return &services.InstallResponse{ + Capabilities: capabilities, + Workflow: grpc.TranslateWorkflow(resp.Workflow), + WebhooksConfigs: webhooksConfigs, + }, nil +} + +func (i *impl) Uninstall(ctx context.Context, req *services.UninstallRequest) (*services.UninstallResponse, error) { + i.logger.Info("uninstalling...") + + _, err := i.plugin.Uninstall(ctx, models.UninstallRequest{ + ConnectorID: req.ConnectorId, + }) + if err != nil { + i.logger.Error("uninstall failed: ", err) + return nil, translateErrorToGRPC(err) + } + + i.logger.Info("uninstalled!") + + return &services.UninstallResponse{}, nil +} + +func (i *impl) FetchNextAccounts(ctx context.Context, req *services.FetchNextAccountsRequest) (*services.FetchNextAccountsResponse, error) { + i.logger.Info("fetching next accounts...") + + resp, err := i.plugin.FetchNextAccounts(ctx, models.FetchNextAccountsRequest{ + FromPayload: req.FromPayload, + State: req.State, + PageSize: int(req.PageSize), + }) + if err != nil { + i.logger.Error("fetching next accounts failed: ", err) + return nil, translateErrorToGRPC(err) + } + + accounts := make([]*proto.Account, 0, len(resp.Accounts)) + for _, account := range resp.Accounts { + accounts = append(accounts, grpc.TranslateAccount(account)) + } + + i.logger.Info("fetched next accounts succeeded!") + + return &services.FetchNextAccountsResponse{ + Accounts: accounts, + NewState: resp.NewState, + HasMore: resp.HasMore, + }, nil +} + +func (i *impl) FetchNextExternalAccounts(ctx context.Context, req *services.FetchNextExternalAccountsRequest) (*services.FetchNextExternalAccountsResponse, error) { + i.logger.Info("fetching next external accounts...") + + resp, err := i.plugin.FetchNextExternalAccounts(ctx, models.FetchNextExternalAccountsRequest{ + FromPayload: req.FromPayload, + State: req.State, + PageSize: int(req.PageSize), + }) + if err != nil { + i.logger.Error("fetching next external accounts failed: ", err) + return nil, translateErrorToGRPC(err) + } + + externalAccounts := make([]*proto.Account, 0, len(resp.ExternalAccounts)) + for _, account := range resp.ExternalAccounts { + externalAccounts = append(externalAccounts, grpc.TranslateAccount(account)) + } + + i.logger.Info("fetched next external accounts succeeded!") + + return &services.FetchNextExternalAccountsResponse{ + Accounts: externalAccounts, + NewState: resp.NewState, + HasMore: resp.HasMore, + }, nil +} + +func (i *impl) FetchNextPayments(ctx context.Context, req *services.FetchNextPaymentsRequest) (*services.FetchNextPaymentsResponse, error) { + i.logger.Info("fetching next payments...") + + resp, err := i.plugin.FetchNextPayments(ctx, models.FetchNextPaymentsRequest{ + FromPayload: req.FromPayload, + State: req.State, + PageSize: int(req.PageSize), + }) + if err != nil { + i.logger.Error("fetching next payments failed: ", err) + return nil, translateErrorToGRPC(err) + } + + payments := make([]*proto.Payment, 0, len(resp.Payments)) + for _, payment := range resp.Payments { + payments = append(payments, grpc.TranslatePayment(payment)) + } + + i.logger.Info("fetched next payments succeeded!") + + return &services.FetchNextPaymentsResponse{ + Payments: payments, + NewState: resp.NewState, + HasMore: resp.HasMore, + }, nil +} + +func (i *impl) FetchNextBalances(ctx context.Context, req *services.FetchNextBalancesRequest) (*services.FetchNextBalancesResponse, error) { + i.logger.Info("fetching next balances...") + + resp, err := i.plugin.FetchNextBalances(ctx, models.FetchNextBalancesRequest{ + FromPayload: req.FromPayload, + State: req.State, + PageSize: int(req.PageSize), + }) + if err != nil { + i.logger.Error("fetching next balances failed: ", err) + return nil, translateErrorToGRPC(err) + } + + balances := make([]*proto.Balance, 0, len(resp.Balances)) + for _, balance := range resp.Balances { + balances = append(balances, grpc.TranslateBalance(balance)) + } + + i.logger.Info("fetched next balances succeeded!") + + return &services.FetchNextBalancesResponse{ + Balances: balances, + NewState: resp.NewState, + HasMore: resp.HasMore, + }, nil +} + +func (i *impl) FetchNextOthers(ctx context.Context, req *services.FetchNextOthersRequest) (*services.FetchNextOthersResponse, error) { + i.logger.Info("fetching next others...") + + resp, err := i.plugin.FetchNextOthers(ctx, models.FetchNextOthersRequest{ + FromPayload: req.FromPayload, + State: req.State, + PageSize: int(req.PageSize), + Name: req.Name, + }) + if err != nil { + i.logger.Error("fetching next others failed: ", err) + return nil, translateErrorToGRPC(err) + } + + others := make([]*proto.Other, 0, len(resp.Others)) + for _, other := range resp.Others { + others = append(others, &proto.Other{ + Id: other.ID, + Other: other.Other, + }) + } + + i.logger.Info("fetched next others succeeded!") + + return &services.FetchNextOthersResponse{ + Others: others, + NewState: resp.NewState, + HasMore: resp.HasMore, + }, nil +} + +func (i *impl) CreateBankAccount(ctx context.Context, req *services.CreateBankAccountRequest) (*services.CreateBankAccountResponse, error) { + i.logger.Info("creating bank account...") + + resp, err := i.plugin.CreateBankAccount(ctx, models.CreateBankAccountRequest{ + BankAccount: grpc.TranslateProtoBankAccount(req.BankAccount), + }) + if err != nil { + i.logger.Error("creating bank account failed: ", err) + return nil, translateErrorToGRPC(err) + } + + i.logger.Info("created bank account succeeded!") + + return &services.CreateBankAccountResponse{ + RelatedAccount: grpc.TranslateAccount(resp.RelatedAccount), + }, nil +} + +func (i *impl) CreateWebhooks(ctx context.Context, req *services.CreateWebhooksRequest) (*services.CreateWebhooksResponse, error) { + i.logger.Info("creating webhooks...") + + resp, err := i.plugin.CreateWebhooks(ctx, models.CreateWebhooksRequest{ + ConnectorID: req.ConnectorId, + FromPayload: req.FromPayload, + }) + if err != nil { + i.logger.Error("creating webhooks failed: ", err) + return nil, translateErrorToGRPC(err) + } + + i.logger.Info("created webhooks succeeded!") + + others := make([]*proto.Other, 0, len(resp.Others)) + for _, other := range resp.Others { + others = append(others, &proto.Other{ + Id: other.ID, + Other: other.Other, + }) + } + + return &services.CreateWebhooksResponse{ + Others: others, + }, nil +} + +func (i *impl) TranslateWebhook(ctx context.Context, req *services.TranslateWebhookRequest) (*services.TranslateWebhookResponse, error) { + i.logger.Info("translating webhook...") + + resp, err := i.plugin.TranslateWebhook(ctx, models.TranslateWebhookRequest{ + Name: req.Name, + Webhook: grpc.TranslateProtoWebhook(req.Webhook), + }) + if err != nil { + i.logger.Error("translating webhook failed: ", err) + return nil, translateErrorToGRPC(err) + } + + i.logger.Info("translated webhook succeeded!") + + responses := make([]*services.TranslateWebhookResponse_Response, 0, len(resp.Responses)) + for _, response := range resp.Responses { + r := &services.TranslateWebhookResponse_Response{ + IdempotencyKey: response.IdempotencyKey, + } + + if response.Account != nil { + r.Translated = &services.TranslateWebhookResponse_Response_Account{ + Account: grpc.TranslateAccount(*response.Account), + } + } + + if response.ExternalAccount != nil { + r.Translated = &services.TranslateWebhookResponse_Response_ExternalAccount{ + ExternalAccount: grpc.TranslateAccount(*response.ExternalAccount), + } + } + + if response.Payment != nil { + r.Translated = &services.TranslateWebhookResponse_Response_Payment{ + Payment: grpc.TranslatePayment(*response.Payment), + } + } + + responses = append(responses, r) + } + + return &services.TranslateWebhookResponse{ + Responses: responses, + }, nil +} + +var _ grpc.PSP = &impl{} + +func translateErrorToGRPC(err error) error { + switch { + case errors.Is(err, models.ErrInvalidConfig): + return status.Errorf(codes.InvalidArgument, err.Error()) + default: + return status.Errorf(codes.Internal, err.Error()) + } +} diff --git a/components/payments/internal/connectors/plugins/public/bankingcircle/accounts.go b/components/payments/internal/connectors/plugins/public/bankingcircle/accounts.go new file mode 100644 index 0000000000..0465478188 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/bankingcircle/accounts.go @@ -0,0 +1,116 @@ +package bankingcircle + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/connectors/plugins/currency" + "github.com/formancehq/payments/internal/connectors/plugins/public/bankingcircle/client" + "github.com/formancehq/payments/internal/models" +) + +type accountsState struct { + LastAccountID string `json:"lastAccountID"` + FromOpeningDate time.Time `json:"fromOpeningDate"` +} + +func (p Plugin) fetchNextAccounts(ctx context.Context, req models.FetchNextAccountsRequest) (models.FetchNextAccountsResponse, error) { + var oldState accountsState + if req.State != nil { + if err := json.Unmarshal(req.State, &oldState); err != nil { + return models.FetchNextAccountsResponse{}, err + } + } + + newState := accountsState{ + LastAccountID: oldState.LastAccountID, + FromOpeningDate: oldState.FromOpeningDate, + } + + var accounts []models.PSPAccount + hasMore := false + for page := 1; ; page++ { + pagedAccounts, err := p.client.GetAccounts(ctx, page, req.PageSize, oldState.FromOpeningDate) + if err != nil { + return models.FetchNextAccountsResponse{}, nil + } + + if len(pagedAccounts) == 0 { + break + } + + filteredAccounts := filterAccounts(pagedAccounts, oldState.LastAccountID) + for _, account := range filteredAccounts { + openingDate, err := time.Parse("2006-01-02T15:04:05.999999999+00:00", account.OpeningDate) + if err != nil { + return models.FetchNextAccountsResponse{}, fmt.Errorf("failed to parse opening date: %w", err) + } + + raw, err := json.Marshal(account) + if err != nil { + return models.FetchNextAccountsResponse{}, fmt.Errorf("failed to marshal account: %w", err) + } + + accounts = append(accounts, models.PSPAccount{ + Reference: account.AccountID, + CreatedAt: openingDate, + Name: &account.AccountDescription, + DefaultAsset: pointer.For(currency.FormatAsset(supportedCurrenciesWithDecimal, account.Currency)), + Raw: raw, + }) + + newState.LastAccountID = account.AccountID + newState.FromOpeningDate = openingDate + + if len(accounts) >= req.PageSize { + break + } + } + + if len(accounts) >= req.PageSize { + hasMore = true + break + } + } + + payload, err := json.Marshal(newState) + if err != nil { + return models.FetchNextAccountsResponse{}, err + } + + return models.FetchNextAccountsResponse{ + Accounts: accounts, + NewState: payload, + HasMore: hasMore, + }, nil +} + +func filterAccounts(pagedAccounts []client.Account, lastAccountID string) []client.Account { + if lastAccountID == "" { + return pagedAccounts + } + + var filteredAccounts []client.Account + found := false + for _, account := range pagedAccounts { + if !found && account.AccountID != lastAccountID { + continue + } + + if !found && account.AccountID == lastAccountID { + found = true + continue + } + + filteredAccounts = append(filteredAccounts, account) + } + + if !found { + return pagedAccounts + } + + return filteredAccounts +} diff --git a/components/payments/internal/connectors/plugins/public/bankingcircle/balances.go b/components/payments/internal/connectors/plugins/public/bankingcircle/balances.go new file mode 100644 index 0000000000..1054e49031 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/bankingcircle/balances.go @@ -0,0 +1,65 @@ +package bankingcircle + +import ( + "context" + "encoding/json" + "errors" + "math/big" + "time" + + "github.com/formancehq/payments/internal/connectors/plugins/currency" + "github.com/formancehq/payments/internal/models" +) + +func (p Plugin) fetchNextBalances(ctx context.Context, req models.FetchNextBalancesRequest) (models.FetchNextBalancesResponse, error) { + var from models.PSPAccount + if req.FromPayload == nil { + return models.FetchNextBalancesResponse{}, errors.New("missing from payload when fetching balances") + } + if err := json.Unmarshal(req.FromPayload, &from); err != nil { + return models.FetchNextBalancesResponse{}, err + } + + account, err := p.client.GetAccount(ctx, from.Reference) + if err != nil { + return models.FetchNextBalancesResponse{}, err + } + + var balances []models.PSPBalance + for _, balance := range account.Balances { + // Note(polo): the last transaction timestamp is wrong in the banking + // circle response. We will use the current time instead. + // lastTransactionTimestamp, err := time.Parse("2006-01-02T15:04:05.999999999+00:00", balance.LastTransactionTimestamp) + // if err != nil { + // return models.FetchNextBalancesResponse{}, fmt.Errorf("failed to parse opening date: %w", err) + // } + lastTransactionTimestamp := time.Now().UTC() + + precision := supportedCurrenciesWithDecimal[balance.Currency] + + beginOfDayAmount, err := currency.GetAmountWithPrecisionFromString(balance.BeginOfDayAmount.String(), precision) + if err != nil { + return models.FetchNextBalancesResponse{}, err + } + + intraDayAmount, err := currency.GetAmountWithPrecisionFromString(balance.IntraDayAmount.String(), precision) + if err != nil { + return models.FetchNextBalancesResponse{}, err + } + + amount := big.NewInt(0).Add(beginOfDayAmount, intraDayAmount) + + balances = append(balances, models.PSPBalance{ + AccountReference: from.Reference, + CreatedAt: lastTransactionTimestamp, + Amount: amount, + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, balance.Currency), + }) + } + + return models.FetchNextBalancesResponse{ + Balances: balances, + NewState: []byte{}, + HasMore: false, + }, nil +} diff --git a/components/payments/internal/connectors/plugins/public/bankingcircle/bank_account_creation.go b/components/payments/internal/connectors/plugins/public/bankingcircle/bank_account_creation.go new file mode 100644 index 0000000000..fdb665a421 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/bankingcircle/bank_account_creation.go @@ -0,0 +1,27 @@ +package bankingcircle + +import ( + "context" + "encoding/json" + + "github.com/formancehq/payments/internal/models" +) + +func (p Plugin) createBankAccount(ctx context.Context, req models.CreateBankAccountRequest) (models.CreateBankAccountResponse, error) { + // We can't create bank accounts in Banking Circle since they do not store + // the bank account information. We just have to return the related formance + // account in order to use it in the future. + raw, err := json.Marshal(req.BankAccount) + if err != nil { + return models.CreateBankAccountResponse{}, err + } + + return models.CreateBankAccountResponse{ + RelatedAccount: models.PSPAccount{ + Reference: req.BankAccount.ID.String(), + CreatedAt: req.BankAccount.CreatedAt, + Name: &req.BankAccount.Name, + Raw: raw, + }, + }, nil +} diff --git a/components/payments/internal/connectors/plugins/public/bankingcircle/capabilities.go b/components/payments/internal/connectors/plugins/public/bankingcircle/capabilities.go new file mode 100644 index 0000000000..01d2cc45b6 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/bankingcircle/capabilities.go @@ -0,0 +1,9 @@ +package bankingcircle + +import "github.com/formancehq/payments/internal/models" + +var capabilities = []models.Capability{ + models.CAPABILITY_FETCH_ACCOUNTS, + models.CAPABILITY_FETCH_PAYMENTS, + models.CAPABILITY_FETCH_BALANCES, +} diff --git a/components/payments/cmd/connectors/internal/connectors/bankingcircle/client/accounts.go b/components/payments/internal/connectors/plugins/public/bankingcircle/client/accounts.go similarity index 60% rename from components/payments/cmd/connectors/internal/connectors/bankingcircle/client/accounts.go rename to components/payments/internal/connectors/plugins/public/bankingcircle/client/accounts.go index 2e1ded4476..7f8a99af22 100644 --- a/components/payments/cmd/connectors/internal/connectors/bankingcircle/client/accounts.go +++ b/components/payments/internal/connectors/plugins/public/bankingcircle/client/accounts.go @@ -4,11 +4,10 @@ import ( "context" "encoding/json" "fmt" - "io" "net/http" "time" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" + "github.com/formancehq/payments/internal/connectors/httpwrapper" ) type Account struct { @@ -35,14 +34,15 @@ type Account struct { } `json:"balances"` } -func (c *Client) GetAccounts(ctx context.Context, page int) ([]*Account, error) { +func (c *Client) GetAccounts(ctx context.Context, page int, pageSize int, fromOpeningDate time.Time) ([]Account, error) { if err := c.ensureAccessTokenIsValid(ctx); err != nil { return nil, err } - f := connectors.ClientMetrics(ctx, "bankingcircle", "list_accounts") - now := time.Now() - defer f(ctx, now) + // TODO(polo): metrics + // f := connectors.ClientMetrics(ctx, "bankingcircle", "list_accounts") + // now := time.Now() + // defer f(ctx, now) req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.endpoint+"/api/v1/accounts", http.NoBody) if err != nil { @@ -50,45 +50,33 @@ func (c *Client) GetAccounts(ctx context.Context, page int) ([]*Account, error) } q := req.URL.Query() - q.Add("PageSize", "100") + q.Add("PageSize", fmt.Sprint(pageSize)) q.Add("PageNumber", fmt.Sprint(page)) - + if !fromOpeningDate.IsZero() { + q.Add("OpeningDateFrom", fromOpeningDate.Format(time.DateOnly)) + } req.URL.RawQuery = q.Encode() req.Header.Set("Authorization", "Bearer "+c.accessToken) - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to get accounts: %w", err) - } - - defer func() { - err = resp.Body.Close() - if err != nil { - c.logger.Error(err) - } - }() - - responseBody, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read accounts response body: %w", err) - } - type response struct { - Result []*Account `json:"result"` + Result []Account `json:"result"` PageInfo struct { CurrentPage int `json:"currentPage"` PageSize int `json:"pageSize"` } `json:"pageInfo"` } - var res response - - if err = json.Unmarshal(responseBody, &res); err != nil { - return nil, fmt.Errorf("failed to unmarshal accounts response: %w", err) + res := response{Result: make([]Account, 0)} + statusCode, err := c.httpClient.Do(req, &res, nil) + switch err { + case nil: + return res.Result, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, fmt.Errorf("received status code %d for get accounts", statusCode) } - - return res.Result, nil + return nil, fmt.Errorf("failed to get accounts: %w", err) } func (c *Client) GetAccount(ctx context.Context, accountID string) (*Account, error) { @@ -96,9 +84,10 @@ func (c *Client) GetAccount(ctx context.Context, accountID string) (*Account, er return nil, err } - f := connectors.ClientMetrics(ctx, "bankingcircle", "get_account") - now := time.Now() - defer f(ctx, now) + // TODO(polo): metrics + // f := connectors.ClientMetrics(ctx, "bankingcircle", "get_account") + // now := time.Now() + // defer f(ctx, now) req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/api/v1/accounts/%s", c.endpoint, accountID), http.NoBody) if err != nil { @@ -106,26 +95,14 @@ func (c *Client) GetAccount(ctx context.Context, accountID string) (*Account, er } req.Header.Set("Authorization", "Bearer "+c.accessToken) - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to get accounts: %w", err) - } - - defer func() { - err = resp.Body.Close() - if err != nil { - c.logger.Error(err) - } - }() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("wrong status code: %d", resp.StatusCode) - } - var account Account - if err := json.NewDecoder(resp.Body).Decode(&account); err != nil { - return nil, fmt.Errorf("failed to decode account response: %w", err) + statusCode, err := c.httpClient.Do(req, &account, nil) + switch err { + case nil: + return &account, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, fmt.Errorf("received status code %d for get account", statusCode) } - - return &account, nil + return nil, fmt.Errorf("failed to get account: %w", err) } diff --git a/components/payments/cmd/connectors/internal/connectors/bankingcircle/client/auth.go b/components/payments/internal/connectors/plugins/public/bankingcircle/client/auth.go similarity index 53% rename from components/payments/cmd/connectors/internal/connectors/bankingcircle/client/auth.go rename to components/payments/internal/connectors/plugins/public/bankingcircle/client/auth.go index 7374edd9e0..83675bbbb2 100644 --- a/components/payments/cmd/connectors/internal/connectors/bankingcircle/client/auth.go +++ b/components/payments/internal/connectors/plugins/public/bankingcircle/client/auth.go @@ -2,20 +2,19 @@ package client import ( "context" - "encoding/json" "fmt" - "io" "net/http" "strconv" "time" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" + "github.com/formancehq/payments/internal/connectors/httpwrapper" ) func (c *Client) login(ctx context.Context) error { - f := connectors.ClientMetrics(ctx, "bankingcircle", "authorize") - now := time.Now() - defer f(ctx, now) + // TODO(polo): add metrics + // f := connectors.ClientMetrics(ctx, "bankingcircle", "authorize") + // now := time.Now() + // defer f(ctx, now) req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.authorizationEndpoint+"/api/v1/authorizations/authorize", http.NoBody) @@ -25,59 +24,36 @@ func (c *Client) login(ctx context.Context) error { req.SetBasicAuth(c.username, c.password) - resp, err := c.httpClient.Do(req) - if err != nil { - return fmt.Errorf("failed to login: %w", err) - } - - defer func() { - err = resp.Body.Close() - if err != nil { - c.logger.Error(err) - } - }() - - responseBody, err := io.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("failed to read login response body: %w", err) - } - - if resp.StatusCode != http.StatusOK { - type responseError struct { - ErrorCode string `json:"errorCode"` - ErrorText string `json:"errorText"` - } - var errors []responseError - if err = json.Unmarshal(responseBody, &errors); err != nil { - return fmt.Errorf("failed to unmarshal login response: %w", err) - } - if len(errors) > 0 { - return fmt.Errorf("failed to login: %s %s", errors[0].ErrorCode, errors[0].ErrorText) - } - return fmt.Errorf("failed to login: %s", resp.Status) - } - //nolint:tagliatelle // allow for client-side structures type response struct { AccessToken string `json:"access_token"` ExpiresIn string `json:"expires_in"` } + type responseError struct { + ErrorCode string `json:"errorCode"` + ErrorText string `json:"errorText"` + } var res response - - if err = json.Unmarshal(responseBody, &res); err != nil { - return fmt.Errorf("failed to unmarshal login response: %w", err) + var errors []responseError + statusCode, err := c.httpClient.Do(req, &res, &errors) + switch err { + case nil: + // fallthrough + case httpwrapper.ErrStatusCodeUnexpected: + if len(errors) > 0 { + return fmt.Errorf("failed to login: %s %s", errors[0].ErrorCode, errors[0].ErrorText) + } + return fmt.Errorf("failed to login: %d", statusCode) } + return fmt.Errorf("failed make login request: %w", err) c.accessToken = res.AccessToken - expiresIn, err := strconv.Atoi(res.ExpiresIn) if err != nil { return fmt.Errorf("failed to convert expires_in to int: %w", err) } - c.accessTokenExpiresAt = time.Now().Add(time.Duration(expiresIn) * time.Second) - return nil } diff --git a/components/payments/cmd/connectors/internal/connectors/bankingcircle/client/client.go b/components/payments/internal/connectors/plugins/public/bankingcircle/client/client.go similarity index 57% rename from components/payments/cmd/connectors/internal/connectors/bankingcircle/client/client.go rename to components/payments/internal/connectors/plugins/public/bankingcircle/client/client.go index 319833bff0..d7af45f623 100644 --- a/components/payments/cmd/connectors/internal/connectors/bankingcircle/client/client.go +++ b/components/payments/internal/connectors/plugins/public/bankingcircle/client/client.go @@ -5,12 +5,11 @@ import ( "net/http" "time" - "github.com/formancehq/go-libs/logging" - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "github.com/formancehq/payments/internal/connectors/httpwrapper" ) type Client struct { - httpClient *http.Client + httpClient httpwrapper.Client username string password string @@ -18,14 +17,16 @@ type Client struct { endpoint string authorizationEndpoint string - logger logging.Logger - accessToken string accessTokenExpiresAt time.Time } -func newHTTPClient(userCertificate, userCertificateKey string) (*http.Client, error) { - cert, err := tls.X509KeyPair([]byte(userCertificate), []byte(userCertificateKey)) +func New( + username, password, + endpoint, authorizationEndpoint, + uCertificate, uCertificateKey string, +) (*Client, error) { + cert, err := tls.X509KeyPair([]byte(uCertificate), []byte(uCertificateKey)) if err != nil { return nil, err } @@ -35,18 +36,10 @@ func newHTTPClient(userCertificate, userCertificateKey string) (*http.Client, er Certificates: []tls.Certificate{cert}, } - return &http.Client{ - Timeout: 10 * time.Second, - Transport: otelhttp.NewTransport(tr), - }, nil -} - -func NewClient( - username, password, - endpoint, authorizationEndpoint, - uCertificate, uCertificateKey string, - logger logging.Logger) (*Client, error) { - httpClient, err := newHTTPClient(uCertificate, uCertificateKey) + config := &httpwrapper.Config{ + Transport: tr, + } + httpClient, err := httpwrapper.NewClient(config) if err != nil { return nil, err } @@ -58,8 +51,6 @@ func NewClient( password: password, endpoint: endpoint, authorizationEndpoint: authorizationEndpoint, - - logger: logger, } return c, nil diff --git a/components/payments/cmd/connectors/internal/connectors/bankingcircle/client/payments.go b/components/payments/internal/connectors/plugins/public/bankingcircle/client/payments.go similarity index 65% rename from components/payments/cmd/connectors/internal/connectors/bankingcircle/client/payments.go rename to components/payments/internal/connectors/plugins/public/bankingcircle/client/payments.go index 544790096d..26db5469b9 100644 --- a/components/payments/cmd/connectors/internal/connectors/bankingcircle/client/payments.go +++ b/components/payments/internal/connectors/plugins/public/bankingcircle/client/payments.go @@ -4,23 +4,24 @@ import ( "context" "encoding/json" "fmt" - "io" "net/http" "time" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" + "github.com/formancehq/payments/internal/connectors/httpwrapper" ) //nolint:tagliatelle // allow for client-side structures type Payment struct { - PaymentID string `json:"paymentId"` - TransactionReference string `json:"transactionReference"` - ConcurrencyToken string `json:"concurrencyToken"` - Classification string `json:"classification"` - Status string `json:"status"` - Errors interface{} `json:"errors"` - LastChangedTimestamp time.Time `json:"lastChangedTimestamp"` - DebtorInformation struct { + PaymentID string `json:"paymentId"` + TransactionReference string `json:"transactionReference"` + ConcurrencyToken string `json:"concurrencyToken"` + Classification string `json:"classification"` + Status string `json:"status"` + Errors interface{} `json:"errors"` + ProcessedTimestamp time.Time `json:"processedTimestamp"` + LatestStatusChangedTimestamp time.Time `json:"latestStatusChangedTimestamp"` + LastChangedTimestamp time.Time `json:"lastChangedTimestamp"` + DebtorInformation struct { PaymentBulkID interface{} `json:"paymentBulkId"` AccountID string `json:"accountId"` Account struct { @@ -80,14 +81,15 @@ type Payment struct { } `json:"creditorInformation"` } -func (c *Client) GetPayments(ctx context.Context, page int) ([]*Payment, error) { +func (c *Client) GetPayments(ctx context.Context, page int, pageSize int) ([]Payment, error) { if err := c.ensureAccessTokenIsValid(ctx); err != nil { return nil, err } - f := connectors.ClientMetrics(ctx, "bankingcircle", "list_payments") - now := time.Now() - defer f(ctx, now) + // TODO(polo): metrics + // f := connectors.ClientMetrics(ctx, "bankingcircle", "list_payments") + // now := time.Now() + // defer f(ctx, now) req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.endpoint+"/api/v1/payments/singles", http.NoBody) if err != nil { @@ -95,45 +97,30 @@ func (c *Client) GetPayments(ctx context.Context, page int) ([]*Payment, error) } q := req.URL.Query() - q.Add("PageSize", "100") + q.Add("PageSize", fmt.Sprint(pageSize)) q.Add("PageNumber", fmt.Sprint(page)) - req.URL.RawQuery = q.Encode() req.Header.Set("Authorization", "Bearer "+c.accessToken) - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to get payments: %w", err) - } - - defer func() { - err = resp.Body.Close() - if err != nil { - c.logger.Error(err) - } - }() - - responseBody, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read payments response body: %w", err) - } - type response struct { - Result []*Payment `json:"result"` + Result []Payment `json:"result"` PageInfo struct { CurrentPage int `json:"currentPage"` PageSize int `json:"pageSize"` } `json:"pageInfo"` } - var res response - - if err = json.Unmarshal(responseBody, &res); err != nil { - return nil, fmt.Errorf("failed to unmarshal payments response: %w", err) + res := response{Result: make([]Payment, 0)} + statusCode, err := c.httpClient.Do(req, &res, nil) + switch err { + case nil: + return res.Result, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, fmt.Errorf("received status code %d for get payments", statusCode) } - - return res.Result, nil + return nil, fmt.Errorf("failed to get payments: %w", err) } type StatusResponse struct { @@ -145,9 +132,10 @@ func (c *Client) GetPaymentStatus(ctx context.Context, paymentID string) (*Statu return nil, err } - f := connectors.ClientMetrics(ctx, "bankingcircle", "get_payment_status") - now := time.Now() - defer f(ctx, now) + // TODO(polo): metrics + // f := connectors.ClientMetrics(ctx, "bankingcircle", "get_payment_status") + // now := time.Now() + // defer f(ctx, now) req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/api/v1/payments/singles/%s/status", c.endpoint, paymentID), http.NoBody) if err != nil { @@ -155,27 +143,14 @@ func (c *Client) GetPaymentStatus(ctx context.Context, paymentID string) (*Statu } req.Header.Set("Authorization", "Bearer "+c.accessToken) - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to get payments: %w", err) - } - - defer func() { - err = resp.Body.Close() - if err != nil { - c.logger.Error(err) - } - }() - - responseBody, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read payments response body: %w", err) - } - var res StatusResponse - if err = json.Unmarshal(responseBody, &res); err != nil { - return nil, fmt.Errorf("failed to unmarshal payments response: %w", err) + statusCode, err := c.httpClient.Do(req, &res, nil) + switch err { + case nil: + return &res, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, fmt.Errorf("received status code %d for get payment status", statusCode) } - - return &res, nil + return nil, fmt.Errorf("failed to get payments status: %w", err) } diff --git a/components/payments/cmd/connectors/internal/connectors/bankingcircle/client/transfer_payouts.go b/components/payments/internal/connectors/plugins/public/bankingcircle/client/transfer_payouts.go similarity index 69% rename from components/payments/cmd/connectors/internal/connectors/bankingcircle/client/transfer_payouts.go rename to components/payments/internal/connectors/plugins/public/bankingcircle/client/transfer_payouts.go index 77ab1434c6..e2decf2e82 100644 --- a/components/payments/cmd/connectors/internal/connectors/bankingcircle/client/transfer_payouts.go +++ b/components/payments/internal/connectors/plugins/public/bankingcircle/client/transfer_payouts.go @@ -8,7 +8,7 @@ import ( "net/http" "time" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" + "github.com/formancehq/payments/internal/connectors/httpwrapper" ) type PaymentAccount struct { @@ -41,9 +41,10 @@ func (c *Client) InitiateTransferOrPayouts(ctx context.Context, transferRequest return nil, err } - f := connectors.ClientMetrics(ctx, "bankingcircle", "create_transfers_payouts") - now := time.Now() - defer f(ctx, now) + // TODO(polo): add metrics + // f := connectors.ClientMetrics(ctx, "bankingcircle", "create_transfers_payouts") + // now := time.Now() + // defer f(ctx, now) body, err := json.Marshal(transferRequest) if err != nil { @@ -57,26 +58,14 @@ func (c *Client) InitiateTransferOrPayouts(ctx context.Context, transferRequest req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+c.accessToken) - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to make transfer: %w", err) - } - - defer func() { - err = resp.Body.Close() - if err != nil { - c.logger.Error(err) - } - }() - - if resp.StatusCode != http.StatusCreated { - return nil, fmt.Errorf("failed to make transfer: %w", err) + var res PaymentResponse + statusCode, err := c.httpClient.Do(req, &res, nil) + switch err { + case nil: + return &res, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, fmt.Errorf("received status code %d for make payout", statusCode) } - - var transferResponse PaymentResponse - if err := json.NewDecoder(resp.Body).Decode(&transferResponse); err != nil { - return nil, fmt.Errorf("failed to unmarshal wallets response body: %w", err) - } - - return &transferResponse, nil + return nil, fmt.Errorf("failed to make payout: %w", err) } diff --git a/components/payments/internal/connectors/plugins/public/bankingcircle/cmd/main.go b/components/payments/internal/connectors/plugins/public/bankingcircle/cmd/main.go new file mode 100644 index 0000000000..c03a9701d1 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/bankingcircle/cmd/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "github.com/formancehq/payments/internal/connectors/grpc" + "github.com/formancehq/payments/internal/connectors/plugins" + "github.com/formancehq/payments/internal/connectors/plugins/public/bankingcircle" + "github.com/hashicorp/go-plugin" +) + +func main() { + // TODO(polo): metrics + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: grpc.Handshake, + Plugins: map[string]plugin.Plugin{ + "psp": &grpc.PSPGRPCPlugin{Impl: plugins.NewGRPCImplem(&bankingcircle.Plugin{})}, + }, + + // A non-nil value here enables gRPC serving for this plugin... + GRPCServer: plugin.DefaultGRPCServer, + }) +} diff --git a/components/payments/internal/connectors/plugins/public/bankingcircle/config.go b/components/payments/internal/connectors/plugins/public/bankingcircle/config.go new file mode 100644 index 0000000000..af2e64aeb1 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/bankingcircle/config.go @@ -0,0 +1,54 @@ +package bankingcircle + +import ( + "encoding/json" + + "github.com/formancehq/payments/internal/models" + "github.com/pkg/errors" +) + +type Config struct { + Username string `json:"username" yaml:"username" ` + Password string `json:"password" yaml:"password" ` + Endpoint string `json:"endpoint" yaml:"endpoint"` + AuthorizationEndpoint string `json:"authorizationEndpoint" yaml:"authorizationEndpoint" ` + UserCertificate string `json:"userCertificate" yaml:"userCertificate" ` + UserCertificateKey string `json:"userCertificateKey" yaml:"userCertificateKey"` +} + +func (c Config) validate() error { + if c.Username == "" { + return errors.Wrap(models.ErrInvalidConfig, "missing username in config") + } + + if c.Password == "" { + return errors.Wrap(models.ErrInvalidConfig, "missing password in config") + } + + if c.Endpoint == "" { + return errors.Wrap(models.ErrInvalidConfig, "missing endpoint in config") + } + + if c.AuthorizationEndpoint == "" { + return errors.Wrap(models.ErrInvalidConfig, "missing authorization endpoint in config") + } + + if c.UserCertificate == "" { + return errors.Wrap(models.ErrInvalidConfig, "missing user certificate in config") + } + + if c.UserCertificateKey == "" { + return errors.Wrap(models.ErrInvalidConfig, "missing user certificate key in config") + } + + return nil +} + +func unmarshalAndValidateConfig(payload []byte) (Config, error) { + var config Config + if err := json.Unmarshal(payload, &config); err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + return config, config.validate() +} diff --git a/components/payments/internal/connectors/plugins/public/bankingcircle/config.json b/components/payments/internal/connectors/plugins/public/bankingcircle/config.json new file mode 100644 index 0000000000..8dbaa54095 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/bankingcircle/config.json @@ -0,0 +1,32 @@ +{ + "username": { + "dataType": "string", + "required": true, + "defaultValue": "" + }, + "password": { + "dataType": "string", + "required": true, + "defaultValue": "" + }, + "endpoint": { + "dataType": "string", + "required": true, + "defaultValue": "" + }, + "authorizationEndpoint": { + "dataType": "string", + "required": true, + "defaultValue": "" + }, + "userCertificate": { + "dataType": "string", + "required": true, + "defaultValue": "" + }, + "userCertificateKey": { + "dataType": "string", + "required": true, + "defaultValue": "" + } +} \ No newline at end of file diff --git a/components/payments/cmd/connectors/internal/connectors/bankingcircle/currencies.go b/components/payments/internal/connectors/plugins/public/bankingcircle/currencies.go similarity index 95% rename from components/payments/cmd/connectors/internal/connectors/bankingcircle/currencies.go rename to components/payments/internal/connectors/plugins/public/bankingcircle/currencies.go index 9ffb3e4de4..c0a19966c9 100644 --- a/components/payments/cmd/connectors/internal/connectors/bankingcircle/currencies.go +++ b/components/payments/internal/connectors/plugins/public/bankingcircle/currencies.go @@ -1,6 +1,6 @@ package bankingcircle -import "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" +import "github.com/formancehq/payments/internal/connectors/plugins/currency" var ( // All supported BankingCircle currencies and decimal are on par with diff --git a/components/payments/internal/connectors/plugins/public/bankingcircle/payments.go b/components/payments/internal/connectors/plugins/public/bankingcircle/payments.go new file mode 100644 index 0000000000..bfbb0c7df7 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/bankingcircle/payments.go @@ -0,0 +1,151 @@ +package bankingcircle + +import ( + "context" + "encoding/json" + "time" + + "github.com/formancehq/payments/internal/connectors/plugins/currency" + "github.com/formancehq/payments/internal/connectors/plugins/public/bankingcircle/client" + "github.com/formancehq/payments/internal/models" +) + +type paymentsState struct { + LatestStatusChangedTimestamp time.Time `json:"latestStatusChangedTimestamp"` +} + +func (p Plugin) fetchNextPayments(ctx context.Context, req models.FetchNextPaymentsRequest) (models.FetchNextPaymentsResponse, error) { + var oldState paymentsState + if req.State != nil { + if err := json.Unmarshal(req.State, &oldState); err != nil { + return models.FetchNextPaymentsResponse{}, err + } + } + + newState := paymentsState{ + LatestStatusChangedTimestamp: oldState.LatestStatusChangedTimestamp, + } + + var payments []models.PSPPayment + hasMore := false + for page := 1; ; page++ { + pagedPayments, err := p.client.GetPayments(ctx, page, req.PageSize) + if err != nil { + return models.FetchNextPaymentsResponse{}, err + } + + if len(pagedPayments) == 0 { + break + } + + for _, payment := range pagedPayments { + switch payment.LatestStatusChangedTimestamp.Compare(oldState.LatestStatusChangedTimestamp) { + case -1, 0: + continue + default: + } + + p, err := translatePayment(payment) + if err != nil { + return models.FetchNextPaymentsResponse{}, err + } + + if p != nil { + payments = append(payments, *p) + } + + if payment.LatestStatusChangedTimestamp.After(newState.LatestStatusChangedTimestamp) { + newState.LatestStatusChangedTimestamp = payment.LatestStatusChangedTimestamp + } + + if len(payments) >= req.PageSize { + break + } + } + + if len(payments) >= req.PageSize { + hasMore = true + break + } + } + + payload, err := json.Marshal(newState) + if err != nil { + return models.FetchNextPaymentsResponse{}, err + } + + return models.FetchNextPaymentsResponse{ + Payments: payments, + NewState: payload, + HasMore: hasMore, + }, err +} + +func translatePayment(from client.Payment) (*models.PSPPayment, error) { + raw, err := json.Marshal(from) + if err != nil { + return nil, err + } + + paymentType := matchPaymentType(from.Classification) + + precision, ok := supportedCurrenciesWithDecimal[from.Transfer.Amount.Currency] + if !ok { + return nil, nil + } + + amount, err := currency.GetAmountWithPrecisionFromString(from.Transfer.Amount.Amount.String(), precision) + if err != nil { + return nil, err + } + + payment := models.PSPPayment{ + Reference: from.PaymentID, + CreatedAt: from.ProcessedTimestamp, + Type: paymentType, + Amount: amount, + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, from.Transfer.Amount.Currency), + Scheme: models.PAYMENT_SCHEME_OTHER, + Status: matchPaymentStatus(from.Status), + Raw: raw, + } + + if from.DebtorInformation.AccountID != "" { + payment.SourceAccountReference = &from.DebtorInformation.AccountID + } + + if from.CreditorInformation.AccountID != "" { + payment.DestinationAccountReference = &from.CreditorInformation.AccountID + } + + return &payment, nil +} + +func matchPaymentStatus(paymentStatus string) models.PaymentStatus { + switch paymentStatus { + case "Processed": + return models.PAYMENT_STATUS_SUCCEEDED + // On MissingFunding - the payment is still in progress. + // If there will be funds available within 10 days - the payment will be processed. + // Otherwise - it will be cancelled. + case "PendingProcessing", "MissingFunding": + return models.PAYMENT_STATUS_PENDING + case "Rejected", "Cancelled", "Reversed", "Returned": + return models.PAYMENT_STATUS_FAILED + } + + return models.PAYMENT_STATUS_OTHER +} + +func matchPaymentType(paymentType string) models.PaymentType { + switch paymentType { + case "Incoming": + return models.PAYMENT_TYPE_PAYIN + case "Outgoing": + return models.PAYMENT_TYPE_PAYOUT + case "Own": + return models.PAYMENT_TYPE_TRANSFER + } + + return models.PAYMENT_TYPE_OTHER +} diff --git a/components/payments/internal/connectors/plugins/public/bankingcircle/plugin.go b/components/payments/internal/connectors/plugins/public/bankingcircle/plugin.go new file mode 100644 index 0000000000..665e2673a7 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/bankingcircle/plugin.go @@ -0,0 +1,82 @@ +package bankingcircle + +import ( + "context" + + "github.com/formancehq/payments/internal/connectors/plugins" + "github.com/formancehq/payments/internal/connectors/plugins/public/bankingcircle/client" + "github.com/formancehq/payments/internal/models" +) + +type Plugin struct { + client *client.Client +} + +func (p *Plugin) Install(ctx context.Context, req models.InstallRequest) (models.InstallResponse, error) { + config, err := unmarshalAndValidateConfig(req.Config) + if err != nil { + return models.InstallResponse{}, err + } + + client, err := client.New(config.Username, config.Password, config.Endpoint, config.AuthorizationEndpoint, config.UserCertificate, config.UserCertificateKey) + if err != nil { + return models.InstallResponse{}, err + } + p.client = client + + return models.InstallResponse{ + Capabilities: capabilities, + Workflow: workflow(), + }, nil +} + +func (p Plugin) Uninstall(ctx context.Context, req models.UninstallRequest) (models.UninstallResponse, error) { + return models.UninstallResponse{}, nil +} + +func (p Plugin) FetchNextAccounts(ctx context.Context, req models.FetchNextAccountsRequest) (models.FetchNextAccountsResponse, error) { + if p.client == nil { + return models.FetchNextAccountsResponse{}, plugins.ErrNotYetInstalled + } + + return p.fetchNextAccounts(ctx, req) +} + +func (p Plugin) FetchNextBalances(ctx context.Context, req models.FetchNextBalancesRequest) (models.FetchNextBalancesResponse, error) { + if p.client == nil { + return models.FetchNextBalancesResponse{}, plugins.ErrNotYetInstalled + } + return p.fetchNextBalances(ctx, req) +} + +func (p Plugin) FetchNextExternalAccounts(ctx context.Context, req models.FetchNextExternalAccountsRequest) (models.FetchNextExternalAccountsResponse, error) { + return models.FetchNextExternalAccountsResponse{}, plugins.ErrNotImplemented +} + +func (p Plugin) FetchNextPayments(ctx context.Context, req models.FetchNextPaymentsRequest) (models.FetchNextPaymentsResponse, error) { + if p.client == nil { + return models.FetchNextPaymentsResponse{}, plugins.ErrNotYetInstalled + } + return p.fetchNextPayments(ctx, req) +} + +func (p Plugin) FetchNextOthers(ctx context.Context, req models.FetchNextOthersRequest) (models.FetchNextOthersResponse, error) { + return models.FetchNextOthersResponse{}, plugins.ErrNotImplemented +} + +func (p Plugin) CreateBankAccount(ctx context.Context, req models.CreateBankAccountRequest) (models.CreateBankAccountResponse, error) { + if p.client == nil { + return models.CreateBankAccountResponse{}, plugins.ErrNotYetInstalled + } + return p.createBankAccount(ctx, req) +} + +func (p Plugin) CreateWebhooks(ctx context.Context, req models.CreateWebhooksRequest) (models.CreateWebhooksResponse, error) { + return models.CreateWebhooksResponse{}, plugins.ErrNotImplemented +} + +func (p Plugin) TranslateWebhook(ctx context.Context, req models.TranslateWebhookRequest) (models.TranslateWebhookResponse, error) { + return models.TranslateWebhookResponse{}, plugins.ErrNotImplemented +} + +var _ models.Plugin = &Plugin{} diff --git a/components/payments/internal/connectors/plugins/public/bankingcircle/workflow.go b/components/payments/internal/connectors/plugins/public/bankingcircle/workflow.go new file mode 100644 index 0000000000..cd00e1f952 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/bankingcircle/workflow.go @@ -0,0 +1,27 @@ +package bankingcircle + +import "github.com/formancehq/payments/internal/models" + +func workflow() models.Tasks { + return []models.TaskTree{ + { + TaskType: models.TASK_FETCH_ACCOUNTS, + Name: "fetch_accounts", + Periodically: true, + NextTasks: []models.TaskTree{ + { + TaskType: models.TASK_FETCH_BALANCES, + Name: "fetch_balances", + Periodically: true, + NextTasks: []models.TaskTree{}, + }, + }, + }, + { + TaskType: models.TASK_FETCH_PAYMENTS, + Name: "fetch_payments", + Periodically: true, + NextTasks: []models.TaskTree{}, + }, + } +} diff --git a/components/payments/internal/connectors/plugins/public/currencycloud/accounts.go b/components/payments/internal/connectors/plugins/public/currencycloud/accounts.go new file mode 100644 index 0000000000..f39a34a6c3 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/currencycloud/accounts.go @@ -0,0 +1,97 @@ +package currencycloud + +import ( + "context" + "encoding/json" + "time" + + "github.com/formancehq/payments/internal/models" +) + +type accountsState struct { + LastPage int `json:"lastPage"` + LastCreatedAt time.Time `json:"lastCreatedAt"` +} + +func (p Plugin) fetchNextAccounts(ctx context.Context, req models.FetchNextAccountsRequest) (models.FetchNextAccountsResponse, error) { + var oldState accountsState + if req.State != nil { + if err := json.Unmarshal(req.State, &oldState); err != nil { + return models.FetchNextAccountsResponse{}, err + } + } + + if oldState.LastPage == 0 { + oldState.LastPage = 1 + } + + newState := accountsState{ + LastPage: oldState.LastPage, + LastCreatedAt: oldState.LastCreatedAt, + } + + var accounts []models.PSPAccount + hasMore := false + page := oldState.LastPage + for { + pagedAccounts, nextPage, err := p.client.GetAccounts(ctx, page, req.PageSize) + if err != nil { + return models.FetchNextAccountsResponse{}, err + } + + if len(pagedAccounts) == 0 { + break + } + + for _, account := range pagedAccounts { + switch account.CreatedAt.Compare(oldState.LastCreatedAt) { + case -1, 0: + // Account already ingested, skip + continue + default: + } + + raw, err := json.Marshal(account) + if err != nil { + return models.FetchNextAccountsResponse{}, err + } + + accounts = append(accounts, models.PSPAccount{ + Reference: account.ID, + CreatedAt: account.CreatedAt, + Name: &account.AccountName, + Raw: raw, + }) + + newState.LastCreatedAt = account.CreatedAt + + if len(accounts) >= req.PageSize { + break + } + } + + if len(accounts) >= req.PageSize { + hasMore = true + break + } + + if nextPage == -1 { + break + } + + page = nextPage + } + + newState.LastPage = page + + payload, err := json.Marshal(newState) + if err != nil { + return models.FetchNextAccountsResponse{}, err + } + + return models.FetchNextAccountsResponse{ + Accounts: accounts, + NewState: payload, + HasMore: hasMore, + }, nil +} diff --git a/components/payments/internal/connectors/plugins/public/currencycloud/balances.go b/components/payments/internal/connectors/plugins/public/currencycloud/balances.go new file mode 100644 index 0000000000..46692bdd07 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/currencycloud/balances.go @@ -0,0 +1,50 @@ +package currencycloud + +import ( + "context" + + "github.com/formancehq/payments/internal/connectors/plugins/currency" + "github.com/formancehq/payments/internal/models" +) + +func (p Plugin) fetchNextBalances(ctx context.Context, req models.FetchNextBalancesRequest) (models.FetchNextBalancesResponse, error) { + page := 1 + balances := make([]models.PSPBalance, 0) + for { + if page < 0 { + break + } + + pagedBalances, nextPage, err := p.client.GetBalances(ctx, page, req.PageSize) + if err != nil { + return models.FetchNextBalancesResponse{}, err + } + + page = nextPage + + for _, balance := range pagedBalances { + precision, ok := supportedCurrenciesWithDecimal[balance.Currency] + if !ok { + return models.FetchNextBalancesResponse{}, nil + } + + amount, err := currency.GetAmountWithPrecisionFromString(balance.Amount.String(), precision) + if err != nil { + return models.FetchNextBalancesResponse{}, err + } + + balances = append(balances, models.PSPBalance{ + AccountReference: balance.AccountID, + CreatedAt: balance.UpdatedAt, + Amount: amount, + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, balance.Currency), + }) + } + } + + return models.FetchNextBalancesResponse{ + Balances: balances, + NewState: []byte{}, + HasMore: false, + }, nil +} diff --git a/components/payments/internal/connectors/plugins/public/currencycloud/capabilities.go b/components/payments/internal/connectors/plugins/public/currencycloud/capabilities.go new file mode 100644 index 0000000000..e45000eaab --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/currencycloud/capabilities.go @@ -0,0 +1,11 @@ +package currencycloud + +import "github.com/formancehq/payments/internal/models" + +var capabilities = []models.Capability{ + models.CAPABILITY_FETCH_ACCOUNTS, + models.CAPABILITY_FETCH_EXTERNAL_ACCOUNTS, + models.CAPABILITY_FETCH_PAYMENTS, + models.CAPABILITY_FETCH_OTHERS, + models.CAPABILITY_FETCH_BALANCES, +} diff --git a/components/payments/cmd/connectors/internal/connectors/currencycloud/client/accounts.go b/components/payments/internal/connectors/plugins/public/currencycloud/client/accounts.go similarity index 54% rename from components/payments/cmd/connectors/internal/connectors/currencycloud/client/accounts.go rename to components/payments/internal/connectors/plugins/public/currencycloud/client/accounts.go index 0d2a2ae609..fdb9cd667c 100644 --- a/components/payments/cmd/connectors/internal/connectors/currencycloud/client/accounts.go +++ b/components/payments/internal/connectors/plugins/public/currencycloud/client/accounts.go @@ -2,12 +2,11 @@ package client import ( "context" - "encoding/json" "fmt" "net/http" "time" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" + "github.com/formancehq/payments/internal/connectors/httpwrapper" ) type Account struct { @@ -17,10 +16,11 @@ type Account struct { UpdatedAt time.Time `json:"updated_at"` } -func (c *Client) GetAccounts(ctx context.Context, page int) ([]*Account, int, error) { - f := connectors.ClientMetrics(ctx, "currencycloud", "list_accounts") - now := time.Now() - defer f(ctx, now) +func (c *Client) GetAccounts(ctx context.Context, page int, pageSize int) ([]*Account, int, error) { + // TODO(polo): metrics + // f := connectors.ClientMetrics(ctx, "currencycloud", "list_accounts") + // now := time.Now() + // defer f(ctx, now) req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.buildEndpoint("v2/accounts/find"), http.NoBody) @@ -29,7 +29,7 @@ func (c *Client) GetAccounts(ctx context.Context, page int) ([]*Account, int, er } q := req.URL.Query() - q.Add("per_page", "25") + q.Add("per_page", fmt.Sprint(pageSize)) q.Add("page", fmt.Sprint(page)) q.Add("order", "updated_at") q.Add("order_asc_desc", "asc") @@ -37,16 +37,6 @@ func (c *Client) GetAccounts(ctx context.Context, page int) ([]*Account, int, er req.Header.Add("Accept", "application/json") - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, 0, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, 0, unmarshalError(resp.StatusCode, resp.Body).Error() - } - //nolint:tagliatelle // allow for client code type response struct { Accounts []*Account `json:"accounts"` @@ -55,10 +45,15 @@ func (c *Client) GetAccounts(ctx context.Context, page int) ([]*Account, int, er } `json:"pagination"` } - var res response - if err = json.NewDecoder(resp.Body).Decode(&res); err != nil { - return nil, 0, err + res := response{Accounts: make([]*Account, 0)} + var errRes currencyCloudError + _, err = c.httpClient.Do(req, &res, &errRes) + switch err { + case nil: + return res.Accounts, res.Pagination.NextPage, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, 0, errRes.Error() } - - return res.Accounts, res.Pagination.NextPage, nil + return nil, 0, fmt.Errorf("failed to get accounts: %w", err) } diff --git a/components/payments/cmd/connectors/internal/connectors/currencycloud/client/auth.go b/components/payments/internal/connectors/plugins/public/currencycloud/client/auth.go similarity index 77% rename from components/payments/cmd/connectors/internal/connectors/currencycloud/client/auth.go rename to components/payments/internal/connectors/plugins/public/currencycloud/client/auth.go index e846f67876..5dba981387 100644 --- a/components/payments/cmd/connectors/internal/connectors/currencycloud/client/auth.go +++ b/components/payments/internal/connectors/plugins/public/currencycloud/client/auth.go @@ -7,15 +7,13 @@ import ( "net/http" "net/url" "strings" - "time" - - "github.com/formancehq/payments/cmd/connectors/internal/connectors" ) -func (c *Client) authenticate(ctx context.Context) (string, error) { - f := connectors.ClientMetrics(ctx, "currencycloud", "authenticate") - now := time.Now() - defer f(ctx, now) +func (c *Client) authenticate(ctx context.Context, httpClient *http.Client) (string, error) { + // TODO(polo): metrics + // f := connectors.ClientMetrics(ctx, "currencycloud", "authenticate") + // now := time.Now() + // defer f(ctx, now) form := make(url.Values) @@ -31,7 +29,7 @@ func (c *Client) authenticate(ctx context.Context) (string, error) { req.Header.Add("Content-Type", "application/x-www-form-urlencoded") req.Header.Add("Accept", "application/json") - resp, err := c.httpClient.Do(req) + resp, err := httpClient.Do(req) if err != nil { return "", fmt.Errorf("failed to do get request: %w", err) } diff --git a/components/payments/cmd/connectors/internal/connectors/currencycloud/client/balances.go b/components/payments/internal/connectors/plugins/public/currencycloud/client/balances.go similarity index 57% rename from components/payments/cmd/connectors/internal/connectors/currencycloud/client/balances.go rename to components/payments/internal/connectors/plugins/public/currencycloud/client/balances.go index fc0527519a..b2545108e5 100644 --- a/components/payments/cmd/connectors/internal/connectors/currencycloud/client/balances.go +++ b/components/payments/internal/connectors/plugins/public/currencycloud/client/balances.go @@ -7,7 +7,7 @@ import ( "net/http" "time" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" + "github.com/formancehq/payments/internal/connectors/httpwrapper" ) type Balance struct { @@ -19,10 +19,11 @@ type Balance struct { UpdatedAt time.Time `json:"updated_at"` } -func (c *Client) GetBalances(ctx context.Context, page int) ([]*Balance, int, error) { - f := connectors.ClientMetrics(ctx, "currencycloud", "list_balances") - now := time.Now() - defer f(ctx, now) +func (c *Client) GetBalances(ctx context.Context, page int, pageSize int) ([]*Balance, int, error) { + // TODO(polo): metrics + // f := connectors.ClientMetrics(ctx, "currencycloud", "list_balances") + // now := time.Now() + // defer f(ctx, now) req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.buildEndpoint("v2/balances/find"), http.NoBody) @@ -31,24 +32,14 @@ func (c *Client) GetBalances(ctx context.Context, page int) ([]*Balance, int, er } q := req.URL.Query() + q.Add("per_page", fmt.Sprint(pageSize)) q.Add("page", fmt.Sprint(page)) - q.Add("per_page", "25") q.Add("order", "created_at") q.Add("order_asc_desc", "asc") req.URL.RawQuery = q.Encode() req.Header.Add("Accept", "application/json") - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, 0, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, 0, unmarshalError(resp.StatusCode, resp.Body).Error() - } - //nolint:tagliatelle // allow for client code type response struct { Balances []*Balance `json:"balances"` @@ -57,10 +48,15 @@ func (c *Client) GetBalances(ctx context.Context, page int) ([]*Balance, int, er } `json:"pagination"` } - var res response - if err = json.NewDecoder(resp.Body).Decode(&res); err != nil { - return nil, 0, err + res := response{Balances: make([]*Balance, 0)} + var errRes currencyCloudError + _, err = c.httpClient.Do(req, &res, nil) + switch err { + case nil: + return res.Balances, res.Pagination.NextPage, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, 0, errRes.Error() } - - return res.Balances, res.Pagination.NextPage, nil + return nil, 0, fmt.Errorf("failed to get balances %w", err) } diff --git a/components/payments/cmd/connectors/internal/connectors/currencycloud/client/beneficiaries.go b/components/payments/internal/connectors/plugins/public/currencycloud/client/beneficiaries.go similarity index 58% rename from components/payments/cmd/connectors/internal/connectors/currencycloud/client/beneficiaries.go rename to components/payments/internal/connectors/plugins/public/currencycloud/client/beneficiaries.go index ea498f5fb9..7cb8ea3562 100644 --- a/components/payments/cmd/connectors/internal/connectors/currencycloud/client/beneficiaries.go +++ b/components/payments/internal/connectors/plugins/public/currencycloud/client/beneficiaries.go @@ -2,12 +2,11 @@ package client import ( "context" - "encoding/json" "fmt" "net/http" "time" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" + "github.com/formancehq/payments/internal/connectors/httpwrapper" ) type Beneficiary struct { @@ -19,10 +18,11 @@ type Beneficiary struct { // Contains a lot more fields that will be not used on our side for now } -func (c *Client) GetBeneficiaries(ctx context.Context, page int) ([]*Beneficiary, int, error) { - f := connectors.ClientMetrics(ctx, "currencycloud", "list_beneficiaries") - now := time.Now() - defer f(ctx, now) +func (c *Client) GetBeneficiaries(ctx context.Context, page int, pageSize int) ([]*Beneficiary, int, error) { + // TODO(polo): metrics + // f := connectors.ClientMetrics(ctx, "currencycloud", "list_beneficiaries") + // now := time.Now() + // defer f(ctx, now) req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.buildEndpoint("v2/beneficiaries/find"), http.NoBody) @@ -32,23 +32,13 @@ func (c *Client) GetBeneficiaries(ctx context.Context, page int) ([]*Beneficiary q := req.URL.Query() q.Add("page", fmt.Sprint(page)) - q.Add("per_page", "25") + q.Add("per_page", fmt.Sprint(pageSize)) q.Add("order", "created_at") q.Add("order_asc_desc", "asc") req.URL.RawQuery = q.Encode() req.Header.Add("Accept", "application/json") - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, 0, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, 0, unmarshalError(resp.StatusCode, resp.Body).Error() - } - //nolint:tagliatelle // allow for client code type response struct { Beneficiaries []*Beneficiary `json:"beneficiaries"` @@ -57,10 +47,15 @@ func (c *Client) GetBeneficiaries(ctx context.Context, page int) ([]*Beneficiary } `json:"pagination"` } - var res response - if err = json.NewDecoder(resp.Body).Decode(&res); err != nil { - return nil, 0, err + res := response{Beneficiaries: make([]*Beneficiary, 0)} + var errRes currencyCloudError + _, err = c.httpClient.Do(req, &res, nil) + switch err { + case nil: + return res.Beneficiaries, res.Pagination.NextPage, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, 0, errRes.Error() } - - return res.Beneficiaries, res.Pagination.NextPage, nil + return nil, 0, fmt.Errorf("failed to get beneficiaries %w", err) } diff --git a/components/payments/cmd/connectors/internal/connectors/currencycloud/client/client.go b/components/payments/internal/connectors/plugins/public/currencycloud/client/client.go similarity index 59% rename from components/payments/cmd/connectors/internal/connectors/currencycloud/client/client.go rename to components/payments/internal/connectors/plugins/public/currencycloud/client/client.go index 4b11e34032..5aac60ad68 100644 --- a/components/payments/cmd/connectors/internal/connectors/currencycloud/client/client.go +++ b/components/payments/internal/connectors/plugins/public/currencycloud/client/client.go @@ -6,6 +6,7 @@ import ( "net/http" "time" + "github.com/formancehq/payments/internal/connectors/httpwrapper" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) @@ -21,7 +22,7 @@ func (t *apiTransport) RoundTrip(req *http.Request) (*http.Response, error) { } type Client struct { - httpClient *http.Client + httpClient httpwrapper.Client endpoint string loginID string apiKey string @@ -33,42 +34,45 @@ func (c *Client) buildEndpoint(path string, args ...interface{}) string { const DevAPIEndpoint = "https://devapi.currencycloud.com" -func newAuthenticatedHTTPClient(authToken string) *http.Client { - return &http.Client{ - Transport: &apiTransport{ - authToken: authToken, - underlying: otelhttp.NewTransport(http.DefaultTransport), - }, - } -} - func newHTTPClient() *http.Client { return &http.Client{ Transport: otelhttp.NewTransport(http.DefaultTransport), } } -// NewClient creates a new client for the CurrencyCloud API. -func NewClient(loginID, apiKey, endpoint string) (*Client, error) { +// New creates a new client for the CurrencyCloud API. +func New(ctx context.Context, loginID, apiKey, endpoint string) (*Client, error) { if endpoint == "" { endpoint = DevAPIEndpoint } + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + c := &Client{ - httpClient: newHTTPClient(), - endpoint: endpoint, - loginID: loginID, - apiKey: apiKey, + endpoint: endpoint, + loginID: loginID, + apiKey: apiKey, } - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - authToken, err := c.authenticate(ctx) + // Tokens expire after 30 minutes of inactivity which should not be the case + // for us since we're polling the API frequently. + // TODO(polo): add refreh + authToken, err := c.authenticate(ctx, newHTTPClient()) if err != nil { return nil, err } - c.httpClient = newAuthenticatedHTTPClient(authToken) - + config := &httpwrapper.Config{ + Transport: &apiTransport{ + authToken: authToken, + underlying: otelhttp.NewTransport(http.DefaultTransport), + }, + } + httpClient, err := httpwrapper.NewClient(config) + if err != nil { + return nil, err + } + c.httpClient = httpClient return c, nil } diff --git a/components/payments/internal/connectors/plugins/public/currencycloud/client/contacts.go b/components/payments/internal/connectors/plugins/public/currencycloud/client/contacts.go new file mode 100644 index 0000000000..bd295c6671 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/currencycloud/client/contacts.go @@ -0,0 +1,50 @@ +package client + +import ( + "context" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/formancehq/payments/internal/connectors/httpwrapper" +) + +type Contact struct { + ID string `json:"id"` +} + +func (c *Client) GetContactID(ctx context.Context, accountID string) (*Contact, error) { + // TODO(polo): add metrics + // f := connectors.ClientMetrics(ctx, "currencycloud", "list_contacts") + // now := time.Now() + // defer f(ctx, now) + + form := url.Values{} + form.Set("account_id", accountID) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, + c.buildEndpoint("v2/contacts/find"), strings.NewReader(form.Encode())) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + type Contacts struct { + Contacts []*Contact `json:"contacts"` + } + + res := Contacts{Contacts: make([]*Contact, 0)} + var errRes currencyCloudError + _, err = c.httpClient.Do(req, &res, nil) + switch err { + case nil: + if len(res.Contacts) == 0 { + return nil, fmt.Errorf("no contact found for account %s", accountID) + } + return res.Contacts[0], nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, errRes.Error() + } + return nil, fmt.Errorf("failed to get contacts %w", err) +} diff --git a/components/payments/cmd/connectors/internal/connectors/currencycloud/client/error.go b/components/payments/internal/connectors/plugins/public/currencycloud/client/error.go similarity index 100% rename from components/payments/cmd/connectors/internal/connectors/currencycloud/client/error.go rename to components/payments/internal/connectors/plugins/public/currencycloud/client/error.go diff --git a/components/payments/cmd/connectors/internal/connectors/currencycloud/client/transactions.go b/components/payments/internal/connectors/plugins/public/currencycloud/client/transactions.go similarity index 66% rename from components/payments/cmd/connectors/internal/connectors/currencycloud/client/transactions.go rename to components/payments/internal/connectors/plugins/public/currencycloud/client/transactions.go index 6b86b23d2c..d45425c5b3 100644 --- a/components/payments/cmd/connectors/internal/connectors/currencycloud/client/transactions.go +++ b/components/payments/internal/connectors/plugins/public/currencycloud/client/transactions.go @@ -7,7 +7,7 @@ import ( "net/http" "time" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" + "github.com/formancehq/payments/internal/connectors/httpwrapper" ) //nolint:tagliatelle // allow different styled tags in client @@ -24,14 +24,15 @@ type Transaction struct { Amount json.Number `json:"amount"` } -func (c *Client) GetTransactions(ctx context.Context, page int, updatedAtFrom time.Time) ([]Transaction, int, error) { +func (c *Client) GetTransactions(ctx context.Context, page int, pageSize int, updatedAtFrom time.Time) ([]Transaction, int, error) { if page < 1 { return nil, 0, fmt.Errorf("page must be greater than 0") } - f := connectors.ClientMetrics(ctx, "currencycloud", "list_transactions") - now := time.Now() - defer f(ctx, now) + // TODO(polo): metrics + // f := connectors.ClientMetrics(ctx, "currencycloud", "list_transactions") + // now := time.Now() + // defer f(ctx, now) req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.buildEndpoint("v2/transactions/find"), http.NoBody) @@ -41,7 +42,7 @@ func (c *Client) GetTransactions(ctx context.Context, page int, updatedAtFrom ti q := req.URL.Query() q.Add("page", fmt.Sprint(page)) - q.Add("per_page", "25") + q.Add("per_page", fmt.Sprint(pageSize)) if !updatedAtFrom.IsZero() { q.Add("updated_at_from", updatedAtFrom.Format(time.DateOnly)) } @@ -51,17 +52,6 @@ func (c *Client) GetTransactions(ctx context.Context, page int, updatedAtFrom ti req.Header.Add("Accept", "application/json") - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, 0, err - } - - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, 0, unmarshalError(resp.StatusCode, resp.Body).Error() - } - //nolint:tagliatelle // allow for client code type response struct { Transactions []Transaction `json:"transactions"` @@ -70,10 +60,15 @@ func (c *Client) GetTransactions(ctx context.Context, page int, updatedAtFrom ti } `json:"pagination"` } - var res response - if err = json.NewDecoder(resp.Body).Decode(&res); err != nil { - return nil, 0, err + res := response{Transactions: make([]Transaction, 0)} + var errRes currencyCloudError + _, err = c.httpClient.Do(req, &res, nil) + switch err { + case nil: + return res.Transactions, res.Pagination.NextPage, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, 0, errRes.Error() } - - return res.Transactions, res.Pagination.NextPage, nil + return nil, 0, fmt.Errorf("failed to get transactions %w", err) } diff --git a/components/payments/internal/connectors/plugins/public/currencycloud/cmd/main.go b/components/payments/internal/connectors/plugins/public/currencycloud/cmd/main.go new file mode 100644 index 0000000000..ecabbf844f --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/currencycloud/cmd/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "github.com/formancehq/payments/internal/connectors/grpc" + "github.com/formancehq/payments/internal/connectors/plugins" + "github.com/formancehq/payments/internal/connectors/plugins/public/currencycloud" + "github.com/hashicorp/go-plugin" +) + +func main() { + // TODO(polo): metrics + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: grpc.Handshake, + Plugins: map[string]plugin.Plugin{ + "psp": &grpc.PSPGRPCPlugin{Impl: plugins.NewGRPCImplem(¤cycloud.Plugin{})}, + }, + + // A non-nil value here enables gRPC serving for this plugin... + GRPCServer: plugin.DefaultGRPCServer, + }) +} diff --git a/components/payments/internal/connectors/plugins/public/currencycloud/config.go b/components/payments/internal/connectors/plugins/public/currencycloud/config.go new file mode 100644 index 0000000000..7b5c0f86fe --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/currencycloud/config.go @@ -0,0 +1,39 @@ +package currencycloud + +import ( + "encoding/json" + + "github.com/formancehq/payments/internal/models" + "github.com/pkg/errors" +) + +type Config struct { + LoginID string `json:"loginID"` + APIKey string `json:"apiKey"` + Endpoint string `json:"endpoint"` +} + +func (c Config) validate() error { + if c.LoginID == "" { + return errors.Wrap(models.ErrInvalidConfig, "missing clientID in config") + } + + if c.APIKey == "" { + return errors.Wrap(models.ErrInvalidConfig, "missing api key in config") + } + + if c.Endpoint == "" { + return errors.Wrap(models.ErrInvalidConfig, "missing endpoint in config") + } + + return nil +} + +func unmarshalAndValidateConfig(payload json.RawMessage) (Config, error) { + var config Config + if err := json.Unmarshal(payload, &config); err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + return config, config.validate() +} diff --git a/components/payments/internal/connectors/plugins/public/currencycloud/config.json b/components/payments/internal/connectors/plugins/public/currencycloud/config.json new file mode 100644 index 0000000000..ba46c4ee57 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/currencycloud/config.json @@ -0,0 +1,17 @@ +{ + "loginID": { + "dataType": "string", + "required": true, + "defaultValue": "" + }, + "apiKey": { + "dataType": "string", + "required": true, + "defaultValue": "" + }, + "endpoint": { + "dataType": "string", + "required": true, + "defaultValue": "" + } +} \ No newline at end of file diff --git a/components/payments/cmd/connectors/internal/connectors/currencycloud/currencies.go b/components/payments/internal/connectors/plugins/public/currencycloud/currencies.go similarity index 96% rename from components/payments/cmd/connectors/internal/connectors/currencycloud/currencies.go rename to components/payments/internal/connectors/plugins/public/currencycloud/currencies.go index dd05ae31d3..0af408f833 100644 --- a/components/payments/cmd/connectors/internal/connectors/currencycloud/currencies.go +++ b/components/payments/internal/connectors/plugins/public/currencycloud/currencies.go @@ -1,6 +1,6 @@ package currencycloud -import "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" +import "github.com/formancehq/payments/internal/connectors/plugins/currency" var ( // c.f.: https://support.currencycloud.com/hc/en-gb/articles/7840216562972-Currency-Decimal-Places diff --git a/components/payments/internal/connectors/plugins/public/currencycloud/external_accounts.go b/components/payments/internal/connectors/plugins/public/currencycloud/external_accounts.go new file mode 100644 index 0000000000..afc932fd84 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/currencycloud/external_accounts.go @@ -0,0 +1,98 @@ +package currencycloud + +import ( + "context" + "encoding/json" + "time" + + "github.com/formancehq/payments/internal/models" +) + +type externalAccountsState struct { + LastPage int `json:"lastPage"` + LastCreatedAt time.Time `json:"lastCreatedAt"` +} + +func (p Plugin) fetchNextExternalAccounts(ctx context.Context, req models.FetchNextExternalAccountsRequest) (models.FetchNextExternalAccountsResponse, error) { + var oldState externalAccountsState + if req.State != nil { + if err := json.Unmarshal(req.State, &oldState); err != nil { + return models.FetchNextExternalAccountsResponse{}, err + } + } + + if oldState.LastPage == 0 { + oldState.LastPage = 1 + } + + newState := externalAccountsState{ + LastPage: oldState.LastPage, + LastCreatedAt: oldState.LastCreatedAt, + } + + var accounts []models.PSPAccount + hasMore := false + page := oldState.LastPage + for { + pagedBeneficiarise, nextPage, err := p.client.GetBeneficiaries(ctx, page, req.PageSize) + if err != nil { + return models.FetchNextExternalAccountsResponse{}, err + } + + if len(pagedBeneficiarise) == 0 { + break + } + + for _, beneficiary := range pagedBeneficiarise { + switch beneficiary.CreatedAt.Compare(oldState.LastCreatedAt) { + case -1, 0: + // Account already ingested, skip + continue + default: + } + + raw, err := json.Marshal(beneficiary) + if err != nil { + return models.FetchNextExternalAccountsResponse{}, err + } + + accounts = append(accounts, models.PSPAccount{ + Reference: beneficiary.ID, + CreatedAt: beneficiary.CreatedAt, + Name: &beneficiary.Name, + DefaultAsset: &beneficiary.Currency, + Raw: raw, + }) + + newState.LastCreatedAt = beneficiary.CreatedAt + + if len(accounts) >= req.PageSize { + break + } + } + + if len(accounts) >= req.PageSize { + hasMore = true + break + } + + if nextPage == -1 { + break + } + + page = nextPage + } + + newState.LastPage = page + + payload, err := json.Marshal(newState) + if err != nil { + return models.FetchNextExternalAccountsResponse{}, err + } + + return models.FetchNextExternalAccountsResponse{ + ExternalAccounts: accounts, + NewState: payload, + HasMore: hasMore, + }, nil +} diff --git a/components/payments/internal/connectors/plugins/public/currencycloud/payments.go b/components/payments/internal/connectors/plugins/public/currencycloud/payments.go new file mode 100644 index 0000000000..eff521343e --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/currencycloud/payments.go @@ -0,0 +1,157 @@ +package currencycloud + +import ( + "context" + "encoding/json" + "errors" + "time" + + "github.com/formancehq/payments/internal/connectors/plugins/currency" + "github.com/formancehq/payments/internal/connectors/plugins/public/currencycloud/client" + "github.com/formancehq/payments/internal/models" +) + +type paymentsState struct { + LastUpdatedAt time.Time `json:"lastUpdatedAt"` +} + +func (p Plugin) fetchNextPayments(ctx context.Context, req models.FetchNextPaymentsRequest) (models.FetchNextPaymentsResponse, error) { + var oldState paymentsState + if req.State != nil { + if err := json.Unmarshal(req.State, &oldState); err != nil { + return models.FetchNextPaymentsResponse{}, err + } + } + + var from models.PSPAccount + if req.FromPayload == nil { + return models.FetchNextPaymentsResponse{}, errors.New("missing from payload when fetching payments") + } + if err := json.Unmarshal(req.FromPayload, &from); err != nil { + return models.FetchNextPaymentsResponse{}, err + } + + newState := paymentsState{ + LastUpdatedAt: oldState.LastUpdatedAt, + } + + var payments []models.PSPPayment + hasMore := false + page := 1 + for { + pagedTransactions, nextPage, err := p.client.GetTransactions(ctx, page, req.PageSize, newState.LastUpdatedAt) + if err != nil { + return models.FetchNextPaymentsResponse{}, err + } + + if len(pagedTransactions) == 0 { + break + } + + for _, transaction := range pagedTransactions { + switch transaction.UpdatedAt.Compare(newState.LastUpdatedAt) { + case -1, 0: + continue + default: + } + + payment, err := transactionToPayment(transaction) + if err != nil { + return models.FetchNextPaymentsResponse{}, err + } + + if payment != nil { + payments = append(payments, *payment) + } + + newState.LastUpdatedAt = transaction.UpdatedAt + + if len(payments) >= req.PageSize { + break + } + } + + if len(payments) >= req.PageSize { + hasMore = true + break + } + + if nextPage == -1 { + break + } + + page = nextPage + } + + payload, err := json.Marshal(newState) + if err != nil { + return models.FetchNextPaymentsResponse{}, err + } + + return models.FetchNextPaymentsResponse{ + Payments: payments, + NewState: payload, + HasMore: hasMore, + }, nil +} + +func transactionToPayment(transaction client.Transaction) (*models.PSPPayment, error) { + raw, err := json.Marshal(transaction) + if err != nil { + return nil, err + } + + precision, ok := supportedCurrenciesWithDecimal[transaction.Currency] + if !ok { + return nil, nil + } + + amount, err := currency.GetAmountWithPrecisionFromString(transaction.Amount.String(), precision) + if err != nil { + return nil, err + } + + paymentType := matchTransactionType(transaction.Type) + + payment := &models.PSPPayment{ + Reference: transaction.ID, + CreatedAt: transaction.CreatedAt, + Type: paymentType, + Amount: amount, + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transaction.Currency), + Scheme: models.PAYMENT_SCHEME_OTHER, + Status: matchTransactionStatus(transaction.Status), + Raw: raw, + } + + switch paymentType { + case models.PAYMENT_TYPE_PAYOUT: + payment.SourceAccountReference = &transaction.AccountID + default: + payment.DestinationAccountReference = &transaction.AccountID + } + + return payment, nil +} + +func matchTransactionType(transactionType string) models.PaymentType { + switch transactionType { + case "credit": + return models.PAYMENT_TYPE_PAYIN + case "debit": + return models.PAYMENT_TYPE_PAYOUT + } + return models.PAYMENT_TYPE_OTHER +} + +func matchTransactionStatus(transactionStatus string) models.PaymentStatus { + switch transactionStatus { + case "completed": + return models.PAYMENT_STATUS_SUCCEEDED + case "pending": + return models.PAYMENT_STATUS_PENDING + case "deleted": + return models.PAYMENT_STATUS_FAILED + } + return models.PAYMENT_STATUS_OTHER +} diff --git a/components/payments/internal/connectors/plugins/public/currencycloud/plugin.go b/components/payments/internal/connectors/plugins/public/currencycloud/plugin.go new file mode 100644 index 0000000000..b44aaaba47 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/currencycloud/plugin.go @@ -0,0 +1,81 @@ +package currencycloud + +import ( + "context" + + "github.com/formancehq/payments/internal/connectors/plugins" + "github.com/formancehq/payments/internal/connectors/plugins/public/currencycloud/client" + "github.com/formancehq/payments/internal/models" +) + +type Plugin struct { + client *client.Client +} + +func (p *Plugin) Install(ctx context.Context, req models.InstallRequest) (models.InstallResponse, error) { + config, err := unmarshalAndValidateConfig(req.Config) + if err != nil { + return models.InstallResponse{}, err + } + + client, err := client.New(ctx, config.LoginID, config.APIKey, config.Endpoint) + if err != nil { + return models.InstallResponse{}, err + } + p.client = client + + return models.InstallResponse{ + Capabilities: capabilities, + Workflow: workflow(), + }, nil +} + +func (p Plugin) Uninstall(ctx context.Context, req models.UninstallRequest) (models.UninstallResponse, error) { + return models.UninstallResponse{}, nil +} + +func (p Plugin) FetchNextAccounts(ctx context.Context, req models.FetchNextAccountsRequest) (models.FetchNextAccountsResponse, error) { + if p.client == nil { + return models.FetchNextAccountsResponse{}, plugins.ErrNotYetInstalled + } + return p.fetchNextAccounts(ctx, req) +} + +func (p Plugin) FetchNextBalances(ctx context.Context, req models.FetchNextBalancesRequest) (models.FetchNextBalancesResponse, error) { + if p.client == nil { + return models.FetchNextBalancesResponse{}, plugins.ErrNotYetInstalled + } + return p.fetchNextBalances(ctx, req) +} + +func (p Plugin) FetchNextExternalAccounts(ctx context.Context, req models.FetchNextExternalAccountsRequest) (models.FetchNextExternalAccountsResponse, error) { + if p.client == nil { + return models.FetchNextExternalAccountsResponse{}, plugins.ErrNotYetInstalled + } + return p.fetchNextExternalAccounts(ctx, req) +} + +func (p Plugin) FetchNextPayments(ctx context.Context, req models.FetchNextPaymentsRequest) (models.FetchNextPaymentsResponse, error) { + if p.client == nil { + return models.FetchNextPaymentsResponse{}, plugins.ErrNotYetInstalled + } + return p.fetchNextPayments(ctx, req) +} + +func (p Plugin) FetchNextOthers(ctx context.Context, req models.FetchNextOthersRequest) (models.FetchNextOthersResponse, error) { + return models.FetchNextOthersResponse{}, plugins.ErrNotImplemented +} + +func (p Plugin) CreateBankAccount(ctx context.Context, req models.CreateBankAccountRequest) (models.CreateBankAccountResponse, error) { + return models.CreateBankAccountResponse{}, plugins.ErrNotImplemented +} + +func (p Plugin) CreateWebhooks(ctx context.Context, req models.CreateWebhooksRequest) (models.CreateWebhooksResponse, error) { + return models.CreateWebhooksResponse{}, plugins.ErrNotImplemented +} + +func (p Plugin) TranslateWebhook(ctx context.Context, req models.TranslateWebhookRequest) (models.TranslateWebhookResponse, error) { + return models.TranslateWebhookResponse{}, plugins.ErrNotImplemented +} + +var _ models.Plugin = &Plugin{} diff --git a/components/payments/internal/connectors/plugins/public/currencycloud/workflow.go b/components/payments/internal/connectors/plugins/public/currencycloud/workflow.go new file mode 100644 index 0000000000..a4d1fcf867 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/currencycloud/workflow.go @@ -0,0 +1,34 @@ +package currencycloud + +import "github.com/formancehq/payments/internal/models" + +func workflow() models.Tasks { + return []models.TaskTree{ + { + TaskType: models.TASK_FETCH_ACCOUNTS, + Name: "fetch_accounts", + Periodically: true, + NextTasks: []models.TaskTree{ + { + TaskType: models.TASK_FETCH_PAYMENTS, + Name: "fetch_payments", + Periodically: true, + NextTasks: []models.TaskTree{}, + }, + }, + }, + { + TaskType: models.TASK_FETCH_EXTERNAL_ACCOUNTS, + Name: "fetch_beneficiaries", + Periodically: true, + NextTasks: []models.TaskTree{}, + }, + { + + TaskType: models.TASK_FETCH_BALANCES, + Name: "fetch_balances", + Periodically: true, + NextTasks: []models.TaskTree{}, + }, + } +} diff --git a/components/payments/internal/connectors/plugins/public/mangopay/accounts.go b/components/payments/internal/connectors/plugins/public/mangopay/accounts.go new file mode 100644 index 0000000000..608a6cd474 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/mangopay/accounts.go @@ -0,0 +1,115 @@ +package mangopay + +import ( + "context" + "encoding/json" + "errors" + "time" + + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/connectors/plugins/currency" + "github.com/formancehq/payments/internal/connectors/plugins/public/mangopay/client" + "github.com/formancehq/payments/internal/models" +) + +type accountsState struct { + LastPage int `json:"lastPage"` + LastCreationDate time.Time `json:"lastCreationDate"` +} + +func (p Plugin) fetchNextAccounts(ctx context.Context, req models.FetchNextAccountsRequest) (models.FetchNextAccountsResponse, error) { + var oldState accountsState + if req.State != nil { + if err := json.Unmarshal(req.State, &oldState); err != nil { + return models.FetchNextAccountsResponse{}, err + } + } else { + oldState = accountsState{ + LastPage: 1, + } + } + + var from client.User + if req.FromPayload == nil { + return models.FetchNextAccountsResponse{}, errors.New("missing from payload when fetching accounts") + } + if err := json.Unmarshal(req.FromPayload, &from); err != nil { + return models.FetchNextAccountsResponse{}, err + } + + newState := accountsState{ + LastPage: oldState.LastPage, + LastCreationDate: oldState.LastCreationDate, + } + + var accounts []models.PSPAccount + hasMore := false + page := oldState.LastPage + for { + pagedAccounts, err := p.client.GetWallets(ctx, from.ID, page, req.PageSize) + if err != nil { + return models.FetchNextAccountsResponse{}, err + } + + if len(pagedAccounts) == 0 { + break + } + + for _, account := range pagedAccounts { + accountCreationDate := time.Unix(account.CreationDate, 0) + switch accountCreationDate.Compare(oldState.LastCreationDate) { + case -1, 0: + // creationDate <= state.LastCreationDate, nothing to do, + // we already processed this account. + continue + default: + } + + raw, err := json.Marshal(account) + if err != nil { + return models.FetchNextAccountsResponse{}, err + } + + accounts = append(accounts, models.PSPAccount{ + Reference: account.ID, + CreatedAt: accountCreationDate, + Name: &account.Description, + DefaultAsset: pointer.For(currency.FormatAsset(supportedCurrenciesWithDecimal, account.Currency)), + Metadata: map[string]string{ + "user_id": from.ID, + }, + Raw: raw, + }) + + newState.LastCreationDate = accountCreationDate + + if len(accounts) >= req.PageSize { + break + } + } + + if len(pagedAccounts) < req.PageSize { + break + } + + if len(accounts) >= req.PageSize { + hasMore = true + break + } + + page++ + } + + newState.LastPage = page + + payload, err := json.Marshal(newState) + if err != nil { + return models.FetchNextAccountsResponse{}, err + } + + return models.FetchNextAccountsResponse{ + Accounts: accounts, + NewState: payload, + HasMore: hasMore, + }, nil +} diff --git a/components/payments/internal/connectors/plugins/public/mangopay/balances.go b/components/payments/internal/connectors/plugins/public/mangopay/balances.go new file mode 100644 index 0000000000..032c4c259f --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/mangopay/balances.go @@ -0,0 +1,47 @@ +package mangopay + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "math/big" + "time" + + "github.com/formancehq/payments/internal/connectors/plugins/currency" + "github.com/formancehq/payments/internal/models" +) + +func (p Plugin) fetchNextBalances(ctx context.Context, req models.FetchNextBalancesRequest) (models.FetchNextBalancesResponse, error) { + var from models.PSPAccount + if req.FromPayload == nil { + return models.FetchNextBalancesResponse{}, errors.New("missing from payload when fetching payments") + } + if err := json.Unmarshal(req.FromPayload, &from); err != nil { + return models.FetchNextBalancesResponse{}, err + } + + wallet, err := p.client.GetWallet(ctx, from.Reference) + if err != nil { + return models.FetchNextBalancesResponse{}, err + } + + var amount big.Int + _, ok := amount.SetString(wallet.Balance.Amount.String(), 10) + if !ok { + return models.FetchNextBalancesResponse{}, fmt.Errorf("failed to parse amount: %s", wallet.Balance.Amount.String()) + } + + balance := models.PSPBalance{ + AccountReference: from.Reference, + CreatedAt: time.Now().UTC(), + Amount: &amount, + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, wallet.Balance.Currency), + } + + return models.FetchNextBalancesResponse{ + Balances: []models.PSPBalance{balance}, + NewState: []byte{}, + HasMore: false, + }, nil +} diff --git a/components/payments/internal/connectors/plugins/public/mangopay/bank_account_creation.go b/components/payments/internal/connectors/plugins/public/mangopay/bank_account_creation.go new file mode 100644 index 0000000000..06211ac223 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/mangopay/bank_account_creation.go @@ -0,0 +1,167 @@ +package mangopay + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/connectors/plugins/public/mangopay/client" + "github.com/formancehq/payments/internal/models" +) + +func (p Plugin) createBankAccount(ctx context.Context, req models.CreateBankAccountRequest) (models.CreateBankAccountResponse, error) { + userID := models.ExtractNamespacedMetadata(req.BankAccount.Metadata, client.MangopayUserIDMetadataKey) + if userID == "" { + return models.CreateBankAccountResponse{}, fmt.Errorf("missing userID in bank account metadata") + } + + ownerAddress := client.OwnerAddress{ + AddressLine1: models.ExtractNamespacedMetadata(req.BankAccount.Metadata, models.BankAccountOwnerAddressLine1MetadataKey), + AddressLine2: models.ExtractNamespacedMetadata(req.BankAccount.Metadata, models.BankAccountOwnerAddressLine2MetadataKey), + City: models.ExtractNamespacedMetadata(req.BankAccount.Metadata, models.BankAccountOwnerCityMetadataKey), + Region: models.ExtractNamespacedMetadata(req.BankAccount.Metadata, models.BankAccountOwnerRegionMetadataKey), + PostalCode: models.ExtractNamespacedMetadata(req.BankAccount.Metadata, models.BankAccountOwnerPostalCodeMetadataKey), + Country: func() string { + if req.BankAccount.Country == nil { + return "" + } + return *req.BankAccount.Country + }(), + } + + var mangopayBankAccount *client.BankAccount + if req.BankAccount.IBAN != nil { + req := &client.CreateIBANBankAccountRequest{ + OwnerName: req.BankAccount.Name, + OwnerAddress: &ownerAddress, + IBAN: *req.BankAccount.IBAN, + BIC: func() string { + if req.BankAccount.SwiftBicCode == nil { + return "" + } + return *req.BankAccount.SwiftBicCode + }(), + Tag: models.ExtractNamespacedMetadata(req.BankAccount.Metadata, client.MangopayTagMetadataKey), + } + + var err error + mangopayBankAccount, err = p.client.CreateIBANBankAccount(ctx, userID, req) + if err != nil { + return models.CreateBankAccountResponse{}, err + } + } else { + if req.BankAccount.Country == nil { + req.BankAccount.Country = pointer.For("") + } + switch *req.BankAccount.Country { + case "US": + if req.BankAccount.AccountNumber == nil { + return models.CreateBankAccountResponse{}, fmt.Errorf("missing account number in bank account metadata") + } + + req := &client.CreateUSBankAccountRequest{ + OwnerName: req.BankAccount.Name, + OwnerAddress: &ownerAddress, + AccountNumber: *req.BankAccount.AccountNumber, + ABA: models.ExtractNamespacedMetadata(req.BankAccount.Metadata, client.MangopayABAMetadataKey), + DepositAccountType: models.ExtractNamespacedMetadata(req.BankAccount.Metadata, client.MangopayDepositAccountTypeMetadataKey), + Tag: models.ExtractNamespacedMetadata(req.BankAccount.Metadata, client.MangopayTagMetadataKey), + } + + var err error + mangopayBankAccount, err = p.client.CreateUSBankAccount(ctx, userID, req) + if err != nil { + return models.CreateBankAccountResponse{}, err + } + + case "CA": + if req.BankAccount.AccountNumber == nil { + return models.CreateBankAccountResponse{}, fmt.Errorf("missing account number in bank account metadata") + } + req := &client.CreateCABankAccountRequest{ + OwnerName: req.BankAccount.Name, + OwnerAddress: &ownerAddress, + AccountNumber: *req.BankAccount.AccountNumber, + InstitutionNumber: models.ExtractNamespacedMetadata(req.BankAccount.Metadata, client.MangopayInstitutionNumberMetadataKey), + BranchCode: models.ExtractNamespacedMetadata(req.BankAccount.Metadata, client.MangopayBranchCodeMetadataKey), + BankName: models.ExtractNamespacedMetadata(req.BankAccount.Metadata, client.MangopayBankNameMetadataKey), + Tag: models.ExtractNamespacedMetadata(req.BankAccount.Metadata, client.MangopayTagMetadataKey), + } + + var err error + mangopayBankAccount, err = p.client.CreateCABankAccount(ctx, userID, req) + if err != nil { + return models.CreateBankAccountResponse{}, err + } + + case "GB": + if req.BankAccount.AccountNumber == nil { + return models.CreateBankAccountResponse{}, fmt.Errorf("missing account number in bank account metadata") + } + + req := &client.CreateGBBankAccountRequest{ + OwnerName: req.BankAccount.Name, + OwnerAddress: &ownerAddress, + AccountNumber: *req.BankAccount.AccountNumber, + SortCode: models.ExtractNamespacedMetadata(req.BankAccount.Metadata, client.MangopaySortCodeMetadataKey), + Tag: models.ExtractNamespacedMetadata(req.BankAccount.Metadata, client.MangopayTagMetadataKey), + } + + var err error + mangopayBankAccount, err = p.client.CreateGBBankAccount(ctx, userID, req) + if err != nil { + return models.CreateBankAccountResponse{}, err + } + + default: + if req.BankAccount.AccountNumber == nil { + return models.CreateBankAccountResponse{}, fmt.Errorf("missing account number in bank account metadata") + } + + req := &client.CreateOtherBankAccountRequest{ + OwnerName: req.BankAccount.Name, + OwnerAddress: &ownerAddress, + AccountNumber: *req.BankAccount.AccountNumber, + BIC: func() string { + if req.BankAccount.SwiftBicCode == nil { + return "" + } + return *req.BankAccount.SwiftBicCode + }(), + Country: *req.BankAccount.Country, + Tag: models.ExtractNamespacedMetadata(req.BankAccount.Metadata, client.MangopayTagMetadataKey), + } + + var err error + mangopayBankAccount, err = p.client.CreateOtherBankAccount(ctx, userID, req) + if err != nil { + return models.CreateBankAccountResponse{}, err + } + } + } + + var account models.PSPAccount + if mangopayBankAccount != nil { + raw, err := json.Marshal(mangopayBankAccount) + if err != nil { + return models.CreateBankAccountResponse{}, err + } + + account = models.PSPAccount{ + Reference: mangopayBankAccount.ID, + CreatedAt: time.Unix(mangopayBankAccount.CreationDate, 0), + Name: &mangopayBankAccount.OwnerName, + Metadata: map[string]string{ + "user_id": userID, + }, + Raw: raw, + } + + } + + return models.CreateBankAccountResponse{ + RelatedAccount: account, + }, nil +} diff --git a/components/payments/internal/connectors/plugins/public/mangopay/capabilities.go b/components/payments/internal/connectors/plugins/public/mangopay/capabilities.go new file mode 100644 index 0000000000..22ac316f8b --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/mangopay/capabilities.go @@ -0,0 +1,10 @@ +package mangopay + +import "github.com/formancehq/payments/internal/models" + +var capabilities = []models.Capability{ + models.CAPABILITY_FETCH_ACCOUNTS, + models.CAPABILITY_FETCH_EXTERNAL_ACCOUNTS, + models.CAPABILITY_FETCH_PAYMENTS, + models.CAPABILITY_FETCH_OTHERS, +} diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/client/bank_accounts.go b/components/payments/internal/connectors/plugins/public/mangopay/client/bank_accounts.go similarity index 72% rename from components/payments/cmd/connectors/internal/connectors/mangopay/client/bank_accounts.go rename to components/payments/internal/connectors/plugins/public/mangopay/client/bank_accounts.go index 78e5eb262e..ba4adf2860 100644 --- a/components/payments/cmd/connectors/internal/connectors/mangopay/client/bank_accounts.go +++ b/components/payments/internal/connectors/plugins/public/mangopay/client/bank_accounts.go @@ -7,9 +7,8 @@ import ( "fmt" "net/http" "strconv" - "time" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" + "github.com/formancehq/payments/internal/connectors/httpwrapper" ) type OwnerAddress struct { @@ -33,9 +32,10 @@ type CreateIBANBankAccountRequest struct { } func (c *Client) CreateIBANBankAccount(ctx context.Context, userID string, req *CreateIBANBankAccountRequest) (*BankAccount, error) { - f := connectors.ClientMetrics(ctx, "mangopay", "create_iban_bank_account") - now := time.Now() - defer f(ctx, now) + // TODO(polo): add metrics + // f := connectors.ClientMetrics(ctx, "mangopay", "create_iban_bank_account") + // now := time.Now() + // defer f(ctx, now) endpoint := fmt.Sprintf("%s/v2.01/%s/users/%s/bankaccounts/iban", c.endpoint, c.clientID, userID) return c.createBankAccount(ctx, endpoint, req) @@ -51,9 +51,10 @@ type CreateUSBankAccountRequest struct { } func (c *Client) CreateUSBankAccount(ctx context.Context, userID string, req *CreateUSBankAccountRequest) (*BankAccount, error) { - f := connectors.ClientMetrics(ctx, "mangopay", "create_us_bank_account") - now := time.Now() - defer f(ctx, now) + // TODO(polo): add metrics + // f := connectors.ClientMetrics(ctx, "mangopay", "create_us_bank_account") + // now := time.Now() + // defer f(ctx, now) endpoint := fmt.Sprintf("%s/v2.01/%s/users/%s/bankaccounts/us", c.endpoint, c.clientID, userID) return c.createBankAccount(ctx, endpoint, req) @@ -70,9 +71,10 @@ type CreateCABankAccountRequest struct { } func (c *Client) CreateCABankAccount(ctx context.Context, userID string, req *CreateCABankAccountRequest) (*BankAccount, error) { - f := connectors.ClientMetrics(ctx, "mangopay", "create_ca_bank_account") - now := time.Now() - defer f(ctx, now) + // TODO(polo): add metrics + // f := connectors.ClientMetrics(ctx, "mangopay", "create_ca_bank_account") + // now := time.Now() + // defer f(ctx, now) endpoint := fmt.Sprintf("%s/v2.01/%s/users/%s/bankaccounts/ca", c.endpoint, c.clientID, userID) return c.createBankAccount(ctx, endpoint, req) @@ -87,9 +89,10 @@ type CreateGBBankAccountRequest struct { } func (c *Client) CreateGBBankAccount(ctx context.Context, userID string, req *CreateGBBankAccountRequest) (*BankAccount, error) { - f := connectors.ClientMetrics(ctx, "mangopay", "create_gb_bank_account") - now := time.Now() - defer f(ctx, now) + // TODO(polo): add metrics + // f := connectors.ClientMetrics(ctx, "mangopay", "create_gb_bank_account") + // now := time.Now() + // defer f(ctx, now) endpoint := fmt.Sprintf("%s/v2.01/%s/users/%s/bankaccounts/gb", c.endpoint, c.clientID, userID) return c.createBankAccount(ctx, endpoint, req) @@ -105,9 +108,10 @@ type CreateOtherBankAccountRequest struct { } func (c *Client) CreateOtherBankAccount(ctx context.Context, userID string, req *CreateOtherBankAccountRequest) (*BankAccount, error) { - f := connectors.ClientMetrics(ctx, "mangopay", "create_other_bank_account") - now := time.Now() - defer f(ctx, now) + // TODO(polo): add metrics + // f := connectors.ClientMetrics(ctx, "mangopay", "create_other_bank_account") + // now := time.Now() + // defer f(ctx, now) endpoint := fmt.Sprintf("%s/v2.01/%s/users/%s/bankaccounts/other", c.endpoint, c.clientID, userID) return c.createBankAccount(ctx, endpoint, req) @@ -125,29 +129,17 @@ func (c *Client) createBankAccount(ctx context.Context, endpoint string, req any } httpReq.Header.Set("Content-Type", "application/json") - resp, err := c.httpClient.Do(httpReq) - if err != nil { - return nil, fmt.Errorf("failed to create bank account: %w", err) - } - - defer func() { - err = resp.Body.Close() - if err != nil { - c.logger.Error(err) - } - }() - - if resp.StatusCode != http.StatusOK { - // Never retry bank account creation - return nil, unmarshalErrorWithoutRetry(resp.StatusCode, resp.Body).Error() - } - var bankAccount BankAccount - if err := json.NewDecoder(resp.Body).Decode(&bankAccount); err != nil { - return nil, fmt.Errorf("failed to unmarshal bank account response body: %w", err) + _, err = c.httpClient.Do(httpReq, &bankAccount, nil) + switch err { + case nil: + return &bankAccount, nil + case httpwrapper.ErrStatusCodeUnexpected: + // Never retry bank account creation + // TODO(polo): retry ? + return nil, err } - - return &bankAccount, nil + return nil, fmt.Errorf("failed to create bank account: %w", err) } type BankAccount struct { @@ -156,10 +148,11 @@ type BankAccount struct { CreationDate int64 `json:"CreationDate"` } -func (c *Client) GetBankAccounts(ctx context.Context, userID string, page, pageSize int) ([]*BankAccount, error) { - f := connectors.ClientMetrics(ctx, "mangopay", "list_bank_accounts") - now := time.Now() - defer f(ctx, now) +func (c *Client) GetBankAccounts(ctx context.Context, userID string, page, pageSize int) ([]BankAccount, error) { + // TODO(polo): add metrics + // f := connectors.ClientMetrics(ctx, "mangopay", "list_bank_accounts") + // now := time.Now() + // defer f(ctx, now) endpoint := fmt.Sprintf("%s/v2.01/%s/users/%s/bankaccounts", c.endpoint, c.clientID, userID) req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody) @@ -173,26 +166,14 @@ func (c *Client) GetBankAccounts(ctx context.Context, userID string, page, pageS q.Add("Sort", "CreationDate:ASC") req.URL.RawQuery = q.Encode() - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to get wallets: %w", err) - } - - defer func() { - err = resp.Body.Close() - if err != nil { - c.logger.Error(err) - } - }() - - if resp.StatusCode != http.StatusOK { - return nil, unmarshalErrorWithRetry(resp.StatusCode, resp.Body).Error() - } - - var bankAccounts []*BankAccount - if err := json.NewDecoder(resp.Body).Decode(&bankAccounts); err != nil { - return nil, fmt.Errorf("failed to unmarshal wallets response body: %w", err) + var bankAccounts []BankAccount + _, err = c.httpClient.Do(req, &bankAccounts, nil) + switch err { + case nil: + return bankAccounts, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, err } - - return bankAccounts, nil + return nil, fmt.Errorf("failed to get bank accounts: %w", err) } diff --git a/components/payments/internal/connectors/plugins/public/mangopay/client/client.go b/components/payments/internal/connectors/plugins/public/mangopay/client/client.go new file mode 100644 index 0000000000..78caecfac4 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/mangopay/client/client.go @@ -0,0 +1,37 @@ +package client + +import ( + "strings" + + "github.com/formancehq/payments/internal/connectors/httpwrapper" + "golang.org/x/oauth2/clientcredentials" +) + +// TODO(polo): Fetch Client wallets (FEES, ...) in the future +type Client struct { + httpClient httpwrapper.Client + + clientID string + endpoint string +} + +func New(clientID, apiKey, endpoint string) (*Client, error) { + endpoint = strings.TrimSuffix(endpoint, "/") + + config := &httpwrapper.Config{ + OAuthConfig: &clientcredentials.Config{ + ClientID: clientID, + ClientSecret: apiKey, + TokenURL: endpoint + "/v2.01/oauth/token", + }, + } + httpClient, err := httpwrapper.NewClient(config) + + c := &Client{ + httpClient: httpClient, + + clientID: clientID, + endpoint: endpoint, + } + return c, err +} diff --git a/components/payments/internal/connectors/plugins/public/mangopay/client/error.go b/components/payments/internal/connectors/plugins/public/mangopay/client/error.go new file mode 100644 index 0000000000..1807ebcb7d --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/mangopay/client/error.go @@ -0,0 +1,31 @@ +package client + +import ( + "fmt" +) + +type mangopayError struct { + StatusCode int `json:"-"` + Message string `json:"Message"` + Type string `json:"Type"` + Errors map[string]string `json:"Errors"` +} + +func (me *mangopayError) Error() error { + var errorMessage string + if len(me.Errors) > 0 { + for _, message := range me.Errors { + errorMessage = message + break + } + } + + var err error + if errorMessage == "" { + err = fmt.Errorf("unexpected status code: %d", me.StatusCode) + } else { + err = fmt.Errorf("%d: %s", me.StatusCode, errorMessage) + } + + return err +} diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/client/metadata.go b/components/payments/internal/connectors/plugins/public/mangopay/client/metadata.go similarity index 100% rename from components/payments/cmd/connectors/internal/connectors/mangopay/client/metadata.go rename to components/payments/internal/connectors/plugins/public/mangopay/client/metadata.go diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/client/payin.go b/components/payments/internal/connectors/plugins/public/mangopay/client/payin.go similarity index 63% rename from components/payments/cmd/connectors/internal/connectors/mangopay/client/payin.go rename to components/payments/internal/connectors/plugins/public/mangopay/client/payin.go index 4f2209457b..0791e3359f 100644 --- a/components/payments/cmd/connectors/internal/connectors/mangopay/client/payin.go +++ b/components/payments/internal/connectors/plugins/public/mangopay/client/payin.go @@ -2,12 +2,10 @@ package client import ( "context" - "encoding/json" "fmt" "net/http" - "time" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" + "github.com/formancehq/payments/internal/connectors/httpwrapper" ) type PayinResponse struct { @@ -30,9 +28,10 @@ type PayinResponse struct { } func (c *Client) GetPayin(ctx context.Context, payinID string) (*PayinResponse, error) { - f := connectors.ClientMetrics(ctx, "mangopay", "get_payin") - now := time.Now() - defer f(ctx, now) + // TODO(polo): metrics + // f := connectors.ClientMetrics(ctx, "mangopay", "get_payin") + // now := time.Now() + // defer f(ctx, now) endpoint := fmt.Sprintf("%s/v2.01/%s/payins/%s", c.endpoint, c.clientID, payinID) @@ -41,26 +40,14 @@ func (c *Client) GetPayin(ctx context.Context, payinID string) (*PayinResponse, return nil, fmt.Errorf("failed to create get payin request: %w", err) } - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to get payin: %w", err) - } - - defer func() { - err = resp.Body.Close() - if err != nil { - c.logger.Error(err) - } - }() - - if resp.StatusCode != http.StatusOK { - return nil, unmarshalErrorWithRetry(resp.StatusCode, resp.Body).Error() - } - var payinResponse PayinResponse - if err := json.NewDecoder(resp.Body).Decode(&payinResponse); err != nil { - return nil, fmt.Errorf("failed to unmarshal payin response body: %w", err) + _, err = c.httpClient.Do(req, &payinResponse, nil) + switch err { + case nil: + return &payinResponse, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, err } - - return &payinResponse, nil + return nil, fmt.Errorf("failed to get payin response: %w", err) } diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/client/payout.go b/components/payments/internal/connectors/plugins/public/mangopay/client/payout.go similarity index 64% rename from components/payments/cmd/connectors/internal/connectors/mangopay/client/payout.go rename to components/payments/internal/connectors/plugins/public/mangopay/client/payout.go index c83a1a4675..7862fb818e 100644 --- a/components/payments/cmd/connectors/internal/connectors/mangopay/client/payout.go +++ b/components/payments/internal/connectors/plugins/public/mangopay/client/payout.go @@ -6,9 +6,8 @@ import ( "encoding/json" "fmt" "net/http" - "time" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" + "github.com/formancehq/payments/internal/connectors/httpwrapper" ) type PayoutRequest struct { @@ -44,9 +43,10 @@ type PayoutResponse struct { } func (c *Client) InitiatePayout(ctx context.Context, payoutRequest *PayoutRequest) (*PayoutResponse, error) { - f := connectors.ClientMetrics(ctx, "mangopay", "initiate_payout") - now := time.Now() - defer f(ctx, now) + // TODO(polo): add metrics + // f := connectors.ClientMetrics(ctx, "mangopay", "initiate_payout") + // now := time.Now() + // defer f(ctx, now) endpoint := fmt.Sprintf("%s/v2.01/%s/payouts/bankwire", c.endpoint, c.clientID) @@ -61,35 +61,24 @@ func (c *Client) InitiatePayout(ctx context.Context, payoutRequest *PayoutReques } req.Header.Set("Content-Type", "application/json") - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to get wallets: %w", err) - } - - defer func() { - err = resp.Body.Close() - if err != nil { - c.logger.Error(err) - } - }() - - if resp.StatusCode != http.StatusOK { - // Never retry payout initiation - return nil, unmarshalErrorWithoutRetry(resp.StatusCode, resp.Body).Error() - } - var payoutResponse PayoutResponse - if err := json.NewDecoder(resp.Body).Decode(&payoutResponse); err != nil { - return nil, fmt.Errorf("failed to unmarshal wallets response body: %w", err) + _, err = c.httpClient.Do(req, &payoutResponse, nil) + switch err { + case nil: + return &payoutResponse, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + // Never retry payout initiation + return nil, err } - - return &payoutResponse, nil + return nil, fmt.Errorf("failed to get payout response: %w", err) } func (c *Client) GetPayout(ctx context.Context, payoutID string) (*PayoutResponse, error) { - f := connectors.ClientMetrics(ctx, "mangopay", "get_payout") - now := time.Now() - defer f(ctx, now) + // TODO(polo): metrics + // f := connectors.ClientMetrics(ctx, "mangopay", "get_payout") + // now := time.Now() + // defer f(ctx, now) endpoint := fmt.Sprintf("%s/v2.01/%s/payouts/%s", c.endpoint, c.clientID, payoutID) @@ -98,26 +87,14 @@ func (c *Client) GetPayout(ctx context.Context, payoutID string) (*PayoutRespons return nil, fmt.Errorf("failed to create get payout request: %w", err) } - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to get payout: %w", err) - } - - defer func() { - err = resp.Body.Close() - if err != nil { - c.logger.Error(err) - } - }() - - if resp.StatusCode != http.StatusOK { - return nil, unmarshalErrorWithRetry(resp.StatusCode, resp.Body).Error() - } - var payoutResponse PayoutResponse - if err := json.NewDecoder(resp.Body).Decode(&payoutResponse); err != nil { - return nil, fmt.Errorf("failed to unmarshal payout response body: %w", err) + _, err = c.httpClient.Do(req, &payoutResponse, nil) + switch err { + case nil: + return &payoutResponse, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, err } - - return &payoutResponse, nil + return nil, fmt.Errorf("failed to get payout response: %w", err) } diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/client/refund.go b/components/payments/internal/connectors/plugins/public/mangopay/client/refund.go similarity index 66% rename from components/payments/cmd/connectors/internal/connectors/mangopay/client/refund.go rename to components/payments/internal/connectors/plugins/public/mangopay/client/refund.go index 293f3e3cec..b2e957cec8 100644 --- a/components/payments/cmd/connectors/internal/connectors/mangopay/client/refund.go +++ b/components/payments/internal/connectors/plugins/public/mangopay/client/refund.go @@ -2,12 +2,10 @@ package client import ( "context" - "encoding/json" "fmt" "net/http" - "time" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" + "github.com/formancehq/payments/internal/connectors/httpwrapper" ) type Refund struct { @@ -31,9 +29,10 @@ type Refund struct { } func (c *Client) GetRefund(ctx context.Context, refundID string) (*Refund, error) { - f := connectors.ClientMetrics(ctx, "mangopay", "get_refund") - now := time.Now() - defer f(ctx, now) + // TODO(polo): metrics + // f := connectors.ClientMetrics(ctx, "mangopay", "get_refund") + // now := time.Now() + // defer f(ctx, now) endpoint := fmt.Sprintf("%s/v2.01/%s/refunds/%s", c.endpoint, c.clientID, refundID) @@ -42,26 +41,14 @@ func (c *Client) GetRefund(ctx context.Context, refundID string) (*Refund, error return nil, fmt.Errorf("failed to create get refund request: %w", err) } - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to get refund: %w", err) - } - - defer func() { - err = resp.Body.Close() - if err != nil { - c.logger.Error(err) - } - }() - - if resp.StatusCode != http.StatusOK { - return nil, unmarshalErrorWithRetry(resp.StatusCode, resp.Body).Error() - } - var refund Refund - if err := json.NewDecoder(resp.Body).Decode(&refund); err != nil { - return nil, fmt.Errorf("failed to unmarshal refund response body: %w", err) + _, err = c.httpClient.Do(req, &refund, nil) + switch err { + case nil: + return &refund, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, err } - - return &refund, nil + return nil, fmt.Errorf("failed to get refund: %w", err) } diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/client/transactions.go b/components/payments/internal/connectors/plugins/public/mangopay/client/transactions.go similarity index 68% rename from components/payments/cmd/connectors/internal/connectors/mangopay/client/transactions.go rename to components/payments/internal/connectors/plugins/public/mangopay/client/transactions.go index 82fe31ec70..0ea70d8f7f 100644 --- a/components/payments/cmd/connectors/internal/connectors/mangopay/client/transactions.go +++ b/components/payments/internal/connectors/plugins/public/mangopay/client/transactions.go @@ -8,7 +8,7 @@ import ( "strconv" "time" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" + "github.com/formancehq/payments/internal/connectors/httpwrapper" ) type Payment struct { @@ -39,10 +39,11 @@ type Payment struct { DebitedWalletID string `json:"DebitedWalletId"` } -func (c *Client) GetTransactions(ctx context.Context, walletsID string, page, pageSize int, afterCreatedAt time.Time) ([]*Payment, error) { - f := connectors.ClientMetrics(ctx, "mangopay", "list_transactions") - now := time.Now() - defer f(ctx, now) +func (c *Client) GetTransactions(ctx context.Context, walletsID string, page, pageSize int, afterCreatedAt time.Time) ([]Payment, error) { + // TODO(polo): add metrics + // f := connectors.ClientMetrics(ctx, "mangopay", "list_transactions") + // now := time.Now() + // defer f(ctx, now) endpoint := fmt.Sprintf("%s/v2.01/%s/wallets/%s/transactions", c.endpoint, c.clientID, walletsID) req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody) @@ -59,26 +60,14 @@ func (c *Client) GetTransactions(ctx context.Context, walletsID string, page, pa } req.URL.RawQuery = q.Encode() - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to get transactions: %w", err) - } - - defer func() { - err = resp.Body.Close() - if err != nil { - c.logger.Error(err) - } - }() - - if resp.StatusCode != http.StatusOK { - return nil, unmarshalErrorWithRetry(resp.StatusCode, resp.Body).Error() + var payments []Payment + _, err = c.httpClient.Do(req, &payments, nil) + switch err { + case nil: + return payments, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, err } - - var payments []*Payment - if err := json.NewDecoder(resp.Body).Decode(&payments); err != nil { - return nil, fmt.Errorf("failed to unmarshal transactions response body: %w", err) - } - - return payments, nil + return nil, fmt.Errorf("failed to get transactions: %w", err) } diff --git a/components/payments/internal/connectors/plugins/public/mangopay/client/transfer.go b/components/payments/internal/connectors/plugins/public/mangopay/client/transfer.go new file mode 100644 index 0000000000..b437b9bcbf --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/mangopay/client/transfer.go @@ -0,0 +1,66 @@ +package client + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/formancehq/payments/internal/connectors/httpwrapper" +) + +type Funds struct { + Currency string `json:"Currency"` + Amount json.Number `json:"Amount"` +} + +type TransferRequest struct { + AuthorID string `json:"AuthorId"` + CreditedUserID string `json:"CreditedUserId,omitempty"` + DebitedFunds Funds `json:"DebitedFunds"` + Fees Funds `json:"Fees"` + DebitedWalletID string `json:"DebitedWalletId"` + CreditedWalletID string `json:"CreditedWalletId"` +} + +type TransferResponse struct { + ID string `json:"Id"` + CreationDate int64 `json:"CreationDate"` + AuthorID string `json:"AuthorId"` + CreditedUserID string `json:"CreditedUserId"` + DebitedFunds Funds `json:"DebitedFunds"` + Fees Funds `json:"Fees"` + CreditedFunds Funds `json:"CreditedFunds"` + Status string `json:"Status"` + ResultCode string `json:"ResultCode"` + ResultMessage string `json:"ResultMessage"` + Type string `json:"Type"` + ExecutionDate int64 `json:"ExecutionDate"` + Nature string `json:"Nature"` + DebitedWalletID string `json:"DebitedWalletId"` + CreditedWalletID string `json:"CreditedWalletId"` +} + +func (c *Client) GetWalletTransfer(ctx context.Context, transferID string) (TransferResponse, error) { + // TODO(polo): metrics + // f := connectors.ClientMetrics(ctx, "mangopay", "get_transfer") + // now := time.Now() + // defer f(ctx, now) + + endpoint := fmt.Sprintf("%s/v2.01/%s/transfers/%s", c.endpoint, c.clientID, transferID) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody) + if err != nil { + return TransferResponse{}, fmt.Errorf("failed to create login request: %w", err) + } + + var transfer TransferResponse + _, err = c.httpClient.Do(req, &transfer, nil) + switch err { + case nil: + return transfer, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return transfer, err + } + return transfer, fmt.Errorf("failed to get transfer response: %w", err) +} diff --git a/components/payments/internal/connectors/plugins/public/mangopay/client/users.go b/components/payments/internal/connectors/plugins/public/mangopay/client/users.go new file mode 100644 index 0000000000..4b7c0ebb29 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/mangopay/client/users.go @@ -0,0 +1,45 @@ +package client + +import ( + "context" + "fmt" + "net/http" + "strconv" + + "github.com/formancehq/payments/internal/connectors/httpwrapper" +) + +type User struct { + ID string `json:"Id"` + CreationDate int64 `json:"CreationDate"` +} + +func (c *Client) GetUsers(ctx context.Context, page int, pageSize int) ([]User, error) { + // TODO(polo): add metrics + // f := connectors.ClientMetrics(ctx, "mangopay", "list_users") + // now := time.Now() + // defer f(ctx, now) + + endpoint := fmt.Sprintf("%s/v2.01/%s/users", c.endpoint, c.clientID) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody) + if err != nil { + return nil, fmt.Errorf("failed to create login request: %w", err) + } + + q := req.URL.Query() + q.Add("per_page", strconv.Itoa(pageSize)) + q.Add("page", fmt.Sprint(page)) + q.Add("Sort", "CreationDate:ASC") + req.URL.RawQuery = q.Encode() + + var users []User + _, err = c.httpClient.Do(req, &users, nil) + switch err { + case nil: + return users, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, err + } + return nil, fmt.Errorf("failed to get user response: %w", err) +} diff --git a/components/payments/internal/connectors/plugins/public/mangopay/client/wallets.go b/components/payments/internal/connectors/plugins/public/mangopay/client/wallets.go new file mode 100644 index 0000000000..3033d4fb08 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/mangopay/client/wallets.go @@ -0,0 +1,79 @@ +package client + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strconv" + + "github.com/formancehq/payments/internal/connectors/httpwrapper" +) + +type Wallet struct { + ID string `json:"Id"` + Owners []string `json:"Owners"` + Description string `json:"Description"` + CreationDate int64 `json:"CreationDate"` + Currency string `json:"Currency"` + Balance struct { + Currency string `json:"Currency"` + Amount json.Number `json:"Amount"` + } `json:"Balance"` +} + +func (c *Client) GetWallets(ctx context.Context, userID string, page, pageSize int) ([]Wallet, error) { + // TODO(polo): add metrics + // f := connectors.ClientMetrics(ctx, "mangopay", "list_wallets") + // now := time.Now() + // defer f(ctx, now) + + endpoint := fmt.Sprintf("%s/v2.01/%s/users/%s/wallets", c.endpoint, c.clientID, userID) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody) + if err != nil { + return nil, fmt.Errorf("failed to create login request: %w", err) + } + + q := req.URL.Query() + q.Add("per_page", strconv.Itoa(pageSize)) + q.Add("page", fmt.Sprint(page)) + q.Add("Sort", "CreationDate:ASC") + req.URL.RawQuery = q.Encode() + + var wallets []Wallet + var errRes mangopayError + _, err = c.httpClient.Do(req, &wallets, errRes) + switch err { + case nil: + return wallets, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, errRes.Error() + } + return nil, fmt.Errorf("failed to get wallets %w", err) +} + +func (c *Client) GetWallet(ctx context.Context, walletID string) (*Wallet, error) { + // TODO(polo): metrics + // f := connectors.ClientMetrics(ctx, "mangopay", "get_wallets") + // now := time.Now() + // defer f(ctx, now) + + endpoint := fmt.Sprintf("%s/v2.01/%s/wallets/%s", c.endpoint, c.clientID, walletID) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody) + if err != nil { + return nil, fmt.Errorf("failed to create wallet request: %w", err) + } + + var wallet Wallet + var errRes mangopayError + _, err = c.httpClient.Do(req, &wallet, errRes) + switch err { + case nil: + return &wallet, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, errRes.Error() + } + return nil, fmt.Errorf("failed to get wallet %w", err) +} diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/client/webhooks.go b/components/payments/internal/connectors/plugins/public/mangopay/client/webhooks.go similarity index 73% rename from components/payments/cmd/connectors/internal/connectors/mangopay/client/webhooks.go rename to components/payments/internal/connectors/plugins/public/mangopay/client/webhooks.go index dc89a279f4..2bbdbcd98f 100644 --- a/components/payments/cmd/connectors/internal/connectors/mangopay/client/webhooks.go +++ b/components/payments/internal/connectors/plugins/public/mangopay/client/webhooks.go @@ -6,9 +6,8 @@ import ( "encoding/json" "fmt" "net/http" - "time" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" + "github.com/formancehq/payments/internal/connectors/httpwrapper" ) type EventType string @@ -70,18 +69,10 @@ var ( type Webhook struct { ResourceID string `json:"ResourceId"` + Date int64 `json:"Date"` EventType EventType `json:"EventType"` } -func (c *Client) UnmarshalWebhooks(req string) (*Webhook, error) { - res := Webhook{} - err := json.Unmarshal([]byte(req), &res) - if err != nil { - return nil, err - } - return &res, nil -} - type Hook struct { ID string `json:"Id"` URL string `json:"Url"` @@ -91,9 +82,10 @@ type Hook struct { } func (c *Client) ListAllHooks(ctx context.Context) ([]*Hook, error) { - f := connectors.ClientMetrics(ctx, "mangopay", "list_hooks") - now := time.Now() - defer f(ctx, now) + // TODO(polo): metrics + // f := connectors.ClientMetrics(ctx, "mangopay", "list_hooks") + // now := time.Now() + // defer f(ctx, now) endpoint := fmt.Sprintf("%s/v2.01/%s/hooks", c.endpoint, c.clientID) req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody) @@ -106,28 +98,17 @@ func (c *Client) ListAllHooks(ctx context.Context) ([]*Hook, error) { q.Add("Sort", "CreationDate:ASC") req.URL.RawQuery = q.Encode() - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to get wallet: %w", err) - } - - defer func() { - err = resp.Body.Close() - if err != nil { - c.logger.Error(err) - } - }() - - if resp.StatusCode != http.StatusOK { - return nil, unmarshalErrorWithoutRetry(resp.StatusCode, resp.Body).Error() - } - var hooks []*Hook - if err := json.NewDecoder(resp.Body).Decode(&hooks); err != nil { - return nil, fmt.Errorf("failed to unmarshal hooks response body: %w", err) + var errRes mangopayError + _, err = c.httpClient.Do(req, &hooks, errRes) + switch err { + case nil: + return hooks, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, errRes.Error() } - - return hooks, nil + return nil, fmt.Errorf("failed to list hooks %w", err) } type CreateHookRequest struct { @@ -136,9 +117,10 @@ type CreateHookRequest struct { } func (c *Client) CreateHook(ctx context.Context, eventType EventType, URL string) error { - f := connectors.ClientMetrics(ctx, "mangopay", "create_hook") - now := time.Now() - defer f(ctx, now) + // TODO(polo): add metrics + // f := connectors.ClientMetrics(ctx, "mangopay", "create_hook") + // now := time.Now() + // defer f(ctx, now) body, err := json.Marshal(&CreateHookRequest{ EventType: eventType, @@ -155,22 +137,11 @@ func (c *Client) CreateHook(ctx context.Context, eventType EventType, URL string } req.Header.Set("Content-Type", "application/json") - resp, err := c.httpClient.Do(req) + var errRes mangopayError + _, err = c.httpClient.Do(req, nil, &errRes) if err != nil { - return fmt.Errorf("failed to create hook: %w", err) + return errRes.Error() } - - defer func() { - err = resp.Body.Close() - if err != nil { - c.logger.Error(err) - } - }() - - if resp.StatusCode != http.StatusOK { - return unmarshalErrorWithoutRetry(resp.StatusCode, resp.Body).Error() - } - return nil } @@ -180,9 +151,10 @@ type UpdateHookRequest struct { } func (c *Client) UpdateHook(ctx context.Context, hookID string, URL string) error { - f := connectors.ClientMetrics(ctx, "mangopay", "udpate_hook") - now := time.Now() - defer f(ctx, now) + // TODO(polo): add metrics + // f := connectors.ClientMetrics(ctx, "mangopay", "udpate_hook") + // now := time.Now() + // defer f(ctx, now) body, err := json.Marshal(&UpdateHookRequest{ URL: URL, @@ -199,21 +171,10 @@ func (c *Client) UpdateHook(ctx context.Context, hookID string, URL string) erro } req.Header.Set("Content-Type", "application/json") - resp, err := c.httpClient.Do(req) + var errRes mangopayError + _, err = c.httpClient.Do(req, nil, &errRes) if err != nil { - return fmt.Errorf("failed to update hook: %w", err) + return errRes.Error() } - - defer func() { - err = resp.Body.Close() - if err != nil { - c.logger.Error(err) - } - }() - - if resp.StatusCode != http.StatusOK { - return unmarshalErrorWithoutRetry(resp.StatusCode, resp.Body).Error() - } - return nil } diff --git a/components/payments/internal/connectors/plugins/public/mangopay/cmd/main.go b/components/payments/internal/connectors/plugins/public/mangopay/cmd/main.go new file mode 100644 index 0000000000..4f91e1535b --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/mangopay/cmd/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "github.com/formancehq/payments/internal/connectors/grpc" + "github.com/formancehq/payments/internal/connectors/plugins" + "github.com/formancehq/payments/internal/connectors/plugins/public/mangopay" + "github.com/hashicorp/go-plugin" +) + +func main() { + // TODO(polo): metrics + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: grpc.Handshake, + Plugins: map[string]plugin.Plugin{ + "psp": &grpc.PSPGRPCPlugin{Impl: plugins.NewGRPCImplem(&mangopay.Plugin{})}, + }, + + // A non-nil value here enables gRPC serving for this plugin... + GRPCServer: plugin.DefaultGRPCServer, + }) +} diff --git a/components/payments/internal/connectors/plugins/public/mangopay/config.go b/components/payments/internal/connectors/plugins/public/mangopay/config.go new file mode 100644 index 0000000000..42611a45c7 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/mangopay/config.go @@ -0,0 +1,39 @@ +package mangopay + +import ( + "encoding/json" + + "github.com/formancehq/payments/internal/models" + "github.com/pkg/errors" +) + +type Config struct { + ClientID string `json:"clientID"` + APIKey string `json:"apiKey"` + Endpoint string `json:"endpoint"` +} + +func (c Config) validate() error { + if c.ClientID == "" { + return errors.Wrap(models.ErrInvalidConfig, "missing clientID in config") + } + + if c.APIKey == "" { + return errors.Wrap(models.ErrInvalidConfig, "missing api key in config") + } + + if c.Endpoint == "" { + return errors.Wrap(models.ErrInvalidConfig, "missing endpoint in config") + } + + return nil +} + +func unmarshalAndValidateConfig(payload json.RawMessage) (Config, error) { + var config Config + if err := json.Unmarshal(payload, &config); err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + return config, config.validate() +} diff --git a/components/payments/internal/connectors/plugins/public/mangopay/config.json b/components/payments/internal/connectors/plugins/public/mangopay/config.json new file mode 100644 index 0000000000..59bacea9dc --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/mangopay/config.json @@ -0,0 +1,17 @@ +{ + "clientID": { + "dataType": "string", + "required": true, + "defaultValue": "" + }, + "apiKey": { + "dataType": "string", + "required": true, + "defaultValue": "" + }, + "endpoint": { + "dataType": "string", + "required": true, + "defaultValue": "" + } +} \ No newline at end of file diff --git a/components/payments/cmd/connectors/internal/connectors/mangopay/currencies.go b/components/payments/internal/connectors/plugins/public/mangopay/currencies.go similarity index 92% rename from components/payments/cmd/connectors/internal/connectors/mangopay/currencies.go rename to components/payments/internal/connectors/plugins/public/mangopay/currencies.go index 261e1b780c..0ee643f801 100644 --- a/components/payments/cmd/connectors/internal/connectors/mangopay/currencies.go +++ b/components/payments/internal/connectors/plugins/public/mangopay/currencies.go @@ -1,6 +1,6 @@ package mangopay -import "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" +import "github.com/formancehq/payments/internal/connectors/plugins/currency" var ( // c.f. https://mangopay.com/docs/api-basics/data-formats diff --git a/components/payments/internal/connectors/plugins/public/mangopay/external_accounts.go b/components/payments/internal/connectors/plugins/public/mangopay/external_accounts.go new file mode 100644 index 0000000000..523982439a --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/mangopay/external_accounts.go @@ -0,0 +1,110 @@ +package mangopay + +import ( + "context" + "encoding/json" + "errors" + "time" + + "github.com/formancehq/payments/internal/connectors/plugins/public/mangopay/client" + "github.com/formancehq/payments/internal/models" +) + +type externalAccountsState struct { + LastPage int `json:"last_page"` + LastCreationDate time.Time `json:"last_creation_date"` +} + +func (p Plugin) fetchNextExternalAccounts(ctx context.Context, req models.FetchNextExternalAccountsRequest) (models.FetchNextExternalAccountsResponse, error) { + var oldState externalAccountsState + if req.State != nil { + if err := json.Unmarshal(req.State, &oldState); err != nil { + return models.FetchNextExternalAccountsResponse{}, err + } + } else { + oldState = externalAccountsState{ + // Mangopay pages start at 1 + LastPage: 1, + } + } + + var from client.User + if req.FromPayload == nil { + return models.FetchNextExternalAccountsResponse{}, errors.New("missing from payload when fetching external accounts") + } + if err := json.Unmarshal(req.FromPayload, &from); err != nil { + return models.FetchNextExternalAccountsResponse{}, err + } + + newState := externalAccountsState{ + LastPage: oldState.LastPage, + LastCreationDate: oldState.LastCreationDate, + } + + var accounts []models.PSPAccount + hasMore := false + for page := oldState.LastPage; ; page++ { + newState.LastPage = page + + pagedExternalAccounts, err := p.client.GetBankAccounts(ctx, from.ID, page, req.PageSize) + if err != nil { + return models.FetchNextExternalAccountsResponse{}, err + } + + if len(pagedExternalAccounts) == 0 { + break + } + + for _, bankAccount := range pagedExternalAccounts { + creationDate := time.Unix(bankAccount.CreationDate, 0) + switch creationDate.Compare(oldState.LastCreationDate) { + case -1, 0: + // creationDate <= state.LastCreationDate, nothing to do, + // we already processed this bank account. + continue + default: + } + + raw, err := json.Marshal(bankAccount) + if err != nil { + return models.FetchNextExternalAccountsResponse{}, err + } + + accounts = append(accounts, models.PSPAccount{ + Reference: bankAccount.ID, + CreatedAt: creationDate, + Name: &bankAccount.OwnerName, + Metadata: map[string]string{ + "user_id": from.ID, + }, + Raw: raw, + }) + + newState.LastCreationDate = creationDate + + if len(accounts) >= req.PageSize { + break + } + } + + if len(pagedExternalAccounts) < req.PageSize { + break + } + + if len(accounts) >= req.PageSize { + hasMore = true + break + } + } + + payload, err := json.Marshal(newState) + if err != nil { + return models.FetchNextExternalAccountsResponse{}, err + } + + return models.FetchNextExternalAccountsResponse{ + ExternalAccounts: accounts, + NewState: payload, + HasMore: hasMore, + }, nil +} diff --git a/components/payments/internal/connectors/plugins/public/mangopay/payments.go b/components/payments/internal/connectors/plugins/public/mangopay/payments.go new file mode 100644 index 0000000000..350454d39a --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/mangopay/payments.go @@ -0,0 +1,163 @@ +package mangopay + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "math/big" + "time" + + "github.com/formancehq/payments/internal/connectors/plugins/currency" + "github.com/formancehq/payments/internal/connectors/plugins/public/mangopay/client" + "github.com/formancehq/payments/internal/models" +) + +type paymentsState struct { + LastPage int `json:"lastPage"` + LastCreationDate time.Time `json:"lastCreationDate"` +} + +func (p Plugin) fetchNextPayments(ctx context.Context, req models.FetchNextPaymentsRequest) (models.FetchNextPaymentsResponse, error) { + var oldState paymentsState + if req.State != nil { + if err := json.Unmarshal(req.State, &oldState); err != nil { + return models.FetchNextPaymentsResponse{}, err + } + } else { + oldState = paymentsState{ + LastPage: 1, + } + } + + var from models.PSPAccount + if req.FromPayload == nil { + return models.FetchNextPaymentsResponse{}, errors.New("missing from payload when fetching payments") + } + if err := json.Unmarshal(req.FromPayload, &from); err != nil { + return models.FetchNextPaymentsResponse{}, err + } + + newState := paymentsState{ + LastPage: oldState.LastPage, + LastCreationDate: oldState.LastCreationDate, + } + + var payments []models.PSPPayment + hasMore := false + page := oldState.LastPage + for { + pagedTransactions, err := p.client.GetTransactions(ctx, from.Reference, page, req.PageSize, oldState.LastCreationDate) + if err != nil { + return models.FetchNextPaymentsResponse{}, err + } + + if len(pagedTransactions) == 0 { + break + } + + for _, transaction := range pagedTransactions { + payment, err := transactionToPayment(transaction) + if err != nil { + return models.FetchNextPaymentsResponse{}, err + } + + if payment != nil { + payments = append(payments, *payment) + } + + newState.LastCreationDate = payment.CreatedAt + + if len(payments) >= req.PageSize { + break + } + } + + if len(pagedTransactions) < req.PageSize { + break + } + + if len(payments) >= req.PageSize { + hasMore = true + break + } + + page++ + } + + newState.LastPage = page + + payload, err := json.Marshal(newState) + if err != nil { + return models.FetchNextPaymentsResponse{}, err + } + + return models.FetchNextPaymentsResponse{ + Payments: payments, + NewState: payload, + HasMore: hasMore, + }, nil +} + +func transactionToPayment(from client.Payment) (*models.PSPPayment, error) { + raw, err := json.Marshal(&from) + if err != nil { + return nil, err + } + + paymentType := matchPaymentType(from.Type) + paymentStatus := matchPaymentStatus(from.Status) + + var amount big.Int + _, ok := amount.SetString(from.DebitedFunds.Amount.String(), 10) + if !ok { + return nil, fmt.Errorf("failed to parse amount %s", from.DebitedFunds.Amount.String()) + } + + payment := models.PSPPayment{ + Reference: from.Id, + CreatedAt: time.Unix(from.CreationDate, 0), + Type: paymentType, + Amount: &amount, + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, from.DebitedFunds.Currency), + Scheme: models.PAYMENT_SCHEME_OTHER, + Status: paymentStatus, + Raw: raw, + } + + if from.DebitedWalletID != "" { + payment.SourceAccountReference = &from.DebitedWalletID + } + + if from.CreditedWalletID != "" { + payment.DestinationAccountReference = &from.CreditedWalletID + } + + return &payment, nil +} + +func matchPaymentType(paymentType string) models.PaymentType { + switch paymentType { + case "PAYIN": + return models.PAYMENT_TYPE_PAYIN + case "PAYOUT": + return models.PAYMENT_TYPE_PAYOUT + case "TRANSFER": + return models.PAYMENT_TYPE_TRANSFER + } + + return models.PAYMENT_TYPE_OTHER +} + +func matchPaymentStatus(paymentStatus string) models.PaymentStatus { + switch paymentStatus { + case "CREATED": + return models.PAYMENT_STATUS_PENDING + case "SUCCEEDED": + return models.PAYMENT_STATUS_SUCCEEDED + case "FAILED": + return models.PAYMENT_STATUS_FAILED + } + + return models.PAYMENT_STATUS_OTHER +} diff --git a/components/payments/internal/connectors/plugins/public/mangopay/plugin.go b/components/payments/internal/connectors/plugins/public/mangopay/plugin.go new file mode 100644 index 0000000000..a923b84182 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/mangopay/plugin.go @@ -0,0 +1,149 @@ +package mangopay + +import ( + "context" + "errors" + "strconv" + + "github.com/formancehq/payments/internal/connectors/plugins" + "github.com/formancehq/payments/internal/connectors/plugins/public/mangopay/client" + "github.com/formancehq/payments/internal/models" +) + +type Plugin struct { + client *client.Client +} + +func (p *Plugin) Install(_ context.Context, req models.InstallRequest) (models.InstallResponse, error) { + config, err := unmarshalAndValidateConfig(req.Config) + if err != nil { + return models.InstallResponse{}, err + } + + client, err := client.New(config.ClientID, config.APIKey, config.Endpoint) + if err != nil { + return models.InstallResponse{}, err + } + p.client = client + p.initWebhookConfig() + + configs := make([]models.PSPWebhookConfig, 0, len(webhookConfigs)) + for name, config := range webhookConfigs { + configs = append(configs, models.PSPWebhookConfig{ + Name: string(name), + URLPath: config.urlPath, + }) + } + + return models.InstallResponse{ + Capabilities: capabilities, + WebhooksConfigs: configs, + Workflow: workflow(), + }, nil +} + +func (p Plugin) Uninstall(ctx context.Context, req models.UninstallRequest) (models.UninstallResponse, error) { + return models.UninstallResponse{}, nil +} + +func (p Plugin) FetchNextAccounts(ctx context.Context, req models.FetchNextAccountsRequest) (models.FetchNextAccountsResponse, error) { + if p.client == nil { + return models.FetchNextAccountsResponse{}, plugins.ErrNotYetInstalled + } + return p.fetchNextAccounts(ctx, req) +} + +func (p Plugin) FetchNextBalances(ctx context.Context, req models.FetchNextBalancesRequest) (models.FetchNextBalancesResponse, error) { + if p.client == nil { + return models.FetchNextBalancesResponse{}, plugins.ErrNotYetInstalled + } + return p.fetchNextBalances(ctx, req) +} + +func (p Plugin) FetchNextExternalAccounts(ctx context.Context, req models.FetchNextExternalAccountsRequest) (models.FetchNextExternalAccountsResponse, error) { + if p.client == nil { + return models.FetchNextExternalAccountsResponse{}, plugins.ErrNotYetInstalled + } + return p.fetchNextExternalAccounts(ctx, req) +} + +func (p Plugin) FetchNextPayments(ctx context.Context, req models.FetchNextPaymentsRequest) (models.FetchNextPaymentsResponse, error) { + if p.client == nil { + return models.FetchNextPaymentsResponse{}, plugins.ErrNotYetInstalled + } + return p.fetchNextPayments(ctx, req) +} + +func (p Plugin) FetchNextOthers(ctx context.Context, req models.FetchNextOthersRequest) (models.FetchNextOthersResponse, error) { + if p.client == nil { + return models.FetchNextOthersResponse{}, plugins.ErrNotYetInstalled + } + + switch req.Name { + case fetchUsersName: + return p.fetchNextUsers(ctx, req) + default: + return models.FetchNextOthersResponse{}, plugins.ErrNotImplemented + } +} + +func (p Plugin) CreateBankAccount(ctx context.Context, req models.CreateBankAccountRequest) (models.CreateBankAccountResponse, error) { + if p.client == nil { + return models.CreateBankAccountResponse{}, plugins.ErrNotYetInstalled + } + return p.createBankAccount(ctx, req) +} + +func (p Plugin) CreateWebhooks(ctx context.Context, req models.CreateWebhooksRequest) (models.CreateWebhooksResponse, error) { + if p.client == nil { + return models.CreateWebhooksResponse{}, plugins.ErrNotYetInstalled + } + err := p.createWebhooks(ctx, req) + return models.CreateWebhooksResponse{}, err +} + +func (p Plugin) TranslateWebhook(ctx context.Context, req models.TranslateWebhookRequest) (models.TranslateWebhookResponse, error) { + // Mangopay does not send us the event inside the body, but using + // URL query. + eventType, ok := req.Webhook.QueryValues["EventType"] + if !ok || len(eventType) == 0 { + return models.TranslateWebhookResponse{}, errors.New("missing EventType query parameter") + } + resourceID, ok := req.Webhook.QueryValues["RessourceId"] + if !ok || len(resourceID) == 0 { + return models.TranslateWebhookResponse{}, errors.New("missing RessourceId query parameter") + } + v, ok := req.Webhook.QueryValues["Date"] + if !ok || len(v) == 0 { + return models.TranslateWebhookResponse{}, errors.New("missing Date query parameter") + } + date, err := strconv.ParseInt(v[0], 10, 64) + if err != nil { + return models.TranslateWebhookResponse{}, errors.New("invalid Date query parameter") + } + + webhook := client.Webhook{ + ResourceID: resourceID[0], + Date: date, + EventType: client.EventType(eventType[0]), + } + + config, ok := webhookConfigs[webhook.EventType] + if !ok { + return models.TranslateWebhookResponse{}, errors.New("unsupported webhook event type") + } + + webhookResponse, err := config.fn(ctx, webhookTranslateRequest{ + req: req, + webhook: &webhook, + }) + if err != nil { + return models.TranslateWebhookResponse{}, err + } + + return models.TranslateWebhookResponse{ + Responses: []models.WebhookResponse{webhookResponse}, + }, nil +} + +var _ models.Plugin = &Plugin{} diff --git a/components/payments/internal/connectors/plugins/public/mangopay/users.go b/components/payments/internal/connectors/plugins/public/mangopay/users.go new file mode 100644 index 0000000000..3d5c0efd1d --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/mangopay/users.go @@ -0,0 +1,97 @@ +package mangopay + +import ( + "context" + "encoding/json" + "time" + + "github.com/formancehq/payments/internal/models" +) + +type usersState struct { + LastPage int `json:"lastPage"` + LastCreationDate time.Time `json:"lastCreationDate"` +} + +func (p Plugin) fetchNextUsers(ctx context.Context, req models.FetchNextOthersRequest) (models.FetchNextOthersResponse, error) { + var oldState usersState + if req.State != nil { + if err := json.Unmarshal(req.State, &oldState); err != nil { + return models.FetchNextOthersResponse{}, err + } + } else { + oldState = usersState{ + LastPage: 1, + } + } + + newState := usersState{ + LastPage: oldState.LastPage, + LastCreationDate: oldState.LastCreationDate, + } + + var users []models.PSPOther + hasMore := false + page := oldState.LastPage + for { + pagedUsers, err := p.client.GetUsers(ctx, page, req.PageSize) + if err != nil { + return models.FetchNextOthersResponse{}, err + } + + if len(pagedUsers) == 0 { + break + } + + for _, user := range pagedUsers { + userCreationDate := time.Unix(user.CreationDate, 0) + switch userCreationDate.Compare(oldState.LastCreationDate) { + case -1, 0: + // creationDate <= state.LastCreationDate, nothing to do, + // we already processed this user. + continue + default: + } + + raw, err := json.Marshal(user) + if err != nil { + return models.FetchNextOthersResponse{}, err + } + + users = append(users, models.PSPOther{ + ID: user.ID, + Other: raw, + }) + + newState.LastCreationDate = userCreationDate + + if len(users) >= req.PageSize { + break + } + } + + if len(users) < req.PageSize { + break + } + + if len(users) >= req.PageSize { + hasMore = true + break + } + + page++ + } + + newState.LastPage = page + + payload, err := json.Marshal(newState) + if err != nil { + return models.FetchNextOthersResponse{}, err + } + + return models.FetchNextOthersResponse{ + Others: users, + NewState: payload, + HasMore: hasMore, + }, nil +} diff --git a/components/payments/internal/connectors/plugins/public/mangopay/webhooks.go b/components/payments/internal/connectors/plugins/public/mangopay/webhooks.go new file mode 100644 index 0000000000..f02e645299 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/mangopay/webhooks.go @@ -0,0 +1,324 @@ +package mangopay + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "math/big" + "net/url" + "os" + "time" + + "github.com/formancehq/payments/internal/connectors/plugins/currency" + "github.com/formancehq/payments/internal/connectors/plugins/public/mangopay/client" + "github.com/formancehq/payments/internal/models" +) + +type webhookTranslateRequest struct { + req models.TranslateWebhookRequest + webhook *client.Webhook +} + +type webhookConfig struct { + urlPath string + fn func(context.Context, webhookTranslateRequest) (models.WebhookResponse, error) +} + +var webhookConfigs map[client.EventType]webhookConfig + +func (p Plugin) initWebhookConfig() { + webhookConfigs = map[client.EventType]webhookConfig{ + client.EventTypeTransferNormalCreated: { + urlPath: "/transfer/created", + fn: p.translateTransfer, + }, + client.EventTypeTransferNormalFailed: { + urlPath: "/transfer/failed", + fn: p.translateTransfer, + }, + client.EventTypeTransferNormalSucceeded: { + urlPath: "/transfer/succeeded", + fn: p.translateTransfer, + }, + + client.EventTypePayoutNormalCreated: { + urlPath: "/payout/normal/created", + fn: p.translatePayout, + }, + client.EventTypePayoutNormalFailed: { + urlPath: "/payout/normal/failed", + fn: p.translatePayout, + }, + client.EventTypePayoutNormalSucceeded: { + urlPath: "/payout/normal/succeeded", + fn: p.translatePayout, + }, + client.EventTypePayoutInstantFailed: { + urlPath: "/payout/instant/failed", + fn: p.translatePayout, + }, + client.EventTypePayoutInstantSucceeded: { + urlPath: "/payout/instant/succeeded", + fn: p.translatePayout, + }, + + client.EventTypePayinNormalCreated: { + urlPath: "/payin/normal/created", + fn: p.translatePayin, + }, + client.EventTypePayinNormalSucceeded: { + urlPath: "/payin/normal/succeeded", + fn: p.translatePayin, + }, + client.EventTypePayinNormalFailed: { + urlPath: "/payin/normal/failed", + fn: p.translatePayin, + }, + + client.EventTypeTransferRefundFailed: { + urlPath: "/refund/transfer/failed", + fn: p.translateRefund, + }, + client.EventTypeTransferRefundSucceeded: { + urlPath: "/refund/transfer/succeeded", + fn: p.translateRefund, + }, + client.EventTypePayOutRefundFailed: { + urlPath: "/refund/payout/failed", + fn: p.translateRefund, + }, + client.EventTypePayOutRefundSucceeded: { + urlPath: "/refund/payout/succeeded", + fn: p.translateRefund, + }, + client.EventTypePayinRefundFailed: { + urlPath: "/refund/payin/failed", + fn: p.translateRefund, + }, + client.EventTypePayinRefundSucceeded: { + urlPath: "/refund/payin/succeeded", + fn: p.translateRefund, + }, + } +} + +func (p Plugin) createWebhooks(ctx context.Context, req models.CreateWebhooksRequest) error { + stackPublicURL := os.Getenv("STACK_PUBLIC_URL") + if stackPublicURL == "" { + err := errors.New("STACK_PUBLIC_URL is not set") + return err + } + + activeHooks, err := p.getActiveHooks(ctx) + if err != nil { + return err + } + + webhookURL := fmt.Sprintf("%s/api/payments/v3/connectors/webhooks/%s", stackPublicURL, req.ConnectorID) + for eventType, config := range webhookConfigs { + url, err := url.JoinPath(webhookURL, config.urlPath) + if err != nil { + return err + } + + if v, ok := activeHooks[eventType]; ok { + // Already created, continue + + if v.URL != webhookURL { + // If the URL is different, update it + err := p.client.UpdateHook(ctx, v.ID, url) + if err != nil { + return err + } + } + + continue + } + + // Otherwise, create it + err = p.client.CreateHook(ctx, eventType, url) + if err != nil { + return err + } + } + + return nil +} + +func (p Plugin) getActiveHooks(ctx context.Context) (map[client.EventType]*client.Hook, error) { + alreadyExistingHooks, err := p.client.ListAllHooks(ctx) + if err != nil { + return nil, err + } + + activeHooks := make(map[client.EventType]*client.Hook) + for _, hook := range alreadyExistingHooks { + // Mangopay allows only one active hook per event type. + if hook.Validity == "VALID" { + activeHooks[hook.EventType] = hook + } + } + + return activeHooks, nil +} + +func (p Plugin) translateTransfer(ctx context.Context, req webhookTranslateRequest) (models.WebhookResponse, error) { + transfer, err := p.client.GetWalletTransfer(ctx, req.webhook.ResourceID) + if err != nil { + return models.WebhookResponse{}, err + } + + raw, err := json.Marshal(transfer) + if err != nil { + return models.WebhookResponse{}, fmt.Errorf("failed to marshal transfer: %w", err) + } + + paymentStatus := matchPaymentStatus(transfer.Status) + + var amount big.Int + _, ok := amount.SetString(transfer.DebitedFunds.Amount.String(), 10) + if !ok { + return models.WebhookResponse{}, fmt.Errorf("failed to parse amount %s", transfer.DebitedFunds.Amount.String()) + } + + payment := models.PSPPayment{ + Reference: transfer.ID, + CreatedAt: time.Unix(transfer.CreationDate, 0), + Type: models.PAYMENT_TYPE_TRANSFER, + Amount: &amount, + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transfer.DebitedFunds.Currency), + Scheme: models.PAYMENT_SCHEME_OTHER, + Status: paymentStatus, + Raw: raw, + } + + if transfer.DebitedWalletID != "" { + payment.SourceAccountReference = &transfer.DebitedWalletID + } + + if transfer.CreditedWalletID != "" { + payment.DestinationAccountReference = &transfer.CreditedWalletID + } + + return models.WebhookResponse{ + IdempotencyKey: fmt.Sprintf("%s-%s-%d", req.webhook.ResourceID, string(req.webhook.EventType), req.webhook.Date), + Payment: &payment, + }, nil +} + +func (p Plugin) translatePayout(ctx context.Context, req webhookTranslateRequest) (models.WebhookResponse, error) { + payout, err := p.client.GetPayout(ctx, req.webhook.ResourceID) + if err != nil { + return models.WebhookResponse{}, err + } + + raw, err := json.Marshal(payout) + if err != nil { + return models.WebhookResponse{}, fmt.Errorf("failed to marshal transfer: %w", err) + } + + paymentStatus := matchPaymentStatus(payout.Status) + + var amount big.Int + _, ok := amount.SetString(payout.DebitedFunds.Amount.String(), 10) + if !ok { + return models.WebhookResponse{}, fmt.Errorf("failed to parse amount %s", payout.DebitedFunds.Amount.String()) + } + + payment := models.PSPPayment{ + Reference: payout.ID, + CreatedAt: time.Unix(payout.CreationDate, 0), + Type: models.PAYMENT_TYPE_PAYOUT, + Amount: &amount, + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, payout.DebitedFunds.Currency), + Scheme: models.PAYMENT_SCHEME_OTHER, + Status: paymentStatus, + Raw: raw, + } + + if payout.DebitedWalletID != "" { + payment.DestinationAccountReference = &payout.DebitedWalletID + } + + return models.WebhookResponse{ + IdempotencyKey: fmt.Sprintf("%s-%s-%d", req.webhook.ResourceID, string(req.webhook.EventType), req.webhook.Date), + Payment: &payment, + }, nil +} + +func (p Plugin) translatePayin(ctx context.Context, req webhookTranslateRequest) (models.WebhookResponse, error) { + payin, err := p.client.GetPayin(ctx, req.webhook.ResourceID) + if err != nil { + return models.WebhookResponse{}, err + } + + raw, err := json.Marshal(payin) + if err != nil { + return models.WebhookResponse{}, fmt.Errorf("failed to marshal transfer: %w", err) + } + + paymentStatus := matchPaymentStatus(payin.Status) + + var amount big.Int + _, ok := amount.SetString(payin.DebitedFunds.Amount.String(), 10) + if !ok { + return models.WebhookResponse{}, fmt.Errorf("failed to parse amount %s", payin.DebitedFunds.Amount.String()) + } + + payment := models.PSPPayment{ + Reference: payin.ID, + CreatedAt: time.Unix(payin.CreationDate, 0), + Type: models.PAYMENT_TYPE_PAYIN, + Amount: &amount, + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, payin.DebitedFunds.Currency), + Scheme: models.PAYMENT_SCHEME_OTHER, + Status: paymentStatus, + Raw: raw, + } + + if payin.CreditedWalletID != "" { + payment.DestinationAccountReference = &payin.CreditedWalletID + } + + return models.WebhookResponse{ + IdempotencyKey: fmt.Sprintf("%s-%s-%d", req.webhook.ResourceID, string(req.webhook.EventType), req.webhook.Date), + Payment: &payment, + }, nil +} + +func (p Plugin) translateRefund(ctx context.Context, req webhookTranslateRequest) (models.WebhookResponse, error) { + refund, err := p.client.GetRefund(ctx, req.webhook.ResourceID) + if err != nil { + return models.WebhookResponse{}, err + } + + raw, err := json.Marshal(refund) + if err != nil { + return models.WebhookResponse{}, fmt.Errorf("failed to marshal transfer: %w", err) + } + + paymentType := matchPaymentType(refund.InitialTransactionType) + + var amountRefunded big.Int + _, ok := amountRefunded.SetString(refund.DebitedFunds.Amount.String(), 10) + if !ok { + return models.WebhookResponse{}, fmt.Errorf("failed to parse amount %s", refund.DebitedFunds.Amount.String()) + } + + payment := models.PSPPayment{ + Reference: refund.InitialTransactionID, + CreatedAt: time.Unix(refund.CreationDate, 0), + Type: paymentType, + Amount: &amountRefunded, + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, refund.DebitedFunds.Currency), + Scheme: models.PAYMENT_SCHEME_OTHER, + Status: models.PAYMENT_STATUS_REFUNDED, + Raw: raw, + } + + return models.WebhookResponse{ + IdempotencyKey: fmt.Sprintf("%s-%s-%d", req.webhook.ResourceID, string(req.webhook.EventType), req.webhook.Date), + Payment: &payment, + }, nil +} diff --git a/components/payments/internal/connectors/plugins/public/mangopay/workflow.go b/components/payments/internal/connectors/plugins/public/mangopay/workflow.go new file mode 100644 index 0000000000..c09d687a2f --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/mangopay/workflow.go @@ -0,0 +1,50 @@ +package mangopay + +import "github.com/formancehq/payments/internal/models" + +const ( + fetchUsersName = "fetch_users" +) + +func workflow() models.Tasks { + return []models.TaskTree{ + { + TaskType: models.TASK_FETCH_OTHERS, + Name: fetchUsersName, + Periodically: true, + NextTasks: []models.TaskTree{ + { + TaskType: models.TASK_FETCH_ACCOUNTS, + Name: "fetch_accounts", + Periodically: true, + NextTasks: []models.TaskTree{ + { + TaskType: models.TASK_FETCH_PAYMENTS, + Name: "fetch_payments", + Periodically: false, // We will be using webhooks after polling the history + NextTasks: []models.TaskTree{}, + }, + { + TaskType: models.TASK_FETCH_BALANCES, + Name: "fetch_balances", + Periodically: true, + NextTasks: []models.TaskTree{}, + }, + }, + }, + { + TaskType: models.TASK_FETCH_EXTERNAL_ACCOUNTS, + Name: "fetch_external_accounts", + Periodically: true, + NextTasks: []models.TaskTree{}, + }, + }, + }, + { + TaskType: models.TASK_CREATE_WEBHOOKS, + Name: "create_webhooks", + Periodically: false, + NextTasks: []models.TaskTree{}, + }, + } +} diff --git a/components/payments/internal/connectors/plugins/public/modulr/accounts.go b/components/payments/internal/connectors/plugins/public/modulr/accounts.go new file mode 100644 index 0000000000..33c28eec9a --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/modulr/accounts.go @@ -0,0 +1,90 @@ +package modulr + +import ( + "context" + "encoding/json" + "time" + + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/connectors/plugins/currency" + "github.com/formancehq/payments/internal/models" +) + +type accountsState struct { + LastCreatedAt time.Time `json:"lastCreatedAt"` +} + +func (p Plugin) fetchNextAccounts(ctx context.Context, req models.FetchNextAccountsRequest) (models.FetchNextAccountsResponse, error) { + var oldState accountsState + if req.State != nil { + if err := json.Unmarshal(req.State, &oldState); err != nil { + return models.FetchNextAccountsResponse{}, err + } + } + + newState := accountsState{ + LastCreatedAt: oldState.LastCreatedAt, + } + + var accounts []models.PSPAccount + hasMore := false + for page := 0; ; page++ { + pagedAccounts, err := p.client.GetAccounts(ctx, page, req.PageSize, oldState.LastCreatedAt) + if err != nil { + return models.FetchNextAccountsResponse{}, err + } + + if len(pagedAccounts.Content) == 0 { + break + } + + for _, account := range pagedAccounts.Content { + createdTime, err := time.Parse("2006-01-02T15:04:05.999-0700", account.CreatedDate) + if err != nil { + return models.FetchNextAccountsResponse{}, err + } + + switch createdTime.Compare(oldState.LastCreatedAt) { + case -1, 0: + // Account already ingested, skip + continue + default: + } + + raw, err := json.Marshal(account) + if err != nil { + return models.FetchNextAccountsResponse{}, err + } + + accounts = append(accounts, models.PSPAccount{ + Reference: account.ID, + CreatedAt: createdTime, + Name: &account.Name, + DefaultAsset: pointer.For(currency.FormatAsset(supportedCurrenciesWithDecimal, account.Currency)), + Raw: raw, + }) + + newState.LastCreatedAt = createdTime + + if len(accounts) >= req.PageSize { + break + } + } + + if len(accounts) >= req.PageSize { + hasMore = true + break + } + } + + payload, err := json.Marshal(newState) + if err != nil { + return models.FetchNextAccountsResponse{}, err + } + + return models.FetchNextAccountsResponse{ + Accounts: accounts, + NewState: payload, + HasMore: hasMore, + }, nil +} diff --git a/components/payments/internal/connectors/plugins/public/modulr/balances.go b/components/payments/internal/connectors/plugins/public/modulr/balances.go new file mode 100644 index 0000000000..f1b5d704f3 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/modulr/balances.go @@ -0,0 +1,47 @@ +package modulr + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/formancehq/payments/internal/connectors/plugins/currency" + "github.com/formancehq/payments/internal/models" +) + +func (p Plugin) fetchNextBalances(ctx context.Context, req models.FetchNextBalancesRequest) (models.FetchNextBalancesResponse, error) { + var from models.PSPAccount + if req.FromPayload == nil { + return models.FetchNextBalancesResponse{}, errors.New("missing from payload when fetching payments") + } + if err := json.Unmarshal(req.FromPayload, &from); err != nil { + return models.FetchNextBalancesResponse{}, err + } + + account, err := p.client.GetAccount(ctx, from.Reference) + if err != nil { + return models.FetchNextBalancesResponse{}, err + } + + precision := supportedCurrenciesWithDecimal[account.Currency] + + amount, err := currency.GetAmountWithPrecisionFromString(account.Balance, precision) + if err != nil { + return models.FetchNextBalancesResponse{}, fmt.Errorf("failed to parse amount %s: %w", account.Balance, err) + } + + balance := models.PSPBalance{ + AccountReference: from.Reference, + CreatedAt: time.Now().UTC(), + Amount: amount, + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, account.Currency), + } + + return models.FetchNextBalancesResponse{ + Balances: []models.PSPBalance{balance}, + NewState: []byte{}, + HasMore: false, + }, nil +} diff --git a/components/payments/internal/connectors/plugins/public/modulr/capabilities.go b/components/payments/internal/connectors/plugins/public/modulr/capabilities.go new file mode 100644 index 0000000000..4161923b1c --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/modulr/capabilities.go @@ -0,0 +1,10 @@ +package modulr + +import "github.com/formancehq/payments/internal/models" + +var capabilities = []models.Capability{ + models.CAPABILITY_FETCH_ACCOUNTS, + models.CAPABILITY_FETCH_EXTERNAL_ACCOUNTS, + models.CAPABILITY_FETCH_PAYMENTS, + models.CAPABILITY_FETCH_BALANCES, +} diff --git a/components/payments/internal/connectors/plugins/public/modulr/client/accounts.go b/components/payments/internal/connectors/plugins/public/modulr/client/accounts.go new file mode 100644 index 0000000000..d26b835bcc --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/modulr/client/accounts.go @@ -0,0 +1,86 @@ +package client + +import ( + "context" + "fmt" + "net/http" + "strconv" + "time" + + "github.com/formancehq/payments/internal/connectors/httpwrapper" +) + +//nolint:tagliatelle // allow for clients +type Account struct { + ID string `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + Balance string `json:"balance"` + Currency string `json:"currency"` + CustomerID string `json:"customerId"` + Identifiers []struct { + AccountNumber string `json:"accountNumber"` + SortCode string `json:"sortCode"` + Type string `json:"type"` + } `json:"identifiers"` + DirectDebit bool `json:"directDebit"` + CreatedDate string `json:"createdDate"` +} + +func (c *Client) GetAccounts(ctx context.Context, page, pageSize int, fromCreatedAt time.Time) (*responseWrapper[[]Account], error) { + // TODO(polo): add metrics + // f := connectors.ClientMetrics(ctx, "modulr", "list_accounts") + // now := time.Now() + // defer f(ctx, now) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.buildEndpoint("accounts"), http.NoBody) + if err != nil { + return nil, fmt.Errorf("failed to create accounts request: %w", err) + } + + q := req.URL.Query() + q.Add("page", strconv.Itoa(page)) + q.Add("size", strconv.Itoa(pageSize)) + q.Add("sortField", "createdDate") + q.Add("sortOrder", "asc") + if !fromCreatedAt.IsZero() { + q.Add("fromCreatedDate", fromCreatedAt.Format("2006-01-02T15:04:05-0700")) + } + req.URL.RawQuery = q.Encode() + + var res responseWrapper[[]Account] + var errRes modulrError + _, err = c.httpClient.Do(req, &res, &errRes) + switch err { + case nil: + return &res, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, errRes.Error() + } + return nil, fmt.Errorf("failed to get accounts: %w", err) +} + +func (c *Client) GetAccount(ctx context.Context, accountID string) (*Account, error) { + // TODO(polo): add metrics + // f := connectors.ClientMetrics(ctx, "modulr", "list_accounts") + // now := time.Now() + // defer f(ctx, now) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.buildEndpoint("accounts/%s", accountID), http.NoBody) + if err != nil { + return nil, fmt.Errorf("failed to create accounts request: %w", err) + } + + var res Account + var errRes modulrError + _, err = c.httpClient.Do(req, &res, &errRes) + switch err { + case nil: + return &res, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, errRes.Error() + } + return nil, fmt.Errorf("failed to get account: %w", err) +} diff --git a/components/payments/internal/connectors/plugins/public/modulr/client/beneficiaries.go b/components/payments/internal/connectors/plugins/public/modulr/client/beneficiaries.go new file mode 100644 index 0000000000..69296ee994 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/modulr/client/beneficiaries.go @@ -0,0 +1,49 @@ +package client + +import ( + "context" + "fmt" + "net/http" + "strconv" + "time" + + "github.com/formancehq/payments/internal/connectors/httpwrapper" +) + +type Beneficiary struct { + ID string `json:"id"` + Name string `json:"name"` + Created string `json:"created"` +} + +func (c *Client) GetBeneficiaries(ctx context.Context, page, pageSize int, modifiedSince time.Time) (*responseWrapper[[]Beneficiary], error) { + // TODO(polo): add metrics + // f := connectors.ClientMetrics(ctx, "modulr", "list_beneficiaries") + // now := time.Now() + // defer f(ctx, now) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.buildEndpoint("beneficiaries"), http.NoBody) + if err != nil { + return nil, fmt.Errorf("failed to create accounts request: %w", err) + } + + q := req.URL.Query() + q.Add("page", strconv.Itoa(page)) + q.Add("size", strconv.Itoa(pageSize)) + if !modifiedSince.IsZero() { + q.Add("modifiedSince", modifiedSince.Format("2006-01-02T15:04:05-0700")) + } + req.URL.RawQuery = q.Encode() + + var res responseWrapper[[]Beneficiary] + var errRes modulrError + _, err = c.httpClient.Do(req, &res, &errRes) + switch err { + case nil: + return &res, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, errRes.Error() + } + return nil, fmt.Errorf("failed to get beneficiaries %w", err) +} diff --git a/components/payments/cmd/connectors/internal/connectors/modulr/client/client.go b/components/payments/internal/connectors/plugins/public/modulr/client/client.go similarity index 69% rename from components/payments/cmd/connectors/internal/connectors/modulr/client/client.go rename to components/payments/internal/connectors/plugins/public/modulr/client/client.go index 7fd340c4fb..1b5f30b33a 100644 --- a/components/payments/cmd/connectors/internal/connectors/modulr/client/client.go +++ b/components/payments/internal/connectors/plugins/public/modulr/client/client.go @@ -5,7 +5,8 @@ import ( "net/http" "strings" - "github.com/formancehq/payments/cmd/connectors/internal/connectors/modulr/hmac" + "github.com/formancehq/payments/internal/connectors/httpwrapper" + "github.com/formancehq/payments/internal/connectors/plugins/public/modulr/client/hmac" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) @@ -30,7 +31,7 @@ type responseWrapper[t any] struct { } type Client struct { - httpClient *http.Client + httpClient httpwrapper.Client endpoint string } @@ -41,7 +42,7 @@ func (m *Client) buildEndpoint(path string, args ...interface{}) string { const SandboxAPIEndpoint = "https://api-sandbox.modulrfinance.com/api-sandbox-token" -func NewClient(apiKey, apiSecret, endpoint string) (*Client, error) { +func New(apiKey, apiSecret, endpoint string) (*Client, error) { if endpoint == "" { endpoint = SandboxAPIEndpoint } @@ -50,16 +51,21 @@ func NewClient(apiKey, apiSecret, endpoint string) (*Client, error) { if err != nil { return nil, fmt.Errorf("failed to generate headers: %w", err) } + config := &httpwrapper.Config{ + Transport: &apiTransport{ + headers: headers, + apiKey: apiKey, + underlying: otelhttp.NewTransport(http.DefaultTransport), + }, + } + httpClient, err := httpwrapper.NewClient(config) + if err != nil { + return nil, fmt.Errorf("failed to create modulr client: %w", err) + } return &Client{ - httpClient: &http.Client{ - Transport: &apiTransport{ - headers: headers, - apiKey: apiKey, - underlying: otelhttp.NewTransport(http.DefaultTransport), - }, - }, - endpoint: endpoint, + httpClient: httpClient, + endpoint: endpoint, }, nil } diff --git a/components/payments/internal/connectors/plugins/public/modulr/client/error.go b/components/payments/internal/connectors/plugins/public/modulr/client/error.go new file mode 100644 index 0000000000..dff0488b07 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/modulr/client/error.go @@ -0,0 +1,25 @@ +package client + +import ( + "fmt" +) + +type modulrError struct { + StatusCode int `json:"-"` + Field string `json:"field"` + Code string `json:"code"` + Message string `json:"message"` + ErrorCode string `json:"errorCode"` + SourceService string `json:"sourceService"` +} + +func (me *modulrError) Error() error { + var err error + if me.Message == "" { + err = fmt.Errorf("unexpected status code: %d", me.StatusCode) + } else { + err = fmt.Errorf("%d: %s", me.StatusCode, me.Message) + } + + return err +} diff --git a/components/payments/cmd/connectors/internal/connectors/modulr/hmac/hmac.go b/components/payments/internal/connectors/plugins/public/modulr/client/hmac/hmac.go similarity index 100% rename from components/payments/cmd/connectors/internal/connectors/modulr/hmac/hmac.go rename to components/payments/internal/connectors/plugins/public/modulr/client/hmac/hmac.go diff --git a/components/payments/cmd/connectors/internal/connectors/modulr/hmac/hmac_test.go b/components/payments/internal/connectors/plugins/public/modulr/client/hmac/hmac_test.go similarity index 100% rename from components/payments/cmd/connectors/internal/connectors/modulr/hmac/hmac_test.go rename to components/payments/internal/connectors/plugins/public/modulr/client/hmac/hmac_test.go diff --git a/components/payments/cmd/connectors/internal/connectors/modulr/hmac/signature_generator.go b/components/payments/internal/connectors/plugins/public/modulr/client/hmac/signature_generator.go similarity index 100% rename from components/payments/cmd/connectors/internal/connectors/modulr/hmac/signature_generator.go rename to components/payments/internal/connectors/plugins/public/modulr/client/hmac/signature_generator.go diff --git a/components/payments/cmd/connectors/internal/connectors/modulr/hmac/signature_test.go b/components/payments/internal/connectors/plugins/public/modulr/client/hmac/signature_test.go similarity index 100% rename from components/payments/cmd/connectors/internal/connectors/modulr/hmac/signature_test.go rename to components/payments/internal/connectors/plugins/public/modulr/client/hmac/signature_test.go diff --git a/components/payments/internal/connectors/plugins/public/modulr/client/payout.go b/components/payments/internal/connectors/plugins/public/modulr/client/payout.go new file mode 100644 index 0000000000..83e5b3ef71 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/modulr/client/payout.go @@ -0,0 +1,86 @@ +package client + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/formancehq/payments/internal/connectors/httpwrapper" +) + +type PayoutRequest struct { + SourceAccountID string `json:"sourceAccountId"` + Destination struct { + Type string `json:"type"` + ID string `json:"id"` + } `json:"destination"` + Currency string `json:"currency"` + Amount json.Number `json:"amount"` + Reference string `json:"reference"` + ExternalReference string `json:"externalReference"` +} + +type PayoutResponse struct { + ID string `json:"id"` + Status string `json:"status"` + CreatedDate string `json:"createdDate"` + ExternalReference string `json:"externalReference"` + ApprovalStatus string `json:"approvalStatus"` + Message string `json:"message"` +} + +func (c *Client) InitiatePayout(ctx context.Context, payoutRequest *PayoutRequest) (*PayoutResponse, error) { + // TODO(polo): add metrics + // f := connectors.ClientMetrics(ctx, "modulr", "initiate_payout") + // now := time.Now() + // defer f(ctx, now) + + body, err := json.Marshal(payoutRequest) + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.buildEndpoint("payments"), bytes.NewBuffer(body)) + if err != nil { + return nil, fmt.Errorf("failed to create payout request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + + var res PayoutResponse + var errRes modulrError + _, err = c.httpClient.Do(req, &res, &errRes) + switch err { + case nil: + return &res, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, errRes.Error() + } + return nil, fmt.Errorf("failed to create payout %w", err) +} + +func (c *Client) GetPayout(ctx context.Context, payoutID string) (PayoutResponse, error) { + // TODO(polo): add metrics + // f := connectors.ClientMetrics(ctx, "modulr", "get_payout") + // now := time.Now() + // defer f(ctx, now) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.buildEndpoint("payments?id=%s", payoutID), nil) + if err != nil { + return PayoutResponse{}, fmt.Errorf("failed to create get payout request: %w", err) + } + + var res PayoutResponse + var errRes modulrError + _, err = c.httpClient.Do(req, &res, &errRes) + switch err { + case nil: + return res, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return PayoutResponse{}, errRes.Error() + } + return PayoutResponse{}, fmt.Errorf("failed to get payout %w", err) +} diff --git a/components/payments/cmd/connectors/internal/connectors/modulr/client/transactions.go b/components/payments/internal/connectors/plugins/public/modulr/client/transactions.go similarity index 51% rename from components/payments/cmd/connectors/internal/connectors/modulr/client/transactions.go rename to components/payments/internal/connectors/plugins/public/modulr/client/transactions.go index 8a15baa81e..ad381a17c2 100644 --- a/components/payments/cmd/connectors/internal/connectors/modulr/client/transactions.go +++ b/components/payments/internal/connectors/plugins/public/modulr/client/transactions.go @@ -8,7 +8,7 @@ import ( "strconv" "time" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" + "github.com/formancehq/payments/internal/connectors/httpwrapper" ) //nolint:tagliatelle // allow different styled tags in client @@ -25,12 +25,13 @@ type Transaction struct { AdditionalInfo interface{} `json:"additionalInfo"` } -func (m *Client) GetTransactions(ctx context.Context, accountID string, page, pageSize int, fromTransactionDate string) (*responseWrapper[[]*Transaction], error) { - f := connectors.ClientMetrics(ctx, "modulr", "list_transactions") - now := time.Now() - defer f(ctx, now) +func (c *Client) GetTransactions(ctx context.Context, accountID string, page, pageSize int, fromTransactionDate time.Time) (*responseWrapper[[]Transaction], error) { + // TODO(polo): add metrics + // f := connectors.ClientMetrics(ctx, "modulr", "list_transactions") + // now := time.Now() + // defer f(ctx, now) - req, err := http.NewRequestWithContext(ctx, http.MethodGet, m.buildEndpoint("accounts/%s/transactions", accountID), http.NoBody) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.buildEndpoint("accounts/%s/transactions", accountID), http.NoBody) if err != nil { return nil, fmt.Errorf("failed to create accounts request: %w", err) } @@ -38,26 +39,20 @@ func (m *Client) GetTransactions(ctx context.Context, accountID string, page, pa q := req.URL.Query() q.Add("page", strconv.Itoa(page)) q.Add("size", strconv.Itoa(pageSize)) - if fromTransactionDate != "" { - q.Add("fromTransactionDate", fromTransactionDate) + if !fromTransactionDate.IsZero() { + q.Add("fromTransactionDate", fromTransactionDate.Format("2006-01-02T15:04:05-0700")) } req.URL.RawQuery = q.Encode() - resp, err := m.httpClient.Do(req) - if err != nil { - return nil, err - } - - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, unmarshalErrorWithRetry(resp.StatusCode, resp.Body).Error() + var res responseWrapper[[]Transaction] + var errRes modulrError + _, err = c.httpClient.Do(req, &res, &errRes) + switch err { + case nil: + return &res, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, errRes.Error() } - - var res responseWrapper[[]*Transaction] - if err = json.NewDecoder(resp.Body).Decode(&res); err != nil { - return nil, err - } - - return &res, nil + return nil, fmt.Errorf("failed to get transactions %w", err) } diff --git a/components/payments/cmd/connectors/internal/connectors/modulr/client/transfer.go b/components/payments/internal/connectors/plugins/public/modulr/client/transfer.go similarity index 55% rename from components/payments/cmd/connectors/internal/connectors/modulr/client/transfer.go rename to components/payments/internal/connectors/plugins/public/modulr/client/transfer.go index 435370abf3..9e15fe97dd 100644 --- a/components/payments/cmd/connectors/internal/connectors/modulr/client/transfer.go +++ b/components/payments/internal/connectors/plugins/public/modulr/client/transfer.go @@ -6,9 +6,8 @@ import ( "encoding/json" "fmt" "net/http" - "time" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" + "github.com/formancehq/payments/internal/connectors/httpwrapper" ) type DestinationType string @@ -34,7 +33,7 @@ type TransferRequest struct { } type getTransferResponse struct { - Content []*TransferResponse `json:"content"` + Content []TransferResponse `json:"content"` } type TransferResponse struct { @@ -47,9 +46,10 @@ type TransferResponse struct { } func (c *Client) InitiateTransfer(ctx context.Context, transferRequest *TransferRequest) (*TransferResponse, error) { - f := connectors.ClientMetrics(ctx, "modulr", "initiate_transfer") - now := time.Now() - defer f(ctx, now) + // TODO(polo): add metrics + // f := connectors.ClientMetrics(ctx, "modulr", "initiate_transfer") + // now := time.Now() + // defer f(ctx, now) body, err := json.Marshal(transferRequest) if err != nil { @@ -62,47 +62,42 @@ func (c *Client) InitiateTransfer(ctx context.Context, transferRequest *Transfer } req.Header.Set("Content-Type", "application/json") - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to initiate transfer: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusCreated { - return nil, unmarshalErrorWithoutRetry(resp.StatusCode, resp.Body).Error() - } - var res TransferResponse - if err = json.NewDecoder(resp.Body).Decode(&res); err != nil { - return nil, err + var errRes modulrError + _, err = c.httpClient.Do(req, &res, &errRes) + switch err { + case nil: + return &res, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, errRes.Error() } - - return &res, nil + return nil, fmt.Errorf("failed to initiate transfer: %w", err) } -func (c *Client) GetTransfer(ctx context.Context, transferID string) (*TransferResponse, error) { - f := connectors.ClientMetrics(ctx, "modulr", "get_transfer") - now := time.Now() - defer f(ctx, now) +func (c *Client) GetTransfer(ctx context.Context, transferID string) (TransferResponse, error) { + // TODO(polo): add metrics + // f := connectors.ClientMetrics(ctx, "modulr", "get_transfer") + // now := time.Now() + // defer f(ctx, now) - resp, err := c.httpClient.Get(c.buildEndpoint("payments?id=%s", transferID)) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.buildEndpoint("payments?id=%s", transferID), nil) if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, unmarshalErrorWithRetry(resp.StatusCode, resp.Body).Error() + return TransferResponse{}, fmt.Errorf("failed to create get transfer request: %w", err) } var res getTransferResponse - if err = json.NewDecoder(resp.Body).Decode(&res); err != nil { - return nil, err + var errRes modulrError + _, err = c.httpClient.Do(req, &res, &errRes) + switch err { + case nil: + if len(res.Content) == 0 { + return TransferResponse{}, fmt.Errorf("transfer not found") + } + return res.Content[0], nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return TransferResponse{}, errRes.Error() } - - if len(res.Content) == 0 { - return nil, fmt.Errorf("transfer not found") - } - - return res.Content[0], nil + return TransferResponse{}, fmt.Errorf("failed to get transactions %w", err) } diff --git a/components/payments/internal/connectors/plugins/public/modulr/cmd/main.go b/components/payments/internal/connectors/plugins/public/modulr/cmd/main.go new file mode 100644 index 0000000000..5d883ece07 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/modulr/cmd/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "github.com/formancehq/payments/internal/connectors/grpc" + "github.com/formancehq/payments/internal/connectors/plugins" + "github.com/formancehq/payments/internal/connectors/plugins/public/modulr" + "github.com/hashicorp/go-plugin" +) + +func main() { + // TODO(polo): metrics + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: grpc.Handshake, + Plugins: map[string]plugin.Plugin{ + "psp": &grpc.PSPGRPCPlugin{Impl: plugins.NewGRPCImplem(&modulr.Plugin{})}, + }, + + // A non-nil value here enables gRPC serving for this plugin... + GRPCServer: plugin.DefaultGRPCServer, + }) +} diff --git a/components/payments/internal/connectors/plugins/public/modulr/config.go b/components/payments/internal/connectors/plugins/public/modulr/config.go new file mode 100644 index 0000000000..b0551374b9 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/modulr/config.go @@ -0,0 +1,39 @@ +package modulr + +import ( + "encoding/json" + + "github.com/formancehq/payments/internal/models" + "github.com/pkg/errors" +) + +type Config struct { + APIKey string `json:"apiKey"` + APISecret string `json:"apiSecret"` + Endpoint string `json:"endpoint"` +} + +func (c Config) validate() error { + if c.APIKey == "" { + return errors.Wrap(models.ErrInvalidConfig, "missing api key in config") + } + + if c.APISecret == "" { + return errors.Wrap(models.ErrInvalidConfig, "missing api secret in config") + } + + if c.Endpoint == "" { + return errors.Wrap(models.ErrInvalidConfig, "missing endpoint in config") + } + + return nil +} + +func unmarshalAndValidateConfig(payload []byte) (Config, error) { + var config Config + if err := json.Unmarshal(payload, &config); err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + return config, config.validate() +} diff --git a/components/payments/internal/connectors/plugins/public/modulr/config.json b/components/payments/internal/connectors/plugins/public/modulr/config.json new file mode 100644 index 0000000000..c2b6ee2d03 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/modulr/config.json @@ -0,0 +1,17 @@ +{ + "apiKey": { + "dataType": "string", + "required": true, + "defaultValue": "" + }, + "apiSecret": { + "dataType": "string", + "required": true, + "defaultValue": "" + }, + "endpoint": { + "dataType": "string", + "required": true, + "defaultValue": "" + } +} \ No newline at end of file diff --git a/components/payments/cmd/connectors/internal/connectors/modulr/currencies.go b/components/payments/internal/connectors/plugins/public/modulr/currencies.go similarity index 90% rename from components/payments/cmd/connectors/internal/connectors/modulr/currencies.go rename to components/payments/internal/connectors/plugins/public/modulr/currencies.go index d54ca96535..e0f653aa16 100644 --- a/components/payments/cmd/connectors/internal/connectors/modulr/currencies.go +++ b/components/payments/internal/connectors/plugins/public/modulr/currencies.go @@ -1,6 +1,6 @@ package modulr -import "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" +import "github.com/formancehq/payments/internal/connectors/plugins/currency" var ( // c.f. https://modulr.readme.io/docs/international-payments diff --git a/components/payments/internal/connectors/plugins/public/modulr/external_accounts.go b/components/payments/internal/connectors/plugins/public/modulr/external_accounts.go new file mode 100644 index 0000000000..fb09746d98 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/modulr/external_accounts.go @@ -0,0 +1,87 @@ +package modulr + +import ( + "context" + "encoding/json" + "time" + + "github.com/formancehq/payments/internal/models" +) + +type externalAccountsState struct { + LastCreatedAt time.Time `json:"lastCreatedAt"` +} + +func (p Plugin) fetchNextExternalAccounts(ctx context.Context, req models.FetchNextExternalAccountsRequest) (models.FetchNextExternalAccountsResponse, error) { + var oldState externalAccountsState + if req.State != nil { + if err := json.Unmarshal(req.State, &oldState); err != nil { + return models.FetchNextExternalAccountsResponse{}, err + } + } + + newState := externalAccountsState{ + LastCreatedAt: oldState.LastCreatedAt, + } + + var accounts []models.PSPAccount + hasMore := false + for page := 0; ; page++ { + pagedBeneficiarise, err := p.client.GetBeneficiaries(ctx, page, req.PageSize, oldState.LastCreatedAt) + if err != nil { + return models.FetchNextExternalAccountsResponse{}, err + } + + if len(pagedBeneficiarise.Content) == 0 { + break + } + + for _, beneficiary := range pagedBeneficiarise.Content { + createdTime, err := time.Parse("2006-01-02T15:04:05.999-0700", beneficiary.Created) + if err != nil { + return models.FetchNextExternalAccountsResponse{}, err + } + + switch createdTime.Compare(oldState.LastCreatedAt) { + case -1, 0: + // Account already ingested, skip + continue + default: + } + + raw, err := json.Marshal(beneficiary) + if err != nil { + return models.FetchNextExternalAccountsResponse{}, err + } + + accounts = append(accounts, models.PSPAccount{ + Reference: beneficiary.ID, + CreatedAt: createdTime, + Name: &beneficiary.Name, + Raw: raw, + }) + + newState.LastCreatedAt = createdTime + + if len(accounts) >= req.PageSize { + break + } + } + + if len(accounts) >= req.PageSize { + hasMore = true + break + } + } + + payload, err := json.Marshal(newState) + if err != nil { + return models.FetchNextExternalAccountsResponse{}, err + } + + return models.FetchNextExternalAccountsResponse{ + ExternalAccounts: accounts, + NewState: payload, + HasMore: hasMore, + }, err +} diff --git a/components/payments/internal/connectors/plugins/public/modulr/payments.go b/components/payments/internal/connectors/plugins/public/modulr/payments.go new file mode 100644 index 0000000000..28c2adb557 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/modulr/payments.go @@ -0,0 +1,166 @@ +package modulr + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + "github.com/formancehq/payments/internal/connectors/plugins/currency" + "github.com/formancehq/payments/internal/connectors/plugins/public/modulr/client" + "github.com/formancehq/payments/internal/models" +) + +type paymentsState struct { + LastTransactionTime time.Time `json:"lastTransactionTime"` +} + +func (p Plugin) fetchNextPayments(ctx context.Context, req models.FetchNextPaymentsRequest) (models.FetchNextPaymentsResponse, error) { + var oldState paymentsState + if req.State != nil { + if err := json.Unmarshal(req.State, &oldState); err != nil { + return models.FetchNextPaymentsResponse{}, err + } + } + + var from models.PSPAccount + if req.FromPayload == nil { + return models.FetchNextPaymentsResponse{}, errors.New("missing from payload when fetching payments") + } + if err := json.Unmarshal(req.FromPayload, &from); err != nil { + return models.FetchNextPaymentsResponse{}, err + } + + newState := paymentsState{ + LastTransactionTime: oldState.LastTransactionTime, + } + + var payments []models.PSPPayment + hasMore := false + for page := 0; ; page++ { + pagedTransactions, err := p.client.GetTransactions(ctx, from.Reference, page, req.PageSize, oldState.LastTransactionTime) + if err != nil { + return models.FetchNextPaymentsResponse{}, err + } + + if len(pagedTransactions.Content) == 0 { + break + } + + for _, transaction := range pagedTransactions.Content { + createdTime, err := time.Parse("2006-01-02T15:04:05.999-0700", transaction.TransactionDate) + if err != nil { + return models.FetchNextPaymentsResponse{}, err + } + + switch createdTime.Compare(oldState.LastTransactionTime) { + case -1, 0: + // Account already ingested, skip + continue + default: + } + + payment, err := transactionToPayment(transaction, from) + if err != nil { + return models.FetchNextPaymentsResponse{}, err + } + + if payment != nil { + payments = append(payments, *payment) + } + + newState.LastTransactionTime = createdTime + + if len(payments) >= req.PageSize { + break + } + } + + if len(payments) >= req.PageSize { + hasMore = true + break + } + } + + payload, err := json.Marshal(newState) + if err != nil { + return models.FetchNextPaymentsResponse{}, err + } + + return models.FetchNextPaymentsResponse{ + Payments: payments, + NewState: payload, + HasMore: hasMore, + }, nil +} + +func transactionToPayment(transaction client.Transaction, from models.PSPAccount) (*models.PSPPayment, error) { + raw, err := json.Marshal(transaction) + if err != nil { + return nil, err + } + + paymentType := matchTransactionType(transaction.Type) + + precision, ok := supportedCurrenciesWithDecimal[transaction.Account.Currency] + if !ok { + return nil, nil + } + + amount, err := currency.GetAmountWithPrecisionFromString(transaction.Amount.String(), precision) + if err != nil { + return nil, fmt.Errorf("failed to parse amount %s: %w", transaction.Amount, err) + } + + createdAt, err := time.Parse("2006-01-02T15:04:05-0700", transaction.PostedDate) + if err != nil { + return nil, fmt.Errorf("failed to parse posted date %s: %w", transaction.PostedDate, err) + } + + payment := &models.PSPPayment{ + Reference: transaction.ID, + CreatedAt: createdAt, + Type: paymentType, + Amount: amount, + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transaction.Account.Currency), + Scheme: models.PAYMENT_SCHEME_OTHER, + Status: models.PAYMENT_STATUS_SUCCEEDED, + Raw: raw, + } + + switch paymentType { + case models.PAYMENT_TYPE_PAYIN: + payment.DestinationAccountReference = &from.Reference + case models.PAYMENT_TYPE_PAYOUT: + payment.SourceAccountReference = &from.Reference + default: + if transaction.Credit { + payment.DestinationAccountReference = &from.Reference + } else { + payment.SourceAccountReference = &from.Reference + } + } + + return payment, nil +} + +func matchTransactionType(transactionType string) models.PaymentType { + if transactionType == "PI_REV" || + transactionType == "PO_REV" || + transactionType == "ADHOC" || + transactionType == "INT_INTERC" { + return models.PAYMENT_TYPE_OTHER + } + + if strings.HasPrefix(transactionType, "PI_") { + return models.PAYMENT_TYPE_PAYIN + } + + if strings.HasPrefix(transactionType, "PO_") { + return models.PAYMENT_TYPE_PAYOUT + } + + return models.PAYMENT_TYPE_OTHER +} diff --git a/components/payments/internal/connectors/plugins/public/modulr/plugin.go b/components/payments/internal/connectors/plugins/public/modulr/plugin.go new file mode 100644 index 0000000000..ee00c3159f --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/modulr/plugin.go @@ -0,0 +1,81 @@ +package modulr + +import ( + "context" + + "github.com/formancehq/payments/internal/connectors/plugins" + "github.com/formancehq/payments/internal/connectors/plugins/public/modulr/client" + "github.com/formancehq/payments/internal/models" +) + +type Plugin struct { + client *client.Client +} + +func (p *Plugin) Install(ctx context.Context, req models.InstallRequest) (models.InstallResponse, error) { + config, err := unmarshalAndValidateConfig(req.Config) + if err != nil { + return models.InstallResponse{}, err + } + + client, err := client.New(config.APIKey, config.APISecret, config.Endpoint) + if err != nil { + return models.InstallResponse{}, err + } + p.client = client + + return models.InstallResponse{ + Capabilities: capabilities, + Workflow: workflow(), + }, nil +} + +func (p Plugin) Uninstall(ctx context.Context, req models.UninstallRequest) (models.UninstallResponse, error) { + return models.UninstallResponse{}, nil +} + +func (p Plugin) FetchNextAccounts(ctx context.Context, req models.FetchNextAccountsRequest) (models.FetchNextAccountsResponse, error) { + if p.client == nil { + return models.FetchNextAccountsResponse{}, plugins.ErrNotYetInstalled + } + return p.fetchNextAccounts(ctx, req) +} + +func (p Plugin) FetchNextBalances(ctx context.Context, req models.FetchNextBalancesRequest) (models.FetchNextBalancesResponse, error) { + if p.client == nil { + return models.FetchNextBalancesResponse{}, plugins.ErrNotYetInstalled + } + return p.fetchNextBalances(ctx, req) +} + +func (p Plugin) FetchNextExternalAccounts(ctx context.Context, req models.FetchNextExternalAccountsRequest) (models.FetchNextExternalAccountsResponse, error) { + if p.client == nil { + return models.FetchNextExternalAccountsResponse{}, plugins.ErrNotYetInstalled + } + return p.fetchNextExternalAccounts(ctx, req) +} + +func (p Plugin) FetchNextPayments(ctx context.Context, req models.FetchNextPaymentsRequest) (models.FetchNextPaymentsResponse, error) { + if p.client == nil { + return models.FetchNextPaymentsResponse{}, plugins.ErrNotYetInstalled + } + return p.fetchNextPayments(ctx, req) +} + +func (p Plugin) FetchNextOthers(ctx context.Context, req models.FetchNextOthersRequest) (models.FetchNextOthersResponse, error) { + return models.FetchNextOthersResponse{}, plugins.ErrNotImplemented +} + +func (p Plugin) CreateBankAccount(ctx context.Context, req models.CreateBankAccountRequest) (models.CreateBankAccountResponse, error) { + return models.CreateBankAccountResponse{}, plugins.ErrNotImplemented +} + +func (p Plugin) CreateWebhooks(ctx context.Context, req models.CreateWebhooksRequest) (models.CreateWebhooksResponse, error) { + return models.CreateWebhooksResponse{}, plugins.ErrNotImplemented +} + +func (p Plugin) TranslateWebhook(ctx context.Context, req models.TranslateWebhookRequest) (models.TranslateWebhookResponse, error) { + return models.TranslateWebhookResponse{}, plugins.ErrNotImplemented +} + +var _ models.Plugin = &Plugin{} diff --git a/components/payments/internal/connectors/plugins/public/modulr/workflow.go b/components/payments/internal/connectors/plugins/public/modulr/workflow.go new file mode 100644 index 0000000000..5516a543af --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/modulr/workflow.go @@ -0,0 +1,33 @@ +package modulr + +import "github.com/formancehq/payments/internal/models" + +func workflow() models.Tasks { + return []models.TaskTree{ + { + TaskType: models.TASK_FETCH_ACCOUNTS, + Name: "fetch_accounts", + Periodically: true, + NextTasks: []models.TaskTree{ + { + TaskType: models.TASK_FETCH_PAYMENTS, + Name: "fetch_payments", + Periodically: true, + NextTasks: []models.TaskTree{}, + }, + { + TaskType: models.TASK_FETCH_BALANCES, + Name: "fetch_balances", + Periodically: true, + NextTasks: []models.TaskTree{}, + }, + }, + }, + { + TaskType: models.TASK_FETCH_EXTERNAL_ACCOUNTS, + Name: "fetch_beneficiaries", + Periodically: true, + NextTasks: []models.TaskTree{}, + }, + } +} diff --git a/components/payments/internal/connectors/plugins/public/moneycorp/accounts.go b/components/payments/internal/connectors/plugins/public/moneycorp/accounts.go new file mode 100644 index 0000000000..c484cc1914 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/moneycorp/accounts.go @@ -0,0 +1,90 @@ +package moneycorp + +import ( + "context" + "encoding/json" + "time" + + "github.com/formancehq/payments/internal/models" +) + +type accountsState struct { + LastPage int `json:"lastPage"` + // Moneycorp does not send the creation date for accounts, but we can still + // sort by ID created (which is incremental when creating accounts). + LastIDCreated string `json:"lastIDCreated"` +} + +func (p Plugin) fetchNextAccounts(ctx context.Context, req models.FetchNextAccountsRequest) (models.FetchNextAccountsResponse, error) { + var oldState accountsState + if req.State != nil { + if err := json.Unmarshal(req.State, &oldState); err != nil { + return models.FetchNextAccountsResponse{}, err + } + } + + newState := accountsState{ + LastPage: oldState.LastPage, + LastIDCreated: oldState.LastIDCreated, + } + + var accounts []models.PSPAccount + hasMore := false + for page := oldState.LastPage; ; page++ { + newState.LastPage = page + + pagedAccounts, err := p.client.GetAccounts(ctx, page, req.PageSize) + if err != nil { + return models.FetchNextAccountsResponse{}, err + } + + if len(pagedAccounts) == 0 { + break + } + + for _, account := range pagedAccounts { + if account.ID <= oldState.LastIDCreated { + continue + } + + raw, err := json.Marshal(account) + if err != nil { + return models.FetchNextAccountsResponse{}, err + } + + accounts = append(accounts, models.PSPAccount{ + Reference: account.ID, + // Moneycorp does not send the opening date of the account + CreatedAt: time.Now().UTC(), + Name: &account.Attributes.AccountName, + Raw: raw, + }) + + newState.LastIDCreated = account.ID + + if len(accounts) >= req.PageSize { + break + } + } + + if len(pagedAccounts) < req.PageSize { + break + } + + if len(accounts) >= req.PageSize { + hasMore = true + break + } + } + + payload, err := json.Marshal(newState) + if err != nil { + return models.FetchNextAccountsResponse{}, err + } + + return models.FetchNextAccountsResponse{ + Accounts: accounts, + NewState: payload, + HasMore: hasMore, + }, nil +} diff --git a/components/payments/internal/connectors/plugins/public/moneycorp/balances.go b/components/payments/internal/connectors/plugins/public/moneycorp/balances.go new file mode 100644 index 0000000000..9a7496fe7a --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/moneycorp/balances.go @@ -0,0 +1,52 @@ +package moneycorp + +import ( + "context" + "encoding/json" + "time" + + "github.com/formancehq/payments/internal/connectors/plugins/currency" + "github.com/formancehq/payments/internal/models" + "github.com/pkg/errors" +) + +func (p Plugin) fetchNextBalances(ctx context.Context, req models.FetchNextBalancesRequest) (models.FetchNextBalancesResponse, error) { + var from models.PSPAccount + if req.FromPayload == nil { + return models.FetchNextBalancesResponse{}, errors.New("missing from payload when fetching balances") + } + if err := json.Unmarshal(req.FromPayload, &from); err != nil { + return models.FetchNextBalancesResponse{}, err + } + + balances, err := p.client.GetAccountBalances(ctx, from.Reference) + if err != nil { + return models.FetchNextBalancesResponse{}, err + } + + var accountBalances []models.PSPBalance + for _, balance := range balances { + precision, err := currency.GetPrecision(supportedCurrenciesWithDecimal, balance.Attributes.CurrencyCode) + if err != nil { + return models.FetchNextBalancesResponse{}, err + } + + amount, err := currency.GetAmountWithPrecisionFromString(balance.Attributes.AvailableBalance.String(), precision) + if err != nil { + return models.FetchNextBalancesResponse{}, err + } + + accountBalances = append(accountBalances, models.PSPBalance{ + AccountReference: from.Reference, + CreatedAt: time.Now(), + Amount: amount, + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, balance.Attributes.CurrencyCode), + }) + } + + return models.FetchNextBalancesResponse{ + Balances: accountBalances, + NewState: []byte{}, + HasMore: false, + }, nil +} diff --git a/components/payments/internal/connectors/plugins/public/moneycorp/capabilities.go b/components/payments/internal/connectors/plugins/public/moneycorp/capabilities.go new file mode 100644 index 0000000000..dd38e8d5e3 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/moneycorp/capabilities.go @@ -0,0 +1,10 @@ +package moneycorp + +import "github.com/formancehq/payments/internal/models" + +var capabilities = []models.Capability{ + models.CAPABILITY_FETCH_ACCOUNTS, + models.CAPABILITY_FETCH_EXTERNAL_ACCOUNTS, + models.CAPABILITY_FETCH_PAYMENTS, + models.CAPABILITY_FETCH_BALANCES, +} diff --git a/components/payments/cmd/connectors/internal/connectors/moneycorp/client/accounts.go b/components/payments/internal/connectors/plugins/public/moneycorp/client/accounts.go similarity index 50% rename from components/payments/cmd/connectors/internal/connectors/moneycorp/client/accounts.go rename to components/payments/internal/connectors/plugins/public/moneycorp/client/accounts.go index c452a518f8..10e015ab1d 100644 --- a/components/payments/cmd/connectors/internal/connectors/moneycorp/client/accounts.go +++ b/components/payments/internal/connectors/plugins/public/moneycorp/client/accounts.go @@ -2,13 +2,11 @@ package client import ( "context" - "encoding/json" "fmt" "net/http" "strconv" - "time" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" + "github.com/formancehq/payments/internal/connectors/httpwrapper" ) type accountsResponse struct { @@ -23,9 +21,11 @@ type Account struct { } func (c *Client) GetAccounts(ctx context.Context, page int, pageSize int) ([]*Account, error) { - f := connectors.ClientMetrics(ctx, "moneycorp", "list_accounts") - now := time.Now() - defer f(ctx, now) + // TODO(polo, crimson): metrics + // metrics can also be embedded in wrapper + // f := connectors.ClientMetrics(ctx, "moneycorp", "list_accounts") + // now := time.Now() + // defer f(ctx, now) endpoint := fmt.Sprintf("%s/accounts", c.endpoint) req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody) @@ -33,6 +33,7 @@ func (c *Client) GetAccounts(ctx context.Context, page int, pageSize int) ([]*Ac return nil, fmt.Errorf("failed to create accounts request: %w", err) } + // TODO generic headers can be set in wrapper req.Header.Set("Content-Type", "application/json") q := req.URL.Query() @@ -41,30 +42,15 @@ func (c *Client) GetAccounts(ctx context.Context, page int, pageSize int) ([]*Ac q.Add("sortBy", "id.asc") req.URL.RawQuery = q.Encode() - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to get accounts: %w", err) + accounts := accountsResponse{Accounts: make([]*Account, 0)} + var errRes moneycorpError + _, err = c.httpClient.Do(req, &accounts, &errRes) + switch err { + case nil: + return accounts.Accounts, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, errRes.Error() } - - defer func() { - err = resp.Body.Close() - if err != nil { - c.logger.Error(err) - } - }() - - if resp.StatusCode == http.StatusNotFound { - return []*Account{}, nil - } - - if resp.StatusCode != http.StatusOK { - return nil, unmarshalErrorWithRetry(resp.StatusCode, resp.Body).Error() - } - - var accounts accountsResponse - if err := json.NewDecoder(resp.Body).Decode(&accounts); err != nil { - return nil, fmt.Errorf("failed to unmarshal accounts response body: %w", err) - } - - return accounts.Accounts, nil + return nil, fmt.Errorf("failed to get accounts: %w", err) } diff --git a/components/payments/cmd/connectors/internal/connectors/moneycorp/client/auth.go b/components/payments/internal/connectors/plugins/public/moneycorp/client/auth.go similarity index 85% rename from components/payments/cmd/connectors/internal/connectors/moneycorp/client/auth.go rename to components/payments/internal/connectors/plugins/public/moneycorp/client/auth.go index 2db18bc997..5bd5d4f2b5 100644 --- a/components/payments/cmd/connectors/internal/connectors/moneycorp/client/auth.go +++ b/components/payments/internal/connectors/plugins/public/moneycorp/client/auth.go @@ -8,8 +8,6 @@ import ( "net/http" "time" - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) @@ -17,8 +15,6 @@ import ( // is only accepting request with "application/json" content type, and the lib // sets it as application/x-www-form-urlencoded, giving us a 415 error. type apiTransport struct { - logger logging.Logger - clientID string apiKey string endpoint string @@ -70,9 +66,10 @@ func (t *apiTransport) login(ctx context.Context) error { return fmt.Errorf("failed to marshal login request: %w", err) } - f := connectors.ClientMetrics(ctx, "moneycorp", "login") - now := time.Now() - defer f(ctx, now) + // TODO(polo): re-introduce metrics + // f := connectors.ClientMetrics(ctx, "moneycorp", "login") + // now := time.Now() + // defer f(ctx, now) req, err := http.NewRequestWithContext(ctx, http.MethodPost, t.endpoint+"/login", bytes.NewBuffer(requestBody)) @@ -82,6 +79,7 @@ func (t *apiTransport) login(ctx context.Context) error { req.Header.Set("Content-Type", "application/json") + // TODO: default client doesn't have a timeout, so we should be careful about using it here resp, err := http.DefaultClient.Do(req) if err != nil { return fmt.Errorf("failed to login: %w", err) @@ -90,12 +88,14 @@ func (t *apiTransport) login(ctx context.Context) error { defer func() { err = resp.Body.Close() if err != nil { - t.logger.Error(err) + // TODO(polo): log the error + _ = err } }() if resp.StatusCode != http.StatusOK { - return unmarshalErrorWithRetry(resp.StatusCode, resp.Body).Error() + // TODO(polo): retryable errors + return unmarshalError(resp.StatusCode, resp.Body).Error() } var res loginResponse diff --git a/components/payments/cmd/connectors/internal/connectors/moneycorp/client/balances.go b/components/payments/internal/connectors/plugins/public/moneycorp/client/balances.go similarity index 54% rename from components/payments/cmd/connectors/internal/connectors/moneycorp/client/balances.go rename to components/payments/internal/connectors/plugins/public/moneycorp/client/balances.go index 267193d821..d104b5edc7 100644 --- a/components/payments/cmd/connectors/internal/connectors/moneycorp/client/balances.go +++ b/components/payments/internal/connectors/plugins/public/moneycorp/client/balances.go @@ -5,9 +5,8 @@ import ( "encoding/json" "fmt" "net/http" - "time" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" + "github.com/formancehq/payments/internal/connectors/httpwrapper" ) type balancesResponse struct { @@ -27,9 +26,10 @@ type Balance struct { } func (c *Client) GetAccountBalances(ctx context.Context, accountID string) ([]*Balance, error) { - f := connectors.ClientMetrics(ctx, "moneycorp", "list_account_balances") - now := time.Now() - defer f(ctx, now) + // TODO(polo): metrics + // f := connectors.ClientMetrics(ctx, "moneycorp", "list_account_balances") + // now := time.Now() + // defer f(ctx, now) endpoint := fmt.Sprintf("%s/accounts/%s/balances", c.endpoint, accountID) req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody) @@ -38,30 +38,16 @@ func (c *Client) GetAccountBalances(ctx context.Context, accountID string) ([]*B } req.Header.Set("Content-Type", "application/json") - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to get account balances: %w", err) - } - - defer func() { - err = resp.Body.Close() - if err != nil { - c.logger.Error(err) - } - }() - - if resp.StatusCode == http.StatusNotFound { - return []*Balance{}, nil - } - - if resp.StatusCode != http.StatusOK { - return nil, unmarshalErrorWithRetry(resp.StatusCode, resp.Body).Error() - } + balances := balancesResponse{Balances: make([]*Balance, 0)} + var errRes moneycorpError - var balances balancesResponse - if err := json.NewDecoder(resp.Body).Decode(&balances); err != nil { - return nil, fmt.Errorf("failed to unmarshal balances response body: %w", err) + _, err = c.httpClient.Do(req, &balances, &errRes) + switch err { + case nil: + return balances.Balances, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, errRes.Error() } - - return balances.Balances, nil + return nil, fmt.Errorf("failed to get account balances: %w", err) } diff --git a/components/payments/internal/connectors/plugins/public/moneycorp/client/client.go b/components/payments/internal/connectors/plugins/public/moneycorp/client/client.go new file mode 100644 index 0000000000..d524d6992d --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/moneycorp/client/client.go @@ -0,0 +1,43 @@ +package client + +import ( + "net/http" + "strings" + + "github.com/formancehq/payments/internal/connectors/httpwrapper" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" +) + +type Client struct { + httpClient httpwrapper.Client + endpoint string +} + +func New(clientID, apiKey, endpoint string) (*Client, error) { + config := &httpwrapper.Config{ + Transport: &apiTransport{ + clientID: clientID, + apiKey: apiKey, + endpoint: endpoint, + underlying: otelhttp.NewTransport(http.DefaultTransport), + }, + HttpErrorCheckerFn: func(statusCode int) error { + if statusCode == http.StatusNotFound { + return nil + } + if statusCode >= http.StatusBadRequest { + return httpwrapper.ErrStatusCodeUnexpected + } + return nil + + }, + } + endpoint = strings.TrimSuffix(endpoint, "/") + + httpClient, err := httpwrapper.NewClient(config) + c := &Client{ + httpClient: httpClient, + endpoint: endpoint, + } + return c, err +} diff --git a/components/payments/internal/connectors/plugins/public/moneycorp/client/error.go b/components/payments/internal/connectors/plugins/public/moneycorp/client/error.go new file mode 100644 index 0000000000..b82e77f6bc --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/moneycorp/client/error.go @@ -0,0 +1,51 @@ +package client + +import ( + "encoding/json" + "fmt" + "io" +) + +// TODO(polo): add retryable errors with temporal + +type moneycorpErrors struct { + Errors []*moneycorpError `json:"errors"` +} + +type moneycorpError struct { + StatusCode int `json:"-"` + Code string `json:"code"` + Title string `json:"title"` + Detail string `json:"detail"` + // WithRetry bool `json:"-"` +} + +func (me *moneycorpError) Error() error { + var err error + if me.Detail == "" { + err = fmt.Errorf("unexpected status code: %d", me.StatusCode) + } else { + err = fmt.Errorf("%d: %s", me.StatusCode, me.Detail) + } + + return err +} + +func unmarshalError(statusCode int, body io.ReadCloser) *moneycorpError { + var ces moneycorpErrors + _ = json.NewDecoder(body).Decode(&ces) + + if len(ces.Errors) == 0 { + return &moneycorpError{ + StatusCode: statusCode, + // WithRetry: withRetry, + } + } + + return &moneycorpError{ + StatusCode: statusCode, + Code: ces.Errors[0].Code, + Title: ces.Errors[0].Title, + Detail: ces.Errors[0].Detail, + } +} diff --git a/components/payments/cmd/connectors/internal/connectors/moneycorp/client/recipients.go b/components/payments/internal/connectors/plugins/public/moneycorp/client/recipients.go similarity index 54% rename from components/payments/cmd/connectors/internal/connectors/moneycorp/client/recipients.go rename to components/payments/internal/connectors/plugins/public/moneycorp/client/recipients.go index ace7da2059..8d19d468d3 100644 --- a/components/payments/cmd/connectors/internal/connectors/moneycorp/client/recipients.go +++ b/components/payments/internal/connectors/plugins/public/moneycorp/client/recipients.go @@ -2,13 +2,11 @@ package client import ( "context" - "encoding/json" "fmt" "net/http" "strconv" - "time" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" + "github.com/formancehq/payments/internal/connectors/httpwrapper" ) type recipientsResponse struct { @@ -25,9 +23,10 @@ type Recipient struct { } func (c *Client) GetRecipients(ctx context.Context, accountID string, page int, pageSize int) ([]*Recipient, error) { - f := connectors.ClientMetrics(ctx, "moneycorp", "list_recipients") - now := time.Now() - defer f(ctx, now) + // TODO(polo): add metrics + // f := connectors.ClientMetrics(ctx, "moneycorp", "list_recipients") + // now := time.Now() + // defer f(ctx, now) endpoint := fmt.Sprintf("%s/accounts/%s/recipients", c.endpoint, accountID) req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody) @@ -43,30 +42,15 @@ func (c *Client) GetRecipients(ctx context.Context, accountID string, page int, q.Add("sortBy", "createdAt.asc") req.URL.RawQuery = q.Encode() - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to get accounts: %w", err) + recipients := recipientsResponse{Recipients: make([]*Recipient, 0)} + var errRes moneycorpError + _, err = c.httpClient.Do(req, &recipients, &errRes) + switch err { + case nil: + return recipients.Recipients, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, errRes.Error() } - - defer func() { - err = resp.Body.Close() - if err != nil { - c.logger.Error(err) - } - }() - - if resp.StatusCode == http.StatusNotFound { - return []*Recipient{}, nil - } - - if resp.StatusCode != http.StatusOK { - return nil, unmarshalErrorWithRetry(resp.StatusCode, resp.Body).Error() - } - - var recipients recipientsResponse - if err := json.NewDecoder(resp.Body).Decode(&recipients); err != nil { - return nil, fmt.Errorf("failed to unmarshal recipients response body: %w", err) - } - - return recipients.Recipients, nil + return nil, fmt.Errorf("failed to get recipients %w", err) } diff --git a/components/payments/cmd/connectors/internal/connectors/moneycorp/client/transactions.go b/components/payments/internal/connectors/plugins/public/moneycorp/client/transactions.go similarity index 71% rename from components/payments/cmd/connectors/internal/connectors/moneycorp/client/transactions.go rename to components/payments/internal/connectors/plugins/public/moneycorp/client/transactions.go index 48b56ff493..60b14d3c59 100644 --- a/components/payments/cmd/connectors/internal/connectors/moneycorp/client/transactions.go +++ b/components/payments/internal/connectors/plugins/public/moneycorp/client/transactions.go @@ -10,7 +10,7 @@ import ( "strconv" "time" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" + "github.com/formancehq/payments/internal/connectors/httpwrapper" ) type transactionsResponse struct { @@ -41,9 +41,10 @@ type Transaction struct { } func (c *Client) GetTransactions(ctx context.Context, accountID string, page, pageSize int, lastCreatedAt time.Time) ([]*Transaction, error) { - f := connectors.ClientMetrics(ctx, "moneycorp", "list_transactions") - now := time.Now() - defer f(ctx, now) + // TODO(polo): metrics + // f := connectors.ClientMetrics(ctx, "moneycorp", "list_transactions") + // now := time.Now() + // defer f(ctx, now) var body io.Reader if !lastCreatedAt.IsZero() { @@ -74,7 +75,7 @@ func (c *Client) GetTransactions(ctx context.Context, accountID string, page, pa endpoint := fmt.Sprintf("%s/accounts/%s/transactions/find", c.endpoint, accountID) req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body) if err != nil { - return nil, fmt.Errorf("failed to create login request: %w", err) + return nil, fmt.Errorf("failed to create transactions request: %w", err) } req.Header.Set("Content-Type", "application/json") @@ -85,30 +86,15 @@ func (c *Client) GetTransactions(ctx context.Context, accountID string, page, pa q.Add("sortBy", "createdAt.asc") req.URL.RawQuery = q.Encode() - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to get transactions: %w", err) - } - - defer func() { - err = resp.Body.Close() - if err != nil { - c.logger.Error(err) - } - }() - - if resp.StatusCode == http.StatusNotFound { - return []*Transaction{}, nil + transactions := transactionsResponse{Transactions: make([]*Transaction, 0)} + var errRes moneycorpError + _, err = c.httpClient.Do(req, &transactions, &errRes) + switch err { + case nil: + return transactions.Transactions, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, errRes.Error() } - - if resp.StatusCode != http.StatusOK { - return nil, unmarshalErrorWithRetry(resp.StatusCode, resp.Body).Error() - } - - var transactions transactionsResponse - if err := json.NewDecoder(resp.Body).Decode(&transactions); err != nil { - return nil, fmt.Errorf("failed to unmarshal transactions response body: %w", err) - } - - return transactions.Transactions, nil + return nil, fmt.Errorf("failed to get transactions %w", err) } diff --git a/components/payments/internal/connectors/plugins/public/moneycorp/cmd/main.go b/components/payments/internal/connectors/plugins/public/moneycorp/cmd/main.go new file mode 100644 index 0000000000..a9be4bc2bd --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/moneycorp/cmd/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "github.com/formancehq/payments/internal/connectors/grpc" + "github.com/formancehq/payments/internal/connectors/plugins" + "github.com/formancehq/payments/internal/connectors/plugins/public/moneycorp" + "github.com/hashicorp/go-plugin" +) + +func main() { + // TODO(polo): metrics + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: grpc.Handshake, + Plugins: map[string]plugin.Plugin{ + "psp": &grpc.PSPGRPCPlugin{Impl: plugins.NewGRPCImplem(&moneycorp.Plugin{})}, + }, + + // A non-nil value here enables gRPC serving for this plugin... + GRPCServer: plugin.DefaultGRPCServer, + }) +} diff --git a/components/payments/internal/connectors/plugins/public/moneycorp/config.go b/components/payments/internal/connectors/plugins/public/moneycorp/config.go new file mode 100644 index 0000000000..b5a439a187 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/moneycorp/config.go @@ -0,0 +1,39 @@ +package moneycorp + +import ( + "encoding/json" + + "github.com/formancehq/payments/internal/models" + "github.com/pkg/errors" +) + +type Config struct { + ClientID string `json:"clientID"` + APIKey string `json:"apiKey"` + Endpoint string `json:"endpoint"` +} + +func (c Config) validate() error { + if c.ClientID == "" { + return errors.Wrap(models.ErrInvalidConfig, "missing clientID in config") + } + + if c.APIKey == "" { + return errors.Wrap(models.ErrInvalidConfig, "missing api key in config") + } + + if c.Endpoint == "" { + return errors.Wrap(models.ErrInvalidConfig, "missing endpoint in config") + } + + return nil +} + +func unmarshalAndValidateConfig(payload json.RawMessage) (Config, error) { + var config Config + if err := json.Unmarshal(payload, &config); err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + return config, config.validate() +} diff --git a/components/payments/internal/connectors/plugins/public/moneycorp/config.json b/components/payments/internal/connectors/plugins/public/moneycorp/config.json new file mode 100644 index 0000000000..59bacea9dc --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/moneycorp/config.json @@ -0,0 +1,17 @@ +{ + "clientID": { + "dataType": "string", + "required": true, + "defaultValue": "" + }, + "apiKey": { + "dataType": "string", + "required": true, + "defaultValue": "" + }, + "endpoint": { + "dataType": "string", + "required": true, + "defaultValue": "" + } +} \ No newline at end of file diff --git a/components/payments/cmd/connectors/internal/connectors/moneycorp/currencies.go b/components/payments/internal/connectors/plugins/public/moneycorp/currencies.go similarity index 97% rename from components/payments/cmd/connectors/internal/connectors/moneycorp/currencies.go rename to components/payments/internal/connectors/plugins/public/moneycorp/currencies.go index f0bff20789..da428d54dc 100644 --- a/components/payments/cmd/connectors/internal/connectors/moneycorp/currencies.go +++ b/components/payments/internal/connectors/plugins/public/moneycorp/currencies.go @@ -1,6 +1,6 @@ package moneycorp -import "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" +import "github.com/formancehq/payments/internal/connectors/plugins/currency" var ( supportedCurrenciesWithDecimal = map[string]int{ diff --git a/components/payments/internal/connectors/plugins/public/moneycorp/external_accounts.go b/components/payments/internal/connectors/plugins/public/moneycorp/external_accounts.go new file mode 100644 index 0000000000..c4b3a1c4a9 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/moneycorp/external_accounts.go @@ -0,0 +1,110 @@ +package moneycorp + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/connectors/plugins/currency" + "github.com/formancehq/payments/internal/models" + "github.com/pkg/errors" +) + +type externalAccountsState struct { + LastPage int `json:"last_page"` + // Moneycorp does not allow us to sort by , but we can still + // sort by ID created (which is incremental when creating accounts). + LastCreatedAt time.Time `json:"last_created_at"` +} + +func (p Plugin) fetchNextExternalAccounts(ctx context.Context, req models.FetchNextExternalAccountsRequest) (models.FetchNextExternalAccountsResponse, error) { + var oldState externalAccountsState + if req.State != nil { + if err := json.Unmarshal(req.State, &oldState); err != nil { + return models.FetchNextExternalAccountsResponse{}, err + } + } + + var from models.PSPAccount + if req.FromPayload == nil { + return models.FetchNextExternalAccountsResponse{}, errors.New("missing from payload when fetching external accounts") + } + if err := json.Unmarshal(req.FromPayload, &from); err != nil { + return models.FetchNextExternalAccountsResponse{}, err + } + + newState := externalAccountsState{ + LastPage: oldState.LastPage, + LastCreatedAt: oldState.LastCreatedAt, + } + + var accounts []models.PSPAccount + hasMore := false + for page := oldState.LastPage; ; page++ { + newState.LastPage = page + + pagedRecipients, err := p.client.GetRecipients(ctx, from.Reference, page, req.PageSize) + if err != nil { + return models.FetchNextExternalAccountsResponse{}, err + } + + if len(pagedRecipients) == 0 { + break + } + + for _, recipient := range pagedRecipients { + createdAt, err := time.Parse("2006-01-02T15:04:05.999999999", recipient.Attributes.CreatedAt) + if err != nil { + return models.FetchNextExternalAccountsResponse{}, fmt.Errorf("failed to parse transaction date: %v", err) + } + + switch createdAt.Compare(oldState.LastCreatedAt) { + case -1, 0: + continue + default: + } + + raw, err := json.Marshal(recipient) + if err != nil { + return models.FetchNextExternalAccountsResponse{}, err + } + + accounts = append(accounts, models.PSPAccount{ + Reference: recipient.ID, + // Moneycorp does not send the opening date of the account + CreatedAt: createdAt, + Name: &recipient.Attributes.BankAccountName, + DefaultAsset: pointer.For(currency.FormatAsset(supportedCurrenciesWithDecimal, recipient.Attributes.BankAccountCurrency)), + Raw: raw, + }) + + newState.LastCreatedAt = createdAt + + if len(accounts) >= req.PageSize { + break + } + } + + if len(accounts) >= req.PageSize { + hasMore = true + break + } + + if len(pagedRecipients) < req.PageSize { + break + } + } + + payload, err := json.Marshal(newState) + if err != nil { + return models.FetchNextExternalAccountsResponse{}, err + } + + return models.FetchNextExternalAccountsResponse{ + ExternalAccounts: accounts, + NewState: payload, + HasMore: hasMore, + }, nil +} diff --git a/components/payments/internal/connectors/plugins/public/moneycorp/payments.go b/components/payments/internal/connectors/plugins/public/moneycorp/payments.go new file mode 100644 index 0000000000..36f9625b9b --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/moneycorp/payments.go @@ -0,0 +1,172 @@ +package moneycorp + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strconv" + "time" + + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/connectors/plugins/currency" + "github.com/formancehq/payments/internal/connectors/plugins/public/moneycorp/client" + "github.com/formancehq/payments/internal/models" +) + +type paymentsState struct { + LastCreatedAt time.Time `json:"lastCreatedAt"` +} + +func (p Plugin) fetchNextPayments(ctx context.Context, req models.FetchNextPaymentsRequest) (models.FetchNextPaymentsResponse, error) { + var oldState paymentsState + if req.State != nil { + if err := json.Unmarshal(req.State, &oldState); err != nil { + return models.FetchNextPaymentsResponse{}, err + } + } + + var from models.PSPAccount + if req.FromPayload == nil { + return models.FetchNextPaymentsResponse{}, errors.New("missing from payload when fetching payments") + } + if err := json.Unmarshal(req.FromPayload, &from); err != nil { + return models.FetchNextPaymentsResponse{}, err + } + + newState := paymentsState{ + LastCreatedAt: oldState.LastCreatedAt, + } + + var payments []models.PSPPayment + hasMore := false + for page := 0; ; page++ { + pagedTransactions, err := p.client.GetTransactions(ctx, from.Reference, page, req.PageSize, oldState.LastCreatedAt) + if err != nil { + // retryable error already handled by the client + return models.FetchNextPaymentsResponse{}, err + } + + if len(pagedTransactions) == 0 { + break + } + + for _, transaction := range pagedTransactions { + createdAt, err := time.Parse("2006-01-02T15:04:05.999999999", transaction.Attributes.CreatedAt) + if err != nil { + return models.FetchNextPaymentsResponse{}, fmt.Errorf("failed to parse transaction date: %v", err) + } + + switch createdAt.Compare(oldState.LastCreatedAt) { + case -1, 0: + continue + default: + } + + newState.LastCreatedAt = createdAt + + payment, err := transactionToPayment(transaction) + if err != nil { + return models.FetchNextPaymentsResponse{}, err + } + + if payment != nil { + payments = append(payments, *payment) + } + + if len(payments) == req.PageSize { + break + } + } + + if len(pagedTransactions) < req.PageSize { + break + } + + if len(payments) == req.PageSize { + hasMore = true + break + } + } + + payload, err := json.Marshal(newState) + if err != nil { + return models.FetchNextPaymentsResponse{}, err + } + + return models.FetchNextPaymentsResponse{ + Payments: payments, + NewState: payload, + HasMore: hasMore, + }, nil +} + +func transactionToPayment(transaction *client.Transaction) (*models.PSPPayment, error) { + rawData, err := json.Marshal(transaction) + if err != nil { + return nil, fmt.Errorf("failed to marshal transaction: %w", err) + } + + paymentType, shouldBeRecorded := matchPaymentType(transaction.Attributes.Type, transaction.Attributes.Direction) + if !shouldBeRecorded { + return nil, nil + } + + createdAt, err := time.Parse("2006-01-02T15:04:05.999999999", transaction.Attributes.CreatedAt) + if err != nil { + return nil, fmt.Errorf("failed to parse transaction date: %w", err) + } + + c, err := currency.GetPrecision(supportedCurrenciesWithDecimal, transaction.Attributes.Currency) + if err != nil { + return nil, err + } + + amount, err := currency.GetAmountWithPrecisionFromString(transaction.Attributes.Amount.String(), c) + if err != nil { + return nil, err + } + + payment := models.PSPPayment{ + Reference: transaction.ID, + CreatedAt: createdAt, + Type: paymentType, + Amount: amount, + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transaction.Attributes.Currency), + Scheme: models.PAYMENT_SCHEME_OTHER, + Status: models.PAYMENT_STATUS_SUCCEEDED, + Metadata: map[string]string{}, + Raw: rawData, + } + + switch paymentType { + case models.PAYMENT_TYPE_PAYIN: + payment.DestinationAccountReference = pointer.For(strconv.Itoa(int(transaction.Attributes.AccountID))) + case models.PAYMENT_TYPE_PAYOUT: + payment.SourceAccountReference = pointer.For(strconv.Itoa(int(transaction.Attributes.AccountID))) + default: + if transaction.Attributes.Direction == "Debit" { + payment.SourceAccountReference = pointer.For(strconv.Itoa(int(transaction.Attributes.AccountID))) + } else { + payment.DestinationAccountReference = pointer.For(strconv.Itoa(int(transaction.Attributes.AccountID))) + } + } + + return &payment, nil +} + +func matchPaymentType(transactionType string, transactionDirection string) (models.PaymentType, bool) { + switch transactionType { + case "Transfer": + return models.PAYMENT_TYPE_TRANSFER, true + case "Payment", "Exchange", "Charge", "Refund": + switch transactionDirection { + case "Debit": + return models.PAYMENT_TYPE_PAYOUT, true + case "Credit": + return models.PAYMENT_TYPE_PAYIN, true + } + } + + return models.PAYMENT_TYPE_OTHER, false +} diff --git a/components/payments/internal/connectors/plugins/public/moneycorp/plugin.go b/components/payments/internal/connectors/plugins/public/moneycorp/plugin.go new file mode 100644 index 0000000000..cd58829a9c --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/moneycorp/plugin.go @@ -0,0 +1,81 @@ +package moneycorp + +import ( + "context" + + "github.com/formancehq/payments/internal/connectors/plugins" + "github.com/formancehq/payments/internal/connectors/plugins/public/moneycorp/client" + "github.com/formancehq/payments/internal/models" +) + +type Plugin struct { + client *client.Client +} + +func (p *Plugin) Install(_ context.Context, req models.InstallRequest) (models.InstallResponse, error) { + config, err := unmarshalAndValidateConfig(req.Config) + if err != nil { + return models.InstallResponse{}, err + } + + client, err := client.New(config.ClientID, config.APIKey, config.Endpoint) + if err != nil { + return models.InstallResponse{}, err + } + p.client = client + + return models.InstallResponse{ + Capabilities: capabilities, + Workflow: workflow(), + }, nil +} + +func (p Plugin) Uninstall(ctx context.Context, req models.UninstallRequest) (models.UninstallResponse, error) { + return models.UninstallResponse{}, nil +} + +func (p Plugin) FetchNextAccounts(ctx context.Context, req models.FetchNextAccountsRequest) (models.FetchNextAccountsResponse, error) { + if p.client == nil { + return models.FetchNextAccountsResponse{}, plugins.ErrNotYetInstalled + } + return p.fetchNextAccounts(ctx, req) +} + +func (p Plugin) FetchNextBalances(ctx context.Context, req models.FetchNextBalancesRequest) (models.FetchNextBalancesResponse, error) { + if p.client == nil { + return models.FetchNextBalancesResponse{}, plugins.ErrNotYetInstalled + } + return p.fetchNextBalances(ctx, req) +} + +func (p Plugin) FetchNextExternalAccounts(ctx context.Context, req models.FetchNextExternalAccountsRequest) (models.FetchNextExternalAccountsResponse, error) { + if p.client == nil { + return models.FetchNextExternalAccountsResponse{}, plugins.ErrNotYetInstalled + } + return p.fetchNextExternalAccounts(ctx, req) +} + +func (p Plugin) FetchNextPayments(ctx context.Context, req models.FetchNextPaymentsRequest) (models.FetchNextPaymentsResponse, error) { + if p.client == nil { + return models.FetchNextPaymentsResponse{}, plugins.ErrNotYetInstalled + } + return p.fetchNextPayments(ctx, req) +} + +func (p Plugin) FetchNextOthers(ctx context.Context, req models.FetchNextOthersRequest) (models.FetchNextOthersResponse, error) { + return models.FetchNextOthersResponse{}, plugins.ErrNotImplemented +} + +func (p Plugin) CreateBankAccount(ctx context.Context, req models.CreateBankAccountRequest) (models.CreateBankAccountResponse, error) { + return models.CreateBankAccountResponse{}, plugins.ErrNotImplemented +} + +func (p Plugin) CreateWebhooks(ctx context.Context, req models.CreateWebhooksRequest) (models.CreateWebhooksResponse, error) { + return models.CreateWebhooksResponse{}, plugins.ErrNotImplemented +} + +func (p Plugin) TranslateWebhook(ctx context.Context, req models.TranslateWebhookRequest) (models.TranslateWebhookResponse, error) { + return models.TranslateWebhookResponse{}, plugins.ErrNotImplemented +} + +var _ models.Plugin = &Plugin{} diff --git a/components/payments/internal/connectors/plugins/public/moneycorp/workflow.go b/components/payments/internal/connectors/plugins/public/moneycorp/workflow.go new file mode 100644 index 0000000000..eefb13cc84 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/moneycorp/workflow.go @@ -0,0 +1,33 @@ +package moneycorp + +import "github.com/formancehq/payments/internal/models" + +func workflow() models.Tasks { + return []models.TaskTree{ + { + TaskType: models.TASK_FETCH_ACCOUNTS, + Name: "fetch_accounts", + Periodically: true, + NextTasks: []models.TaskTree{ + { + TaskType: models.TASK_FETCH_BALANCES, + Name: "fetch_balances", + Periodically: true, + NextTasks: []models.TaskTree{}, + }, + { + TaskType: models.TASK_FETCH_PAYMENTS, + Name: "fetch_payments", + Periodically: true, + NextTasks: []models.TaskTree{}, + }, + { + TaskType: models.TASK_FETCH_EXTERNAL_ACCOUNTS, + Name: "fetch_recipients", + Periodically: true, + NextTasks: []models.TaskTree{}, + }, + }, + }, + } +} diff --git a/components/payments/internal/connectors/plugins/public/wise/accounts.go b/components/payments/internal/connectors/plugins/public/wise/accounts.go new file mode 100644 index 0000000000..62844287b8 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/wise/accounts.go @@ -0,0 +1,87 @@ +package wise + +import ( + "context" + "encoding/json" + "errors" + "strconv" + + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/connectors/plugins/currency" + "github.com/formancehq/payments/internal/connectors/plugins/public/wise/client" + "github.com/formancehq/payments/internal/models" +) + +type accountsState struct { + // Accounts are ordered by their ID + LastAccountID uint64 `json:"lastAccountID"` +} + +func (p Plugin) fetchNextAccounts(ctx context.Context, req models.FetchNextAccountsRequest) (models.FetchNextAccountsResponse, error) { + var oldState accountsState + if req.State != nil { + if err := json.Unmarshal(req.State, &oldState); err != nil { + return models.FetchNextAccountsResponse{}, err + } + } + + var from client.Profile + if req.FromPayload == nil { + return models.FetchNextAccountsResponse{}, errors.New("missing from payload when fetching accounts") + } + if err := json.Unmarshal(req.FromPayload, &from); err != nil { + return models.FetchNextAccountsResponse{}, err + } + + newState := accountsState{ + LastAccountID: oldState.LastAccountID, + } + + var accounts []models.PSPAccount + hasMore := false + // Wise balances are considered as accounts on our side. + balances, err := p.client.GetBalances(ctx, from.ID) + if err != nil { + return models.FetchNextAccountsResponse{}, err + } + + for _, balance := range balances { + if balance.ID <= oldState.LastAccountID { + continue + } + + raw, err := json.Marshal(balance) + if err != nil { + return models.FetchNextAccountsResponse{}, err + } + + accounts = append(accounts, models.PSPAccount{ + Reference: strconv.FormatUint(balance.ID, 10), + CreatedAt: balance.CreationTime, + Name: &balance.Name, + DefaultAsset: pointer.For(currency.FormatAsset(supportedCurrenciesWithDecimal, balance.Amount.Currency)), + Metadata: map[string]string{ + metadataProfileIDKey: strconv.FormatUint(from.ID, 10), + }, + Raw: raw, + }) + + newState.LastAccountID = balance.ID + + if len(accounts) >= req.PageSize { + hasMore = true + break + } + } + + payload, err := json.Marshal(newState) + if err != nil { + return models.FetchNextAccountsResponse{}, err + } + + return models.FetchNextAccountsResponse{ + Accounts: accounts, + NewState: payload, + HasMore: hasMore, + }, nil +} diff --git a/components/payments/internal/connectors/plugins/public/wise/balances.go b/components/payments/internal/connectors/plugins/public/wise/balances.go new file mode 100644 index 0000000000..e46e48757b --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/wise/balances.go @@ -0,0 +1,64 @@ +package wise + +import ( + "context" + "encoding/json" + "errors" + "strconv" + + "github.com/formancehq/payments/internal/connectors/plugins/currency" + "github.com/formancehq/payments/internal/models" +) + +func (p Plugin) fetchNextBalances(ctx context.Context, req models.FetchNextBalancesRequest) (models.FetchNextBalancesResponse, error) { + var from models.PSPAccount + if req.FromPayload == nil { + return models.FetchNextBalancesResponse{}, errors.New("missing from payload when fetching balances") + } + if err := json.Unmarshal(req.FromPayload, &from); err != nil { + return models.FetchNextBalancesResponse{}, err + } + + balanceID, err := strconv.ParseUint(from.Reference, 10, 64) + if err != nil { + return models.FetchNextBalancesResponse{}, err + } + + pID, ok := from.Metadata[metadataProfileIDKey] + if !ok { + return models.FetchNextBalancesResponse{}, errors.New("missing profile ID in from payload when fetching balances") + } + + profileID, err := strconv.ParseUint(pID, 10, 64) + if err != nil { + return models.FetchNextBalancesResponse{}, err + } + + balance, err := p.client.GetBalance(ctx, profileID, balanceID) + if err != nil { + return models.FetchNextBalancesResponse{}, err + } + + precision, ok := supportedCurrenciesWithDecimal[balance.Amount.Currency] + if !ok { + return models.FetchNextBalancesResponse{}, errors.New("unsupported currency") + } + + amount, err := currency.GetAmountWithPrecisionFromString(balance.Amount.Value.String(), precision) + if err != nil { + return models.FetchNextBalancesResponse{}, err + } + + return models.FetchNextBalancesResponse{ + Balances: []models.PSPBalance{ + { + AccountReference: from.Reference, + CreatedAt: balance.ModificationTime, + Amount: amount, + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, balance.Amount.Currency), + }, + }, + NewState: []byte{}, + HasMore: false, + }, nil +} diff --git a/components/payments/internal/connectors/plugins/public/wise/capabilities.go b/components/payments/internal/connectors/plugins/public/wise/capabilities.go new file mode 100644 index 0000000000..8463ea3501 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/wise/capabilities.go @@ -0,0 +1,11 @@ +package wise + +import "github.com/formancehq/payments/internal/models" + +var capabilities = []models.Capability{ + models.CAPABILITY_FETCH_ACCOUNTS, + models.CAPABILITY_FETCH_OTHERS, + models.CAPABILITY_FETCH_EXTERNAL_ACCOUNTS, + models.CAPABILITY_FETCH_PAYMENTS, + models.CAPABILITY_FETCH_BALANCES, +} diff --git a/components/payments/internal/connectors/plugins/public/wise/client/balances.go b/components/payments/internal/connectors/plugins/public/wise/client/balances.go new file mode 100644 index 0000000000..0c974e897c --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/wise/client/balances.go @@ -0,0 +1,87 @@ +package client + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/formancehq/payments/internal/connectors/httpwrapper" +) + +type Balance struct { + ID uint64 `json:"id"` + Currency string `json:"currency"` + Type string `json:"type"` + Name string `json:"name"` + Amount struct { + Value json.Number `json:"value"` + Currency string `json:"currency"` + } `json:"amount"` + ReservedAmount struct { + Value json.Number `json:"value"` + Currency string `json:"currency"` + } `json:"reservedAmount"` + CashAmount struct { + Value json.Number `json:"value"` + Currency string `json:"currency"` + } `json:"cashAmount"` + TotalWorth struct { + Value json.Number `json:"value"` + Currency string `json:"currency"` + } `json:"totalWorth"` + CreationTime time.Time `json:"creationTime"` + ModificationTime time.Time `json:"modificationTime"` + Visible bool `json:"visible"` +} + +func (c *Client) GetBalances(ctx context.Context, profileID uint64) ([]Balance, error) { + // TODO(polo): metrics + // f := connectors.ClientMetrics(ctx, "wise", "list_balances") + // now := time.Now() + // defer f(ctx, now) + + req, err := http.NewRequestWithContext(ctx, + http.MethodGet, c.endpoint(fmt.Sprintf("v4/profiles/%d/balances?types=STANDARD", profileID)), http.NoBody) + if err != nil { + return nil, err + } + + var balances []Balance + var errRes wiseErrors + statusCode, err := c.httpClient.Do(req, &balances, &errRes) + switch err { + case nil: + return balances, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return balances, errRes.Error(statusCode).Error() + } + return balances, fmt.Errorf("failed to get balances: %w", err) +} + +func (c *Client) GetBalance(ctx context.Context, profileID uint64, balanceID uint64) (*Balance, error) { + // TODO(polo): metrics + // f := connectors.ClientMetrics(ctx, "wise", "list_balances") + // now := time.Now() + // defer f(ctx, now) + + req, err := http.NewRequestWithContext(ctx, + http.MethodGet, c.endpoint(fmt.Sprintf("v4/profiles/%d/balances/%d", profileID, balanceID)), http.NoBody) + if err != nil { + return nil, err + } + + var balance Balance + var errRes wiseErrors + statusCode, err := c.httpClient.Do(req, &balance, &errRes) + switch err { + case nil: + return &balance, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, errRes.Error(statusCode).Error() + } + return nil, fmt.Errorf("failed to get balances: %w", err) +} diff --git a/components/payments/cmd/connectors/internal/connectors/wise/client/client.go b/components/payments/internal/connectors/plugins/public/wise/client/client.go similarity index 80% rename from components/payments/cmd/connectors/internal/connectors/wise/client/client.go rename to components/payments/internal/connectors/plugins/public/wise/client/client.go index bfe308c044..c43b1ad54c 100644 --- a/components/payments/cmd/connectors/internal/connectors/wise/client/client.go +++ b/components/payments/internal/connectors/plugins/public/wise/client/client.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" + "github.com/formancehq/payments/internal/connectors/httpwrapper" lru "github.com/hashicorp/golang-lru/v2" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) @@ -22,7 +23,7 @@ func (t *apiTransport) RoundTrip(req *http.Request) (*http.Response, error) { } type Client struct { - httpClient *http.Client + httpClient httpwrapper.Client recipientAccountsCache *lru.Cache[uint64, *RecipientAccount] } @@ -31,17 +32,18 @@ func (w *Client) endpoint(path string) string { return fmt.Sprintf("%s/%s", apiEndpoint, path) } -func NewClient(apiKey string) *Client { +func New(apiKey string) (*Client, error) { recipientsCache, _ := lru.New[uint64, *RecipientAccount](2048) - httpClient := &http.Client{ + config := &httpwrapper.Config{ Transport: &apiTransport{ APIKey: apiKey, underlying: otelhttp.NewTransport(http.DefaultTransport), }, } + httpClient, err := httpwrapper.NewClient(config) return &Client{ httpClient: httpClient, recipientAccountsCache: recipientsCache, - } + }, err } diff --git a/components/payments/cmd/connectors/internal/connectors/wise/client/error.go b/components/payments/internal/connectors/plugins/public/wise/client/error.go similarity index 53% rename from components/payments/cmd/connectors/internal/connectors/wise/client/error.go rename to components/payments/internal/connectors/plugins/public/wise/client/error.go index 13f3813d28..61d3b47150 100644 --- a/components/payments/cmd/connectors/internal/connectors/wise/client/error.go +++ b/components/payments/internal/connectors/plugins/public/wise/client/error.go @@ -1,15 +1,21 @@ package client import ( - "encoding/json" "fmt" - "io" ) type wiseErrors struct { Errors []*wiseError `json:"errors"` } +func (we *wiseErrors) Error(statusCode int) *wiseError { + if len(we.Errors) == 0 { + return &wiseError{StatusCode: statusCode} + } + we.Errors[0].StatusCode = statusCode + return we.Errors[0] +} + type wiseError struct { StatusCode int `json:"-"` Code string `json:"code"` @@ -23,20 +29,3 @@ func (me *wiseError) Error() error { return fmt.Errorf("%s: %s", me.Code, me.Message) } - -func unmarshalError(statusCode int, body io.ReadCloser) *wiseError { - var ces wiseErrors - _ = json.NewDecoder(body).Decode(&ces) - - if len(ces.Errors) == 0 { - return &wiseError{ - StatusCode: statusCode, - } - } - - return &wiseError{ - StatusCode: statusCode, - Code: ces.Errors[0].Code, - Message: ces.Errors[0].Message, - } -} diff --git a/components/payments/cmd/connectors/internal/connectors/wise/client/payouts.go b/components/payments/internal/connectors/plugins/public/wise/client/payouts.go similarity index 56% rename from components/payments/cmd/connectors/internal/connectors/wise/client/payouts.go rename to components/payments/internal/connectors/plugins/public/wise/client/payouts.go index a87111e460..18dac3df10 100644 --- a/components/payments/cmd/connectors/internal/connectors/wise/client/payouts.go +++ b/components/payments/internal/connectors/plugins/public/wise/client/payouts.go @@ -5,11 +5,10 @@ import ( "context" "encoding/json" "fmt" - "io" "net/http" "time" - "github.com/formancehq/payments/cmd/connectors/internal/connectors" + "github.com/formancehq/payments/internal/connectors/httpwrapper" ) type Payout struct { @@ -62,47 +61,38 @@ func (t *Payout) UnmarshalJSON(data []byte) error { return nil } -func (w *Client) GetPayout(ctx context.Context, payoutID string) (*Payout, error) { - f := connectors.ClientMetrics(ctx, "wise", "get_payout") - now := time.Now() - defer f(ctx, now) +func (c *Client) GetPayout(ctx context.Context, payoutID string) (*Payout, error) { + // TODO(polo): metrics + // f := connectors.ClientMetrics(ctx, "wise", "get_payout") + // now := time.Now() + // defer f(ctx, now) req, err := http.NewRequestWithContext(ctx, - http.MethodGet, w.endpoint("v1/transfers/"+payoutID), http.NoBody) + http.MethodGet, c.endpoint("v1/transfers/"+payoutID), http.NoBody) if err != nil { return nil, err } - res, err := w.httpClient.Do(req) - if err != nil { - return nil, err - } - defer res.Body.Close() - - if res.StatusCode != http.StatusOK { - return nil, unmarshalError(res.StatusCode, res.Body).Error() - } - - body, err := io.ReadAll(res.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - var payout Payout - err = json.Unmarshal(body, &payout) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal transfer: %w", err) + var errRes wiseErrors + statusCode, err := c.httpClient.Do(req, &payout, &errRes) + switch err { + case nil: + return &payout, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, errRes.Error(statusCode).Error() } - - return &payout, nil + return nil, fmt.Errorf("failed to get payout: %w", err) } -func (w *Client) CreatePayout(ctx context.Context, quote Quote, targetAccount uint64, transactionID string) (*Payout, error) { - f := connectors.ClientMetrics(ctx, "wise", "initiate_payout") - now := time.Now() - defer f(ctx, now) +func (c *Client) CreatePayout(ctx context.Context, quote Quote, targetAccount uint64, transactionID string) (*Payout, error) { + // TODO(polo): metrics + // f := connectors.ClientMetrics(ctx, "wise", "initiate_payout") + // now := time.Now() + // defer f(ctx, now) - req, err := json.Marshal(map[string]interface{}{ + reqBody, err := json.Marshal(map[string]interface{}{ "targetAccount": targetAccount, "quoteUuid": quote.ID.String(), "customerTransactionId": transactionID, @@ -111,21 +101,21 @@ func (w *Client) CreatePayout(ctx context.Context, quote Quote, targetAccount ui return nil, err } - res, err := w.httpClient.Post(w.endpoint("v1/transfers"), "application/json", bytes.NewBuffer(req)) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint("v1/transfers"), bytes.NewBuffer(reqBody)) if err != nil { return nil, err } - defer res.Body.Close() - - if res.StatusCode != http.StatusOK { - return nil, unmarshalError(res.StatusCode, res.Body).Error() - } + req.Header.Set("Content-Type", "application/json") - var response Payout - err = json.NewDecoder(res.Body).Decode(&response) - if err != nil { - return nil, fmt.Errorf("failed to get response from transfer: %w", err) + var payout Payout + var errRes wiseErrors + statusCode, err := c.httpClient.Do(req, &payout, &errRes) + switch err { + case nil: + return &payout, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, errRes.Error(statusCode).Error() } - - return &response, nil + return nil, fmt.Errorf("failed to make payout: %w", err) } diff --git a/components/payments/internal/connectors/plugins/public/wise/client/profiles.go b/components/payments/internal/connectors/plugins/public/wise/client/profiles.go new file mode 100644 index 0000000000..0c97fd337c --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/wise/client/profiles.go @@ -0,0 +1,38 @@ +package client + +import ( + "context" + "fmt" + "net/http" + + "github.com/formancehq/payments/internal/connectors/httpwrapper" +) + +type Profile struct { + ID uint64 `json:"id"` + Type string `json:"type"` +} + +func (c *Client) GetProfiles(ctx context.Context) ([]Profile, error) { + // TODO(polo): metrics + // f := connectors.ClientMetrics(ctx, "wise", "list_profiles") + // now := time.Now() + // defer f(ctx, now) + + var profiles []Profile + req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.endpoint("v2/profiles"), http.NoBody) + if err != nil { + return profiles, err + } + + var errRes wiseErrors + statusCode, err := c.httpClient.Do(req, &profiles, &errRes) + switch err { + case nil: + return profiles, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return profiles, errRes.Error(statusCode).Error() + } + return profiles, fmt.Errorf("failed to get profiles: %w", err) +} diff --git a/components/payments/internal/connectors/plugins/public/wise/client/quotes.go b/components/payments/internal/connectors/plugins/public/wise/client/quotes.go new file mode 100644 index 0000000000..04e83d070b --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/wise/client/quotes.go @@ -0,0 +1,56 @@ +package client + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/formancehq/payments/internal/connectors/httpwrapper" + "github.com/google/uuid" +) + +type Quote struct { + ID uuid.UUID `json:"id"` +} + +func (c *Client) CreateQuote(ctx context.Context, profileID, currency string, amount json.Number) (Quote, error) { + // TODO(polo): metrics + // f := connectors.ClientMetrics(ctx, "wise", "create_quote") + // now := time.Now() + // defer f(ctx, now) + + var quote Quote + + reqBody, err := json.Marshal(map[string]interface{}{ + "sourceCurrency": currency, + "targetCurrency": currency, + "sourceAmount": amount, + }) + if err != nil { + return quote, err + } + + req, err := http.NewRequestWithContext( + ctx, + http.MethodPost, + c.endpoint("v3/profiles/"+profileID+"/quotes"), + bytes.NewBuffer(reqBody), + ) + if err != nil { + return quote, err + } + req.Header.Set("Content-Type", "application/json") + + var errRes wiseErrors + statusCode, err := c.httpClient.Do(req, "e, &errRes) + switch err { + case nil: + return quote, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return quote, errRes.Error(statusCode).Error() + } + return quote, fmt.Errorf("failed to get response from quote: %w", err) +} diff --git a/components/payments/internal/connectors/plugins/public/wise/client/recipient_accounts.go b/components/payments/internal/connectors/plugins/public/wise/client/recipient_accounts.go new file mode 100644 index 0000000000..e8e8e7a54d --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/wise/client/recipient_accounts.go @@ -0,0 +1,95 @@ +package client + +import ( + "context" + "fmt" + "net/http" + + "github.com/formancehq/payments/internal/connectors/httpwrapper" +) + +type RecipientAccountsResponse struct { + Content []*RecipientAccount `json:"content"` + SeekPositionForCurrent uint64 `json:"seekPositionForCurrent"` + SeekPositionForNext uint64 `json:"seekPositionForNext"` + Size int `json:"size"` +} + +type RecipientAccount struct { + ID uint64 `json:"id"` + Profile uint64 `json:"profileId"` + Currency string `json:"currency"` + Name struct { + FullName string `json:"fullName"` + } `json:"name"` +} + +func (c *Client) GetRecipientAccounts(ctx context.Context, profileID uint64, pageSize int, seekPositionForNext uint64) (*RecipientAccountsResponse, error) { + // TODO(polo): metrics + // f := connectors.ClientMetrics(ctx, "wise", "list_recipient_accounts") + // now := time.Now() + // defer f(ctx, now) + + req, err := http.NewRequestWithContext(ctx, + http.MethodGet, c.endpoint("v2/accounts"), http.NoBody) + if err != nil { + return nil, err + } + + q := req.URL.Query() + q.Add("profile", fmt.Sprintf("%d", profileID)) + q.Add("size", fmt.Sprintf("%d", pageSize)) + q.Add("sort", "id,asc") + if seekPositionForNext > 0 { + q.Add("seekPosition", fmt.Sprintf("%d", seekPositionForNext)) + } + req.URL.RawQuery = q.Encode() + + var accounts RecipientAccountsResponse + var errRes wiseErrors + statusCode, err := c.httpClient.Do(req, &accounts, &errRes) + switch err { + case nil: + return &accounts, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, errRes.Error(statusCode).Error() + } + return nil, fmt.Errorf("failed to get recipient accounts: %w", err) +} + +func (c *Client) GetRecipientAccount(ctx context.Context, accountID uint64) (*RecipientAccount, error) { + // TODO(polo): metrics + // f := connectors.ClientMetrics(ctx, "wise", "get_recipient_account") + // now := time.Now() + // defer f(ctx, now) + + if rc, ok := c.recipientAccountsCache.Get(accountID); ok { + return rc, nil + } + + req, err := http.NewRequestWithContext(ctx, + http.MethodGet, c.endpoint(fmt.Sprintf("v1/accounts/%d", accountID)), http.NoBody) + if err != nil { + return nil, err + } + + var res RecipientAccount + var errRes wiseErrors + statusCode, err := c.httpClient.Do(req, &res, &errRes) + switch err { + case nil: + c.recipientAccountsCache.Add(accountID, &res) + return &res, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + e := errRes.Error(statusCode) + if e.Code == "RECIPIENT_MISSING" { + // This is a valid response, we just don't have the account amoungs + // our recipients. + return &RecipientAccount{}, nil + } + return nil, e.Error() + } + return nil, fmt.Errorf("failed to get recipient account: %w", err) +} diff --git a/components/payments/internal/connectors/plugins/public/wise/client/transfers.go b/components/payments/internal/connectors/plugins/public/wise/client/transfers.go new file mode 100644 index 0000000000..c028ac4554 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/wise/client/transfers.go @@ -0,0 +1,225 @@ +package client + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/formancehq/payments/internal/connectors/httpwrapper" +) + +type Transfer struct { + ID uint64 `json:"id"` + Reference string `json:"reference"` + Status string `json:"status"` + SourceAccount uint64 `json:"sourceAccount"` + SourceCurrency string `json:"sourceCurrency"` + SourceValue json.Number `json:"sourceValue"` + TargetAccount uint64 `json:"targetAccount"` + TargetCurrency string `json:"targetCurrency"` + TargetValue json.Number `json:"targetValue"` + Business uint64 `json:"business"` + Created string `json:"created"` + //nolint:tagliatelle // allow for clients + CustomerTransactionID string `json:"customerTransactionId"` + Details struct { + Reference string `json:"reference"` + } `json:"details"` + Rate float64 `json:"rate"` + User uint64 `json:"user"` + + SourceBalanceID uint64 `json:"-"` + DestinationBalanceID uint64 `json:"-"` + + CreatedAt time.Time `json:"-"` +} + +func (t *Transfer) UnmarshalJSON(data []byte) error { + type Alias Transfer + + aux := &struct { + Created string `json:"created"` + *Alias + }{ + Alias: (*Alias)(t), + } + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + var err error + + t.CreatedAt, err = time.Parse("2006-01-02 15:04:05", aux.Created) + if err != nil { + return fmt.Errorf("failed to parse created time: %w", err) + } + + return nil +} + +func (c *Client) GetTransfers(ctx context.Context, profileID uint64, offset int, limit int) ([]Transfer, error) { + // TODO(polo): metrics + // f := connectors.ClientMetrics(ctx, "wise", "list_transfers") + // now := time.Now() + // defer f(ctx, now) + + req, err := http.NewRequestWithContext(ctx, + http.MethodGet, c.endpoint("v1/transfers"), http.NoBody) + if err != nil { + return nil, err + } + + q := req.URL.Query() + q.Add("limit", fmt.Sprintf("%d", limit)) + q.Add("profile", fmt.Sprintf("%d", profileID)) + q.Add("offset", fmt.Sprintf("%d", offset)) + req.URL.RawQuery = q.Encode() + + var transfers []Transfer + var errRes wiseErrors + statusCode, err := c.httpClient.Do(req, &transfers, &errRes) + switch err { + case nil: + // fallthrough + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return transfers, errRes.Error(statusCode).Error() + default: + return transfers, fmt.Errorf("failed to get transfers: %w", err) + } + + for i, transfer := range transfers { + var sourceProfileID, targetProfileID uint64 + if transfer.SourceAccount != 0 { + recipientAccount, err := c.GetRecipientAccount(ctx, transfer.SourceAccount) + if err != nil { + return nil, fmt.Errorf("failed to get source profile id: %w", err) + } + + sourceProfileID = recipientAccount.Profile + } + + if transfer.TargetAccount != 0 { + recipientAccount, err := c.GetRecipientAccount(ctx, transfer.TargetAccount) + if err != nil { + return nil, fmt.Errorf("failed to get target profile id: %w", err) + } + + targetProfileID = recipientAccount.Profile + } + + // TODO(polo): fetching balances for each transfer is not efficient + // and can be quite long. We should consider caching balances, but + // at the same time we will develop a feature soon to get balances + // for every accounts, so caching is not a solution. + switch { + case sourceProfileID == 0 && targetProfileID == 0: + // Do nothing + case sourceProfileID == targetProfileID && sourceProfileID != 0: + // Same profile id for target and source + balances, err := c.GetBalances(ctx, sourceProfileID) + if err != nil { + return nil, fmt.Errorf("failed to get balances: %w", err) + } + for _, balance := range balances { + if balance.Currency == transfer.SourceCurrency { + transfers[i].SourceBalanceID = balance.ID + } + + if balance.Currency == transfer.TargetCurrency { + transfers[i].DestinationBalanceID = balance.ID + } + } + default: + if sourceProfileID != 0 { + balances, err := c.GetBalances(ctx, sourceProfileID) + if err != nil { + return nil, fmt.Errorf("failed to get balances: %w", err) + } + for _, balance := range balances { + if balance.Currency == transfer.SourceCurrency { + transfers[i].SourceBalanceID = balance.ID + } + } + } + + if targetProfileID != 0 { + balances, err := c.GetBalances(ctx, targetProfileID) + if err != nil { + return nil, fmt.Errorf("failed to get balances: %w", err) + } + for _, balance := range balances { + if balance.Currency == transfer.TargetCurrency { + transfers[i].DestinationBalanceID = balance.ID + } + } + } + + } + } + return transfers, nil +} + +func (c *Client) GetTransfer(ctx context.Context, transferID string) (*Transfer, error) { + // TODO(polo): metrics + // f := connectors.ClientMetrics(ctx, "wise", "get_transfer") + // now := time.Now() + // defer f(ctx, now) + + req, err := http.NewRequestWithContext(ctx, + http.MethodGet, c.endpoint("v1/transfers/"+transferID), http.NoBody) + if err != nil { + return nil, err + } + + var transfer Transfer + var errRes wiseErrors + statusCode, err := c.httpClient.Do(req, &transfer, &errRes) + switch err { + case nil: + return &transfer, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, errRes.Error(statusCode).Error() + } + return nil, fmt.Errorf("failed to get transfer: %w", err) +} + +func (c *Client) CreateTransfer(ctx context.Context, quote Quote, targetAccount uint64, transactionID string) (*Transfer, error) { + // TODO(polo): metrics + // metrics.GetMetricsRegistry().ConnectorPSPCalls().Add(ctx, 1, metric.WithAttributes([]attribute.KeyValue{ + // attribute.String("connector", "wise"), + // attribute.String("operation", "initiate_transfer"), + // }...)) + + reqBody, err := json.Marshal(map[string]interface{}{ + "targetAccount": targetAccount, + "quoteUuid": quote.ID.String(), + "customerTransactionId": transactionID, + }) + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, + http.MethodPost, c.endpoint("v1/transfers"), bytes.NewBuffer(reqBody)) + if err != nil { + return nil, err + } + + var transfer Transfer + var errRes wiseErrors + statusCode, err := c.httpClient.Do(req, &transfer, &errRes) + switch err { + case nil: + return &transfer, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, errRes.Error(statusCode).Error() + } + return nil, fmt.Errorf("failed to create transfer: %w", err) +} diff --git a/components/payments/internal/connectors/plugins/public/wise/client/webhooks.go b/components/payments/internal/connectors/plugins/public/wise/client/webhooks.go new file mode 100644 index 0000000000..a69e6390ea --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/wise/client/webhooks.go @@ -0,0 +1,186 @@ +package client + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/formancehq/payments/internal/connectors/httpwrapper" +) + +type webhookSubscription struct { + Name string `json:"name"` + TriggerOn string `json:"trigger_on"` + Delivery struct { + Version string `json:"version"` + URL string `json:"url"` + } `json:"delivery"` +} + +type webhookSubscriptionResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Delivery struct { + Version string `json:"version"` + URL string `json:"url"` + } `json:"delivery"` + TriggerOn string `json:"trigger_on"` + Scope struct { + Domain string `json:"domain"` + } `json:"scope"` + CreatedBy struct { + Type string `json:"type"` + ID string `json:"id"` + } `json:"created_by"` + CreatedAt string `json:"created_at"` +} + +func (c *Client) CreateWebhook(ctx context.Context, profileID uint64, name, triggerOn, url, version string) (*webhookSubscriptionResponse, error) { + reqBody, err := json.Marshal(webhookSubscription{ + Name: name, + TriggerOn: triggerOn, + Delivery: struct { + Version string `json:"version"` + URL string `json:"url"` + }{ + Version: version, + URL: url, + }, + }) + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, + c.endpoint(fmt.Sprintf("/v3/profiles/%d/subscriptions", profileID)), + bytes.NewBuffer(reqBody), + ) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + + var res webhookSubscriptionResponse + var errRes wiseErrors + statusCode, err := c.httpClient.Do(req, &res, &errRes) + switch err { + case nil: + return &res, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return nil, errRes.Error(statusCode).Error() + } + return nil, fmt.Errorf("failed to create subscription: %w", err) +} + +func (c *Client) ListWebhooksSubscription(ctx context.Context, profileID uint64) ([]webhookSubscriptionResponse, error) { + req, err := http.NewRequestWithContext(ctx, + http.MethodGet, c.endpoint(fmt.Sprintf("/v3/profiles/%d/subscriptions", profileID)), http.NoBody) + if err != nil { + return nil, err + } + + var res []webhookSubscriptionResponse + var errRes wiseErrors + statusCode, err := c.httpClient.Do(req, &res, &errRes) + switch err { + case nil: + return res, nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return res, errRes.Error(statusCode).Error() + } + return res, fmt.Errorf("failed to get subscription: %w", err) +} + +func (c *Client) DeleteWebhooks(ctx context.Context, profileID uint64, subscriptionID string) error { + req, err := http.NewRequestWithContext(ctx, + http.MethodDelete, c.endpoint(fmt.Sprintf("/v3/profiles/%d/subscriptions/%s", profileID, subscriptionID)), http.NoBody) + if err != nil { + return err + } + + var errRes wiseErrors + statusCode, err := c.httpClient.Do(req, nil, &errRes) + switch err { + case nil: + return nil + case httpwrapper.ErrStatusCodeUnexpected: + // TODO(polo): retryable errors + return errRes.Error(statusCode).Error() + } + return fmt.Errorf("failed to get subscription: %w", err) +} + +type transferStateChangedWebhookPayload struct { + Data struct { + Resource struct { + Type string `json:"type"` + ID uint64 `json:"id"` + ProfileID uint64 `json:"profile_id"` + AccountID uint64 `json:"account_id"` + } `json:"resource"` + CurrentState string `json:"current_state"` + PreviousState string `json:"previous_state"` + OccurredAt string `json:"occurred_at"` + } `json:"data"` + SubscriptionID string `json:"subscription_id"` + EventType string `json:"event_type"` + SchemaVersion string `json:"schema_version"` + SentAt string `json:"sent_at"` +} + +func (c *Client) TranslateTransferStateChangedWebhook(ctx context.Context, payload []byte) (Transfer, error) { + var transferStatedChangedEvent transferStateChangedWebhookPayload + err := json.Unmarshal(payload, &transferStatedChangedEvent) + if err != nil { + return Transfer{}, err + } + + transfer, err := c.GetTransfer(ctx, fmt.Sprint(transferStatedChangedEvent.Data.Resource.ID)) + if err != nil { + return Transfer{}, err + } + + transfer.Created = transferStatedChangedEvent.Data.OccurredAt + transfer.CreatedAt, err = time.Parse("2006-01-02 15:04:05", transfer.Created) + if err != nil { + return Transfer{}, fmt.Errorf("failed to parse created time: %w", err) + } + + return *transfer, nil +} + +type balanceUpdateWebhookPayload struct { + Data struct { + Resource struct { + ID uint64 `json:"id"` + ProfileID uint64 `json:"profile_id"` + Type string `json:"type"` + } `json:"resource"` + Amount json.Number `json:"amount"` + BalanceID uint64 `json:"balance_id"` + Currency string `json:"currency"` + TransactionType string `json:"transaction_type"` + OccurredAt string `json:"occurred_at"` + TransferReference string `json:"transfer_reference"` + ChannelName string `json:"channel_name"` + } `json:"data"` + SubscriptionID string `json:"subscription_id"` + EventType string `json:"event_type"` + SchemaVersion string `json:"schema_version"` + SentAt string `json:"sent_at"` +} + +func (c *Client) TranslateBalanceUpdateWebhook(ctx context.Context, payload []byte) (balanceUpdateWebhookPayload, error) { + var balanceUpdateEvent balanceUpdateWebhookPayload + err := json.Unmarshal(payload, &balanceUpdateEvent) + if err != nil { + return balanceUpdateWebhookPayload{}, err + } + + return balanceUpdateEvent, nil +} diff --git a/components/payments/internal/connectors/plugins/public/wise/cmd/main.go b/components/payments/internal/connectors/plugins/public/wise/cmd/main.go new file mode 100644 index 0000000000..b09cf2c155 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/wise/cmd/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "github.com/formancehq/payments/internal/connectors/grpc" + "github.com/formancehq/payments/internal/connectors/plugins" + "github.com/formancehq/payments/internal/connectors/plugins/public/wise" + "github.com/hashicorp/go-plugin" +) + +func main() { + // TODO(polo): metrics + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: grpc.Handshake, + Plugins: map[string]plugin.Plugin{ + "psp": &grpc.PSPGRPCPlugin{Impl: plugins.NewGRPCImplem(&wise.Plugin{})}, + }, + + // A non-nil value here enables gRPC serving for this plugin... + GRPCServer: plugin.DefaultGRPCServer, + }) +} diff --git a/components/payments/internal/connectors/plugins/public/wise/config.go b/components/payments/internal/connectors/plugins/public/wise/config.go new file mode 100644 index 0000000000..5d26afacad --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/wise/config.go @@ -0,0 +1,57 @@ +package wise + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + + "github.com/formancehq/payments/internal/models" + "github.com/pkg/errors" +) + +type Config struct { + APIKey string `json:"apiKey"` + WebhookPublicKey string `json:"webhookPublicKey"` + + webhookPublicKey *rsa.PublicKey `json:"-"` +} + +func (c *Config) validate() error { + if c.APIKey == "" { + return errors.Wrap(models.ErrInvalidConfig, "missing api key in config") + } + + if c.WebhookPublicKey == "" { + return errors.Wrap(models.ErrInvalidConfig, "missing webhook public key in config") + } + + p, _ := pem.Decode([]byte(c.WebhookPublicKey)) + if p == nil { + return errors.Wrap(models.ErrInvalidConfig, "invalid webhook public key in config") + } + + publicKey, err := x509.ParsePKIXPublicKey(p.Bytes) + if err != nil { + return errors.Wrap(models.ErrInvalidConfig, fmt.Sprintf("invalid webhook public key in config: %v", err)) + } + + switch pub := publicKey.(type) { + case *rsa.PublicKey: + c.webhookPublicKey = pub + default: + return errors.Wrap(models.ErrInvalidConfig, "invalid webhook public key in config") + } + + return nil +} + +func unmarshalAndValidateConfig(payload json.RawMessage) (Config, error) { + var config Config + if err := json.Unmarshal(payload, &config); err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + return config, config.validate() +} diff --git a/components/payments/internal/connectors/plugins/public/wise/config.json b/components/payments/internal/connectors/plugins/public/wise/config.json new file mode 100644 index 0000000000..0c049a96e5 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/wise/config.json @@ -0,0 +1,12 @@ +{ + "apiKey": { + "dataType": "string", + "required": true, + "defaultValue": "" + }, + "webhookPublicKey": { + "dataType": "string", + "required": true, + "defaultValue": "" + } +} \ No newline at end of file diff --git a/components/payments/cmd/connectors/internal/connectors/wise/currencies.go b/components/payments/internal/connectors/plugins/public/wise/currencies.go similarity index 95% rename from components/payments/cmd/connectors/internal/connectors/wise/currencies.go rename to components/payments/internal/connectors/plugins/public/wise/currencies.go index ad2f38fbc3..681dd86a6a 100644 --- a/components/payments/cmd/connectors/internal/connectors/wise/currencies.go +++ b/components/payments/internal/connectors/plugins/public/wise/currencies.go @@ -1,6 +1,6 @@ package wise -import "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency" +import "github.com/formancehq/payments/internal/connectors/plugins/currency" var ( // c.f. https://wise.com/help/articles/2897238/which-currencies-can-i-add-keep-and-receive-in-my-wise-account diff --git a/components/payments/internal/connectors/plugins/public/wise/external_accounts.go b/components/payments/internal/connectors/plugins/public/wise/external_accounts.go new file mode 100644 index 0000000000..d3ee3cd46c --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/wise/external_accounts.go @@ -0,0 +1,98 @@ +package wise + +import ( + "context" + "encoding/json" + "errors" + "strconv" + "time" + + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/connectors/plugins/currency" + "github.com/formancehq/payments/internal/connectors/plugins/public/wise/client" + "github.com/formancehq/payments/internal/models" +) + +type externalAccountsState struct { + LastSeekPosition uint64 `json:"lastSeekPosition"` +} + +func (p Plugin) fetchExternalAccounts(ctx context.Context, req models.FetchNextExternalAccountsRequest) (models.FetchNextExternalAccountsResponse, error) { + var oldState externalAccountsState + if req.State != nil { + if err := json.Unmarshal(req.State, &oldState); err != nil { + return models.FetchNextExternalAccountsResponse{}, err + } + } + + var from client.Profile + if req.FromPayload == nil { + return models.FetchNextExternalAccountsResponse{}, errors.New("missing from payload when fetching external accounts") + } + if err := json.Unmarshal(req.FromPayload, &from); err != nil { + return models.FetchNextExternalAccountsResponse{}, err + } + + newState := externalAccountsState{ + LastSeekPosition: oldState.LastSeekPosition, + } + + var accounts []models.PSPAccount + hasMore := false + for { + pagedExternalAccounts, err := p.client.GetRecipientAccounts(ctx, from.ID, req.PageSize, newState.LastSeekPosition) + if err != nil { + return models.FetchNextExternalAccountsResponse{}, err + } + + if len(pagedExternalAccounts.Content) == 0 { + break + } + + for _, externalAccount := range pagedExternalAccounts.Content { + if externalAccount.ID <= oldState.LastSeekPosition { + continue + } + + raw, err := json.Marshal(externalAccount) + if err != nil { + return models.FetchNextExternalAccountsResponse{}, err + } + + accounts = append(accounts, models.PSPAccount{ + Reference: strconv.FormatUint(externalAccount.ID, 10), + CreatedAt: time.Now().UTC(), + Name: &externalAccount.Name.FullName, + DefaultAsset: pointer.For(currency.FormatAsset(supportedCurrenciesWithDecimal, externalAccount.Currency)), + Raw: raw, + }) + + if len(accounts) >= req.PageSize { + break + } + } + + if len(accounts) >= req.PageSize { + hasMore = true + break + } + + if pagedExternalAccounts.SeekPositionForNext == 0 { + // No more data to fetch + break + } + + newState.LastSeekPosition = pagedExternalAccounts.SeekPositionForNext + } + + payload, err := json.Marshal(newState) + if err != nil { + return models.FetchNextExternalAccountsResponse{}, err + } + + return models.FetchNextExternalAccountsResponse{ + ExternalAccounts: accounts, + NewState: payload, + HasMore: hasMore, + }, nil +} diff --git a/components/payments/internal/connectors/plugins/public/wise/metadata.go b/components/payments/internal/connectors/plugins/public/wise/metadata.go new file mode 100644 index 0000000000..5261fc4d22 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/wise/metadata.go @@ -0,0 +1,5 @@ +package wise + +const ( + metadataProfileIDKey = "profile_id" +) diff --git a/components/payments/internal/connectors/plugins/public/wise/payments.go b/components/payments/internal/connectors/plugins/public/wise/payments.go new file mode 100644 index 0000000000..a32b12d75b --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/wise/payments.go @@ -0,0 +1,138 @@ +package wise + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/connectors/plugins/currency" + "github.com/formancehq/payments/internal/connectors/plugins/public/wise/client" + "github.com/formancehq/payments/internal/models" +) + +type paymentsState struct { + Offset int +} + +func (p Plugin) fetchNextPayments(ctx context.Context, req models.FetchNextPaymentsRequest) (models.FetchNextPaymentsResponse, error) { + var oldState paymentsState + if req.State != nil { + if err := json.Unmarshal(req.State, &oldState); err != nil { + return models.FetchNextPaymentsResponse{}, err + } + } + + var from client.Profile + if req.FromPayload == nil { + return models.FetchNextPaymentsResponse{}, errors.New("missing from payload when fetching payments") + } + if err := json.Unmarshal(req.FromPayload, &from); err != nil { + return models.FetchNextPaymentsResponse{}, err + } + + newState := paymentsState{ + Offset: oldState.Offset, + } + + var payments []models.PSPPayment + hasMore := false + for { + pagedTransfers, err := p.client.GetTransfers(ctx, from.ID, newState.Offset, req.PageSize) + if err != nil { + return models.FetchNextPaymentsResponse{}, err + } + + if len(pagedTransfers) == 0 { + break + } + + for _, transfer := range pagedTransfers { + payment, err := fromTransferToPayment(transfer) + if err != nil { + return models.FetchNextPaymentsResponse{}, err + } + + payments = append(payments, *payment) + newState.Offset++ + + if len(payments) >= req.PageSize { + break + } + } + + if len(pagedTransfers) < req.PageSize { + break + } + + if len(payments) >= req.PageSize { + hasMore = true + break + } + } + + payload, err := json.Marshal(newState) + if err != nil { + return models.FetchNextPaymentsResponse{}, err + } + + return models.FetchNextPaymentsResponse{ + Payments: payments, + NewState: payload, + HasMore: hasMore, + }, nil +} + +func fromTransferToPayment(from client.Transfer) (*models.PSPPayment, error) { + raw, err := json.Marshal(from) + if err != nil { + return nil, err + } + + precision, ok := supportedCurrenciesWithDecimal[from.TargetCurrency] + if !ok { + return nil, nil + } + + amount, err := currency.GetAmountWithPrecisionFromString(from.TargetValue.String(), precision) + if err != nil { + return nil, err + } + + p := models.PSPPayment{ + Reference: fmt.Sprintf("%d", from.ID), + CreatedAt: from.CreatedAt, + Type: models.PAYMENT_TYPE_TRANSFER, + Amount: amount, + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, from.TargetCurrency), + Scheme: models.PAYMENT_SCHEME_OTHER, + Status: matchTransferStatus(from.Status), + Raw: raw, + } + + if from.SourceBalanceID != 0 { + p.SourceAccountReference = pointer.For(fmt.Sprintf("%d", from.SourceBalanceID)) + } + + if from.DestinationBalanceID != 0 { + p.DestinationAccountReference = pointer.For(fmt.Sprintf("%d", from.DestinationBalanceID)) + } + + return &p, nil +} + +func matchTransferStatus(status string) models.PaymentStatus { + switch status { + case "incoming_payment_waiting", "incoming_payment_initiated", "processing", "funds_converted", "bounced_back": + return models.PAYMENT_STATUS_PENDING + case "outgoing_payment_sent": + return models.PAYMENT_STATUS_SUCCEEDED + case "funds_refunded", "charged_back": + return models.PAYMENT_STATUS_FAILED + case "cancelled": + return models.PAYMENT_STATUS_CANCELLED + } + + return models.PAYMENT_STATUS_OTHER +} diff --git a/components/payments/internal/connectors/plugins/public/wise/plugin.go b/components/payments/internal/connectors/plugins/public/wise/plugin.go new file mode 100644 index 0000000000..27b9ec93cc --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/wise/plugin.go @@ -0,0 +1,161 @@ +package wise + +import ( + "context" + "errors" + "fmt" + + "github.com/formancehq/payments/internal/connectors/plugins" + "github.com/formancehq/payments/internal/connectors/plugins/public/wise/client" + "github.com/formancehq/payments/internal/models" +) + +type Plugin struct { + config Config + client *client.Client +} + +func (p *Plugin) Install(ctx context.Context, req models.InstallRequest) (models.InstallResponse, error) { + config, err := unmarshalAndValidateConfig(req.Config) + if err != nil { + return models.InstallResponse{}, err + } + + client, err := client.New(config.APIKey) + if err != nil { + return models.InstallResponse{}, fmt.Errorf("failed to install wise plugin %w", err) + } + p.client = client + p.config = config + + webhookConfigs = map[string]webhookConfig{ + "transfer_state_changed": { + triggerOn: "transfers#state-change", + urlPath: "/transferstatechanged", + fn: p.translateTransferStateChangedWebhook, + version: "2.0.0", + }, + "balance_update": { + triggerOn: "balances#update", + urlPath: "/balanceupdate", + fn: p.translateBalanceUpdateWebhook, + version: "2.2.0", + }, + } + + configs := make([]models.PSPWebhookConfig, 0, len(webhookConfigs)) + for name, config := range webhookConfigs { + configs = append(configs, models.PSPWebhookConfig{ + Name: name, + URLPath: config.urlPath, + }) + } + + return models.InstallResponse{ + Capabilities: capabilities, + Workflow: workflow(), + WebhooksConfigs: configs, + }, nil +} + +func (p Plugin) Uninstall(ctx context.Context, req models.UninstallRequest) (models.UninstallResponse, error) { + return p.uninstall(ctx, req) +} + +func (p Plugin) FetchNextAccounts(ctx context.Context, req models.FetchNextAccountsRequest) (models.FetchNextAccountsResponse, error) { + if p.client == nil { + return models.FetchNextAccountsResponse{}, plugins.ErrNotYetInstalled + } + return p.fetchNextAccounts(ctx, req) +} + +func (p Plugin) FetchNextBalances(ctx context.Context, req models.FetchNextBalancesRequest) (models.FetchNextBalancesResponse, error) { + if p.client == nil { + return models.FetchNextBalancesResponse{}, plugins.ErrNotYetInstalled + } + return p.fetchNextBalances(ctx, req) +} + +func (p Plugin) FetchNextExternalAccounts(ctx context.Context, req models.FetchNextExternalAccountsRequest) (models.FetchNextExternalAccountsResponse, error) { + if p.client == nil { + return models.FetchNextExternalAccountsResponse{}, plugins.ErrNotYetInstalled + } + return p.fetchExternalAccounts(ctx, req) +} + +func (p Plugin) FetchNextPayments(ctx context.Context, req models.FetchNextPaymentsRequest) (models.FetchNextPaymentsResponse, error) { + if p.client == nil { + return models.FetchNextPaymentsResponse{}, plugins.ErrNotYetInstalled + } + return p.fetchNextPayments(ctx, req) +} + +func (p Plugin) FetchNextOthers(ctx context.Context, req models.FetchNextOthersRequest) (models.FetchNextOthersResponse, error) { + if p.client == nil { + return models.FetchNextOthersResponse{}, plugins.ErrNotYetInstalled + } + + switch req.Name { + case fetchProfileName: + return p.fetchNextProfiles(ctx, req) + default: + return models.FetchNextOthersResponse{}, plugins.ErrNotImplemented + } +} + +func (p Plugin) CreateBankAccount(ctx context.Context, req models.CreateBankAccountRequest) (models.CreateBankAccountResponse, error) { + return models.CreateBankAccountResponse{}, plugins.ErrNotImplemented +} + +func (p Plugin) CreateWebhooks(ctx context.Context, req models.CreateWebhooksRequest) (models.CreateWebhooksResponse, error) { + if p.client == nil { + return models.CreateWebhooksResponse{}, plugins.ErrNotYetInstalled + } + return p.createWebhooks(ctx, req) +} + +func (p Plugin) TranslateWebhook(ctx context.Context, req models.TranslateWebhookRequest) (models.TranslateWebhookResponse, error) { + if p.client == nil { + return models.TranslateWebhookResponse{}, plugins.ErrNotYetInstalled + } + + testNotif, ok := req.Webhook.Headers["X-Test-Notification"] + if ok && len(testNotif) > 0 { + if testNotif[0] == "true" { + return models.TranslateWebhookResponse{}, nil + } + } + + v, ok := req.Webhook.Headers["X-Delivery-Id"] + if !ok || len(v) == 0 { + return models.TranslateWebhookResponse{}, errors.New("missing X-Delivery-Id header") + } + + signatures, ok := req.Webhook.Headers["X-Signature-Sha256"] + if !ok || len(signatures) == 0 { + return models.TranslateWebhookResponse{}, errors.New("missing X-Signature-Sha256 header") + } + + err := p.verifySignature(req.Webhook.Body, signatures[0]) + if err != nil { + return models.TranslateWebhookResponse{}, err + } + + config, ok := webhookConfigs[req.Name] + if !ok { + return models.TranslateWebhookResponse{}, errors.New("unknown webhook name") + } + + res, err := config.fn(ctx, req) + if err != nil { + return models.TranslateWebhookResponse{}, err + } + + res.IdempotencyKey = v[0] + + return models.TranslateWebhookResponse{ + Responses: []models.WebhookResponse{res}, + }, nil +} + +var _ models.Plugin = &Plugin{} diff --git a/components/payments/internal/connectors/plugins/public/wise/profiles.go b/components/payments/internal/connectors/plugins/public/wise/profiles.go new file mode 100644 index 0000000000..1b3ae80173 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/wise/profiles.go @@ -0,0 +1,68 @@ +package wise + +import ( + "context" + "encoding/json" + "strconv" + + "github.com/formancehq/payments/internal/models" +) + +type profilesState struct { + // Profiles are ordered by their ID + LastProfileID uint64 `json:"lastProfileID"` +} + +func (p Plugin) fetchNextProfiles(ctx context.Context, req models.FetchNextOthersRequest) (models.FetchNextOthersResponse, error) { + var oldState profilesState + if req.State != nil { + if err := json.Unmarshal(req.State, &oldState); err != nil { + return models.FetchNextOthersResponse{}, err + } + } + + newState := profilesState{ + LastProfileID: oldState.LastProfileID, + } + + var others []models.PSPOther + hasMore := false + profiles, err := p.client.GetProfiles(ctx) + if err != nil { + return models.FetchNextOthersResponse{}, err + } + + for _, profile := range profiles { + if profile.ID <= oldState.LastProfileID { + continue + } + + raw, err := json.Marshal(profile) + if err != nil { + return models.FetchNextOthersResponse{}, err + } + + others = append(others, models.PSPOther{ + ID: strconv.FormatUint(profile.ID, 10), + Other: raw, + }) + + newState.LastProfileID = profile.ID + + if len(others) >= req.PageSize { + hasMore = true + break + } + } + + payload, err := json.Marshal(newState) + if err != nil { + return models.FetchNextOthersResponse{}, err + } + + return models.FetchNextOthersResponse{ + Others: others, + NewState: payload, + HasMore: hasMore, + }, nil +} diff --git a/components/payments/internal/connectors/plugins/public/wise/uninstall.go b/components/payments/internal/connectors/plugins/public/wise/uninstall.go new file mode 100644 index 0000000000..b44ac2edcf --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/wise/uninstall.go @@ -0,0 +1,34 @@ +package wise + +import ( + "context" + "strings" + + "github.com/formancehq/payments/internal/models" +) + +func (p Plugin) uninstall(ctx context.Context, req models.UninstallRequest) (models.UninstallResponse, error) { + profiles, err := p.client.GetProfiles(ctx) + if err != nil { + return models.UninstallResponse{}, err + } + + for _, profile := range profiles { + webhooks, err := p.client.ListWebhooksSubscription(ctx, profile.ID) + if err != nil { + return models.UninstallResponse{}, err + } + + for _, webhook := range webhooks { + if !strings.Contains(webhook.Delivery.URL, req.ConnectorID) { + continue + } + + if err := p.client.DeleteWebhooks(ctx, profile.ID, webhook.ID); err != nil { + return models.UninstallResponse{}, err + } + } + } + + return models.UninstallResponse{}, nil +} diff --git a/components/payments/internal/connectors/plugins/public/wise/webhooks.go b/components/payments/internal/connectors/plugins/public/wise/webhooks.go new file mode 100644 index 0000000000..13d8594e4b --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/wise/webhooks.go @@ -0,0 +1,161 @@ +package wise + +import ( + "context" + "crypto" + "crypto/rsa" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "os" + "time" + + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/connectors/plugins/currency" + "github.com/formancehq/payments/internal/connectors/plugins/public/wise/client" + "github.com/formancehq/payments/internal/models" +) + +type webhookConfig struct { + triggerOn string + urlPath string + fn func(context.Context, models.TranslateWebhookRequest) (models.WebhookResponse, error) + version string +} + +var webhookConfigs map[string]webhookConfig + +func (p Plugin) createWebhooks(ctx context.Context, req models.CreateWebhooksRequest) (models.CreateWebhooksResponse, error) { + var from client.Profile + if req.FromPayload == nil { + return models.CreateWebhooksResponse{}, errors.New("missing from payload when creating webhooks") + } + if err := json.Unmarshal(req.FromPayload, &from); err != nil { + return models.CreateWebhooksResponse{}, err + } + + stackPublicURL := os.Getenv("STACK_PUBLIC_URL") + if stackPublicURL == "" { + err := errors.New("STACK_PUBLIC_URL is not set") + return models.CreateWebhooksResponse{}, err + } + + webhookURL := fmt.Sprintf("%s/api/payments/v3/connectors/webhooks/%s", stackPublicURL, req.ConnectorID) + others := make([]models.PSPOther, 0, len(webhookConfigs)) + for name, config := range webhookConfigs { + url := fmt.Sprintf("%s%s", webhookURL, config.urlPath) + resp, err := p.client.CreateWebhook(ctx, from.ID, name, config.triggerOn, url, config.version) + if err != nil { + return models.CreateWebhooksResponse{}, err + } + + raw, err := json.Marshal(resp) + if err != nil { + return models.CreateWebhooksResponse{}, err + } + + others = append(others, models.PSPOther{ + ID: resp.ID, + Other: raw, + }) + } + + return models.CreateWebhooksResponse{ + Others: others, + }, nil +} + +func (p Plugin) translateTransferStateChangedWebhook(ctx context.Context, req models.TranslateWebhookRequest) (models.WebhookResponse, error) { + transfer, err := p.client.TranslateTransferStateChangedWebhook(ctx, req.Webhook.Body) + if err != nil { + return models.WebhookResponse{}, err + } + + payment, err := fromTransferToPayment(transfer) + if err != nil { + return models.WebhookResponse{}, err + } + + return models.WebhookResponse{ + Payment: payment, + }, nil +} + +func (p Plugin) translateBalanceUpdateWebhook(ctx context.Context, req models.TranslateWebhookRequest) (models.WebhookResponse, error) { + update, err := p.client.TranslateBalanceUpdateWebhook(ctx, req.Webhook.Body) + if err != nil { + return models.WebhookResponse{}, err + } + + raw, err := json.Marshal(update) + if err != nil { + return models.WebhookResponse{}, err + } + + occuredAt, err := time.Parse(time.RFC3339, update.Data.OccurredAt) + if err != nil { + return models.WebhookResponse{}, fmt.Errorf("failed to parse created time: %w", err) + } + + var paymentType models.PaymentType + if update.Data.TransactionType == "credit" { + paymentType = models.PAYMENT_TYPE_PAYIN + } else { + paymentType = models.PAYMENT_TYPE_PAYOUT + } + + precision, ok := supportedCurrenciesWithDecimal[update.Data.Currency] + if !ok { + return models.WebhookResponse{}, nil + } + + amount, err := currency.GetAmountWithPrecisionFromString(update.Data.Amount.String(), precision) + if err != nil { + return models.WebhookResponse{}, err + } + + payment := models.PSPPayment{ + Reference: update.Data.TransferReference, + CreatedAt: occuredAt, + Type: paymentType, + Amount: amount, + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, update.Data.Currency), + Scheme: models.PAYMENT_SCHEME_OTHER, + Status: models.PAYMENT_STATUS_SUCCEEDED, + Raw: raw, + } + + switch paymentType { + case models.PAYMENT_TYPE_PAYIN: + payment.SourceAccountReference = pointer.For(fmt.Sprintf("%d", update.Data.BalanceID)) + case models.PAYMENT_TYPE_PAYOUT: + payment.DestinationAccountReference = pointer.For(fmt.Sprintf("%d", update.Data.BalanceID)) + } + + return models.WebhookResponse{ + Payment: &payment, + }, nil +} + +func (p Plugin) verifySignature(body []byte, signature string) error { + msgHash := sha256.New() + _, err := msgHash.Write(body) + if err != nil { + return err + } + msgHashSum := msgHash.Sum(nil) + + data, err := base64.StdEncoding.DecodeString(signature) + if err != nil { + return err + } + + err = rsa.VerifyPKCS1v15(p.config.webhookPublicKey, crypto.SHA256, msgHashSum, data) + if err != nil { + return err + } + + return nil +} diff --git a/components/payments/internal/connectors/plugins/public/wise/workflow.go b/components/payments/internal/connectors/plugins/public/wise/workflow.go new file mode 100644 index 0000000000..31513eab36 --- /dev/null +++ b/components/payments/internal/connectors/plugins/public/wise/workflow.go @@ -0,0 +1,50 @@ +package wise + +import "github.com/formancehq/payments/internal/models" + +const ( + fetchProfileName = "fetch_profiles" +) + +func workflow() models.Tasks { + return []models.TaskTree{ + { + TaskType: models.TASK_FETCH_OTHERS, + Name: fetchProfileName, + Periodically: true, + NextTasks: []models.TaskTree{ + { + TaskType: models.TASK_FETCH_ACCOUNTS, + Name: "fetch_accounts", + Periodically: true, + NextTasks: []models.TaskTree{ + { + TaskType: models.TASK_FETCH_BALANCES, + Name: "fetch_balances", + Periodically: true, + NextTasks: []models.TaskTree{}, + }, + }, + }, + { + TaskType: models.TASK_FETCH_EXTERNAL_ACCOUNTS, + Name: "fetch_recipient_accounts", + Periodically: true, + NextTasks: []models.TaskTree{}, + }, + { + TaskType: models.TASK_CREATE_WEBHOOKS, + Name: "create_webhooks", + Periodically: false, + NextTasks: []models.TaskTree{}, + }, + { + TaskType: models.TASK_FETCH_PAYMENTS, + Name: "fetch_payments", + Periodically: false, + NextTasks: []models.TaskTree{}, + }, + }, + }, + } +} diff --git a/components/payments/internal/events/account.go b/components/payments/internal/events/account.go new file mode 100644 index 0000000000..c07ca24266 --- /dev/null +++ b/components/payments/internal/events/account.go @@ -0,0 +1,53 @@ +package events + +import ( + "encoding/json" + "time" + + "github.com/formancehq/go-libs/publish" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/pkg/events" +) + +type accountMessagePayload struct { + ID string `json:"id"` + CreatedAt time.Time `json:"createdAt"` + Reference string `json:"reference"` + Provider string `json:"provider"` + ConnectorID string `json:"connectorId"` + DefaultAsset string `json:"defaultAsset"` + AccountName string `json:"accountName"` + Type string `json:"type"` + Metadata map[string]string `json:"metadata"` + RawData json.RawMessage `json:"rawData"` +} + +func (e Events) NewEventSavedAccounts(account models.Account) publish.EventMessage { + payload := accountMessagePayload{ + ID: account.ID.String(), + ConnectorID: account.ConnectorID.String(), + Provider: account.ConnectorID.Provider, + CreatedAt: account.CreatedAt, + Reference: account.Reference, + Type: string(account.Type), + Metadata: account.Metadata, + RawData: account.Raw, + } + + if account.DefaultAsset != nil { + payload.DefaultAsset = *account.DefaultAsset + } + + if account.Name != nil { + payload.AccountName = *account.Name + } + + return publish.EventMessage{ + IdempotencyKey: account.IdempotencyKey(), + Date: time.Now().UTC(), + App: events.EventApp, + Version: events.EventVersion, + Type: events.EventTypeSavedAccounts, + Payload: payload, + } +} diff --git a/components/payments/internal/events/balance.go b/components/payments/internal/events/balance.go new file mode 100644 index 0000000000..1a57bbdc4d --- /dev/null +++ b/components/payments/internal/events/balance.go @@ -0,0 +1,41 @@ +package events + +import ( + "math/big" + "time" + + "github.com/formancehq/go-libs/publish" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/pkg/events" +) + +type balanceMessagePayload struct { + AccountID string `json:"accountID"` + ConnectorID string `json:"connectorId"` + Provider string `json:"provider"` + CreatedAt time.Time `json:"createdAt"` + LastUpdatedAt time.Time `json:"lastUpdatedAt"` + Asset string `json:"asset"` + Balance *big.Int `json:"balance"` +} + +func (e Events) NewEventSavedBalances(balance models.Balance) publish.EventMessage { + payload := balanceMessagePayload{ + AccountID: balance.AccountID.String(), + ConnectorID: balance.AccountID.ConnectorID.String(), + Provider: balance.AccountID.ConnectorID.Provider, + CreatedAt: balance.CreatedAt, + LastUpdatedAt: balance.LastUpdatedAt, + Asset: balance.Asset, + Balance: balance.Balance, + } + + return publish.EventMessage{ + IdempotencyKey: balance.IdempotencyKey(), + Date: time.Now().UTC(), + App: events.EventApp, + Version: events.EventVersion, + Type: events.EventTypeSavedBalances, + Payload: payload, + } +} diff --git a/components/payments/internal/messages/bank_account.go b/components/payments/internal/events/bank_account.go similarity index 63% rename from components/payments/internal/messages/bank_account.go rename to components/payments/internal/events/bank_account.go index a661f27900..c5e9fdf31f 100644 --- a/components/payments/internal/messages/bank_account.go +++ b/components/payments/internal/events/bank_account.go @@ -1,10 +1,9 @@ -package messages +package events import ( "time" "github.com/formancehq/go-libs/publish" - "github.com/formancehq/payments/internal/models" "github.com/formancehq/payments/pkg/events" ) @@ -21,32 +20,42 @@ type bankAccountMessagePayload struct { } type bankAccountRelatedAccountsPayload struct { - ID string `json:"id"` CreatedAt time.Time `json:"createdAt"` AccountID string `json:"accountID"` ConnectorID string `json:"connectorID"` Provider string `json:"provider"` } -func (m *Messages) NewEventSavedBankAccounts(bankAccount *models.BankAccount) publish.EventMessage { +func (e Events) NewEventSavedBankAccounts(bankAccount models.BankAccount) publish.EventMessage { bankAccount.Offuscate() payload := bankAccountMessagePayload{ - ID: bankAccount.ID.String(), - CreatedAt: bankAccount.CreatedAt, - Name: bankAccount.Name, - AccountNumber: bankAccount.AccountNumber, - IBAN: bankAccount.IBAN, - SwiftBicCode: bankAccount.SwiftBicCode, - Country: bankAccount.Country, + ID: bankAccount.ID.String(), + CreatedAt: bankAccount.CreatedAt, + Name: bankAccount.Name, + } + + if bankAccount.AccountNumber != nil { + payload.AccountNumber = *bankAccount.AccountNumber + } + + if bankAccount.IBAN != nil { + payload.IBAN = *bankAccount.IBAN + } + + if bankAccount.SwiftBicCode != nil { + payload.SwiftBicCode = *bankAccount.SwiftBicCode + } + + if bankAccount.Country != nil { + payload.Country = *bankAccount.Country } for _, relatedAccount := range bankAccount.RelatedAccounts { relatedAccount := bankAccountRelatedAccountsPayload{ - ID: relatedAccount.ID.String(), CreatedAt: relatedAccount.CreatedAt, AccountID: relatedAccount.AccountID.String(), - Provider: relatedAccount.ConnectorID.Provider.String(), + Provider: relatedAccount.ConnectorID.Provider, ConnectorID: relatedAccount.ConnectorID.String(), } @@ -54,10 +63,11 @@ func (m *Messages) NewEventSavedBankAccounts(bankAccount *models.BankAccount) pu } return publish.EventMessage{ - Date: time.Now().UTC(), - App: events.EventApp, - Version: events.EventVersion, - Type: events.EventTypeSavedBankAccount, - Payload: payload, + IdempotencyKey: bankAccount.IdempotencyKey(), + Date: time.Now().UTC(), + App: events.EventApp, + Version: events.EventVersion, + Type: events.EventTypeSavedBankAccount, + Payload: payload, } } diff --git a/components/payments/internal/messages/connectors.go b/components/payments/internal/events/connector.go similarity index 58% rename from components/payments/internal/messages/connectors.go rename to components/payments/internal/events/connector.go index 0349b73326..6ac94a87c3 100644 --- a/components/payments/internal/messages/connectors.go +++ b/components/payments/internal/events/connector.go @@ -1,10 +1,9 @@ -package messages +package events import ( "time" "github.com/formancehq/go-libs/publish" - "github.com/formancehq/payments/internal/models" "github.com/formancehq/payments/pkg/events" ) @@ -14,12 +13,13 @@ type connectorMessagePayload struct { ConnectorID string `json:"connectorId"` } -func (m *Messages) NewEventResetConnector(connectorID models.ConnectorID) publish.EventMessage { +func (e Events) NewEventResetConnector(connectorID models.ConnectorID) publish.EventMessage { return publish.EventMessage{ - Date: time.Now().UTC(), - App: events.EventApp, - Version: events.EventVersion, - Type: events.EventTypeConnectorReset, + IdempotencyKey: connectorID.String(), + Date: time.Now().UTC(), + App: events.EventApp, + Version: events.EventVersion, + Type: events.EventTypeConnectorReset, Payload: connectorMessagePayload{ CreatedAt: time.Now().UTC(), ConnectorID: connectorID.String(), diff --git a/components/payments/internal/events/events.go b/components/payments/internal/events/events.go new file mode 100644 index 0000000000..56c6e72aec --- /dev/null +++ b/components/payments/internal/events/events.go @@ -0,0 +1,27 @@ +package events + +import ( + "context" + + "github.com/ThreeDotsLabs/watermill/message" + "github.com/formancehq/go-libs/publish" + eventsdef "github.com/formancehq/payments/pkg/events" +) + +type Events struct { + publisher message.Publisher + + stackURL string +} + +func New(p message.Publisher, stackURL string) *Events { + return &Events{ + publisher: p, + stackURL: stackURL, + } +} + +func (e *Events) Publish(ctx context.Context, em publish.EventMessage) error { + return e.publisher.Publish(eventsdef.TopicPayments, + publish.NewMessage(ctx, em)) +} diff --git a/components/payments/internal/events/payment.go b/components/payments/internal/events/payment.go new file mode 100644 index 0000000000..5a60551bb6 --- /dev/null +++ b/components/payments/internal/events/payment.go @@ -0,0 +1,83 @@ +package events + +import ( + "encoding/json" + "math/big" + "time" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/go-libs/publish" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/pkg/events" +) + +type paymentMessagePayload struct { + ID string `json:"id"` + ConnectorID string `json:"connectorId"` + Provider string `json:"provider"` + Reference string `json:"reference"` + CreatedAt time.Time `json:"createdAt"` + Type string `json:"type"` + Status string `json:"status"` + Scheme string `json:"scheme"` + Asset string `json:"asset"` + SourceAccountID string `json:"sourceAccountId,omitempty"` + DestinationAccountID string `json:"destinationAccountId,omitempty"` + Links []api.Link `json:"links"` + RawData json.RawMessage `json:"rawData"` + + Amount *big.Int `json:"amount"` + Metadata map[string]string `json:"metadata"` +} + +func (e Events) NewEventSavedPayments(payment models.Payment, adjustment models.PaymentAdjustment) publish.EventMessage { + payload := paymentMessagePayload{ + ID: payment.ID.String(), + Reference: payment.Reference, + Type: payment.Type.String(), + Status: payment.Status.String(), + Amount: payment.Amount, + Scheme: payment.Scheme.String(), + Asset: payment.Asset, + CreatedAt: payment.CreatedAt, + ConnectorID: payment.ConnectorID.String(), + Provider: payment.ConnectorID.Provider, + SourceAccountID: func() string { + if payment.SourceAccountID == nil { + return "" + } + return payment.SourceAccountID.String() + }(), + DestinationAccountID: func() string { + if payment.DestinationAccountID == nil { + return "" + } + return payment.DestinationAccountID.String() + }(), + RawData: adjustment.Raw, + Metadata: payment.Metadata, + } + + if payment.SourceAccountID != nil { + payload.Links = append(payload.Links, api.Link{ + Name: "source_account", + URI: e.stackURL + "/api/payments/accounts/" + payment.SourceAccountID.String(), + }) + } + + if payment.DestinationAccountID != nil { + payload.Links = append(payload.Links, api.Link{ + Name: "destination_account", + URI: e.stackURL + "/api/payments/accounts/" + payment.DestinationAccountID.String(), + }) + } + + return publish.EventMessage{ + IdempotencyKey: adjustment.IdempotencyKey(), + Date: time.Now().UTC(), + App: events.EventApp, + Version: events.EventVersion, + Type: events.EventTypeSavedPayments, + Payload: payload, + } +} diff --git a/components/payments/internal/messages/pools.go b/components/payments/internal/events/pool.go similarity index 62% rename from components/payments/internal/messages/pools.go rename to components/payments/internal/events/pool.go index 9269c3f1fc..bff0eee23e 100644 --- a/components/payments/internal/messages/pools.go +++ b/components/payments/internal/events/pool.go @@ -1,10 +1,9 @@ -package messages +package events import ( "time" "github.com/formancehq/go-libs/publish" - "github.com/formancehq/payments/internal/models" "github.com/formancehq/payments/pkg/events" "github.com/google/uuid" @@ -17,7 +16,7 @@ type poolMessagePayload struct { AccountIDs []string `json:"accountIDs"` } -func (m *Messages) NewEventSavedPool(pool *models.Pool) publish.EventMessage { +func (e Events) NewEventSavedPool(pool models.Pool) publish.EventMessage { payload := poolMessagePayload{ ID: pool.ID.String(), Name: pool.Name, @@ -30,11 +29,12 @@ func (m *Messages) NewEventSavedPool(pool *models.Pool) publish.EventMessage { } return publish.EventMessage{ - Date: time.Now().UTC(), - App: events.EventApp, - Version: events.EventVersion, - Type: events.EventTypeSavedPool, - Payload: payload, + IdempotencyKey: pool.IdempotencyKey(), + Date: time.Now().UTC(), + App: events.EventApp, + Version: events.EventVersion, + Type: events.EventTypeSavedPool, + Payload: payload, } } @@ -43,12 +43,13 @@ type deletePoolMessagePayload struct { ID string `json:"id"` } -func (m *Messages) NewEventDeletePool(id uuid.UUID) publish.EventMessage { +func (e Events) NewEventDeletePool(id uuid.UUID) publish.EventMessage { return publish.EventMessage{ - Date: time.Now().UTC(), - App: events.EventApp, - Version: events.EventVersion, - Type: events.EventTypeDeletePool, + IdempotencyKey: id.String(), + Date: time.Now().UTC(), + App: events.EventApp, + Version: events.EventVersion, + Type: events.EventTypeDeletePool, Payload: deletePoolMessagePayload{ CreatedAt: time.Now().UTC(), ID: id.String(), diff --git a/components/payments/internal/messages/accounts.go b/components/payments/internal/messages/accounts.go deleted file mode 100644 index 07636f9fd3..0000000000 --- a/components/payments/internal/messages/accounts.go +++ /dev/null @@ -1,49 +0,0 @@ -package messages - -import ( - "encoding/json" - "time" - - "github.com/formancehq/go-libs/publish" - - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/pkg/events" -) - -type accountMessagePayload struct { - ID string `json:"id"` - CreatedAt time.Time `json:"createdAt"` - Reference string `json:"reference"` - ConnectorID string `json:"connectorId"` - Provider string `json:"provider"` - DefaultAsset string `json:"defaultAsset"` - AccountName string `json:"accountName"` - Type string `json:"type"` - RawData json.RawMessage `json:"rawData"` -} - -func (m *Messages) NewEventSavedAccounts(provider models.ConnectorProvider, account *models.Account) publish.EventMessage { - payload := accountMessagePayload{ - ID: account.ID.String(), - CreatedAt: account.CreatedAt, - Reference: account.Reference, - ConnectorID: account.ConnectorID.String(), - DefaultAsset: account.DefaultAsset.String(), - AccountName: account.AccountName, - Type: string(account.Type), - Provider: provider.String(), - RawData: account.RawData, - } - - if account.Type == models.AccountTypeExternalFormance { - payload.Type = models.AccountTypeExternal.String() - } - - return publish.EventMessage{ - Date: time.Now().UTC(), - App: events.EventApp, - Version: events.EventVersion, - Type: events.EventTypeSavedAccounts, - Payload: payload, - } -} diff --git a/components/payments/internal/messages/balances.go b/components/payments/internal/messages/balances.go deleted file mode 100644 index f65139c372..0000000000 --- a/components/payments/internal/messages/balances.go +++ /dev/null @@ -1,39 +0,0 @@ -package messages - -import ( - "math/big" - "time" - - "github.com/formancehq/go-libs/publish" - - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/pkg/events" -) - -type balanceMessagePayload struct { - AccountID string `json:"accountID"` - ConnectorID string `json:"connectorId"` - Provider string `json:"provider"` - CreatedAt time.Time `json:"createdAt"` - Asset string `json:"asset"` - Balance *big.Int `json:"balance"` -} - -func (m *Messages) NewEventSavedBalances(balance *models.Balance) publish.EventMessage { - payload := balanceMessagePayload{ - CreatedAt: balance.CreatedAt, - ConnectorID: balance.ConnectorID.String(), - Provider: balance.ConnectorID.Provider.String(), - AccountID: balance.AccountID.String(), - Asset: balance.Asset.String(), - Balance: balance.Balance, - } - - return publish.EventMessage{ - Date: time.Now().UTC(), - App: events.EventApp, - Version: events.EventVersion, - Type: events.EventTypeSavedBalances, - Payload: payload, - } -} diff --git a/components/payments/internal/messages/messages.go b/components/payments/internal/messages/messages.go deleted file mode 100644 index 0da1b6c2ae..0000000000 --- a/components/payments/internal/messages/messages.go +++ /dev/null @@ -1,11 +0,0 @@ -package messages - -type Messages struct { - stackURL string -} - -func NewMessages(stackURL string) *Messages { - return &Messages{ - stackURL: stackURL, - } -} diff --git a/components/payments/internal/messages/payments.go b/components/payments/internal/messages/payments.go deleted file mode 100644 index 8d35854247..0000000000 --- a/components/payments/internal/messages/payments.go +++ /dev/null @@ -1,93 +0,0 @@ -package messages - -import ( - "encoding/json" - "math/big" - "time" - - "github.com/formancehq/go-libs/api" - - "github.com/formancehq/go-libs/publish" - - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/pkg/events" -) - -type paymentMessagePayload struct { - ID string `json:"id"` - Reference string `json:"reference"` - CreatedAt time.Time `json:"createdAt"` - ConnectorID string `json:"connectorId"` - Provider string `json:"provider"` - Type models.PaymentType `json:"type"` - Status models.PaymentStatus `json:"status"` - Scheme models.PaymentScheme `json:"scheme"` - Asset models.Asset `json:"asset"` - SourceAccountID string `json:"sourceAccountId,omitempty"` - DestinationAccountID string `json:"destinationAccountId,omitempty"` - Links []api.Link `json:"links"` - RawData json.RawMessage `json:"rawData"` - - // TODO: Remove 'initialAmount' once frontend has switched to 'amount - InitialAmount *big.Int `json:"initialAmount"` - Amount *big.Int `json:"amount"` - Metadata map[string]string `json:"metadata"` -} - -func (m *Messages) NewEventSavedPayments(provider models.ConnectorProvider, payment *models.Payment) publish.EventMessage { - payload := paymentMessagePayload{ - ID: payment.ID.String(), - Reference: payment.Reference, - Type: payment.Type, - Status: payment.Status, - InitialAmount: payment.InitialAmount, - Amount: payment.Amount, - Scheme: payment.Scheme, - Asset: payment.Asset, - CreatedAt: payment.CreatedAt, - ConnectorID: payment.ConnectorID.String(), - Provider: provider.String(), - SourceAccountID: func() string { - if payment.SourceAccountID == nil { - return "" - } - return payment.SourceAccountID.String() - }(), - DestinationAccountID: func() string { - if payment.DestinationAccountID == nil { - return "" - } - return payment.DestinationAccountID.String() - }(), - RawData: payment.RawData, - Metadata: func() map[string]string { - ret := make(map[string]string) - for _, m := range payment.Metadata { - ret[m.Key] = m.Value - } - return ret - }(), - } - - if payment.SourceAccountID != nil { - payload.Links = append(payload.Links, api.Link{ - Name: "source_account", - URI: m.stackURL + "/api/payments/accounts/" + payment.SourceAccountID.String(), - }) - } - - if payment.DestinationAccountID != nil { - payload.Links = append(payload.Links, api.Link{ - Name: "destination_account", - URI: m.stackURL + "/api/payments/accounts/" + payment.DestinationAccountID.String(), - }) - } - - return publish.EventMessage{ - Date: time.Now().UTC(), - App: events.EventApp, - Version: events.EventVersion, - Type: events.EventTypeSavedPayments, - Payload: payload, - } -} diff --git a/components/payments/internal/messages/transfer_initiations.go b/components/payments/internal/messages/transfer_initiations.go deleted file mode 100644 index 20bcf82d21..0000000000 --- a/components/payments/internal/messages/transfer_initiations.go +++ /dev/null @@ -1,97 +0,0 @@ -package messages - -import ( - "math/big" - "time" - - "github.com/formancehq/go-libs/publish" - - "github.com/formancehq/payments/internal/models" - "github.com/formancehq/payments/pkg/events" -) - -type transferInitiationsPaymentsMessagePayload struct { - TransferInitiationID string `json:"transferInitiationId"` - PaymentID string `json:"paymentId"` - CreatedAt time.Time `json:"createdAt"` - Status string `json:"status"` - Error string `json:"error"` -} - -type transferInitiationsMessagePayload struct { - ID string `json:"id"` - CreatedAt time.Time `json:"createdAt"` - ScheduleAt time.Time `json:"scheduledAt"` - ConnectorID string `json:"connectorId"` - Provider string `json:"provider"` - Description string `json:"description"` - Type string `json:"type"` - SourceAccountID string `json:"sourceAccountId"` - DestinationAccountID string `json:"destinationAccountId"` - Amount *big.Int `json:"amount"` - Asset models.Asset `json:"asset"` - Attempts int `json:"attempts"` - Status string `json:"status"` - Error string `json:"error"` - RelatedPayments []*transferInitiationsPaymentsMessagePayload `json:"relatedPayments"` -} - -func (m *Messages) NewEventSavedTransferInitiations(tf *models.TransferInitiation) publish.EventMessage { - payload := transferInitiationsMessagePayload{ - ID: tf.ID.String(), - CreatedAt: tf.CreatedAt, - ScheduleAt: tf.ScheduledAt, - ConnectorID: tf.ConnectorID.String(), - Provider: tf.Provider.String(), - Description: tf.Description, - Type: tf.Type.String(), - SourceAccountID: tf.SourceAccountID.String(), - DestinationAccountID: tf.DestinationAccountID.String(), - Amount: tf.Amount, - Asset: tf.Asset, - Attempts: len(tf.RelatedAdjustments), - } - - if len(tf.RelatedAdjustments) > 0 { - // Take the status and error from the last adjustment - payload.Status = tf.RelatedAdjustments[0].Status.String() - payload.Error = tf.RelatedAdjustments[0].Error - } - - payload.RelatedPayments = make([]*transferInitiationsPaymentsMessagePayload, len(tf.RelatedPayments)) - for i, p := range tf.RelatedPayments { - payload.RelatedPayments[i] = &transferInitiationsPaymentsMessagePayload{ - TransferInitiationID: p.TransferInitiationID.String(), - PaymentID: p.PaymentID.String(), - CreatedAt: p.CreatedAt, - Status: p.Status.String(), - Error: p.Error, - } - } - - return publish.EventMessage{ - Date: time.Now().UTC(), - App: events.EventApp, - Version: events.EventVersion, - Type: events.EventTypeSavedTransferInitiation, - Payload: payload, - } -} - -type deleteTransferInitiationMessagePayload struct { - CreatedAt time.Time `json:"createdAt"` - ID string `json:"id"` -} - -func (m *Messages) NewEventDeleteTransferInitiation(id models.TransferInitiationID) publish.EventMessage { - return publish.EventMessage{ - Date: time.Now().UTC(), - App: events.EventApp, - Version: events.EventVersion, - Type: events.EventTypeDeleteTransferInitiation, - Payload: deleteTransferInitiationMessagePayload{ - CreatedAt: time.Now().UTC(), - ID: id.String(), - }, - } -} diff --git a/components/payments/internal/models/account.go b/components/payments/internal/models/account.go deleted file mode 100644 index e58ee4052e..0000000000 --- a/components/payments/internal/models/account.go +++ /dev/null @@ -1,128 +0,0 @@ -package models - -import ( - "database/sql/driver" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "time" - - "github.com/gibson042/canonicaljson-go" - "github.com/uptrace/bun" -) - -type Account struct { - bun.BaseModel `bun:"accounts.account"` - - ID AccountID `bun:",pk,type:character varying,nullzero"` - ConnectorID ConnectorID `bun:",type:character varying"` - CreatedAt time.Time `bun:",nullzero"` - Reference string - DefaultAsset Asset `bun:"default_currency"` // Is optional and default to '' - AccountName string // Is optional and default to '' - Type AccountType - Metadata map[string]string - - RawData json.RawMessage - - PoolAccounts []*PoolAccounts `bun:"rel:has-many,join:id=account_id"` -} - -type AccountType string - -const ( - AccountTypeUnknown AccountType = "UNKNOWN" - // Refers to an account that is internal to the psp, an account that we - // can actually fetch the balance. - AccountTypeInternal AccountType = "INTERNAL" - // Refers to an external accounts such as user's bank accounts. - AccountTypeExternal AccountType = "EXTERNAL" - // Refers to an external accounts created inside formance database. - // This is used only internally and will be transformed to EXTERNAL when - // returned to the user. - AccountTypeExternalFormance AccountType = "EXTERNAL_FORMANCE" -) - -func (at AccountType) String() string { - return string(at) -} - -func AccountTypeFromString(t string) (AccountType, error) { - switch t { - case AccountTypeInternal.String(): - return AccountTypeInternal, nil - case AccountTypeExternal.String(): - return AccountTypeExternal, nil - case AccountTypeExternalFormance.String(): - return AccountTypeExternalFormance, nil - } - - return AccountTypeUnknown, fmt.Errorf("unknown account type: %s", t) -} - -type AccountID struct { - Reference string - ConnectorID ConnectorID -} - -func (aid *AccountID) String() string { - if aid == nil || aid.Reference == "" { - return "" - } - - data, err := canonicaljson.Marshal(aid) - if err != nil { - panic(err) - } - - return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(data) -} - -func AccountIDFromString(value string) (*AccountID, error) { - data, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(value) - if err != nil { - return nil, err - } - ret := AccountID{} - err = canonicaljson.Unmarshal(data, &ret) - if err != nil { - return nil, err - } - - return &ret, nil -} - -func MustAccountIDFromString(value string) AccountID { - id, err := AccountIDFromString(value) - if err != nil { - panic(err) - } - return *id -} - -func (aid AccountID) Value() (driver.Value, error) { - return aid.String(), nil -} - -func (aid *AccountID) Scan(value interface{}) error { - if value == nil { - return errors.New("account id is nil") - } - - if s, err := driver.String.ConvertValue(value); err == nil { - - if v, ok := s.(string); ok { - - id, err := AccountIDFromString(v) - if err != nil { - return fmt.Errorf("failed to parse account id %s: %v", v, err) - } - - *aid = *id - return nil - } - } - - return fmt.Errorf("failed to scan account id: %v", value) -} diff --git a/components/payments/internal/models/account_id.go b/components/payments/internal/models/account_id.go new file mode 100644 index 0000000000..8b3d5d0a2e --- /dev/null +++ b/components/payments/internal/models/account_id.go @@ -0,0 +1,78 @@ +package models + +import ( + "database/sql/driver" + "encoding/base64" + "errors" + "fmt" + + "github.com/gibson042/canonicaljson-go" +) + +type AccountID struct { + Reference string + ConnectorID ConnectorID +} + +func (aid *AccountID) String() string { + if aid == nil || aid.Reference == "" { + return "" + } + + data, err := canonicaljson.Marshal(aid) + if err != nil { + panic(err) + } + + return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(data) +} + +func AccountIDFromString(value string) (AccountID, error) { + ret := AccountID{} + + data, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(value) + if err != nil { + return ret, err + } + + err = canonicaljson.Unmarshal(data, &ret) + if err != nil { + return ret, err + } + + return ret, nil +} + +func MustAccountIDFromString(value string) AccountID { + id, err := AccountIDFromString(value) + if err != nil { + panic(err) + } + return id +} + +func (aid AccountID) Value() (driver.Value, error) { + return aid.String(), nil +} + +func (aid *AccountID) Scan(value interface{}) error { + if value == nil { + return errors.New("account id is nil") + } + + if s, err := driver.String.ConvertValue(value); err == nil { + + if v, ok := s.(string); ok { + + id, err := AccountIDFromString(v) + if err != nil { + return fmt.Errorf("failed to parse account id %s: %v", v, err) + } + + *aid = id + return nil + } + } + + return fmt.Errorf("failed to scan account id: %v", value) +} diff --git a/components/payments/internal/models/account_type.go b/components/payments/internal/models/account_type.go new file mode 100644 index 0000000000..0428e0756e --- /dev/null +++ b/components/payments/internal/models/account_type.go @@ -0,0 +1,12 @@ +package models + +type AccountType string + +const ( + ACCOUNT_TYPE_UNKNOWN AccountType = "UNKNOWN" + // Internal accounts refers to user's digital e-wallets. It serves as a + // secure storage for funds within the payments provider environment. + ACCOUNT_TYPE_INTERNAL AccountType = "INTERNAL" + // External accounts represents actual bank accounts of the user. + ACCOUNT_TYPE_EXTERNAL AccountType = "EXTERNAL" +) diff --git a/components/payments/internal/models/accounts.go b/components/payments/internal/models/accounts.go new file mode 100644 index 0000000000..f7f8c62566 --- /dev/null +++ b/components/payments/internal/models/accounts.go @@ -0,0 +1,148 @@ +package models + +import ( + "encoding/json" + "time" +) + +// Internal struct used by the plugins +type PSPAccount struct { + // PSP reference of the account. Should be unique. + Reference string + + // Account's creation date + CreatedAt time.Time + + // Optional, human readable name of the account (if existing) + Name *string + // Optional, if provided the default asset of the account + // in minor currencies unit. + DefaultAsset *string + + // Additional metadata + Metadata map[string]string + + // PSP response in raw + Raw json.RawMessage +} + +type Account struct { + // Unique Account ID generated from account information + ID AccountID `json:"id"` + // Related Connector ID + ConnectorID ConnectorID `json:"connectorID"` + + // PSP reference of the account. Should be unique. + Reference string `json:"reference"` + + // Account's creation date + CreatedAt time.Time `json:"createdAt"` + + // Type of account: INTERNAL, EXTERNAL... + Type AccountType `json:"type"` + + // Optional, human readable name of the account (if existing) + Name *string `json:"name"` + // Optional, if provided the default asset of the account + // in minor currencies unit. + DefaultAsset *string `json:"defaultAsset"` + + // Additional metadata + Metadata map[string]string `json:"metadata"` + + // PSP response in raw + Raw json.RawMessage `json:"raw"` +} + +func (a Account) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + ID string `json:"id"` + ConnectorID string `json:"connectorID"` + Reference string `json:"reference"` + CreatedAt time.Time `json:"createdAt"` + Type AccountType `json:"type"` + Name *string `json:"name"` + DefaultAsset *string `json:"defaultAsset"` + Metadata map[string]string `json:"metadata"` + Raw json.RawMessage `json:"raw"` + }{ + ID: a.ID.String(), + ConnectorID: a.ConnectorID.String(), + Reference: a.Reference, + CreatedAt: a.CreatedAt, + Type: a.Type, + Name: a.Name, + DefaultAsset: a.DefaultAsset, + Metadata: a.Metadata, + Raw: a.Raw, + }) +} + +func (a *Account) IdempotencyKey() string { + return a.ID.String() +} + +func (a *Account) UnmarshalJSON(data []byte) error { + var aux struct { + ID string `json:"id"` + ConnectorID string `json:"connectorID"` + Reference string `json:"reference"` + CreatedAt time.Time `json:"createdAt"` + Type AccountType `json:"type"` + Name *string `json:"name"` + DefaultAsset *string `json:"defaultAsset"` + Metadata map[string]string `json:"metadata"` + Raw json.RawMessage `json:"raw"` + } + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + id, err := AccountIDFromString(aux.ID) + if err != nil { + return err + } + + connectorID, err := ConnectorIDFromString(aux.ConnectorID) + if err != nil { + return err + } + + a.ID = id + a.ConnectorID = connectorID + a.Reference = aux.Reference + a.CreatedAt = aux.CreatedAt + a.Type = aux.Type + a.Name = aux.Name + a.DefaultAsset = aux.DefaultAsset + a.Metadata = aux.Metadata + a.Raw = aux.Raw + + return nil +} + +func FromPSPAccount(from PSPAccount, accountType AccountType, connectorID ConnectorID) Account { + return Account{ + ID: AccountID{ + Reference: from.Reference, + ConnectorID: connectorID, + }, + ConnectorID: connectorID, + Reference: from.Reference, + CreatedAt: from.CreatedAt, + Type: accountType, + Name: from.Name, + DefaultAsset: from.DefaultAsset, + Metadata: from.Metadata, + Raw: from.Raw, + } +} + +func FromPSPAccounts(from []PSPAccount, accountType AccountType, connectorID ConnectorID) []Account { + accounts := make([]Account, 0, len(from)) + for _, a := range from { + accounts = append(accounts, FromPSPAccount(a, accountType, connectorID)) + } + return accounts +} diff --git a/components/payments/internal/models/balance.go b/components/payments/internal/models/balance.go deleted file mode 100644 index 4cc10169b9..0000000000 --- a/components/payments/internal/models/balance.go +++ /dev/null @@ -1,19 +0,0 @@ -package models - -import ( - "math/big" - "time" - - "github.com/uptrace/bun" -) - -type Balance struct { - bun.BaseModel `bun:"accounts.balances"` - - AccountID AccountID `bun:"type:character varying,nullzero"` - Asset Asset `bun:"currency"` - Balance *big.Int `bun:"type:numeric"` - CreatedAt time.Time - LastUpdatedAt time.Time - ConnectorID ConnectorID `bun:"-"` -} diff --git a/components/payments/internal/models/balances.go b/components/payments/internal/models/balances.go new file mode 100644 index 0000000000..5d5e1ad90a --- /dev/null +++ b/components/payments/internal/models/balances.go @@ -0,0 +1,128 @@ +package models + +import ( + "encoding/base64" + "encoding/json" + "math/big" + "time" + + "github.com/gibson042/canonicaljson-go" +) + +type PSPBalance struct { + // PSP account reference of the balance. + AccountReference string + + // Balance Creation date. + CreatedAt time.Time + + // Balance amount. + Amount *big.Int + + // Currency. Should be in minor currencies unit. + // For example: USD/2 + Asset string +} + +type Balance struct { + // Balance related formance account id + AccountID AccountID `json:"accountID"` + // Balance created at + CreatedAt time.Time `json:"createdAt"` + // Balance last updated at + LastUpdatedAt time.Time `json:"lastUpdatedAt"` + + // Currency. Should be in minor currencies unit. + Asset string `json:"asset"` + // Balance amount. + Balance *big.Int `json:"balance"` +} + +func (b *Balance) IdempotencyKey() string { + var ik = struct { + AccountID string + CreatedAt int64 + LastUpdatedAt int64 + }{ + AccountID: b.AccountID.String(), + CreatedAt: b.CreatedAt.UnixNano(), + LastUpdatedAt: b.LastUpdatedAt.UnixNano(), + } + + data, err := canonicaljson.Marshal(ik) + if err != nil { + panic(err) + } + + return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(data) +} + +func (b Balance) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + AccountID string `json:"accountID"` + CreatedAt time.Time `json:"createdAt"` + LastUpdatedAt time.Time `json:"lastUpdatedAt"` + + Asset string `json:"asset"` + Balance *big.Int `json:"balance"` + }{ + AccountID: b.AccountID.String(), + CreatedAt: b.CreatedAt, + LastUpdatedAt: b.LastUpdatedAt, + Asset: b.Asset, + Balance: b.Balance, + }) +} + +func (b *Balance) UnmarshalJSON(data []byte) error { + var aux struct { + AccountID string `json:"accountID"` + CreatedAt time.Time `json:"createdAt"` + LastUpdatedAt time.Time `json:"lastUpdatedAt"` + Asset string `json:"asset"` + Balance *big.Int `json:"balance"` + } + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + accountID, err := AccountIDFromString(aux.AccountID) + if err != nil { + return err + } + + b.AccountID = accountID + b.CreatedAt = aux.CreatedAt + b.LastUpdatedAt = aux.LastUpdatedAt + b.Asset = aux.Asset + b.Balance = aux.Balance + + return nil +} + +type AggregatedBalance struct { + Asset string `json:"asset"` + Amount *big.Int `json:"amount"` +} + +func FromPSPBalance(from PSPBalance, connectorID ConnectorID) Balance { + return Balance{ + AccountID: AccountID{ + Reference: from.AccountReference, + ConnectorID: connectorID, + }, + CreatedAt: from.CreatedAt, + LastUpdatedAt: from.CreatedAt, + Asset: from.Asset, + Balance: from.Amount, + } +} + +func FromPSPBalances(from []PSPBalance, connectorID ConnectorID) []Balance { + balances := make([]Balance, 0, len(from)) + for _, b := range from { + balances = append(balances, FromPSPBalance(b, connectorID)) + } + return balances +} diff --git a/components/payments/internal/models/bank_account.go b/components/payments/internal/models/bank_account.go deleted file mode 100644 index 53ada2648d..0000000000 --- a/components/payments/internal/models/bank_account.go +++ /dev/null @@ -1,67 +0,0 @@ -package models - -import ( - "errors" - "strings" - "time" - - "github.com/google/uuid" - "github.com/uptrace/bun" -) - -type BankAccount struct { - bun.BaseModel `bun:"accounts.bank_account"` - - ID uuid.UUID `bun:",pk,nullzero"` - CreatedAt time.Time `bun:",nullzero"` - Name string - AccountNumber string `bun:"decrypted_account_number,scanonly"` - IBAN string `bun:"decrypted_iban,scanonly"` - SwiftBicCode string `bun:"decrypted_swift_bic_code,scanonly"` - Country string `bun:"country"` - Metadata map[string]string - - RelatedAccounts []*BankAccountRelatedAccount `bun:"rel:has-many,join:id=bank_account_id"` -} - -func (a *BankAccount) Offuscate() error { - if a.IBAN != "" { - length := len(a.IBAN) - if length < 8 { - return errors.New("IBAN is not valid") - } - - a.IBAN = a.IBAN[:4] + strings.Repeat("*", length-8) + a.IBAN[length-4:] - } - - if a.AccountNumber != "" { - length := len(a.AccountNumber) - if length < 5 { - return errors.New("Account number is not valid") - } - - a.AccountNumber = a.AccountNumber[:2] + strings.Repeat("*", length-5) + a.AccountNumber[length-3:] - } - - return nil -} - -type BankAccountRelatedAccount struct { - bun.BaseModel `bun:"accounts.bank_account_related_accounts"` - - ID uuid.UUID `bun:",pk,nullzero"` - CreatedAt time.Time `bun:",nullzero"` - BankAccountID uuid.UUID `bun:",nullzero"` - ConnectorID ConnectorID `bun:",nullzero"` - AccountID AccountID `bun:",nullzero"` -} - -const ( - bankAccountOwnerNamespace = formanceMetadataSpecNamespace + "owner/" - - BankAccountOwnerAddressLine1MetadataKey = bankAccountOwnerNamespace + "addressLine1" - BankAccountOwnerAddressLine2MetadataKey = bankAccountOwnerNamespace + "addressLine2" - BankAccountOwnerCityMetadataKey = bankAccountOwnerNamespace + "city" - BankAccountOwnerRegionMetadataKey = bankAccountOwnerNamespace + "region" - BankAccountOwnerPostalCodeMetadataKey = bankAccountOwnerNamespace + "postalCode" -) diff --git a/components/payments/internal/models/bank_accounts.go b/components/payments/internal/models/bank_accounts.go new file mode 100644 index 0000000000..78dec1d98e --- /dev/null +++ b/components/payments/internal/models/bank_accounts.go @@ -0,0 +1,60 @@ +package models + +import ( + "errors" + "strings" + "time" + + "github.com/google/uuid" +) + +const ( + bankAccountOwnerNamespace = formanceMetadataSpecNamespace + "owner/" + + BankAccountOwnerAddressLine1MetadataKey = bankAccountOwnerNamespace + "addressLine1" + BankAccountOwnerAddressLine2MetadataKey = bankAccountOwnerNamespace + "addressLine2" + BankAccountOwnerCityMetadataKey = bankAccountOwnerNamespace + "city" + BankAccountOwnerRegionMetadataKey = bankAccountOwnerNamespace + "region" + BankAccountOwnerPostalCodeMetadataKey = bankAccountOwnerNamespace + "postalCode" +) + +type BankAccount struct { + ID uuid.UUID `json:"id"` + CreatedAt time.Time `json:"createdAt"` + Name string `json:"name"` + + AccountNumber *string `json:"accountNumber"` + IBAN *string `json:"iban"` + SwiftBicCode *string `json:"swiftBicCode"` + Country *string `json:"country"` + + Metadata map[string]string `json:"metadata"` + + RelatedAccounts []BankAccountRelatedAccount `json:"relatedAccounts"` +} + +func (b *BankAccount) IdempotencyKey() string { + return b.ID.String() +} + +func (a *BankAccount) Offuscate() error { + if a.IBAN != nil { + length := len(*a.IBAN) + if length < 8 { + return errors.New("IBAN is not valid") + } + + *a.IBAN = (*a.IBAN)[:4] + strings.Repeat("*", length-8) + (*a.IBAN)[length-4:] + } + + if a.AccountNumber != nil { + length := len(*a.AccountNumber) + if length < 5 { + return errors.New("Account number is not valid") + } + + *a.AccountNumber = (*a.AccountNumber)[:2] + strings.Repeat("*", length-5) + (*a.AccountNumber)[length-3:] + } + + return nil +} diff --git a/components/payments/internal/models/bank_accounts_related_accounts.go b/components/payments/internal/models/bank_accounts_related_accounts.go new file mode 100644 index 0000000000..077bffdc21 --- /dev/null +++ b/components/payments/internal/models/bank_accounts_related_accounts.go @@ -0,0 +1,59 @@ +package models + +import ( + "encoding/json" + "time" + + "github.com/google/uuid" +) + +type BankAccountRelatedAccount struct { + BankAccountID uuid.UUID `json:"bankAccountID"` + AccountID AccountID `json:"accountID"` + ConnectorID ConnectorID `json:"connectorID"` + CreatedAt time.Time `json:"createdAt"` +} + +func (b BankAccountRelatedAccount) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + BankAccountID uuid.UUID `json:"bankAccountID"` + AccountID string `json:"accountID"` + ConnectorID string `json:"connectorID"` + CreatedAt time.Time `json:"createdAt"` + }{ + BankAccountID: b.BankAccountID, + AccountID: b.AccountID.String(), + ConnectorID: b.ConnectorID.String(), + CreatedAt: b.CreatedAt, + }) +} + +func (b *BankAccountRelatedAccount) UnmarshalJSON(data []byte) error { + var aux struct { + BankAccountID uuid.UUID `json:"bankAccountID"` + AccountID string `json:"accountID"` + ConnectorID string `json:"connectorID"` + CreatedAt time.Time `json:"createdAt"` + } + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + accountID, err := AccountIDFromString(aux.AccountID) + if err != nil { + return err + } + + connectorID, err := ConnectorIDFromString(aux.ConnectorID) + if err != nil { + return err + } + + b.BankAccountID = aux.BankAccountID + b.AccountID = accountID + b.ConnectorID = connectorID + b.CreatedAt = aux.CreatedAt + + return nil +} diff --git a/components/payments/internal/models/capabilities.go b/components/payments/internal/models/capabilities.go new file mode 100644 index 0000000000..419e870f53 --- /dev/null +++ b/components/payments/internal/models/capabilities.go @@ -0,0 +1,100 @@ +package models + +import ( + "database/sql/driver" + "errors" + "fmt" +) + +type Capability int + +const ( + CAPABILITY_FETCH_UNKNOWN Capability = iota + CAPABILITY_FETCH_ACCOUNTS + CAPABILITY_FETCH_BALANCES + CAPABILITY_FETCH_EXTERNAL_ACCOUNTS + CAPABILITY_FETCH_PAYMENTS + CAPABILITY_FETCH_OTHERS + CAPABILITY_WEBHOOKS + CAPABILITY_CREATION_BANK_ACCOUNT + CAPABILITY_CREATION_PAYMENT +) + +func (t Capability) String() string { + switch t { + case CAPABILITY_FETCH_ACCOUNTS: + return "FETCH_ACCOUNTS" + case CAPABILITY_FETCH_EXTERNAL_ACCOUNTS: + return "FETCH_EXTERNAL_ACCOUNTS" + case CAPABILITY_FETCH_PAYMENTS: + return "FETCH_PAYMENTS" + case CAPABILITY_FETCH_OTHERS: + return "FETCH_OTHERS" + case CAPABILITY_WEBHOOKS: + return "WEBHOOKS" + case CAPABILITY_CREATION_BANK_ACCOUNT: + return "CREATION_BANK_ACCOUNT" + case CAPABILITY_CREATION_PAYMENT: + return "CREATION_PAYMENT" + default: + return "UNKNOWN" + } +} + +func (t Capability) Value() (driver.Value, error) { + switch t { + case CAPABILITY_FETCH_ACCOUNTS: + return "FETCH_ACCOUNTS", nil + case CAPABILITY_FETCH_EXTERNAL_ACCOUNTS: + return "FETCH_EXTERNAL_ACCOUNTS", nil + case CAPABILITY_FETCH_PAYMENTS: + return "FETCH_PAYMENTS", nil + case CAPABILITY_FETCH_OTHERS: + return "FETCH_OTHERS", nil + case CAPABILITY_WEBHOOKS: + return "WEBHOOKS", nil + case CAPABILITY_CREATION_BANK_ACCOUNT: + return "CREATION_BANK_ACCOUNT", nil + case CAPABILITY_CREATION_PAYMENT: + return "CREATION_PAYMENT", nil + default: + return nil, fmt.Errorf("unknown capability") + } +} + +func (t *Capability) Scan(value interface{}) error { + if value == nil { + return errors.New("capability is nil") + } + + s, err := driver.String.ConvertValue(value) + if err != nil { + return fmt.Errorf("failed to convert capability") + } + + v, ok := s.(string) + if !ok { + return fmt.Errorf("failed to cast capability") + } + + switch v { + case "FETCH_ACCOUNTS": + *t = CAPABILITY_FETCH_ACCOUNTS + case "FETCH_EXTERNAL_ACCOUNTS": + *t = CAPABILITY_FETCH_EXTERNAL_ACCOUNTS + case "FETCH_PAYMENTS": + *t = CAPABILITY_FETCH_PAYMENTS + case "FETCH_OTHERS": + *t = CAPABILITY_FETCH_OTHERS + case "WEBHOOKS": + *t = CAPABILITY_WEBHOOKS + case "CREATION_BANK_ACCOUNT": + *t = CAPABILITY_CREATION_BANK_ACCOUNT + case "CREATION_PAYMENT": + *t = CAPABILITY_CREATION_PAYMENT + default: + return fmt.Errorf("unknown capability") + } + + return nil +} diff --git a/components/payments/internal/models/config.go b/components/payments/internal/models/config.go new file mode 100644 index 0000000000..461c77a6ac --- /dev/null +++ b/components/payments/internal/models/config.go @@ -0,0 +1,74 @@ +package models + +import ( + "encoding/json" + "errors" + "time" +) + +const ( + defaultPollingPeriod = 2 * time.Minute + defaultPageSize = 25 +) + +type Config struct { + Name string `json:"name"` + PollingPeriod time.Duration `json:"pollingPeriod"` + PageSize int `json:"pageSize"` +} + +func (c Config) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Name string `json:"name"` + PollingPeriod string `json:"pollingPeriod"` + PageSize int `json:"pageSize"` + }{ + Name: c.Name, + PollingPeriod: c.PollingPeriod.String(), + PageSize: c.PageSize, + }) +} + +func (c *Config) UnmarshalJSON(data []byte) error { + var raw struct { + Name string `json:"name"` + PollingPeriod string `json:"pollingPeriod"` + PageSize int `json:"pageSize"` + } + + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + pollingPeriod, err := time.ParseDuration(raw.PollingPeriod) + if err != nil { + return err + } + + c.Name = raw.Name + + if pollingPeriod > 0 { + c.PollingPeriod = pollingPeriod + } + + if raw.PageSize > 0 { + c.PageSize = raw.PageSize + } + + return nil +} + +func (c Config) Validate() error { + if c.Name == "" { + return errors.New("name is required") + } + + return nil +} + +func DefaultConfig() Config { + return Config{ + PollingPeriod: defaultPollingPeriod, + PageSize: defaultPageSize, + } +} diff --git a/components/payments/internal/models/connector.go b/components/payments/internal/models/connector.go deleted file mode 100644 index ad5ea0a61b..0000000000 --- a/components/payments/internal/models/connector.go +++ /dev/null @@ -1,203 +0,0 @@ -package models - -import ( - "database/sql/driver" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "strings" - "time" - - "github.com/gibson042/canonicaljson-go" - "github.com/google/uuid" - "github.com/uptrace/bun" -) - -type Connector struct { - bun.BaseModel `bun:"connectors.connector"` - - ID ConnectorID `bun:",pk,nullzero"` - Name string - CreatedAt time.Time `bun:",nullzero"` - Provider ConnectorProvider - - // EncryptedConfig is a PGP-encrypted JSON string. - EncryptedConfig string `bun:"config"` - - // Config is a decrypted config. It is not stored in the database. - Config json.RawMessage `bun:"decrypted_config,scanonly"` - - Tasks []*Task `bun:"rel:has-many,join:id=connector_id"` -} - -type ConnectorID struct { - Reference uuid.UUID - Provider ConnectorProvider -} - -func (cid *ConnectorID) String() string { - if cid == nil || cid.Reference == uuid.Nil { - return "" - } - - data, err := canonicaljson.Marshal(cid) - if err != nil { - panic(err) - } - - return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(data) -} - -func ConnectorIDFromString(value string) (ConnectorID, error) { - data, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(value) - if err != nil { - return ConnectorID{}, err - } - ret := ConnectorID{} - err = canonicaljson.Unmarshal(data, &ret) - if err != nil { - return ConnectorID{}, err - } - - return ret, nil -} - -func MustConnectorIDFromString(value string) ConnectorID { - id, err := ConnectorIDFromString(value) - if err != nil { - panic(err) - } - return id -} - -func (cid ConnectorID) Value() (driver.Value, error) { - return cid.String(), nil -} - -func (cid *ConnectorID) Scan(value interface{}) error { - if value == nil { - return errors.New("connector id is nil") - } - - if s, err := driver.String.ConvertValue(value); err == nil { - - if v, ok := s.(string); ok { - - id, err := ConnectorIDFromString(v) - if err != nil { - return fmt.Errorf("failed to parse connector id %s: %v", v, err) - } - - *cid = id - return nil - } - } - - return fmt.Errorf("failed to scan connector id: %v", value) -} - -func (c Connector) String() string { - c.EncryptedConfig = "****" - c.Config = nil - - var t any = c - - return fmt.Sprintf("%+v", t) -} - -type ConnectorProvider string - -const ( - ConnectorProviderBankingCircle ConnectorProvider = "BANKING-CIRCLE" - ConnectorProviderCurrencyCloud ConnectorProvider = "CURRENCY-CLOUD" - ConnectorProviderDummyPay ConnectorProvider = "DUMMY-PAY" - ConnectorProviderModulr ConnectorProvider = "MODULR" - ConnectorProviderStripe ConnectorProvider = "STRIPE" - ConnectorProviderWise ConnectorProvider = "WISE" - ConnectorProviderMangopay ConnectorProvider = "MANGOPAY" - ConnectorProviderMoneycorp ConnectorProvider = "MONEYCORP" - ConnectorProviderAtlar ConnectorProvider = "ATLAR" - ConnectorProviderAdyen ConnectorProvider = "ADYEN" - ConnectorProviderGeneric ConnectorProvider = "GENERIC" -) - -func (p ConnectorProvider) String() string { - return string(p) -} - -func (p ConnectorProvider) StringLower() string { - return strings.ToLower(string(p)) -} - -func ConnectorProviderFromString(s string) (ConnectorProvider, error) { - switch s { - case "BANKING-CIRCLE": - return ConnectorProviderBankingCircle, nil - case "CURRENCY-CLOUD": - return ConnectorProviderCurrencyCloud, nil - case "DUMMY-PAY": - return ConnectorProviderDummyPay, nil - case "MODULR": - return ConnectorProviderModulr, nil - case "STRIPE": - return ConnectorProviderStripe, nil - case "WISE": - return ConnectorProviderWise, nil - case "MANGOPAY": - return ConnectorProviderMangopay, nil - case "MONEYCORP": - return ConnectorProviderMoneycorp, nil - case "ATLAR": - return ConnectorProviderAtlar, nil - case "ADYEN": - return ConnectorProviderAdyen, nil - case "GENERIC": - return ConnectorProviderGeneric, nil - default: - return "", errors.New("unknown connector provider") - } -} - -func MustConnectorProviderFromString(s string) ConnectorProvider { - p, err := ConnectorProviderFromString(s) - if err != nil { - panic(err) - } - return p -} - -func (c Connector) ParseConfig(to interface{}) error { - if c.Config == nil { - return nil - } - - err := json.Unmarshal(c.Config, to) - if err != nil { - return fmt.Errorf("failed to parse config (%s): %w", string(c.Config), err) - } - - return nil -} - -type ConnectorConfigObject interface { - ConnectorName() string - Validate() error - Marshal() ([]byte, error) -} - -type EmptyConnectorConfig struct { - Name string -} - -func (cfg EmptyConnectorConfig) ConnectorName() string { - return cfg.Name -} - -func (cfg EmptyConnectorConfig) Validate() error { - return nil -} - -func (cfg EmptyConnectorConfig) Marshal() ([]byte, error) { - return nil, nil -} diff --git a/components/payments/internal/models/connector_id.go b/components/payments/internal/models/connector_id.go new file mode 100644 index 0000000000..b9a60c62d1 --- /dev/null +++ b/components/payments/internal/models/connector_id.go @@ -0,0 +1,78 @@ +package models + +import ( + "database/sql/driver" + "encoding/base64" + "errors" + "fmt" + + "github.com/gibson042/canonicaljson-go" + "github.com/google/uuid" +) + +// TODO(polo): change reference to uuid for temporal purpose +type ConnectorID struct { + Reference uuid.UUID + Provider string +} + +func (cid *ConnectorID) String() string { + if cid == nil || cid.Reference == uuid.Nil { + return "" + } + + data, err := canonicaljson.Marshal(cid) + if err != nil { + panic(err) + } + + return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(data) +} + +func ConnectorIDFromString(value string) (ConnectorID, error) { + data, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(value) + if err != nil { + return ConnectorID{}, err + } + ret := ConnectorID{} + err = canonicaljson.Unmarshal(data, &ret) + if err != nil { + return ConnectorID{}, err + } + + return ret, nil +} + +func MustConnectorIDFromString(value string) ConnectorID { + id, err := ConnectorIDFromString(value) + if err != nil { + panic(err) + } + return id +} + +func (cid ConnectorID) Value() (driver.Value, error) { + return cid.String(), nil +} + +func (cid *ConnectorID) Scan(value interface{}) error { + if value == nil { + return errors.New("connector id is nil") + } + + if s, err := driver.String.ConvertValue(value); err == nil { + + if v, ok := s.(string); ok { + + id, err := ConnectorIDFromString(v) + if err != nil { + return fmt.Errorf("failed to parse connector id %s: %v", v, err) + } + + *cid = id + return nil + } + } + + return fmt.Errorf("failed to scan connector id: %v", value) +} diff --git a/components/payments/internal/models/connectors.go b/components/payments/internal/models/connectors.go new file mode 100644 index 0000000000..42fabb9cfa --- /dev/null +++ b/components/payments/internal/models/connectors.go @@ -0,0 +1,67 @@ +package models + +import ( + "encoding/json" + "time" +) + +type Connector struct { + // Unique ID of the connector + ID ConnectorID `json:"id"` + // Name given by the user to the connector + Name string `json:"name"` + // Creation date + CreatedAt time.Time `json:"createdAt"` + // Provider type + Provider string `json:"provider"` + + // Config given by the user. It will be encrypted when stored + Config json.RawMessage `json:"config"` +} + +func (c *Connector) IdempotencyKey() string { + return c.ID.String() +} + +func (c Connector) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + ID string `json:"id"` + Name string `json:"name"` + CreatedAt time.Time `json:"createdAt"` + Provider string `json:"provider"` + Config json.RawMessage `json:"config"` + }{ + ID: c.ID.String(), + Name: c.Name, + CreatedAt: c.CreatedAt, + Provider: c.Provider, + Config: c.Config, + }) +} + +func (c *Connector) UnmarshalJSON(data []byte) error { + var aux struct { + ID string `json:"id"` + Name string `json:"name"` + CreatedAt time.Time `json:"createdAt"` + Provider string `json:"provider"` + Config json.RawMessage `json:"config"` + } + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + id, err := ConnectorIDFromString(aux.ID) + if err != nil { + return err + } + + c.ID = id + c.Name = aux.Name + c.CreatedAt = aux.CreatedAt + c.Provider = aux.Provider + c.Config = aux.Config + + return nil +} diff --git a/components/payments/internal/models/errors.go b/components/payments/internal/models/errors.go new file mode 100644 index 0000000000..ea376eb9d4 --- /dev/null +++ b/components/payments/internal/models/errors.go @@ -0,0 +1,7 @@ +package models + +import "errors" + +var ( + ErrInvalidConfig = errors.New("invalid config") +) diff --git a/components/payments/internal/models/others.go b/components/payments/internal/models/others.go new file mode 100644 index 0000000000..f682c04194 --- /dev/null +++ b/components/payments/internal/models/others.go @@ -0,0 +1,8 @@ +package models + +import "encoding/json" + +type PSPOther struct { + ID string `json:"id"` + Other json.RawMessage `json:"other"` +} diff --git a/components/payments/internal/models/payment.go b/components/payments/internal/models/payment.go deleted file mode 100644 index 3358adf42e..0000000000 --- a/components/payments/internal/models/payment.go +++ /dev/null @@ -1,321 +0,0 @@ -package models - -import ( - "database/sql/driver" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "math/big" - "strconv" - "strings" - "time" - - "github.com/gibson042/canonicaljson-go" - "github.com/google/uuid" - "github.com/uptrace/bun" -) - -type PaymentReference struct { - Reference string - Type PaymentType -} - -type PaymentID struct { - PaymentReference - ConnectorID ConnectorID -} - -func (pid PaymentID) String() string { - data, err := canonicaljson.Marshal(pid) - if err != nil { - panic(err) - } - - return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(data) -} - -func PaymentIDFromString(value string) (*PaymentID, error) { - data, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(value) - if err != nil { - return nil, err - } - ret := PaymentID{} - err = canonicaljson.Unmarshal(data, &ret) - if err != nil { - return nil, err - } - - return &ret, nil -} - -func MustPaymentIDFromString(value string) *PaymentID { - data, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(value) - if err != nil { - panic(err) - } - ret := PaymentID{} - err = canonicaljson.Unmarshal(data, &ret) - if err != nil { - panic(err) - } - - return &ret -} - -func (pid PaymentID) Value() (driver.Value, error) { - return pid.String(), nil -} - -func (pid *PaymentID) Scan(value interface{}) error { - if value == nil { - return errors.New("payment id is nil") - } - - if s, err := driver.String.ConvertValue(value); err == nil { - - if v, ok := s.(string); ok { - - id, err := PaymentIDFromString(v) - if err != nil { - return fmt.Errorf("failed to parse paymentid %s: %v", v, err) - } - - *pid = *id - return nil - } - } - - return fmt.Errorf("failed to scan paymentid: %v", value) -} - -type Payment struct { - bun.BaseModel `bun:"payments.payment"` - - ID PaymentID `bun:",pk,type:character varying,nullzero"` - ConnectorID ConnectorID `bun:",nullzero"` - CreatedAt time.Time `bun:",nullzero"` - Reference string - Amount *big.Int `bun:"type:numeric"` - InitialAmount *big.Int `bun:"type:numeric"` - Type PaymentType `bun:",type:payment_type"` - Status PaymentStatus `bun:",type:payment_status"` - Scheme PaymentScheme - Asset Asset - - RawData json.RawMessage - - SourceAccountID *AccountID `bun:",type:character varying,nullzero"` - DestinationAccountID *AccountID `bun:",type:character varying,nullzero"` - - // Read only fields - Metadata []*PaymentMetadata `bun:"rel:has-many,join:id=payment_id"` - Adjustments []*PaymentAdjustment `bun:"rel:has-many,join:id=payment_id"` - Connector *Connector `bun:"rel:has-one,join:connector_id=id"` -} - -type ( - PaymentType string - PaymentStatus string - PaymentScheme string - Asset string -) - -const ( - PaymentTypePayIn PaymentType = "PAY-IN" - PaymentTypePayOut PaymentType = "PAYOUT" - PaymentTypeTransfer PaymentType = "TRANSFER" - PaymentTypeOther PaymentType = "OTHER" -) - -const ( - PaymentStatusPending PaymentStatus = "PENDING" - PaymentStatusSucceeded PaymentStatus = "SUCCEEDED" - PaymentStatusCancelled PaymentStatus = "CANCELLED" - PaymentStatusFailed PaymentStatus = "FAILED" - PaymentStatusExpired PaymentStatus = "EXPIRED" - PaymentStatusRefunded PaymentStatus = "REFUNDED" - PaymentStatusRefundedFailure PaymentStatus = "REFUNDED_FAILURE" - PaymentStatusDispute PaymentStatus = "DISPUTE" - PaymentStatusDisputeWon PaymentStatus = "DISPUTE_WON" - PaymentStatusDisputeLost PaymentStatus = "DISPUTE_LOST" - PaymentStatusOther PaymentStatus = "OTHER" -) - -const ( - PaymentSchemeUnknown PaymentScheme = "unknown" - PaymentSchemeOther PaymentScheme = "other" - - PaymentSchemeCardVisa PaymentScheme = "visa" - PaymentSchemeCardMasterCard PaymentScheme = "mastercard" - PaymentSchemeCardAmex PaymentScheme = "amex" - PaymentSchemeCardDiners PaymentScheme = "diners" - PaymentSchemeCardDiscover PaymentScheme = "discover" - PaymentSchemeCardJCB PaymentScheme = "jcb" - PaymentSchemeCardUnionPay PaymentScheme = "unionpay" - PaymentSchemeCardAlipay PaymentScheme = "alipay" - PaymentSchemeCardCUP PaymentScheme = "cup" - - PaymentSchemeSepaDebit PaymentScheme = "sepa debit" - PaymentSchemeSepaCredit PaymentScheme = "sepa credit" - PaymentSchemeSepa PaymentScheme = "sepa" - - PaymentSchemeApplePay PaymentScheme = "apple pay" - PaymentSchemeGooglePay PaymentScheme = "google pay" - - PaymentSchemeDOKU PaymentScheme = "doku" - PaymentSchemeDragonPay PaymentScheme = "dragonpay" - PaymentSchemeMaestro PaymentScheme = "maestro" - PaymentSchemeMolPay PaymentScheme = "molpay" - - PaymentSchemeA2A PaymentScheme = "a2a" - PaymentSchemeACHDebit PaymentScheme = "ach debit" - PaymentSchemeACH PaymentScheme = "ach" - PaymentSchemeRTP PaymentScheme = "rtp" -) - -func (t PaymentType) String() string { - return string(t) -} - -func PaymentTypeFromString(value string) (PaymentType, error) { - switch value { - case "PAY-IN": - return PaymentTypePayIn, nil - case "PAYOUT": - return PaymentTypePayOut, nil - case "TRANSFER": - return PaymentTypeTransfer, nil - case "OTHER": - return PaymentTypeOther, nil - default: - return "", errors.New("invalid payment type") - } -} - -func (t PaymentStatus) String() string { - return string(t) -} - -func PaymentStatusFromString(value string) (PaymentStatus, error) { - switch value { - case "PENDING": - return PaymentStatusPending, nil - case "SUCCEEDED": - return PaymentStatusSucceeded, nil - case "CANCELLED": - return PaymentStatusCancelled, nil - case "FAILED": - return PaymentStatusFailed, nil - case "EXPIRED": - return PaymentStatusExpired, nil - case "REFUNDED": - return PaymentStatusRefunded, nil - case "REFUNDED_FAILURE": - return PaymentStatusRefundedFailure, nil - case "DISPUTE": - return PaymentStatusDispute, nil - case "DISPUTE_WON": - return PaymentStatusDisputeWon, nil - case "DISPUTE_LOST": - return PaymentStatusDisputeLost, nil - case "OTHER": - return PaymentStatusOther, nil - default: - return "", errors.New("invalid payment status") - } -} - -func (t PaymentScheme) String() string { - return string(t) -} - -func PaymentSchemeFromString(value string) (PaymentScheme, error) { - switch strings.ToLower(value) { - case "unknown": - return PaymentSchemeUnknown, nil - case "other": - return PaymentSchemeOther, nil - case "visa": - return PaymentSchemeCardVisa, nil - case "mastercard": - return PaymentSchemeCardMasterCard, nil - case "amex": - return PaymentSchemeCardAmex, nil - case "diners": - return PaymentSchemeCardDiners, nil - case "discover": - return PaymentSchemeCardDiscover, nil - case "jcb": - return PaymentSchemeCardJCB, nil - case "unionpay": - return PaymentSchemeCardUnionPay, nil - case "sepa debit": - return PaymentSchemeSepaDebit, nil - case "sepa credit": - return PaymentSchemeSepaCredit, nil - case "sepa": - return PaymentSchemeSepa, nil - case "apple pay": - return PaymentSchemeApplePay, nil - case "google pay": - return PaymentSchemeGooglePay, nil - case "a2a": - return PaymentSchemeA2A, nil - case "ach debit": - return PaymentSchemeACHDebit, nil - case "ach": - return PaymentSchemeACH, nil - case "rtp": - return PaymentSchemeRTP, nil - default: - return "", errors.New("invalid payment scheme") - } -} - -func (t Asset) String() string { - return string(t) -} - -func GetCurrencyAndPrecisionFromAsset(asset Asset) (string, int64, error) { - parts := strings.Split(asset.String(), "/") - if len(parts) != 2 { - return "", 0, errors.New("invalid asset") - } - - precision, err := strconv.ParseInt(parts[1], 10, 64) - if err != nil { - return "", 0, errors.New("invalid asset precision") - } - - return parts[0], precision, nil -} - -type PaymentAdjustment struct { - bun.BaseModel `bun:"payments.adjustment"` - - ID uuid.UUID `bun:",pk,nullzero"` - PaymentID PaymentID `bun:",pk,nullzero"` - CreatedAt time.Time `bun:",nullzero"` - Reference string - Amount *big.Int - Status PaymentStatus - - RawData json.RawMessage -} - -type PaymentMetadata struct { - bun.BaseModel `bun:"payments.metadata"` - - PaymentID PaymentID `bun:",pk,nullzero"` - CreatedAt time.Time `bun:",nullzero"` - Key string `bun:",pk,nullzero"` - Value string - - Changelog []MetadataChangelog `bun:",nullzero"` -} - -type MetadataChangelog struct { - CreatedAt time.Time `json:"createdAt"` - Value string `json:"value"` -} diff --git a/components/payments/internal/models/payment_adjustment_id.go b/components/payments/internal/models/payment_adjustment_id.go new file mode 100644 index 0000000000..da31f8d81a --- /dev/null +++ b/components/payments/internal/models/payment_adjustment_id.go @@ -0,0 +1,80 @@ +package models + +import ( + "database/sql/driver" + "encoding/base64" + "errors" + "fmt" + "time" + + "github.com/gibson042/canonicaljson-go" +) + +type PaymentAdjustmentID struct { + PaymentID + CreatedAt time.Time + Status PaymentStatus +} + +func (pid PaymentAdjustmentID) String() string { + data, err := canonicaljson.Marshal(pid) + if err != nil { + panic(err) + } + + return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(data) +} + +func PaymentAdjustmentIDFromString(value string) (*PaymentAdjustmentID, error) { + data, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(value) + if err != nil { + return nil, err + } + ret := PaymentAdjustmentID{} + err = canonicaljson.Unmarshal(data, &ret) + if err != nil { + return nil, err + } + + return &ret, nil +} + +func MustPaymentAdjustmentIDFromString(value string) *PaymentAdjustmentID { + data, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(value) + if err != nil { + panic(err) + } + ret := PaymentAdjustmentID{} + err = canonicaljson.Unmarshal(data, &ret) + if err != nil { + panic(err) + } + + return &ret +} + +func (pid PaymentAdjustmentID) Value() (driver.Value, error) { + return pid.String(), nil +} + +func (pid *PaymentAdjustmentID) Scan(value interface{}) error { + if value == nil { + return errors.New("payment adjustment id is nil") + } + + if s, err := driver.String.ConvertValue(value); err == nil { + + if v, ok := s.(string); ok { + + id, err := PaymentAdjustmentIDFromString(v) + if err != nil { + return fmt.Errorf("failed to parse payment adjustment id %s: %v", v, err) + } + + *pid = *id + return nil + } + } + + return fmt.Errorf("failed to scan payment adjustement id: %v", value) +} diff --git a/components/payments/internal/models/payment_adjustments.go b/components/payments/internal/models/payment_adjustments.go new file mode 100644 index 0000000000..ef6cb2cc07 --- /dev/null +++ b/components/payments/internal/models/payment_adjustments.go @@ -0,0 +1,96 @@ +package models + +import ( + "encoding/json" + "math/big" + "time" +) + +type PaymentAdjustment struct { + // Unique ID of the payment adjustment + ID PaymentAdjustmentID `json:"id"` + // Related Payment ID + PaymentID PaymentID `json:"paymentID"` + + // Creation date of the adjustment + CreatedAt time.Time `json:"createdAt"` + + // Status of the payment adjustement + Status PaymentStatus `json:"status"` + + // Optional + // Amount moved + Amount *big.Int `json:"amount"` + // Optional + // Asset related to amount + Asset *string `json:"asset"` + + // Additional metadata + Metadata map[string]string `json:"metadata"` + // PSP response in raw + Raw json.RawMessage `json:"raw"` +} + +func (p *PaymentAdjustment) IdempotencyKey() string { + return p.ID.String() +} + +func (c PaymentAdjustment) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + ID string `json:"id"` + PaymentID string `json:"paymentID"` + CreatedAt time.Time `json:"createdAt"` + Status PaymentStatus `json:"status"` + Amount *big.Int `json:"amount"` + Asset *string `json:"asset"` + Metadata map[string]string `json:"metadata"` + Raw json.RawMessage `json:"raw"` + }{ + ID: c.ID.String(), + PaymentID: c.PaymentID.String(), + CreatedAt: c.CreatedAt, + Status: c.Status, + Amount: c.Amount, + Asset: c.Asset, + Metadata: c.Metadata, + Raw: c.Raw, + }) +} + +func (c *PaymentAdjustment) UnmarshalJSON(data []byte) error { + var aux struct { + ID string `json:"id"` + PaymentID string `json:"paymentID"` + CreatedAt time.Time `json:"createdAt"` + Status PaymentStatus `json:"status"` + Amount *big.Int `json:"amount"` + Asset *string `json:"asset"` + Metadata map[string]string `json:"metadata"` + Raw json.RawMessage `json:"raw"` + } + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + paymentID, err := PaymentIDFromString(aux.PaymentID) + if err != nil { + return err + } + + adjustmentID, err := PaymentAdjustmentIDFromString(aux.ID) + if err != nil { + return err + } + + c.ID = *adjustmentID + c.PaymentID = paymentID + c.CreatedAt = aux.CreatedAt + c.Status = aux.Status + c.Amount = aux.Amount + c.Asset = aux.Asset + c.Metadata = aux.Metadata + c.Raw = aux.Raw + + return nil +} diff --git a/components/payments/internal/models/payment_id.go b/components/payments/internal/models/payment_id.go new file mode 100644 index 0000000000..7f7dd045b3 --- /dev/null +++ b/components/payments/internal/models/payment_id.go @@ -0,0 +1,83 @@ +package models + +import ( + "database/sql/driver" + "encoding/base64" + "errors" + "fmt" + + "github.com/gibson042/canonicaljson-go" +) + +type PaymentReference struct { + Reference string + Type PaymentType +} + +type PaymentID struct { + PaymentReference + ConnectorID ConnectorID +} + +func (pid PaymentID) String() string { + data, err := canonicaljson.Marshal(pid) + if err != nil { + panic(err) + } + + return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(data) +} + +func PaymentIDFromString(value string) (PaymentID, error) { + ret := PaymentID{} + data, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(value) + if err != nil { + return ret, err + } + err = canonicaljson.Unmarshal(data, &ret) + if err != nil { + return ret, err + } + + return ret, nil +} + +func MustPaymentIDFromString(value string) *PaymentID { + data, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(value) + if err != nil { + panic(err) + } + ret := PaymentID{} + err = canonicaljson.Unmarshal(data, &ret) + if err != nil { + panic(err) + } + + return &ret +} + +func (pid PaymentID) Value() (driver.Value, error) { + return pid.String(), nil +} + +func (pid *PaymentID) Scan(value interface{}) error { + if value == nil { + return errors.New("payment id is nil") + } + + if s, err := driver.String.ConvertValue(value); err == nil { + + if v, ok := s.(string); ok { + + id, err := PaymentIDFromString(v) + if err != nil { + return fmt.Errorf("failed to parse paymentid %s: %v", v, err) + } + + *pid = id + return nil + } + } + + return fmt.Errorf("failed to scan paymentid: %v", value) +} diff --git a/components/payments/internal/models/payment_initiation_id.go b/components/payments/internal/models/payment_initiation_id.go new file mode 100644 index 0000000000..1afcfc2679 --- /dev/null +++ b/components/payments/internal/models/payment_initiation_id.go @@ -0,0 +1,78 @@ +package models + +import ( + "database/sql/driver" + "encoding/base64" + "errors" + "fmt" + + "github.com/gibson042/canonicaljson-go" +) + +type PaymentInitiationID struct { + Reference string + ConnectorID ConnectorID +} + +func (pid PaymentInitiationID) String() string { + data, err := canonicaljson.Marshal(pid) + if err != nil { + panic(err) + } + + return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(data) +} + +func PaymentInitiationIDFromString(value string) (PaymentInitiationID, error) { + ret := PaymentInitiationID{} + data, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(value) + if err != nil { + return ret, err + } + err = canonicaljson.Unmarshal(data, &ret) + if err != nil { + return ret, err + } + + return ret, nil +} + +func MustPaymentInitiationIDFromString(value string) *PaymentInitiationID { + data, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(value) + if err != nil { + panic(err) + } + ret := PaymentInitiationID{} + err = canonicaljson.Unmarshal(data, &ret) + if err != nil { + panic(err) + } + + return &ret +} + +func (pid PaymentInitiationID) Value() (driver.Value, error) { + return pid.String(), nil +} + +func (pid *PaymentInitiationID) Scan(value interface{}) error { + if value == nil { + return errors.New("payment initiation id is nil") + } + + if s, err := driver.String.ConvertValue(value); err == nil { + + if v, ok := s.(string); ok { + + id, err := PaymentInitiationIDFromString(v) + if err != nil { + return fmt.Errorf("failed to parse payment initiation id %s: %v", v, err) + } + + *pid = id + return nil + } + } + + return fmt.Errorf("failed to scan payment initiation id: %v", value) +} diff --git a/components/payments/internal/models/payment_scheme.go b/components/payments/internal/models/payment_scheme.go new file mode 100644 index 0000000000..8604cccd44 --- /dev/null +++ b/components/payments/internal/models/payment_scheme.go @@ -0,0 +1,203 @@ +package models + +import ( + "database/sql/driver" + "encoding/json" + "errors" + "fmt" +) + +// TODO(polo): use stringer generator +type PaymentScheme int + +const ( + PAYMENT_SCHEME_UNKNOWN PaymentScheme = iota + + PAYMENT_SCHEME_CARD_VISA + PAYMENT_SCHEME_CARD_MASTERCARD + PAYMENT_SCHEME_CARD_AMEX + PAYMENT_SCHEME_CARD_DINERS + PAYMENT_SCHEME_CARD_DISCOVER + PAYMENT_SCHEME_CARD_JCB + PAYMENT_SCHEME_CARD_UNION_PAY + PAYMENT_SCHEME_CARD_ALIPAY + PAYMENT_SCHEME_CARD_CUP + + PAYMENT_SCHEME_SEPA_DEBIT + PAYMENT_SCHEME_SEPA_CREDIT + PAYMENT_SCHEME_SEPA + + PAYMENT_SCHEME_GOOGLE_PAY + PAYMENT_SCHEME_APPLE_PAY + + PAYMENT_SCHEME_DOKU + PAYMENT_SCHEME_DRAGON_PAY + PAYMENT_SCHEME_MAESTRO + PAYMENT_SCHEME_MOL_PAY + + PAYMENT_SCHEME_A2A + PAYMENT_SCHEME_ACH_DEBIT + PAYMENT_SCHEME_ACH + PAYMENT_SCHEME_RTP + + PAYMENT_SCHEME_OTHER = 100 // match grpc tag +) + +func (s PaymentScheme) String() string { + switch s { + case PAYMENT_SCHEME_UNKNOWN: + return "UNKNOWN" + case PAYMENT_SCHEME_CARD_VISA: + return "CARD_VISA" + case PAYMENT_SCHEME_CARD_MASTERCARD: + return "CARD_MASTERCARD" + case PAYMENT_SCHEME_CARD_AMEX: + return "CARD_AMEX" + case PAYMENT_SCHEME_CARD_DINERS: + return "CARD_DINERS" + case PAYMENT_SCHEME_CARD_DISCOVER: + return "CARD_DISCOVER" + case PAYMENT_SCHEME_CARD_JCB: + return "CARD_JCB" + case PAYMENT_SCHEME_CARD_UNION_PAY: + return "CARD_UNION_PAY" + case PAYMENT_SCHEME_CARD_ALIPAY: + return "CARD_ALIPAY" + case PAYMENT_SCHEME_CARD_CUP: + return "CARD_CUP" + case PAYMENT_SCHEME_SEPA_DEBIT: + return "SEPA_DEBIT" + case PAYMENT_SCHEME_SEPA_CREDIT: + return "SEPA_CREDIT" + case PAYMENT_SCHEME_SEPA: + return "SEPA" + case PAYMENT_SCHEME_GOOGLE_PAY: + return "GOOGLE_PAY" + case PAYMENT_SCHEME_APPLE_PAY: + return "APPLE_PAY" + case PAYMENT_SCHEME_DOKU: + return "DOKU" + case PAYMENT_SCHEME_DRAGON_PAY: + return "DRAGON_PAY" + case PAYMENT_SCHEME_MAESTRO: + return "MAESTRO" + case PAYMENT_SCHEME_MOL_PAY: + return "MOL_PAY" + case PAYMENT_SCHEME_A2A: + return "A2A" + case PAYMENT_SCHEME_ACH_DEBIT: + return "ACH_DEBIT" + case PAYMENT_SCHEME_ACH: + return "ACH" + case PAYMENT_SCHEME_RTP: + return "RTP" + case PAYMENT_SCHEME_OTHER: + return "OTHER" + default: + return "UNKNOWN" + } +} + +func PaymentSchemeFromString(value string) (PaymentScheme, error) { + switch value { + case "CARD_VISA": + return PAYMENT_SCHEME_CARD_VISA, nil + case "CARD_MASTERCARD": + return PAYMENT_SCHEME_CARD_MASTERCARD, nil + case "CARD_AMEX": + return PAYMENT_SCHEME_CARD_AMEX, nil + case "CARD_DINERS": + return PAYMENT_SCHEME_CARD_DINERS, nil + case "CARD_DISCOVER": + return PAYMENT_SCHEME_CARD_DISCOVER, nil + case "CARD_JCB": + return PAYMENT_SCHEME_CARD_JCB, nil + case "CARD_UNION_PAY": + return PAYMENT_SCHEME_CARD_UNION_PAY, nil + case "CARD_ALIPAY": + return PAYMENT_SCHEME_CARD_ALIPAY, nil + case "CARD_CUP": + return PAYMENT_SCHEME_CARD_CUP, nil + case "SEPA_DEBIT": + return PAYMENT_SCHEME_SEPA_DEBIT, nil + case "SEPA_CREDIT": + return PAYMENT_SCHEME_SEPA_CREDIT, nil + case "SEPA": + return PAYMENT_SCHEME_SEPA, nil + case "GOOGLE_PAY": + return PAYMENT_SCHEME_GOOGLE_PAY, nil + case "APPLE_PAY": + return PAYMENT_SCHEME_APPLE_PAY, nil + case "DOKU": + return PAYMENT_SCHEME_DOKU, nil + case "DRAGON_PAY": + return PAYMENT_SCHEME_DRAGON_PAY, nil + case "MAESTRO": + return PAYMENT_SCHEME_MAESTRO, nil + case "MOL_PAY": + return PAYMENT_SCHEME_MOL_PAY, nil + case "A2A": + return PAYMENT_SCHEME_A2A, nil + case "ACH_DEBIT": + return PAYMENT_SCHEME_ACH_DEBIT, nil + case "ACH": + return PAYMENT_SCHEME_ACH, nil + case "RTP": + return PAYMENT_SCHEME_RTP, nil + case "OTHER": + return PAYMENT_SCHEME_OTHER, nil + case "UNKNOWN": + return PAYMENT_SCHEME_UNKNOWN, nil + default: + return PAYMENT_SCHEME_UNKNOWN, fmt.Errorf("unknown payment scheme") + } +} + +func (s PaymentScheme) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf(`"%s"`, s.String())), nil +} + +func (s *PaymentScheme) UnmarshalJSON(data []byte) error { + var v string + if err := json.Unmarshal(data, &v); err != nil { + return err + } + + value, err := PaymentSchemeFromString(v) + if err != nil { + return err + } + + *s = value + + return nil +} + +func (s PaymentScheme) Value() (driver.Value, error) { + return s.String(), nil +} + +func (s *PaymentScheme) Scan(value interface{}) error { + if value == nil { + return errors.New("payment type is nil") + } + + st, err := driver.String.ConvertValue(value) + if err != nil { + return fmt.Errorf("failed to convert payment type") + } + + v, ok := st.(string) + if !ok { + return fmt.Errorf("failed to cast payment type") + } + + res, err := PaymentSchemeFromString(v) + if err != nil { + return err + } + + *s = res + + return nil +} diff --git a/components/payments/internal/models/payment_status.go b/components/payments/internal/models/payment_status.go new file mode 100644 index 0000000000..f19ef366d4 --- /dev/null +++ b/components/payments/internal/models/payment_status.go @@ -0,0 +1,136 @@ +package models + +import ( + "database/sql/driver" + "encoding/json" + "errors" + "fmt" +) + +type PaymentStatus int + +const ( + PAYMENT_STATUS_UNKNOWN PaymentStatus = iota + PAYMENT_STATUS_PENDING + PAYMENT_STATUS_SUCCEEDED + PAYMENT_STATUS_CANCELLED + PAYMENT_STATUS_FAILED + PAYMENT_STATUS_EXPIRED + PAYMENT_STATUS_REFUNDED + PAYMENT_STATUS_REFUNDED_FAILURE + PAYMENT_STATUS_DISPUTE + PAYMENT_STATUS_DISPUTE_WON + PAYMENT_STATUS_DISPUTE_LOST + PAYMENT_STATUS_OTHER = 100 // match grpc tag +) + +func (t PaymentStatus) String() string { + switch t { + case PAYMENT_STATUS_UNKNOWN: + return "UNKNOWN" + case PAYMENT_STATUS_PENDING: + return "PENDING" + case PAYMENT_STATUS_SUCCEEDED: + return "SUCCEEDED" + case PAYMENT_STATUS_CANCELLED: + return "CANCELLED" + case PAYMENT_STATUS_FAILED: + return "FAILED" + case PAYMENT_STATUS_EXPIRED: + return "EXPIRED" + case PAYMENT_STATUS_REFUNDED: + return "REFUNDED" + case PAYMENT_STATUS_REFUNDED_FAILURE: + return "REFUNDED_FAILURE" + case PAYMENT_STATUS_DISPUTE: + return "DISPUTE" + case PAYMENT_STATUS_DISPUTE_WON: + return "DISPUTE_WON" + case PAYMENT_STATUS_DISPUTE_LOST: + return "DISPUTE_LOST" + case PAYMENT_STATUS_OTHER: + return "OTHER" + default: + return "UNKNOWN" + } +} + +func PaymentStatusFromString(value string) (PaymentStatus, error) { + switch value { + case "PENDING": + return PAYMENT_STATUS_PENDING, nil + case "SUCCEEDED": + return PAYMENT_STATUS_SUCCEEDED, nil + case "CANCELLED": + return PAYMENT_STATUS_CANCELLED, nil + case "FAILED": + return PAYMENT_STATUS_FAILED, nil + case "EXPIRED": + return PAYMENT_STATUS_EXPIRED, nil + case "REFUNDED": + return PAYMENT_STATUS_REFUNDED, nil + case "REFUNDED_FAILURE": + return PAYMENT_STATUS_REFUNDED_FAILURE, nil + case "DISPUTE": + return PAYMENT_STATUS_DISPUTE, nil + case "DISPUTE_WON": + return PAYMENT_STATUS_DISPUTE_WON, nil + case "DISPUTE_LOST": + return PAYMENT_STATUS_DISPUTE_LOST, nil + case "OTHER": + return PAYMENT_STATUS_OTHER, nil + case "UNKNOWN": + return PAYMENT_STATUS_UNKNOWN, nil + default: + return PAYMENT_STATUS_UNKNOWN, fmt.Errorf("unknown payment status") + } +} + +func (t PaymentStatus) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf(`"%s"`, t.String())), nil +} + +func (t *PaymentStatus) UnmarshalJSON(data []byte) error { + var v string + if err := json.Unmarshal(data, &v); err != nil { + return err + } + + value, err := PaymentStatusFromString(v) + if err != nil { + return err + } + + *t = value + + return nil +} + +func (t PaymentStatus) Value() (driver.Value, error) { + return t.String(), nil +} + +func (t *PaymentStatus) Scan(value interface{}) error { + if value == nil { + return errors.New("payment status is nil") + } + + s, err := driver.String.ConvertValue(value) + if err != nil { + return fmt.Errorf("failed to convert payment status") + } + + v, ok := s.(string) + if !ok { + return fmt.Errorf("failed to cast payment status") + } + + res, err := PaymentStatusFromString(v) + if err != nil { + return err + } + + *t = res + + return nil +} diff --git a/components/payments/internal/models/payment_type.go b/components/payments/internal/models/payment_type.go new file mode 100644 index 0000000000..8e14b3418a --- /dev/null +++ b/components/payments/internal/models/payment_type.go @@ -0,0 +1,99 @@ +package models + +import ( + "database/sql/driver" + "encoding/json" + "errors" + "fmt" +) + +type PaymentType int + +const ( + PAYMENT_TYPE_UNKNOWN PaymentType = iota + PAYMENT_TYPE_PAYIN + PAYMENT_TYPE_PAYOUT + PAYMENT_TYPE_TRANSFER + PAYMENT_TYPE_OTHER = 100 // match grpc tag +) + +func (t PaymentType) String() string { + switch t { + case PAYMENT_TYPE_PAYIN: + return "PAYIN" + case PAYMENT_TYPE_PAYOUT: + return "PAYOUT" + case PAYMENT_TYPE_TRANSFER: + return "TRANSFER" + case PAYMENT_TYPE_OTHER: + return "OTHER" + default: + return "UNKNOWN" + } +} + +func PaymentTypeFromString(value string) (PaymentType, error) { + switch value { + case "PAYIN": + return PAYMENT_TYPE_PAYIN, nil + case "PAYOUT": + return PAYMENT_TYPE_PAYOUT, nil + case "TRANSFER": + return PAYMENT_TYPE_TRANSFER, nil + case "OTHER": + return PAYMENT_TYPE_OTHER, nil + case "UNKNOWN": + return PAYMENT_TYPE_UNKNOWN, nil + default: + return PAYMENT_TYPE_UNKNOWN, fmt.Errorf("unknown payment type") + } +} + +func (t PaymentType) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf(`"%s"`, t.String())), nil +} + +func (t *PaymentType) UnmarshalJSON(data []byte) error { + var v string + if err := json.Unmarshal(data, &v); err != nil { + return err + } + + value, err := PaymentTypeFromString(v) + if err != nil { + return err + } + + *t = value + + return nil +} + +func (t PaymentType) Value() (driver.Value, error) { + return t.String(), nil +} + +func (t *PaymentType) Scan(value interface{}) error { + if value == nil { + return errors.New("payment type is nil") + } + + s, err := driver.String.ConvertValue(value) + if err != nil { + return fmt.Errorf("failed to convert payment type") + } + + v, ok := s.(string) + if !ok { + return fmt.Errorf("failed to cast payment type") + } + + res, err := PaymentTypeFromString(v) + if err != nil { + return err + } + + *t = res + + return nil +} diff --git a/components/payments/internal/models/payments.go b/components/payments/internal/models/payments.go new file mode 100644 index 0000000000..29b9bb1964 --- /dev/null +++ b/components/payments/internal/models/payments.go @@ -0,0 +1,274 @@ +package models + +import ( + "encoding/json" + "math/big" + "time" + + "github.com/formancehq/go-libs/pointer" +) + +// Internal struct used by the plugins +type PSPPayment struct { + // PSP payment/transaction reference. Should be unique. + Reference string + + // Payment Creation date. + CreatedAt time.Time + + // Type of payment: payin, payout, transfer etc... + Type PaymentType + + // Payment amount. + Amount *big.Int + + // Currency. Should be in minor currencies unit. + // For example: USD/2 + Asset string + + // Payment scheme if existing: visa, mastercard etc... + Scheme PaymentScheme + + // Payment status: pending, failed, succeeded etc... + Status PaymentStatus + + // Optional, can be filled for payouts and transfers for example. + SourceAccountReference *string + // Optional, can be filled for payins and transfers for example. + DestinationAccountReference *string + + // Additional metadata + Metadata map[string]string + + // PSP response in raw + Raw json.RawMessage +} + +type Payment struct { + // Unique Payment ID generated from payments information + ID PaymentID `json:"id"` + // Related Connector ID + ConnectorID ConnectorID `json:"connectorID"` + + // PSP payment/transaction reference. Should be unique. + Reference string `json:"reference"` + + // Payment Creation date. + CreatedAt time.Time `json:"createdAt"` + + // Type of payment: payin, payout, transfer etc... + Type PaymentType `json:"type"` + + // Payment Initial amount + InitialAmount *big.Int `json:"initialAmount"` + // Payment amount. + Amount *big.Int `json:"amount"` + + // Currency. Should be in minor currencies unit. + // For example: USD/2 + Asset string `json:"asset"` + + // Payment scheme if existing: visa, mastercard etc... + Scheme PaymentScheme `json:"scheme"` + + // Payment status: pending, failed, succeeded etc... + Status PaymentStatus `json:"status"` + + // Optional, can be filled for payouts and transfers for example. + SourceAccountID *AccountID `json:"sourceAccountID"` + // Optional, can be filled for payins and transfers for example. + DestinationAccountID *AccountID `json:"destinationAccountID"` + + // Additional metadata + Metadata map[string]string `json:"metadata"` + + // Related adjustment + Adjustments []PaymentAdjustment `json:"adjustments"` +} + +func (p Payment) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + ID string `json:"id"` + ConnectorID string `json:"connectorID"` + Reference string `json:"reference"` + CreatedAt time.Time `json:"createdAt"` + Type PaymentType `json:"type"` + InitialAmount *big.Int `json:"initialAmount"` + Amount *big.Int `json:"amount"` + Asset string `json:"asset"` + Scheme PaymentScheme `json:"scheme"` + Status PaymentStatus `json:"status"` + SourceAccountID *string `json:"sourceAccountID"` + DestinationAccountID *string `json:"destinationAccountID"` + Metadata map[string]string `json:"metadata"` + Adjustments []PaymentAdjustment `json:"adjustments"` + }{ + ID: p.ID.String(), + ConnectorID: p.ConnectorID.String(), + Reference: p.Reference, + CreatedAt: p.CreatedAt, + Type: p.Type, + InitialAmount: p.InitialAmount, + Amount: p.Amount, + Asset: p.Asset, + Scheme: p.Scheme, + Status: p.Status, + SourceAccountID: func() *string { + if p.SourceAccountID == nil { + return nil + } + return pointer.For(p.SourceAccountID.String()) + }(), + DestinationAccountID: func() *string { + if p.DestinationAccountID == nil { + return nil + } + return pointer.For(p.DestinationAccountID.String()) + }(), + Metadata: p.Metadata, + Adjustments: p.Adjustments, + }) +} + +func (c *Payment) UnmarshalJSON(data []byte) error { + var aux struct { + ID string `json:"id"` + ConnectorID string `json:"connectorID"` + Reference string `json:"reference"` + CreatedAt time.Time `json:"createdAt"` + Type PaymentType `json:"type"` + InitialAmount *big.Int `json:"initialAmount"` + Amount *big.Int `json:"amount"` + Asset string `json:"asset"` + Scheme PaymentScheme `json:"scheme"` + Status PaymentStatus `json:"status"` + SourceAccountID *string `json:"sourceAccountID"` + DestinationAccountID *string `json:"destinationAccountID"` + Metadata map[string]string `json:"metadata"` + Adjustments []PaymentAdjustment `json:"adjustments"` + } + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + id, err := PaymentIDFromString(aux.ID) + if err != nil { + return err + } + + connectorID, err := ConnectorIDFromString(aux.ConnectorID) + if err != nil { + return err + } + + var sourceAccountID *AccountID + if aux.SourceAccountID != nil { + id, err := AccountIDFromString(*aux.SourceAccountID) + if err != nil { + return err + } + sourceAccountID = &id + } + + var destinationAccountID *AccountID + if aux.DestinationAccountID != nil { + id, err := AccountIDFromString(*aux.DestinationAccountID) + if err != nil { + return err + } + destinationAccountID = &id + } + + c.ID = id + c.ConnectorID = connectorID + c.Reference = aux.Reference + c.CreatedAt = aux.CreatedAt + c.Type = aux.Type + c.InitialAmount = aux.InitialAmount + c.Amount = aux.Amount + c.Asset = aux.Asset + c.Scheme = aux.Scheme + c.Status = aux.Status + c.SourceAccountID = sourceAccountID + c.DestinationAccountID = destinationAccountID + c.Metadata = aux.Metadata + c.Adjustments = aux.Adjustments + + return nil +} + +func FromPSPPaymentToPayment(from PSPPayment, connectorID ConnectorID) Payment { + return Payment{ + ID: PaymentID{ + PaymentReference: PaymentReference{ + Reference: from.Reference, + Type: from.Type, + }, + ConnectorID: connectorID, + }, + ConnectorID: connectorID, + Reference: from.Reference, + CreatedAt: from.CreatedAt, + Type: from.Type, + InitialAmount: from.Amount, + Amount: from.Amount, + Asset: from.Asset, + Scheme: from.Scheme, + Status: from.Status, + SourceAccountID: func() *AccountID { + if from.SourceAccountReference == nil { + return nil + } + return &AccountID{ + Reference: *from.SourceAccountReference, + ConnectorID: connectorID, + } + }(), + DestinationAccountID: func() *AccountID { + if from.DestinationAccountReference == nil { + return nil + } + return &AccountID{ + Reference: *from.DestinationAccountReference, + ConnectorID: connectorID, + } + }(), + Metadata: from.Metadata, + } +} + +func FromPSPPayments(from []PSPPayment, connectorID ConnectorID) []Payment { + payments := make([]Payment, 0, len(from)) + for _, p := range from { + payment := FromPSPPaymentToPayment(p, connectorID) + payment.Adjustments = append(payment.Adjustments, FromPSPPaymentToPaymentAdjustement(p, connectorID)) + payments = append(payments, payment) + } + return payments +} + +func FromPSPPaymentToPaymentAdjustement(from PSPPayment, connectorID ConnectorID) PaymentAdjustment { + paymentID := PaymentID{ + PaymentReference: PaymentReference{ + Reference: from.Reference, + Type: from.Type, + }, + ConnectorID: connectorID, + } + + return PaymentAdjustment{ + ID: PaymentAdjustmentID{ + PaymentID: paymentID, + CreatedAt: from.CreatedAt, + Status: from.Status, + }, + PaymentID: paymentID, + CreatedAt: from.CreatedAt, + Status: from.Status, + Amount: from.Amount, + Asset: &from.Asset, + Metadata: from.Metadata, + Raw: from.Raw, + } +} diff --git a/components/payments/internal/models/payments_initiations.go b/components/payments/internal/models/payments_initiations.go new file mode 100644 index 0000000000..ce7c55ce6b --- /dev/null +++ b/components/payments/internal/models/payments_initiations.go @@ -0,0 +1,41 @@ +package models + +import ( + "math/big" + "time" +) + +const ( + PaymentInitiationTypeTransfer string = "TRANSFER" + PaymentInitiationTypePayout string = "PAYOUT" +) + +type PaymentInitiation struct { + // Unique Payment initiation ID generated from payments information + ID PaymentInitiationID `json:"id"` + // Related Connector ID + ConnectorID ConnectorID `json:"connectorID"` + // Unique reference of the payment + Reference string `json:"reference"` + + // Time to schedule the payment + ScheduledAt time.Time `json:"scheduledAt"` + + // Description of the payment + Description string `json:"description"` + + // Source account of the payment + SourceAccountID *AccountID `json:"sourceAccountID"` + // Destination account of the payment + DestinationAccountID AccountID `json:"destinationAccountID"` + + // Payment initial amount + InitialAmount *big.Int `json:"initialAmount"` + // Payment current amount (can be changed of reversed, refunded, etc...) + Amount *big.Int `json:"amount"` + // Asset of the payment + Asset string `json:"asset"` + + // Additional metadata + Metadata map[string]string `json:"metadata"` +} diff --git a/components/payments/internal/models/plugin.go b/components/payments/internal/models/plugin.go new file mode 100644 index 0000000000..35cae3b02e --- /dev/null +++ b/components/payments/internal/models/plugin.go @@ -0,0 +1,132 @@ +package models + +import ( + "context" + "encoding/json" +) + +type Plugin interface { + Install(context.Context, InstallRequest) (InstallResponse, error) + Uninstall(context.Context, UninstallRequest) (UninstallResponse, error) + + FetchNextAccounts(context.Context, FetchNextAccountsRequest) (FetchNextAccountsResponse, error) + FetchNextPayments(context.Context, FetchNextPaymentsRequest) (FetchNextPaymentsResponse, error) + FetchNextBalances(context.Context, FetchNextBalancesRequest) (FetchNextBalancesResponse, error) + FetchNextExternalAccounts(context.Context, FetchNextExternalAccountsRequest) (FetchNextExternalAccountsResponse, error) + FetchNextOthers(context.Context, FetchNextOthersRequest) (FetchNextOthersResponse, error) + + CreateBankAccount(context.Context, CreateBankAccountRequest) (CreateBankAccountResponse, error) + + CreateWebhooks(context.Context, CreateWebhooksRequest) (CreateWebhooksResponse, error) + TranslateWebhook(context.Context, TranslateWebhookRequest) (TranslateWebhookResponse, error) +} + +type InstallRequest struct { + Config json.RawMessage +} + +type InstallResponse struct { + Capabilities []Capability + Workflow Tasks + WebhooksConfigs []PSPWebhookConfig +} + +type UninstallRequest struct { + ConnectorID string +} + +type UninstallResponse struct{} + +type FetchNextAccountsRequest struct { + FromPayload json.RawMessage + State json.RawMessage + PageSize int +} + +type FetchNextAccountsResponse struct { + Accounts []PSPAccount + NewState json.RawMessage + HasMore bool +} + +type FetchNextExternalAccountsRequest struct { + FromPayload json.RawMessage + State json.RawMessage + PageSize int +} + +type FetchNextExternalAccountsResponse struct { + ExternalAccounts []PSPAccount + NewState json.RawMessage + HasMore bool +} + +type FetchNextPaymentsRequest struct { + FromPayload json.RawMessage + State json.RawMessage + PageSize int +} + +type FetchNextPaymentsResponse struct { + Payments []PSPPayment + NewState json.RawMessage + HasMore bool +} + +type FetchNextOthersRequest struct { + Name string + FromPayload json.RawMessage + State json.RawMessage + PageSize int +} + +type FetchNextOthersResponse struct { + Others []PSPOther + NewState json.RawMessage + HasMore bool +} + +type FetchNextBalancesRequest struct { + FromPayload json.RawMessage + State json.RawMessage + PageSize int +} + +type FetchNextBalancesResponse struct { + Balances []PSPBalance + NewState json.RawMessage + HasMore bool +} + +type CreateBankAccountRequest struct { + BankAccount BankAccount +} + +type CreateBankAccountResponse struct { + RelatedAccount PSPAccount +} + +type CreateWebhooksRequest struct { + FromPayload json.RawMessage + ConnectorID string +} + +type CreateWebhooksResponse struct { + Others []PSPOther +} + +type TranslateWebhookRequest struct { + Name string + Webhook PSPWebhook +} + +type WebhookResponse struct { + IdempotencyKey string + Account *PSPAccount + ExternalAccount *PSPAccount + Payment *PSPPayment +} + +type TranslateWebhookResponse struct { + Responses []WebhookResponse +} diff --git a/components/payments/internal/models/pool_accounts.go b/components/payments/internal/models/pool_accounts.go new file mode 100644 index 0000000000..f9cbd12eba --- /dev/null +++ b/components/payments/internal/models/pool_accounts.go @@ -0,0 +1,43 @@ +package models + +import ( + "encoding/json" + + "github.com/google/uuid" +) + +type PoolAccounts struct { + PoolID uuid.UUID `json:"poolID"` + AccountID AccountID `json:"accountID"` +} + +func (p PoolAccounts) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + PoolID uuid.UUID `json:"poolID"` + AccountID string `json:"accountID"` + }{ + PoolID: p.PoolID, + AccountID: p.AccountID.String(), + }) +} + +func (p *PoolAccounts) UnmarshalJSON(data []byte) error { + var aux struct { + PoolID uuid.UUID `json:"poolID"` + AccountID string `json:"accountID"` + } + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + accountID, err := AccountIDFromString(aux.AccountID) + if err != nil { + return err + } + + p.PoolID = aux.PoolID + p.AccountID = accountID + + return nil +} diff --git a/components/payments/internal/models/pools.go b/components/payments/internal/models/pools.go index ab865b1359..42103042a8 100644 --- a/components/payments/internal/models/pools.go +++ b/components/payments/internal/models/pools.go @@ -1,25 +1,38 @@ package models import ( + "encoding/base64" "time" + "github.com/gibson042/canonicaljson-go" "github.com/google/uuid" - "github.com/uptrace/bun" ) -type PoolAccounts struct { - bun.BaseModel `bun:"accounts.pool_accounts"` +type Pool struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + CreatedAt time.Time `json:"createdAt"` - PoolID uuid.UUID `bun:",pk,notnull"` - AccountID AccountID `bun:",pk,notnull"` + PoolAccounts []PoolAccounts `json:"poolAccounts"` } -type Pool struct { - bun.BaseModel `bun:"accounts.pools"` +func (p *Pool) IdempotencyKey() string { + relatedAccounts := make([]string, len(p.PoolAccounts)) + for i := range p.PoolAccounts { + relatedAccounts[i] = p.PoolAccounts[i].AccountID.String() + } + var ik = struct { + ID string + RelatedAccounts []string + }{ + ID: p.ID.String(), + RelatedAccounts: relatedAccounts, + } - ID uuid.UUID `bun:",pk,nullzero"` - Name string - CreatedAt time.Time + data, err := canonicaljson.Marshal(ik) + if err != nil { + panic(err) + } - PoolAccounts []*PoolAccounts `bun:"rel:has-many,join:id=pool_id"` + return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(data) } diff --git a/components/payments/internal/models/schedules.go b/components/payments/internal/models/schedules.go new file mode 100644 index 0000000000..a012e2d2e2 --- /dev/null +++ b/components/payments/internal/models/schedules.go @@ -0,0 +1,47 @@ +package models + +import ( + "encoding/json" + "time" +) + +type Schedule struct { + ID string + ConnectorID ConnectorID + CreatedAt time.Time +} + +func (s Schedule) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + ID string `json:"id"` + ConnectorID string `json:"connectorID"` + CreatedAt time.Time `json:"createdAt"` + }{ + ID: s.ID, + ConnectorID: s.ConnectorID.String(), + CreatedAt: s.CreatedAt, + }) +} + +func (s *Schedule) UnmarshalJSON(data []byte) error { + var aux struct { + ID string `json:"id"` + ConnectorID string `json:"connectorID"` + CreatedAt time.Time `json:"createdAt"` + } + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + connectorID, err := ConnectorIDFromString(aux.ConnectorID) + if err != nil { + return err + } + + s.ID = aux.ID + s.ConnectorID = connectorID + s.CreatedAt = aux.CreatedAt + + return nil +} diff --git a/components/payments/internal/models/state_id.go b/components/payments/internal/models/state_id.go new file mode 100644 index 0000000000..1e1f394564 --- /dev/null +++ b/components/payments/internal/models/state_id.go @@ -0,0 +1,76 @@ +package models + +import ( + "database/sql/driver" + "encoding/base64" + "errors" + "fmt" + + "github.com/gibson042/canonicaljson-go" +) + +type StateID struct { + Reference string + ConnectorID ConnectorID +} + +func (aid *StateID) String() string { + if aid == nil || aid.Reference == "" { + return "" + } + + data, err := canonicaljson.Marshal(aid) + if err != nil { + panic(err) + } + + return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(data) +} + +func StateIDFromString(value string) (*StateID, error) { + data, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(value) + if err != nil { + return nil, err + } + ret := StateID{} + err = canonicaljson.Unmarshal(data, &ret) + if err != nil { + return nil, err + } + + return &ret, nil +} + +func MustStateIDFromString(value string) StateID { + id, err := StateIDFromString(value) + if err != nil { + panic(err) + } + return *id +} + +func (aid StateID) Value() (driver.Value, error) { + return aid.String(), nil +} + +func (aid *StateID) Scan(value interface{}) error { + if value == nil { + return errors.New("account id is nil") + } + + if s, err := driver.String.ConvertValue(value); err == nil { + + if v, ok := s.(string); ok { + + id, err := StateIDFromString(v) + if err != nil { + return fmt.Errorf("failed to parse account id %s: %v", v, err) + } + + *aid = *id + return nil + } + } + + return fmt.Errorf("failed to scan account id: %v", value) +} diff --git a/components/payments/internal/models/states.go b/components/payments/internal/models/states.go new file mode 100644 index 0000000000..9a598515fa --- /dev/null +++ b/components/payments/internal/models/states.go @@ -0,0 +1,51 @@ +package models + +import ( + "encoding/json" +) + +type State struct { + ID StateID + ConnectorID ConnectorID + State json.RawMessage +} + +func (s State) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + ID string `json:"id"` + ConnectorID string `json:"connectorID"` + State json.RawMessage `json:"state"` + }{ + ID: s.ID.String(), + ConnectorID: s.ConnectorID.String(), + State: s.State, + }) +} + +func (s *State) UnmarshalJSON(data []byte) error { + var aux struct { + ID string `json:"id"` + ConnectorID string `json:"connectorID"` + State json.RawMessage `json:"state"` + } + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + id, err := StateIDFromString(aux.ID) + if err != nil { + return err + } + + connectorID, err := ConnectorIDFromString(aux.ConnectorID) + if err != nil { + return err + } + + s.ID = *id + s.ConnectorID = connectorID + s.State = aux.State + + return nil +} diff --git a/components/payments/internal/models/task.go b/components/payments/internal/models/task.go deleted file mode 100644 index 566c16305b..0000000000 --- a/components/payments/internal/models/task.go +++ /dev/null @@ -1,124 +0,0 @@ -package models - -import ( - "encoding/base64" - "encoding/json" - "fmt" - "time" - - "github.com/uptrace/bun" - - "github.com/google/uuid" -) - -type TaskID uuid.UUID - -func (id TaskID) String() string { - return uuid.UUID(id).String() -} - -type ScheduleOption int - -const ( - OPTIONS_RUN_NOW ScheduleOption = iota - OPTIONS_RUN_IN_DURATION - OPTIONS_RUN_PERIODICALLY - OPTIONS_RUN_NOW_SYNC - OPTIONS_RUN_SCHEDULED_AT -) - -type RestartOption int - -const ( - OPTIONS_RESTART_NEVER RestartOption = iota - OPTIONS_RESTART_ALWAYS - OPTIONS_RESTART_IF_NOT_ACTIVE - OPTIONS_STOP_AND_RESTART -) - -type Task struct { - bun.BaseModel `bun:"tasks.task"` - - ID uuid.UUID `bun:",pk,nullzero"` - ConnectorID ConnectorID - CreatedAt time.Time `bun:",nullzero"` - UpdatedAt time.Time `bun:",nullzero"` - Name string - Descriptor json.RawMessage - SchedulerOptions TaskSchedulerOptions - Status TaskStatus - Error string - State json.RawMessage - - Connector *Connector `bun:"rel:belongs-to,join:connector_id=id"` -} - -func (t Task) GetDescriptor() TaskDescriptor { - return TaskDescriptor(t.Descriptor) -} - -type TaskSchedulerOptions struct { - ScheduleOption ScheduleOption - Duration time.Duration - ScheduleAt time.Time - - // TODO(polo): Deprecated, will be removed in the next release, use - // RestartOption instead. - // We have to keep it for now for db compatibility. - Restart bool - RestartOption RestartOption -} - -type TaskDescriptor json.RawMessage - -func (td TaskDescriptor) ToMessage() json.RawMessage { - return json.RawMessage(td) -} - -func (td TaskDescriptor) EncodeToString() (string, error) { - data, err := json.Marshal(td) - if err != nil { - return "", fmt.Errorf("failed to encode task descriptor: %w", err) - } - - return base64.StdEncoding.EncodeToString(data), nil -} - -func EncodeTaskDescriptor(descriptor any) (TaskDescriptor, error) { - res, err := json.Marshal(descriptor) - if err != nil { - return nil, fmt.Errorf("failed to encode task descriptor: %w", err) - } - - return res, nil -} - -func DecodeTaskDescriptor[descriptor any](data TaskDescriptor) (descriptor, error) { - var res descriptor - - err := json.Unmarshal(data, &res) - if err != nil { - return res, fmt.Errorf("failed to decode task descriptor: %w", err) - } - - return res, nil -} - -type TaskStatus string - -const ( - TaskStatusStopped TaskStatus = "STOPPED" - TaskStatusPending TaskStatus = "PENDING" - TaskStatusActive TaskStatus = "ACTIVE" - TaskStatusTerminated TaskStatus = "TERMINATED" - TaskStatusFailed TaskStatus = "FAILED" -) - -func (t Task) ParseDescriptor(to interface{}) error { - err := json.Unmarshal(t.Descriptor, to) - if err != nil { - return fmt.Errorf("failed to parse descriptor: %w", err) - } - - return nil -} diff --git a/components/payments/internal/models/tasks.go b/components/payments/internal/models/tasks.go new file mode 100644 index 0000000000..dc8d99ecd6 --- /dev/null +++ b/components/payments/internal/models/tasks.go @@ -0,0 +1,35 @@ +package models + +type TaskType int + +const ( + TASK_FETCH_OTHERS TaskType = iota + TASK_FETCH_ACCOUNTS + TASK_FETCH_BALANCES + TASK_FETCH_EXTERNAL_ACCOUNTS + TASK_FETCH_PAYMENTS + TASK_CREATE_WEBHOOKS +) + +type TaskTreeFetchOther struct{} +type TaskTreeFetchAccounts struct{} +type TaskTreeFetchBalances struct{} +type TaskTreeFetchExternalAccounts struct{} +type TaskTreeFetchPayments struct{} +type TaskTreeCreateWebhooks struct{} + +type TaskTree struct { + TaskType TaskType + Name string + Periodically bool + NextTasks []TaskTree + + TaskTreeFetchOther *TaskTreeFetchOther + TaskTreeFetchAccounts *TaskTreeFetchAccounts + TaskTreeFetchBalances *TaskTreeFetchBalances + TaskTreeFetchExternalAccounts *TaskTreeFetchExternalAccounts + TaskTreeFetchPayments *TaskTreeFetchPayments + TaskTreeCreateWebhooks *TaskTreeCreateWebhooks +} + +type Tasks []TaskTree diff --git a/components/payments/internal/models/transfer.go b/components/payments/internal/models/transfer.go deleted file mode 100644 index cc79f475eb..0000000000 --- a/components/payments/internal/models/transfer.go +++ /dev/null @@ -1,114 +0,0 @@ -package models - -import "errors" - -type TransferInitiationStatus int - -const ( - TransferInitiationStatusWaitingForValidation TransferInitiationStatus = iota - TransferInitiationStatusProcessing - TransferInitiationStatusProcessed - TransferInitiationStatusFailed - TransferInitiationStatusRejected - TransferInitiationStatusValidated - TransferInitiationStatusAskRetried - TransferInitiationStatusAskReversed - TransferInitiationStatusReverseProcessing - TransferInitiationStatusReverseFailed - TransferInitiationStatusPartiallyReversed - TransferInitiationStatusReversed -) - -func (s TransferInitiationStatus) String() string { - return [...]string{ - "WAITING_FOR_VALIDATION", - "PROCESSING", - "PROCESSED", - "FAILED", - "REJECTED", - "VALIDATED", - "ASK_RETRIED", - "ASK_REVERSED", - "REVERSE_PROCESSING", - "REVERSE_FAILED", - "PARTIALLY_REVERSED", - "REVERSED", - }[s] -} - -func TransferInitiationStatusFromString(s string) (TransferInitiationStatus, error) { - switch s { - case "WAITING_FOR_VALIDATION": - return TransferInitiationStatusWaitingForValidation, nil - case "PROCESSING": - return TransferInitiationStatusProcessing, nil - case "PROCESSED": - return TransferInitiationStatusProcessed, nil - case "FAILED": - return TransferInitiationStatusFailed, nil - case "REJECTED": - return TransferInitiationStatusRejected, nil - case "VALIDATED": - return TransferInitiationStatusValidated, nil - case "ASK_RETRIED": - return TransferInitiationStatusAskRetried, nil - case "ASK_REVERSED": - return TransferInitiationStatusAskReversed, nil - case "REVERSE_PROCESSING": - return TransferInitiationStatusReverseProcessing, nil - case "REVERSE_FAILED": - return TransferInitiationStatusReverseFailed, nil - case "PARTIALLY_REVERSED": - return TransferInitiationStatusPartiallyReversed, nil - case "REVERSED": - return TransferInitiationStatusReversed, nil - default: - return TransferInitiationStatusWaitingForValidation, errors.New("invalid status") - } -} - -type TransferReversalStatus int - -const ( - TransferReversalStatusProcessing TransferReversalStatus = iota - TransferReversalStatusProcessed - TransferReversalStatusFailed -) - -func (s TransferReversalStatus) String() string { - return [...]string{ - "CREATED", - "PROCESSING", - "PROCESSED", - "FAILED", - }[s] -} - -func TransferReversalStatusFromString(s string) (TransferReversalStatus, error) { - switch s { - case "PROCESSING": - return TransferReversalStatusProcessing, nil - case "PROCESSED": - return TransferReversalStatusProcessed, nil - case "FAILED": - return TransferReversalStatusFailed, nil - default: - return TransferReversalStatusProcessing, errors.New("invalid status") - } -} - -func (s TransferReversalStatus) ToTransferInitiationStatus(isFullyReversed bool) TransferInitiationStatus { - switch s { - case TransferReversalStatusProcessing: - return TransferInitiationStatusReverseProcessing - case TransferReversalStatusProcessed: - if isFullyReversed { - return TransferInitiationStatusReversed - } - return TransferInitiationStatusPartiallyReversed - case TransferReversalStatusFailed: - return TransferInitiationStatusReverseFailed - default: - return TransferInitiationStatusProcessed - } -} diff --git a/components/payments/internal/models/transfer_initiation.go b/components/payments/internal/models/transfer_initiation.go deleted file mode 100644 index 71860aba04..0000000000 --- a/components/payments/internal/models/transfer_initiation.go +++ /dev/null @@ -1,180 +0,0 @@ -package models - -import ( - "database/sql/driver" - "encoding/base64" - "errors" - "fmt" - "math/big" - "sort" - "time" - - "github.com/gibson042/canonicaljson-go" - "github.com/google/uuid" - "github.com/uptrace/bun" -) - -type TransferInitiationID struct { - Reference string - ConnectorID ConnectorID -} - -func (tid TransferInitiationID) String() string { - data, err := canonicaljson.Marshal(tid) - if err != nil { - panic(err) - } - - return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(data) -} - -func TransferInitiationIDFromString(value string) (TransferInitiationID, error) { - data, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(value) - if err != nil { - return TransferInitiationID{}, err - } - ret := TransferInitiationID{} - err = canonicaljson.Unmarshal(data, &ret) - if err != nil { - return TransferInitiationID{}, err - } - - return ret, nil -} - -func MustTransferInitiationIDFromString(value string) TransferInitiationID { - id, err := TransferInitiationIDFromString(value) - if err != nil { - panic(err) - } - return id -} - -func (tid TransferInitiationID) Value() (driver.Value, error) { - return tid.String(), nil -} - -func (tid *TransferInitiationID) Scan(value interface{}) error { - if value == nil { - return errors.New("payment id is nil") - } - - if s, err := driver.String.ConvertValue(value); err == nil { - - if v, ok := s.(string); ok { - - id, err := TransferInitiationIDFromString(v) - if err != nil { - return fmt.Errorf("failed to parse paymentid %s: %v", v, err) - } - - *tid = id - return nil - } - } - - return fmt.Errorf("failed to scan paymentid: %v", value) -} - -type TransferInitiationType int - -const ( - TransferInitiationTypeTransfer TransferInitiationType = iota - TransferInitiationTypePayout -) - -func (t TransferInitiationType) String() string { - return [...]string{ - "TRANSFER", - "PAYOUT", - }[t] -} - -func TransferInitiationTypeFromString(s string) (TransferInitiationType, error) { - switch s { - case "TRANSFER": - return TransferInitiationTypeTransfer, nil - case "PAYOUT": - return TransferInitiationTypePayout, nil - default: - return TransferInitiationTypeTransfer, errors.New("invalid type") - } -} - -func MustTransferInitiationTypeFromString(s string) TransferInitiationType { - t, err := TransferInitiationTypeFromString(s) - if err != nil { - panic(err) - } - return t -} - -type TransferInitiation struct { - bun.BaseModel `bun:"transfers.transfer_initiation"` - - // Filled when created in DB - ID TransferInitiationID `bun:",pk,nullzero"` - - CreatedAt time.Time `bun:",nullzero"` - ScheduledAt time.Time `bun:",nullzero"` - Description string - - Type TransferInitiationType - - SourceAccountID *AccountID - DestinationAccountID AccountID - Provider ConnectorProvider - ConnectorID ConnectorID - - Amount *big.Int `bun:"type:numeric"` - InitialAmount *big.Int `bun:"type:numeric"` - Asset Asset - - Metadata map[string]string - - SourceAccount *Account `bun:"-"` - DestinationAccount *Account `bun:"-"` - - RelatedAdjustments []*TransferInitiationAdjustment `bun:"rel:has-many,join:id=transfer_initiation_id"` - RelatedPayments []*TransferInitiationPayment `bun:"-"` -} - -func (t *TransferInitiation) SortRelatedAdjustments() { - // Sort adjustments by created_at - sort.Slice(t.RelatedAdjustments, func(i, j int) bool { - return t.RelatedAdjustments[i].CreatedAt.After(t.RelatedAdjustments[j].CreatedAt) - }) -} - -func (t *TransferInitiation) CountRetries() int { - res := 0 - for _, adjustment := range t.RelatedAdjustments { - if adjustment.Status == TransferInitiationStatusAskRetried { - res++ - } - } - - return res -} - -type TransferInitiationPayment struct { - bun.BaseModel `bun:"transfers.transfer_initiation_payments"` - - TransferInitiationID TransferInitiationID `bun:",pk"` - PaymentID PaymentID `bun:",pk"` - - CreatedAt time.Time `bun:",nullzero"` - Status TransferInitiationStatus - Error string -} - -type TransferInitiationAdjustment struct { - bun.BaseModel `bun:"transfers.transfer_initiation_adjustments"` - - ID uuid.UUID `bun:",pk"` - TransferInitiationID TransferInitiationID - CreatedAt time.Time `bun:",nullzero"` - Status TransferInitiationStatus - Error string - Metadata map[string]string -} diff --git a/components/payments/internal/models/transfer_reversal.go b/components/payments/internal/models/transfer_reversal.go deleted file mode 100644 index 357c8da41a..0000000000 --- a/components/payments/internal/models/transfer_reversal.go +++ /dev/null @@ -1,96 +0,0 @@ -package models - -import ( - "database/sql/driver" - "encoding/base64" - "errors" - "fmt" - "math/big" - "time" - - "github.com/gibson042/canonicaljson-go" - "github.com/uptrace/bun" -) - -type TransferReversalID struct { - Reference string - ConnectorID ConnectorID -} - -func (tid TransferReversalID) String() string { - data, err := canonicaljson.Marshal(tid) - if err != nil { - panic(err) - } - - return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(data) -} - -func TransferReversalIDFromString(value string) (TransferReversalID, error) { - data, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(value) - if err != nil { - return TransferReversalID{}, err - } - ret := TransferReversalID{} - err = canonicaljson.Unmarshal(data, &ret) - if err != nil { - return TransferReversalID{}, err - } - - return ret, nil -} - -func MustTransferReversalIDFromString(value string) TransferReversalID { - id, err := TransferReversalIDFromString(value) - if err != nil { - panic(err) - } - return id -} - -func (tid TransferReversalID) Value() (driver.Value, error) { - return tid.String(), nil -} - -func (tid *TransferReversalID) Scan(value interface{}) error { - if value == nil { - return errors.New("payment id is nil") - } - - if s, err := driver.String.ConvertValue(value); err == nil { - - if v, ok := s.(string); ok { - - id, err := TransferReversalIDFromString(v) - if err != nil { - return fmt.Errorf("failed to parse paymentid %s: %v", v, err) - } - - *tid = id - return nil - } - } - - return fmt.Errorf("failed to scan paymentid: %v", value) -} - -type TransferReversal struct { - bun.BaseModel `bun:"transfers.transfer_reversal"` - - ID TransferReversalID `bun:",pk"` - TransferInitiationID TransferInitiationID - - CreatedAt time.Time - UpdatedAt time.Time - Description string - - ConnectorID ConnectorID - - Amount *big.Int - Asset Asset - - Status TransferReversalStatus - Error string - - Metadata map[string]string -} diff --git a/components/payments/internal/models/webhook.go b/components/payments/internal/models/webhook.go deleted file mode 100644 index f1a55a02a9..0000000000 --- a/components/payments/internal/models/webhook.go +++ /dev/null @@ -1,14 +0,0 @@ -package models - -import ( - "github.com/google/uuid" - "github.com/uptrace/bun" -) - -type Webhook struct { - bun.BaseModel `bun:"connectors.webhook"` - - ID uuid.UUID - ConnectorID ConnectorID - RequestBody []byte -} diff --git a/components/payments/internal/models/webhooks.go b/components/payments/internal/models/webhooks.go new file mode 100644 index 0000000000..e273a062a9 --- /dev/null +++ b/components/payments/internal/models/webhooks.go @@ -0,0 +1,26 @@ +package models + +type PSPWebhookConfig struct { + Name string `json:"name"` + URLPath string `json:"urlPath"` +} + +type WebhookConfig struct { + Name string `json:"name"` + ConnectorID ConnectorID `json:"connectorID"` + URLPath string `json:"urlPath"` +} + +type PSPWebhook struct { + QueryValues map[string][]string `json:"queryValues"` + Headers map[string][]string `json:"headers"` + Body []byte `json:"payload"` +} + +type Webhook struct { + ID string `json:"id"` + ConnectorID ConnectorID `json:"connectorID"` + QueryValues map[string][]string `json:"queryValues"` + Headers map[string][]string `json:"headers"` + Body []byte `json:"payload"` +} diff --git a/components/payments/internal/models/workflow_instances.go b/components/payments/internal/models/workflow_instances.go new file mode 100644 index 0000000000..b73db97a70 --- /dev/null +++ b/components/payments/internal/models/workflow_instances.go @@ -0,0 +1,16 @@ +package models + +import ( + "time" +) + +type Instance struct { + ID string + ScheduleID string + ConnectorID ConnectorID + CreatedAt time.Time + UpdatedAt time.Time + Terminated bool + TerminatedAt *time.Time + Error *string +} diff --git a/components/payments/internal/otel/tracer.go b/components/payments/internal/otel/otel.go similarity index 100% rename from components/payments/internal/otel/tracer.go rename to components/payments/internal/otel/otel.go diff --git a/components/payments/internal/storage/accounts.go b/components/payments/internal/storage/accounts.go new file mode 100644 index 0000000000..d0ca97f540 --- /dev/null +++ b/components/payments/internal/storage/accounts.go @@ -0,0 +1,186 @@ +package storage + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/query" + "github.com/formancehq/payments/internal/models" + "github.com/pkg/errors" + "github.com/uptrace/bun" +) + +type account struct { + bun.BaseModel `bun:"table:accounts"` + + // Mandatory fields + ID models.AccountID `bun:"id,pk,type:character varying,notnull"` + ConnectorID models.ConnectorID `bun:"connector_id,type:character varying,notnull"` + CreatedAt time.Time `bun:"created_at,type:timestamp without time zone,notnull"` + Reference string `bun:"reference,type:text,notnull"` + Type string `bun:"type,type:text,notnull"` + Raw json.RawMessage `bun:"raw,type:json,notnull"` + + // Optional fields + // c.f.: https://bun.uptrace.dev/guide/models.html#nulls + DefaultAsset *string `bun:"default_asset,type:text,nullzero"` + Name *string `bun:"name,type:text,nullzero"` + + // Optional fields with default + // c.f. https://bun.uptrace.dev/guide/models.html#default + Metadata map[string]string `bun:"metadata,type:jsonb,nullzero,notnull,default:'{}'"` +} + +func (s *store) AccountsUpsert(ctx context.Context, accounts []models.Account) error { + if len(accounts) == 0 { + return nil + } + + toInsert := make([]account, 0, len(accounts)) + for _, a := range accounts { + toInsert = append(toInsert, fromAccountModels(a)) + } + + _, err := s.db.NewInsert(). + Model(&toInsert). + On("CONFLICT (id) DO NOTHING"). + Exec(ctx) + + return e("failed to insert accounts", err) +} + +func (s *store) AccountsGet(ctx context.Context, id models.AccountID) (*models.Account, error) { + var account account + + err := s.db.NewSelect(). + Model(&account). + Where("id = ?", id). + Scan(ctx) + if err != nil { + return nil, e("failed to get account", err) + } + + res := toAccountModels(account) + return &res, nil +} + +func (s *store) AccountsDeleteFromConnectorID(ctx context.Context, connectorID models.ConnectorID) error { + _, err := s.db.NewDelete(). + Model((*account)(nil)). + Where("connector_id = ?", connectorID). + Exec(ctx) + + return e("failed to delete account", err) +} + +type AccountQuery struct{} + +type ListAccountsQuery bunpaginate.OffsetPaginatedQuery[bunpaginate.PaginatedQueryOptions[AccountQuery]] + +func NewListAccountsQuery(opts bunpaginate.PaginatedQueryOptions[AccountQuery]) ListAccountsQuery { + return ListAccountsQuery{ + Order: bunpaginate.OrderAsc, + PageSize: opts.PageSize, + Options: opts, + } +} + +func (s *store) accountsQueryContext(qb query.Builder) (string, []any, error) { + return qb.Build(query.ContextFn(func(key, operator string, value any) (string, []any, error) { + switch { + case key == "reference", + key == "connector_id", + key == "type", + key == "default_asset", + key == "name": + return fmt.Sprintf("%s %s ?", key, query.DefaultComparisonOperatorsMapping[operator]), []any{value}, nil + case metadataRegex.Match([]byte(key)): + if operator != "$match" { + return "", nil, errors.Wrap(ErrValidation, "'metadata' column can only be used with $match") + } + match := metadataRegex.FindAllStringSubmatch(key, 3) + + key := "metadata" + return key + " @> ?", []any{map[string]any{ + match[0][1]: value, + }}, nil + default: + return "", nil, errors.Wrap(ErrValidation, fmt.Sprintf("unknown key '%s' when building query", key)) + } + })) +} + +func (s *store) AccountsList(ctx context.Context, q ListAccountsQuery) (*bunpaginate.Cursor[models.Account], error) { + var ( + where string + args []any + err error + ) + if q.Options.QueryBuilder != nil { + where, args, err = s.accountsQueryContext(q.Options.QueryBuilder) + if err != nil { + return nil, err + } + } + + cursor, err := paginateWithOffset[bunpaginate.PaginatedQueryOptions[AccountQuery], account](s, ctx, + (*bunpaginate.OffsetPaginatedQuery[bunpaginate.PaginatedQueryOptions[AccountQuery]])(&q), + func(query *bun.SelectQuery) *bun.SelectQuery { + if where != "" { + query = query.Where(where, args...) + } + + // TODO(polo): sorter ? + query = query.Order("created_at DESC") + + return query + }, + ) + if err != nil { + return nil, e("failed to fetch accounts", err) + } + + accounts := make([]models.Account, 0, len(cursor.Data)) + for _, a := range cursor.Data { + accounts = append(accounts, toAccountModels(a)) + } + + return &bunpaginate.Cursor[models.Account]{ + PageSize: cursor.PageSize, + HasMore: cursor.HasMore, + Previous: cursor.Previous, + Next: cursor.Next, + Data: accounts, + }, nil +} + +func fromAccountModels(from models.Account) account { + return account{ + ID: from.ID, + ConnectorID: from.ConnectorID, + CreatedAt: from.CreatedAt, + Reference: from.Reference, + Type: string(from.Type), + DefaultAsset: from.DefaultAsset, + Name: from.Name, + Metadata: from.Metadata, + Raw: from.Raw, + } +} + +func toAccountModels(from account) models.Account { + return models.Account{ + ID: from.ID, + ConnectorID: from.ConnectorID, + Reference: from.Reference, + CreatedAt: from.CreatedAt, + Type: models.AccountType(from.Type), + Name: from.Name, + DefaultAsset: from.DefaultAsset, + Metadata: from.Metadata, + Raw: from.Raw, + } +} diff --git a/components/payments/internal/storage/balances.go b/components/payments/internal/storage/balances.go new file mode 100644 index 0000000000..e21e0fc767 --- /dev/null +++ b/components/payments/internal/storage/balances.go @@ -0,0 +1,271 @@ +package storage + +import ( + "context" + "fmt" + "math/big" + "time" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/models" + "github.com/pkg/errors" + "github.com/uptrace/bun" +) + +type balance struct { + bun.BaseModel `bun:"table:balances"` + + // Mandatory fields + AccountID models.AccountID `bun:"account_id,pk,type:character varying,notnull"` + CreatedAt time.Time `bun:"created_at,pk,type:timestamp without time zone,notnull"` + Asset string `bun:"asset,pk,type:text,notnull"` + + ConnectorID models.ConnectorID `bun:"connector_id,type:character varying,notnull"` + Balance *big.Int `bun:"balance,type:numeric,notnull"` + LastUpdatedAt time.Time `bun:"last_updated_at,type:timestamp without time zone,notnull"` +} + +func (s *store) BalancesUpsert(ctx context.Context, balances []models.Balance) error { + toInsert := fromBalancesModels(balances) + + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return e("failed to start transaction", err) + } + defer tx.Rollback() + + _, err = tx.NewInsert(). + Model((*models.Balance)(nil)). + With("cte1", tx.NewValues(&toInsert)). + Column( + "account_id", + "created_at", + "asset", + "connector_id", + "balance", + "last_updated_at", + ). + TableExpr(` + (SELECT * + FROM cte1 + WHERE cte1.balance != COALESCE((SELECT balance FROM balances WHERE account_id = cte1.account_id AND last_updated_at < cte1.last_updated_at AND asset = cte1.asset ORDER BY last_updated_at DESC LIMIT 1), cte1.balance+1) + ) data`). + On("CONFLICT (account_id, created_at, asset) DO NOTHING"). + Exec(ctx) + if err != nil { + return e("failed to create balances", err) + } + + // Always update the previous row in order to keep the balance history consistent. + _, err = tx.NewUpdate(). + Model((*models.Balance)(nil)). + With("cte1", s.db.NewValues(&toInsert)). + TableExpr(` + (SELECT (SELECT created_at FROM balances WHERE last_updated_at < cte1.last_updated_at AND account_id = cte1.account_id AND asset = cte1.asset ORDER BY last_updated_at DESC LIMIT 1), cte1.account_id, cte1.asset, cte1.last_updated_at FROM cte1) data + `). + Set("last_updated_at = data.last_updated_at"). + Where("balance.account_id = data.account_id AND balance.asset = data.asset AND balance.created_at = data.created_at"). + Exec(ctx) + if err != nil { + return e("failed to update balances", err) + } + + return e("failed to commit transaction", tx.Commit()) +} + +func (s *store) BalancesDeleteForConnectorID(ctx context.Context, connectorID models.ConnectorID) error { + _, err := s.db.NewDelete(). + Model((*balance)(nil)). + Where("connector_id = ?", connectorID). + Exec(ctx) + if err != nil { + return e("delete balances", err) + } + + return nil +} + +type BalanceQuery struct { + AccountID *models.AccountID + Asset string + From time.Time + To time.Time +} + +func NewBalanceQuery() BalanceQuery { + return BalanceQuery{} +} + +func (b BalanceQuery) WithAccountID(accountID *models.AccountID) BalanceQuery { + b.AccountID = accountID + + return b +} + +func (b BalanceQuery) WithAsset(asset string) BalanceQuery { + b.Asset = asset + + return b +} + +func (b BalanceQuery) WithFrom(from time.Time) BalanceQuery { + b.From = from + + return b +} + +func (b BalanceQuery) WithTo(to time.Time) BalanceQuery { + b.To = to + + return b +} + +func applyBalanceQuery(query *bun.SelectQuery, balanceQuery BalanceQuery) *bun.SelectQuery { + if balanceQuery.AccountID != nil { + query = query.Where("balance.account_id = ?", balanceQuery.AccountID) + } + + if balanceQuery.Asset != "" { + query = query.Where("balance.asset = ?", balanceQuery.Asset) + } + + if !balanceQuery.From.IsZero() { + query = query.Where("balance.last_updated_at >= ?", balanceQuery.From) + } + + if !balanceQuery.To.IsZero() { + query = query.Where("(balance.created_at <= ?)", balanceQuery.To) + } + + return query +} + +type ListBalancesQuery bunpaginate.OffsetPaginatedQuery[bunpaginate.PaginatedQueryOptions[BalanceQuery]] + +func NewListBalancesQuery(opts bunpaginate.PaginatedQueryOptions[BalanceQuery]) ListBalancesQuery { + return ListBalancesQuery{ + Order: bunpaginate.OrderAsc, + PageSize: opts.PageSize, + Options: opts, + } +} + +func (s *store) BalancesList(ctx context.Context, q ListBalancesQuery) (*bunpaginate.Cursor[models.Balance], error) { + cursor, err := paginateWithOffset[bunpaginate.PaginatedQueryOptions[BalanceQuery], balance](s, ctx, + (*bunpaginate.OffsetPaginatedQuery[bunpaginate.PaginatedQueryOptions[BalanceQuery]])(&q), + func(query *bun.SelectQuery) *bun.SelectQuery { + query = applyBalanceQuery(query, q.Options.Options) + + query = query.Order("created_at DESC") + + return query + }, + ) + if err != nil { + return nil, e("failed to fetch balances", err) + } + + balances := toBalancesModels(cursor.Data) + + return &bunpaginate.Cursor[models.Balance]{ + PageSize: cursor.PageSize, + HasMore: cursor.HasMore, + Previous: cursor.Previous, + Next: cursor.Next, + Data: balances, + }, nil +} + +func (s *store) balancesListAssets(ctx context.Context, accountID models.AccountID) ([]string, error) { + var assets []string + + err := s.db.NewSelect(). + ColumnExpr("DISTINCT asset"). + Model(&models.Balance{}). + Where("account_id = ?", accountID). + Scan(ctx, &assets) + if err != nil { + return nil, e("failed to list balance assets", err) + } + + return assets, nil +} + +func (s *store) balancesGetAtByAsset(ctx context.Context, accountID models.AccountID, asset string, at time.Time) (*models.Balance, error) { + var balance balance + + err := s.db.NewSelect(). + Model(&balance). + Where("account_id = ?", accountID). + Where("asset = ?", asset). + Where("created_at <= ?", at). + Where("last_updated_at >= ?", at). + Order("last_updated_at DESC"). + Limit(1). + Scan(ctx) + if err != nil { + return nil, e("failed to get balance", err) + } + + return pointer.For(toBalanceModels(balance)), nil +} + +func (s *store) BalancesGetAt(ctx context.Context, accountID models.AccountID, at time.Time) ([]*models.Balance, error) { + assets, err := s.balancesListAssets(ctx, accountID) + if err != nil { + return nil, fmt.Errorf("failed to list balance assets: %w", err) + } + + var balances []*models.Balance + for _, currency := range assets { + balance, err := s.balancesGetAtByAsset(ctx, accountID, currency, at) + if err != nil { + if errors.Is(err, ErrNotFound) { + continue + } + return nil, fmt.Errorf("failed to get balance: %w", err) + } + + balances = append(balances, balance) + } + + return balances, nil +} + +func fromBalancesModels(from []models.Balance) []balance { + var to []balance + for _, b := range from { + to = append(to, fromBalanceModels(b)) + } + return to +} + +func fromBalanceModels(from models.Balance) balance { + return balance{ + AccountID: from.AccountID, + CreatedAt: from.CreatedAt, + Asset: from.Asset, + ConnectorID: from.AccountID.ConnectorID, + Balance: from.Balance, + LastUpdatedAt: from.LastUpdatedAt, + } +} + +func toBalancesModels(from []balance) []models.Balance { + var to []models.Balance + for _, b := range from { + to = append(to, toBalanceModels(b)) + } + return to +} + +func toBalanceModels(from balance) models.Balance { + return models.Balance{ + AccountID: from.AccountID, + CreatedAt: from.CreatedAt, + Asset: from.Asset, + Balance: from.Balance, + LastUpdatedAt: from.LastUpdatedAt, + } +} diff --git a/components/payments/internal/storage/bank_accounts.go b/components/payments/internal/storage/bank_accounts.go new file mode 100644 index 0000000000..58a2b77b56 --- /dev/null +++ b/components/payments/internal/storage/bank_accounts.go @@ -0,0 +1,315 @@ +package storage + +import ( + "context" + "fmt" + "time" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/go-libs/query" + "github.com/formancehq/payments/internal/models" + "github.com/google/uuid" + "github.com/pkg/errors" + "github.com/uptrace/bun" +) + +type bankAccount struct { + bun.BaseModel `bun:"table:bank_accounts"` + + // Mandatory fields + ID uuid.UUID `bun:"id,pk,type:uuid,notnull"` + CreatedAt time.Time `bun:"created_at,type:timestamp without time zone,notnull"` + Name string `bun:"name,type:text,notnull"` + + // Field encrypted + AccountNumber string `bun:"decrypted_account_number,scanonly"` + IBAN string `bun:"decrypted_iban,scanonly"` + SwiftBicCode string `bun:"decrypted_swift_bic_code,scanonly"` + + // Optional fields + // c.f.: https://bun.uptrace.dev/guide/models.html#nulls + Country *string `bun:"country,type:text,nullzero"` + + // Optional fields with default + // c.f. https://bun.uptrace.dev/guide/models.html#default + Metadata map[string]string `bun:"metadata,type:jsonb,nullzero,notnull,default:'{}'"` + + RelatedAccounts []*bankAccountRelatedAccount `bun:"rel:has-many,join:id=bank_account_id"` +} + +func (s *store) BankAccountsUpsert(ctx context.Context, bankAccount models.BankAccount) error { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return e("begin transaction", err) + } + defer tx.Rollback() + + toInsert := fromBankAccountModels(bankAccount) + // Insert or update the bank account + _, err = tx.NewInsert(). + Model(&toInsert). + On("CONFLICT (id) DO NOTHING"). + Exec(ctx) + if err != nil { + return e("insert bank account", err) + } + + // Insert or update the related accounts + _, err = tx.NewInsert(). + Model(&toInsert.RelatedAccounts). + On("CONFLICT (bank_account_id, account_id) DO NOTHING"). + Exec(ctx) + if err != nil { + return e("insert related accounts", err) + } + + return e("commit transaction", tx.Commit()) +} + +func (s *store) BankAccountsUpdateMetadata(ctx context.Context, id uuid.UUID, metadata map[string]string) error { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return e("update bank account metadata", err) + } + defer tx.Rollback() + + var account bankAccount + err = tx.NewSelect(). + Model(&account). + Column("id", "metadata"). + Where("id = ?", id). + Scan(ctx) + if err != nil { + return e("update bank account metadata", err) + } + + if account.Metadata == nil { + account.Metadata = make(map[string]string) + } + + for k, v := range metadata { + account.Metadata[k] = v + } + + _, err = tx.NewUpdate(). + Model(&account). + Column("metadata"). + Where("id = ?", id). + Exec(ctx) + if err != nil { + return e("update bank account metadata", err) + } + + return e("commit transaction", tx.Commit()) +} + +func (s *store) BankAccountsGet(ctx context.Context, id uuid.UUID, expand bool) (*models.BankAccount, error) { + var account bankAccount + query := s.db.NewSelect(). + Model(&account). + Relation("RelatedAccounts") + if !expand { + query = query.Column("id", "created_at", "name", "country", "metadata") + } + err := query.Where("id = ?", id).Scan(ctx) + if err != nil { + return nil, e("get bank account", err) + } + + return pointer.For(toBankAccountModels(account)), nil +} + +type BankAccountQuery struct{} + +type ListBankAccountsQuery bunpaginate.OffsetPaginatedQuery[bunpaginate.PaginatedQueryOptions[BankAccountQuery]] + +func NewListBankAccountsQuery(opts bunpaginate.PaginatedQueryOptions[BankAccountQuery]) ListBankAccountsQuery { + return ListBankAccountsQuery{ + Order: bunpaginate.OrderAsc, + PageSize: opts.PageSize, + Options: opts, + } +} + +func (s *store) bankAccountsQueryContext(qb query.Builder) (string, []any, error) { + return qb.Build(query.ContextFn(func(key, operator string, value any) (string, []any, error) { + switch { + case key == "name", key == "country": + if operator != "$match" { + return "", nil, errors.Wrap(ErrValidation, fmt.Sprintf("'%s' column can only be used with $match", key)) + } + return fmt.Sprintf("%s = ?", key), []any{value}, nil + case metadataRegex.Match([]byte(key)): + if operator != "$match" { + return "", nil, errors.Wrap(ErrValidation, "'metadata' column can only be used with $match") + } + match := metadataRegex.FindAllStringSubmatch(key, 3) + + key := "metadata" + return key + " @> ?", []any{map[string]any{ + match[0][1]: value, + }}, nil + default: + return "", nil, errors.Wrap(ErrValidation, fmt.Sprintf("unknown key '%s' when building query", key)) + } + })) +} + +func (s *store) BankAccountsList(ctx context.Context, q ListBankAccountsQuery) (*bunpaginate.Cursor[models.BankAccount], error) { + var ( + where string + args []any + err error + ) + if q.Options.QueryBuilder != nil { + where, args, err = s.bankAccountsQueryContext(q.Options.QueryBuilder) + if err != nil { + return nil, err + } + } + + cursor, err := paginateWithOffset[bunpaginate.PaginatedQueryOptions[BankAccountQuery], bankAccount](s, ctx, + (*bunpaginate.OffsetPaginatedQuery[bunpaginate.PaginatedQueryOptions[BankAccountQuery]])(&q), + func(query *bun.SelectQuery) *bun.SelectQuery { + query = query.Relation("RelatedAccounts") + if where != "" { + query = query.Where(where, args...) + } + + query = query.Order("created_at DESC") + + return query + }, + ) + if err != nil { + return nil, e("failed to fetch accounts", err) + } + + bankAccounts := make([]models.BankAccount, 0, len(cursor.Data)) + for _, a := range cursor.Data { + bankAccounts = append(bankAccounts, toBankAccountModels(a)) + } + + return &bunpaginate.Cursor[models.BankAccount]{ + PageSize: cursor.PageSize, + HasMore: cursor.HasMore, + Previous: cursor.Previous, + Next: cursor.Next, + Data: bankAccounts, + }, nil +} + +type bankAccountRelatedAccount struct { + bun.BaseModel `bun:"table:bank_accounts_related_accounts"` + + // Mandatory fields + BankAccountID uuid.UUID `bun:"bank_account_id,pk,type:uuid,notnull"` + AccountID models.AccountID `bun:"account_id,pk,type:character varying,notnull"` + ConnectorID models.ConnectorID `bun:"connector_id,type:character varying,notnull"` + CreatedAt time.Time `bun:"created_at,type:timestamp without time zone,notnull"` +} + +func (s *store) BankAccountsAddRelatedAccount(ctx context.Context, relatedAccount models.BankAccountRelatedAccount) error { + toInsert := fromBankAccountRelatedAccountModels(relatedAccount) + + _, err := s.db.NewInsert(). + Model(&toInsert). + On("CONFLICT (bank_account_id, account_id) DO NOTHING"). + Exec(ctx) + if err != nil { + return e("add bank account related account", err) + } + + return nil +} + +func (s *store) BankAccountsDeleteRelatedAccountFromConnectorID(ctx context.Context, connectorID models.ConnectorID) error { + _, err := s.db.NewDelete(). + Model((*bankAccountRelatedAccount)(nil)). + Where("connector_id = ?", connectorID). + Exec(ctx) + if err != nil { + return e("delete bank account related account", err) + } + + return nil +} + +func fromBankAccountModels(from models.BankAccount) bankAccount { + ba := bankAccount{ + ID: from.ID, + CreatedAt: from.CreatedAt, + Name: from.Name, + Country: from.Country, + Metadata: from.Metadata, + } + + if from.AccountNumber != nil { + ba.AccountNumber = *from.AccountNumber + } + + if from.IBAN != nil { + ba.IBAN = *from.IBAN + } + + if from.SwiftBicCode != nil { + ba.SwiftBicCode = *from.SwiftBicCode + } + + relatedAccounts := make([]*bankAccountRelatedAccount, 0, len(from.RelatedAccounts)) + for _, ra := range from.RelatedAccounts { + relatedAccounts = append(relatedAccounts, pointer.For(fromBankAccountRelatedAccountModels(ra))) + } + ba.RelatedAccounts = relatedAccounts + + return ba +} + +func toBankAccountModels(from bankAccount) models.BankAccount { + ba := models.BankAccount{ + ID: from.ID, + CreatedAt: from.CreatedAt, + Name: from.Name, + Country: from.Country, + Metadata: from.Metadata, + } + + if from.AccountNumber != "" { + ba.AccountNumber = &from.AccountNumber + } + + if from.IBAN != "" { + ba.IBAN = &from.IBAN + } + + if from.SwiftBicCode != "" { + ba.SwiftBicCode = &from.SwiftBicCode + } + + relatedAccounts := make([]models.BankAccountRelatedAccount, 0, len(from.RelatedAccounts)) + for _, ra := range from.RelatedAccounts { + relatedAccounts = append(relatedAccounts, toBankAccountRelatedAccountModels(*ra)) + } + ba.RelatedAccounts = relatedAccounts + + return ba +} + +func fromBankAccountRelatedAccountModels(from models.BankAccountRelatedAccount) bankAccountRelatedAccount { + return bankAccountRelatedAccount{ + BankAccountID: from.BankAccountID, + AccountID: from.AccountID, + ConnectorID: from.ConnectorID, + CreatedAt: from.CreatedAt, + } +} + +func toBankAccountRelatedAccountModels(from bankAccountRelatedAccount) models.BankAccountRelatedAccount { + return models.BankAccountRelatedAccount{ + BankAccountID: from.BankAccountID, + AccountID: from.AccountID, + ConnectorID: from.ConnectorID, + CreatedAt: from.CreatedAt, + } +} diff --git a/components/payments/internal/storage/connectors.go b/components/payments/internal/storage/connectors.go new file mode 100644 index 0000000000..ddf9c04d11 --- /dev/null +++ b/components/payments/internal/storage/connectors.go @@ -0,0 +1,171 @@ +package storage + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "time" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/query" + "github.com/formancehq/payments/internal/models" + "github.com/pkg/errors" + "github.com/uptrace/bun" +) + +type connector struct { + bun.BaseModel `bun:"table:connectors"` + + // Mandatory fields + ID models.ConnectorID `bun:"id,pk,type:character varying,notnull"` + Name string `bun:"name,type:text,notnull"` + CreatedAt time.Time `bun:"created_at,type:timestamp without time zone,notnull"` + Provider string `bun:"provider,type:text,notnull"` + + // EncryptedConfig is a PGP-encrypted JSON string. + EncryptedConfig string `bun:"config,type:bytea,notnull"` + + // Config is a decrypted config. It is not stored in the database. + DecryptedConfig json.RawMessage `bun:"decrypted_config,scanonly"` +} + +func (s *store) ConnectorsInstall(ctx context.Context, c models.Connector) error { + tx, err := s.db.BeginTx(ctx, &sql.TxOptions{}) + if err != nil { + return errors.Wrap(err, "cannot begin transaction") + } + defer tx.Rollback() + + toInsert := connector{ + ID: c.ID, + Name: c.Name, + CreatedAt: c.CreatedAt, + Provider: c.Provider, + } + + _, err = tx.NewInsert(). + Model(&toInsert). + On("CONFLICT (id) DO NOTHING"). + Exec(ctx) + if err != nil { + return e("failed to insert connector", err) + } + + _, err = tx.NewUpdate(). + Model((*connector)(nil)). + Set("config = pgp_sym_encrypt(?::TEXT, ?, ?)", c.Config, s.configEncryptionKey, encryptionOptions). + Where("id = ?", toInsert.ID). + Exec(ctx) + if err != nil { + return e("failed to encrypt config", err) + } + + return e("failed to commit transaction", tx.Commit()) +} + +// TODO(polo): find a better way to delete all data +func (s *store) ConnectorsUninstall(ctx context.Context, id models.ConnectorID) error { + _, err := s.db.NewDelete(). + Model((*connector)(nil)). + Where("id = ?", id). + Exec(ctx) + return e("failed to delete connector", err) +} + +func (s *store) ConnectorsGet(ctx context.Context, id models.ConnectorID) (*models.Connector, error) { + var connector connector + + err := s.db.NewSelect(). + Model(&connector). + ColumnExpr("*, pgp_sym_decrypt(config, ?, ?) AS decrypted_config", s.configEncryptionKey, encryptionOptions). + Where("id = ?", id). + Scan(ctx) + if err != nil { + return nil, e("failed to fetch connector", err) + } + + return &models.Connector{ + ID: connector.ID, + Name: connector.Name, + CreatedAt: connector.CreatedAt, + Provider: connector.Provider, + Config: connector.DecryptedConfig, + }, nil +} + +type ConnectorQuery struct{} + +type ListConnectorsQuery bunpaginate.OffsetPaginatedQuery[bunpaginate.PaginatedQueryOptions[ConnectorQuery]] + +func NewListConnectorsQuery(opts bunpaginate.PaginatedQueryOptions[ConnectorQuery]) ListConnectorsQuery { + return ListConnectorsQuery{ + Order: bunpaginate.OrderAsc, + PageSize: opts.PageSize, + Options: opts, + } +} + +func (s *store) connectorsQueryContext(qb query.Builder) (string, []any, error) { + return qb.Build(query.ContextFn(func(key, operator string, value any) (string, []any, error) { + switch { + case key == "name", + key == "provider": + return fmt.Sprintf("%s %s ?", key, query.DefaultComparisonOperatorsMapping[operator]), []any{value}, nil + default: + return "", nil, errors.Wrap(ErrValidation, fmt.Sprintf("unknown key '%s' when building query", key)) + } + })) +} + +func (s *store) ConnectorsList(ctx context.Context, q ListConnectorsQuery) (*bunpaginate.Cursor[models.Connector], error) { + var ( + where string + args []any + err error + ) + if q.Options.QueryBuilder != nil { + where, args, err = s.connectorsQueryContext(q.Options.QueryBuilder) + if err != nil { + return nil, err + } + } + + cursor, err := paginateWithOffset[bunpaginate.PaginatedQueryOptions[ConnectorQuery], connector](s, ctx, + (*bunpaginate.OffsetPaginatedQuery[bunpaginate.PaginatedQueryOptions[ConnectorQuery]])(&q), + func(query *bun.SelectQuery) *bun.SelectQuery { + if where != "" { + query = query.Where(where, args...) + } + + query = query.ColumnExpr("*, pgp_sym_decrypt(config, ?, ?) AS decrypted_config", s.configEncryptionKey, encryptionOptions) + + // TODO(polo): sorter ? + query = query.Order("created_at DESC") + + return query + }, + ) + if err != nil { + return nil, e("failed to fetch connectors", err) + } + + connectors := make([]models.Connector, 0, len(cursor.Data)) + for _, c := range cursor.Data { + connectors = append(connectors, models.Connector{ + ID: c.ID, + Name: c.Name, + CreatedAt: c.CreatedAt, + Provider: c.Provider, + Config: c.DecryptedConfig, + }) + } + + return &bunpaginate.Cursor[models.Connector]{ + PageSize: cursor.PageSize, + HasMore: cursor.HasMore, + Previous: cursor.Previous, + Next: cursor.Next, + Data: connectors, + }, nil +} diff --git a/components/payments/cmd/api/internal/storage/error.go b/components/payments/internal/storage/error.go similarity index 100% rename from components/payments/cmd/api/internal/storage/error.go rename to components/payments/internal/storage/error.go diff --git a/components/payments/internal/storage/main_test.go b/components/payments/internal/storage/main_test.go new file mode 100644 index 0000000000..02d459423e --- /dev/null +++ b/components/payments/internal/storage/main_test.go @@ -0,0 +1,43 @@ +package storage + +// func TestMain(m *testing.M) { +// if err := pgtesting.CreatePostgresServer(); err != nil { +// logging.Error(err) +// os.Exit(1) +// } + +// code := m.Run() +// if err := pgtesting.DestroyPostgresServer(); err != nil { +// logging.Error(err) +// } +// os.Exit(code) +// } + +// func newStore(t *testing.T) Storage { +// t.Helper() + +// pgServer := pgtesting.NewPostgresDatabase(t) + +// config, err := pgx.ParseConfig(pgServer.ConnString()) +// require.NoError(t, err) + +// key := make([]byte, 64) +// _, err = rand.Read(key) +// require.NoError(t, err) + +// db := bun.NewDB(stdlib.OpenDB(*config), pgdialect.New()) +// t.Cleanup(func() { +// _ = db.Close() +// }) + +// // TODO(polo): add migrations +// // err = migrationstorage.Migrate(context.Background(), db) +// // require.NoError(t, err) + +// store := newStorage( +// db, +// string(key), +// ) + +// return store +// } diff --git a/components/payments/internal/storage/migrations.go b/components/payments/internal/storage/migrations.go index 88686eb838..7e9a05c4b4 100644 --- a/components/payments/internal/storage/migrations.go +++ b/components/payments/internal/storage/migrations.go @@ -2,6 +2,7 @@ package storage import ( "context" + _ "embed" "github.com/formancehq/go-libs/migrations" "github.com/uptrace/bun" @@ -13,10 +14,27 @@ import ( //nolint:gochecknoglobals // This is a global variable by design. var EncryptionKey string -func Migrate(ctx context.Context, db *bun.DB) error { +//go:embed migrations/0-init-schema.sql +var initSchema string + +func registerMigrations(migrator *migrations.Migrator) { + migrator.RegisterMigrations( + migrations.Migration{ + Name: "init schema", + UpWithContext: func(ctx context.Context, tx bun.Tx) error { + _, err := tx.ExecContext(ctx, initSchema) + return err + }, + }, + ) +} + +func getMigrator() *migrations.Migrator { migrator := migrations.NewMigrator() - registerMigrationsV0(migrator) - registerMigrationsV1(ctx, migrator) + registerMigrations(migrator) + return migrator +} - return migrator.Up(ctx, db) +func Migrate(ctx context.Context, db bun.IDB) error { + return getMigrator().Up(ctx, db) } diff --git a/components/payments/internal/storage/migrations/0-init-schema.sql b/components/payments/internal/storage/migrations/0-init-schema.sql new file mode 100644 index 0000000000..20c87f2628 --- /dev/null +++ b/components/payments/internal/storage/migrations/0-init-schema.sql @@ -0,0 +1,288 @@ +create extension if not exists pgcrypto; + +-- connectors +create table if not exists connectors ( + -- Mandatory fields + id varchar not null, + name text not null, + created_at timestamp without time zone not null, + provider text not null, + + -- Optional fields + config bytea, + + -- Primary key + primary key (id) +); +create unique index connectors_unique_name on connectors (name); + +-- accounts +create table if not exists accounts ( + -- Mandatory fields + id varchar not null, + connector_id varchar not null, + created_at timestamp without time zone not null, + reference text not null, + type text not null, + raw json not null, + + -- Optional fields + default_asset text, + name text, + + -- Optional fields with default + metadata jsonb not null default '{}'::jsonb, + + -- Primary key + primary key (id) +); +alter table accounts + add constraint accounts_connector_id_fk foreign key (connector_id) + references connectors (id) + on delete cascade; + +-- balances +create table if not exists balances ( + -- Mandatory fields + account_id varchar not null, + connector_id varchar not null, + created_at timestamp without time zone not null, + last_updated_at timestamp without time zone not null, + asset text not null, + balance numeric not null, + + -- Primary key + primary key (account_id, created_at, asset) +); +create index balances_account_id_created_at_asset on balances (account_id, last_updated_at desc, asset); +alter table balances + add constraint balances_connector_id foreign key (connector_id) + references connectors (id) + on delete cascade; + +-- bank accounts +create table if not exists bank_accounts ( + -- Mandatory fields + id uuid not null, + created_at timestamp without time zone not null, + name text not null, + + -- Optional fields + account_number bytea, + iban bytea, + swift_bic_code bytea, + country text, + + -- Optional fields with default + metadata jsonb not null default '{}'::jsonb, + + -- Primary key + primary key (id) +); +create table if not exists bank_accounts_related_accounts ( + -- Mandatory fields + bank_account_id uuid not null, + account_id varchar not null, + connector_id varchar not null, + created_at timestamp without time zone not null, + + -- Primary key + primary key (bank_account_id, account_id) +); +alter table bank_accounts_related_accounts + add constraint bank_accounts_related_accounts_bank_account_id_fk foreign key (bank_account_id) + references bank_accounts (id) + on delete cascade; +alter table bank_accounts_related_accounts + add constraint bank_accounts_related_accounts_account_id_fk foreign key (account_id) + references accounts (id) + on delete cascade; +alter table bank_accounts_related_accounts + add constraint bank_accounts_related_accounts_connector_id_fk foreign key (connector_id) + references connectors (id) + on delete cascade; + +-- payments +create table if not exists payments ( + -- Mandatory fields + id varchar not null, + connector_id varchar not null, + reference text not null, + created_at timestamp without time zone not null, + type text not null, + initial_amount numeric not null, + amount numeric not null, + asset text not null, + scheme text not null, + + -- Optional fields + source_account_id varchar, + destination_account_id varchar, + + -- Optional fields with default + metadata jsonb not null default '{}'::jsonb, + + -- Primary key + primary key (id) +); +alter table payments + add constraint payments_connector_id_fk foreign key (connector_id) + references connectors (id) + on delete cascade; + +-- payment adjustments +create table if not exists payment_adjustments ( + -- Mandatory fields + id varchar not null, + payment_id varchar not null, + created_at timestamp without time zone not null, + status text not null, + raw json not null, + + -- Optional fields + amount numeric, + asset text, + + -- Optional fields with default + metadata jsonb not null default '{}'::jsonb, + + -- Primary key + primary key (id) +); +alter table payment_adjustments + add constraint payment_adjustments_payment_id_fk foreign key (payment_id) + references payments (id) + on delete cascade; + +-- pools +create table if not exists pools ( + -- Mandatory fields + id uuid not null, + name text not null, + created_at timestamp without time zone not null, + + -- Primary key + primary key (id) +); +create unique index pools_unique_name on pools (name); + +create table if not exists pools_related_accounts ( + -- Mandatory fields + pool_id uuid not null, + account_id varchar not null, + + -- Primary key + primary key (pool_id, account_id) +); +alter table pools_related_accounts + add constraint pools_related_accounts_pool_id_fk foreign key (pool_id) + references pools (id) + on delete cascade; +alter table pools_related_accounts + add constraint pools_related_accounts_account_id_fk foreign key (account_id) + references accounts (id) + on delete cascade; + +-- schedules +create table if not exists schedules ( + -- Mandatory fields + id text not null, + connector_id varchar not null, + created_at timestamp without time zone not null, + + -- Primary key + primary key (id, connector_id) +); +alter table schedules + add constraint schedules_connector_id_fk foreign key (connector_id) + references connectors (id) + on delete cascade; + +-- states +create table if not exists states ( + -- Mandatory fields + id varchar not null, + connector_id varchar not null, + + -- Optional fields with default + state json not null default '{}'::json, + + -- Primary key + primary key (id) +); +alter table states + add constraint states_connector_id_fk foreign key (connector_id) + references connectors (id) + on delete cascade; + +-- tasks +create table if not exists tasks ( + -- Mandatory fields + connector_id varchar not null, + tasks json not null, + + -- Primary key + primary key (connector_id) +); +alter table tasks + add constraint tasks_connector_id_fk foreign key (connector_id) + references connectors (id) + on delete cascade; + +-- Workflow instance +create table if not exists workflows_instances ( + -- Mandatory fields + id text not null, + schedule_id text not null, + connector_id varchar not null, + created_at timestamp without time zone not null, + updated_at timestamp without time zone not null, + + -- Optional fields with default + terminated boolean not null default false, + + -- Optional fields + terminated_at timestamp without time zone, + error text, + + -- Primary key + primary key (id, schedule_id, connector_id) +); +alter table workflows_instances + add constraint workflows_instances_connector_id_fk foreign key (connector_id) + references connectors (id) + on delete cascade; + +-- Webhook configs +create table if not exists webhooks_configs ( + -- Mandatory fields + name text not null, + connector_id varchar not null, + url_path text not null, + + -- Primary key + primary key (name, connector_id) +); +alter table webhooks_configs + add constraint webhooks_configs_connector_id_fk foreign key (connector_id) + references connectors (id) + on delete cascade; + +-- Webhooks +create table if not exists webhooks ( + -- Mandatory fields + id text not null, + connector_id varchar not null, + + -- Optional fields + headers json, + query_values json, + body bytea, + + -- Primary key + primary key (id) +); +alter table webhooks + add constraint webhooks_connector_id_fk foreign key (connector_id) + references connectors (id) + on delete cascade; \ No newline at end of file diff --git a/components/payments/internal/storage/migrations_v0.x.go b/components/payments/internal/storage/migrations_v0.x.go deleted file mode 100644 index 29a890ba8f..0000000000 --- a/components/payments/internal/storage/migrations_v0.x.go +++ /dev/null @@ -1,625 +0,0 @@ -package storage - -import ( - "fmt" - - "github.com/formancehq/go-libs/migrations" - "github.com/pkg/errors" - "github.com/uptrace/bun" -) - -func registerMigrationsV0(migrator *migrations.Migrator) { - migrator.RegisterMigrations( - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - CREATE SCHEMA IF NOT EXISTS connectors; - CREATE SCHEMA IF NOT EXISTS tasks; - CREATE SCHEMA IF NOT EXISTS accounts; - CREATE SCHEMA IF NOT EXISTS payments; - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - CREATE TYPE "public".connector_provider AS ENUM ('BANKING-CIRCLE', 'CURRENCY-CLOUD', 'DUMMY-PAY', 'MODULR', 'STRIPE', 'WISE');; - CREATE TABLE connectors.connector ( - id uuid NOT NULL DEFAULT gen_random_uuid(), - created_at timestamp with time zone NOT NULL DEFAULT NOW() CHECK (created_at<=NOW()), - provider connector_provider NOT NULL UNIQUE, - enabled boolean NOT NULL DEFAULT false, - config json NULL, - CONSTRAINT connector_pk PRIMARY KEY (id) - ); - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - CREATE TYPE "public".task_status AS ENUM ('STOPPED', 'PENDING', 'ACTIVE', 'TERMINATED', 'FAILED');; - CREATE TABLE tasks.task ( - id uuid NOT NULL DEFAULT gen_random_uuid(), - connector_id uuid NOT NULL, - created_at timestamp with time zone NOT NULL DEFAULT NOW() CHECK (created_at<=NOW()), - updated_at timestamp with time zone NOT NULL DEFAULT NOW() CHECK (created_at<=updated_at), - name text NOT NULL, - descriptor json NULL, - status task_status NOT NULL, - error text NULL, - state json NULL, - CONSTRAINT task_pk PRIMARY KEY (id) - ); - ALTER TABLE tasks.task ADD CONSTRAINT task_connector - FOREIGN KEY (connector_id) - REFERENCES connectors.connector (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - CREATE TYPE "public".account_type AS ENUM('SOURCE', 'TARGET', 'UNKNOWN');; - - CREATE TABLE accounts.account ( - id uuid NOT NULL DEFAULT gen_random_uuid(), - created_at timestamp with time zone NOT NULL DEFAULT NOW() CHECK (created_at<=NOW()), - reference text NOT NULL UNIQUE, - provider text NOT NULL, - type account_type NOT NULL, - CONSTRAINT account_pk PRIMARY KEY (id) - ); - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - CREATE TYPE "public".payment_type AS ENUM ('PAY-IN', 'PAYOUT', 'TRANSFER', 'OTHER'); - CREATE TYPE "public".payment_status AS ENUM ('SUCCEEDED', 'CANCELLED', 'FAILED', 'PENDING', 'OTHER');; - - CREATE TABLE payments.adjustment ( - id uuid NOT NULL DEFAULT gen_random_uuid(), - payment_id uuid NOT NULL, - created_at timestamp with time zone NOT NULL DEFAULT NOW() CHECK (created_at<=NOW()), - amount bigint NOT NULL DEFAULT 0, - reference text NOT NULL UNIQUE, - status payment_status NOT NULL, - absolute boolean NOT NULL DEFAULT FALSE, - raw_data json NULL, - CONSTRAINT adjustment_pk PRIMARY KEY (id) - ); - - CREATE TABLE payments.metadata ( - payment_id uuid NOT NULL, - created_at timestamp with time zone NOT NULL DEFAULT NOW() CHECK (created_at<=NOW()), - key text NOT NULL, - value text NOT NULL, - changelog jsonb NOT NULL, - CONSTRAINT metadata_pk PRIMARY KEY (payment_id,key) - ); - - CREATE TABLE payments.payment ( - id uuid NOT NULL DEFAULT gen_random_uuid(), - connector_id uuid NOT NULL, - account_id uuid DEFAULT NULL, - created_at timestamp with time zone NOT NULL DEFAULT NOW() CHECK (created_at<=NOW()), - reference text NOT NULL UNIQUE, - type payment_type NOT NULL, - status payment_status NOT NULL, - amount bigint NOT NULL DEFAULT 0, - raw_data json NULL, - scheme text NOT NULL, - asset text NOT NULL, - CONSTRAINT payment_pk PRIMARY KEY (id) - ); - - ALTER TABLE payments.adjustment ADD CONSTRAINT adjustment_payment - FOREIGN KEY (payment_id) - REFERENCES payments.payment (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - - ALTER TABLE payments.metadata ADD CONSTRAINT metadata_payment - FOREIGN KEY (payment_id) - REFERENCES payments.payment (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - - ALTER TABLE payments.payment ADD CONSTRAINT payment_account - FOREIGN KEY (account_id) - REFERENCES accounts.account (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - - ALTER TABLE payments.payment ADD CONSTRAINT payment_connector - FOREIGN KEY (connector_id) - REFERENCES connectors.connector (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - `) - if err != nil { - return err - } - - return nil - }, - }, - //nolint:varnamelen - migrations.Migration{ - Up: func(tx bun.Tx) error { - var exists bool - - err := tx.QueryRow("SELECT EXISTS(SELECT 1 FROM connectors.connector)").Scan(&exists) - if err != nil { - return fmt.Errorf("failed to check if connectors table exists: %w", err) - } - - if exists && EncryptionKey == "" { - return errors.New("encryption key is not set") - } - - _, err = tx.Exec(` - CREATE EXTENSION IF NOT EXISTS pgcrypto; - ALTER TABLE connectors.connector RENAME COLUMN config TO config_unencrypted; - ALTER TABLE connectors.connector ADD COLUMN config bytea NULL; - `) - if err != nil { - return fmt.Errorf("failed to create config column: %w", err) - } - - _, err = tx.Exec(` - UPDATE connectors.connector SET config = pgp_sym_encrypt(config_unencrypted::TEXT, ?, 'compress-algo=1, cipher-algo=aes256'); - `, EncryptionKey) - if err != nil { - return fmt.Errorf("failed to encrypt config: %w", err) - } - - _, err = tx.Exec(` - ALTER TABLE connectors.connector DROP COLUMN config_unencrypted; - `) - if err != nil { - return fmt.Errorf("failed to drop config_unencrypted column: %w", err) - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - CREATE TYPE "public".transfer_status AS ENUM ('PENDING', 'SUCCEEDED', 'FAILED'); - - CREATE TABLE payments.transfers ( - id uuid NOT NULL DEFAULT gen_random_uuid(), - connector_id uuid NOT NULL, - payment_id uuid NULL, - reference text UNIQUE, - created_at timestamp with time zone NOT NULL DEFAULT NOW() CHECK (created_at<=NOW()), - amount bigint NOT NULL DEFAULT 0, - currency text NOT NULL, - source text NOT NULL, - destination text NOT NULL, - status transfer_status NOT NULL DEFAULT 'PENDING', - error text NULL, - CONSTRAINT transfer_pk PRIMARY KEY (id) - ); - - ALTER TABLE payments.transfers ADD CONSTRAINT transfer_connector - FOREIGN KEY (connector_id) - REFERENCES connectors.connector (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - - ALTER TABLE payments.transfers ADD CONSTRAINT transfer_payment - FOREIGN KEY (payment_id) - REFERENCES payments.payment (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - ALTER TABLE payments.payment ALTER COLUMN id DROP DEFAULT; - - ALTER TABLE payments.adjustment drop constraint IF EXISTS adjustment_payment; - ALTER TABLE payments.metadata drop constraint IF EXISTS metadata_payment; - ALTER TABLE payments.transfers drop constraint IF EXISTS transfer_payment; - ALTER TABLE payments.payment ALTER COLUMN id TYPE CHARACTER VARYING; - ALTER TABLE payments.adjustment ALTER COLUMN payment_id TYPE CHARACTER VARYING; - ALTER TABLE payments.metadata ALTER COLUMN payment_id TYPE CHARACTER VARYING; - ALTER TABLE payments.transfers ALTER COLUMN payment_id TYPE CHARACTER VARYING; - - ALTER TABLE payments.metadata ADD CONSTRAINT metadata_payment - FOREIGN KEY (payment_id) - REFERENCES payments.payment (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - - ALTER TABLE payments.adjustment ADD CONSTRAINT adjustment_payment - FOREIGN KEY (payment_id) - REFERENCES payments.payment (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - ALTER TYPE connector_provider ADD VALUE IF NOT EXISTS 'MANGOPAY'; - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - ALTER TYPE connector_provider ADD VALUE IF NOT EXISTS 'MONEYCORP'; - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - ALTER TABLE tasks.task ADD COLUMN IF NOT EXISTS "scheduler_options" json; - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - ALTER TABLE accounts.account DROP COLUMN IF EXISTS "type"; - DROP TYPE IF EXISTS "public".account_type; - CREATE TYPE "public".account_type AS ENUM('INTERNAL', 'EXTERNAL', 'UNKNOWN');; - ALTER TABLE accounts.account ADD COLUMN IF NOT EXISTS "type" "public".account_type; - - ALTER TABLE accounts.account drop constraint IF EXISTS account_reference_key; - ALTER TABLE accounts.account ADD COLUMN IF NOT EXISTS "raw_data" json; - ALTER TABLE accounts.account ADD COLUMN IF NOT EXISTS "default_currency" text NOT NULL DEFAULT ''; - ALTER TABLE accounts.account ADD COLUMN IF NOT EXISTS "account_name" text NOT NULL DEFAULT ''; - - ALTER TABLE accounts.account ALTER COLUMN id DROP DEFAULT; - ALTER TABLE payments.payment DROP CONSTRAINT IF EXISTS payment_account; - ALTER TABLE payments.payment DROP COLUMN IF EXISTS "account_id"; - ALTER TABLE payments.payment ADD COLUMN IF NOT EXISTS "source_account_id" CHARACTER VARYING; - ALTER TABLE payments.payment ADD COLUMN IF NOT EXISTS "destination_account_id" CHARACTER VARYING; - ALTER TABLE accounts.account ALTER COLUMN id TYPE CHARACTER VARYING; - - ALTER TABLE payments.payment DROP CONSTRAINT IF EXISTS payment_source_account; - ALTER TABLE payments.payment ADD CONSTRAINT payment_source_account - FOREIGN KEY (source_account_id) - REFERENCES accounts.account (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - - ALTER TABLE payments.payment DROP CONSTRAINT IF EXISTS payment_destination_account; - ALTER TABLE payments.payment ADD CONSTRAINT payment_destination_account - FOREIGN KEY (destination_account_id) - REFERENCES accounts.account (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - // Since only one connector is inserting accounts, - // let's just delete the table, since connectors will be - // resetted. Delete it cascade, or we will have an error - _, err := tx.Exec(` - DELETE FROM accounts.account CASCADE; - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - // Since only one connector is inserting accounts, - // let's just delete the table, since connectors will be - // resetted. Delete it cascade, or we will have an error - _, err := tx.Exec(` - CREATE TABLE IF NOT EXISTS accounts.balances ( - created_at timestamp with time zone NOT NULL, - account_id CHARACTER VARYING NOT NULL, - currency text NOT NULL, - balance numeric NOT NULL DEFAULT 0, - last_updated_at timestamp with time zone NOT NULL, - PRIMARY KEY (account_id, created_at, currency) - ); - - DROP INDEX IF EXISTS accounts.idx_created_at_account_id_currency; - CREATE INDEX idx_created_at_account_id_currency ON accounts.balances(account_id, last_updated_at desc, currency); - - ALTER TABLE accounts.balances DROP CONSTRAINT IF EXISTS balances_account; - ALTER TABLE accounts.balances ADD CONSTRAINT balances_account - FOREIGN KEY (account_id) - REFERENCES accounts.account (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - ALTER TABLE payments.adjustment ALTER COLUMN amount TYPE numeric; - ALTER TABLE payments.payment ALTER COLUMN amount TYPE numeric; - ALTER TABLE payments.transfers ALTER COLUMN amount TYPE numeric; - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - // In this migration, we have to delete the accounts table since - // we wanna reset the connector, but the connector_id was not - // added, hence the table will not be cleaned up when resetting. - _, err := tx.Exec(` - DELETE FROM accounts.account CASCADE; - ALTER TABLE accounts.account ADD COLUMN IF NOT EXISTS "connector_id" uuid; - - ALTER TABLE accounts.account ADD CONSTRAINT accounts_connector - FOREIGN KEY (connector_id) - REFERENCES connectors.connector (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - ALTER TABLE accounts.account ADD COLUMN IF NOT EXISTS metadata jsonb; - - CREATE SCHEMA IF NOT EXISTS transfers; - - CREATE TABLE IF NOT EXISTS transfers.transfer_initiation ( - id character varying NOT NULL, - reference text, - created_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone NOT NULL, - description text NOT NULL, - type int NOT NULL, - source_account_id character varying NOT NULL, - destination_account_id character varying NOT NULL, - provider connector_provider NOT NULL, - amount numeric NOT NULL, - asset text NOT NULL, - status int NOT NULL, - error text, - PRIMARY KEY (id) - ); - - ALTER TABLE transfers.transfer_initiation ADD CONSTRAINT source_account - FOREIGN KEY (source_account_id) - REFERENCES accounts.account (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - - ALTER TABLE transfers.transfer_initiation ADD CONSTRAINT destination_account - FOREIGN KEY (destination_account_id) - REFERENCES accounts.account (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - ALTER TABLE transfers.transfer_initiation ALTER COLUMN source_account_id DROP NOT NULL; - ALTER TABLE transfers.transfer_initiation RENAME COLUMN reference TO payment_id; - ALTER TYPE "public".account_type ADD VALUE IF NOT EXISTS 'EXTERNAL_FORMANCE'; - ALTER TABLE transfers.transfer_initiation ADD COLUMN attempts int NOT NULL DEFAULT 0; - - CREATE TABLE IF NOT EXISTS accounts.bank_account ( - id uuid NOT NULL DEFAULT gen_random_uuid(), - created_at timestamp with time zone NOT NULL DEFAULT NOW() CHECK (created_at<=NOW()), - name text NOT NULL, - provider connector_provider NOT NULL, - account_number bytea, - iban bytea, - swift_bic_code bytea, - country text, - CONSTRAINT bank_account_pk PRIMARY KEY (id) - ); - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - ALTER TABLE transfers.transfer_initiation DROP COLUMN payment_id; - - CREATE TABLE IF NOT EXISTS transfers.transfer_initiation_payments ( - transfer_initiation_id CHARACTER VARYING NOT NULL, - payment_id CHARACTER VARYING NOT NULL, - created_at timestamp with time zone NOT NULL DEFAULT NOW() CHECK (created_at<=NOW()), - status int NOT NULL, - error text, - PRIMARY KEY (transfer_initiation_id, payment_id) - ); - - ALTER TABLE transfers.transfer_initiation_payments ADD CONSTRAINT transfer_initiation_id_constraint - FOREIGN KEY (transfer_initiation_id) - REFERENCES transfers.transfer_initiation (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - ALTER TABLE accounts.bank_account ADD COLUMN account_id CHARACTER VARYING; - - ALTER TABLE accounts.bank_account ADD CONSTRAINT bank_account_account_id - FOREIGN KEY (account_id) - REFERENCES accounts.account (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - ALTER TABLE transfers.transfer_initiation ADD COLUMN scheduled_at timestamp with time zone; - ; - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - ALTER TABLE accounts.bank_account ADD COLUMN IF NOT EXISTS "connector_id" uuid; - - ALTER TABLE accounts.bank_account ADD CONSTRAINT bank_accounts_connector - FOREIGN KEY (connector_id) - REFERENCES connectors.connector (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - `) - if err != nil { - return err - } - - return nil - }, - }, - ) -} diff --git a/components/payments/internal/storage/migrations_v1.x.go b/components/payments/internal/storage/migrations_v1.x.go deleted file mode 100644 index 2b9e50a988..0000000000 --- a/components/payments/internal/storage/migrations_v1.x.go +++ /dev/null @@ -1,1090 +0,0 @@ -package storage - -import ( - "context" - "database/sql/driver" - "encoding/base64" - "encoding/json" - "fmt" - "time" - - "github.com/formancehq/go-libs/migrations" - "github.com/formancehq/payments/internal/models" - "github.com/gibson042/canonicaljson-go" - "github.com/google/uuid" - "github.com/pkg/errors" - "github.com/uptrace/bun" -) - -func registerMigrationsV1(ctx context.Context, migrator *migrations.Migrator) { - migrator.RegisterMigrations( - migrations.Migration{ - Name: "", - Up: func(tx bun.Tx) error { - if err := migrateConnectors(ctx, tx); err != nil { - return err - } - - if err := migrateAccountID(ctx, tx); err != nil { - return err - } - - if err := migratePaymentID(ctx, tx); err != nil { - return err - } - - if err := migrateTransferInitiationID(ctx, tx); err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - ALTER TYPE connector_provider ADD VALUE IF NOT EXISTS 'ATLAR'; - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - CREATE TABLE IF NOT EXISTS accounts.pool_accounts ( - pool_id uuid NOT NULL, - account_id CHARACTER VARYING NOT NULL, - CONSTRAINT pool_accounts_pk PRIMARY KEY (pool_id, account_id) - ); - - CREATE TABLE IF NOT EXISTS accounts.pools ( - id uuid NOT NULL, - name text NOT NULL UNIQUE, - created_at timestamp with time zone NOT NULL DEFAULT NOW() CHECK (created_at<=NOW()), - CONSTRAINT pools_pk PRIMARY KEY (id) - ); - - ALTER TABLE accounts.pool_accounts ADD CONSTRAINT pool_accounts_pool_id - FOREIGN KEY (pool_id) - REFERENCES accounts.pools (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - - ALTER TABLE accounts.pool_accounts ADD CONSTRAINT pool_accounts_account_id - FOREIGN KEY (account_id) - REFERENCES accounts.account (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - ALTER TYPE connector_provider ADD VALUE IF NOT EXISTS 'ADYEN'; - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - ALTER TABLE accounts.pools ALTER COLUMN id SET DEFAULT gen_random_uuid(); - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - ALTER TABLE accounts.bank_account ADD COLUMN IF NOT EXISTS metadata jsonb; - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - ALTER TABLE payments.payment ADD COLUMN IF NOT EXISTS initial_amount numeric NOT NULL DEFAULT 0; - UPDATE payments.payment SET initial_amount = amount; - ALTER TYPE "public".payment_status ADD VALUE IF NOT EXISTS 'EXPIRED'; - ALTER TYPE "public".payment_status ADD VALUE IF NOT EXISTS 'REFUNDED'; - - CREATE TABLE IF NOT EXISTS connectors.webhook ( - id uuid NOT NULL, - connector_id CHARACTER VARYING NOT NULL, - request_body bytea NOT NULL, - CONSTRAINT webhook_pk PRIMARY KEY (id) - ); - - ALTER TABLE connectors.webhook DROP CONSTRAINT IF EXISTS webhook_connector_id; - ALTER TABLE connectors.webhook ADD CONSTRAINT webhook_connector_id - FOREIGN KEY (connector_id) - REFERENCES connectors.connector (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - ALTER TABLE transfers.transfer_initiation ADD COLUMN IF NOT EXISTS metadata jsonb; - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - ALTER TABLE transfers.transfer_initiation ALTER COLUMN description DROP NOT NULL; - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - CREATE TABLE IF NOT EXISTS transfers.transfer_initiation_adjustments ( - id uuid NOT NULL, - transfer_initiation_id CHARACTER VARYING NOT NULL, - created_at timestamp with time zone NOT NULL DEFAULT NOW() CHECK (created_at<=NOW()), - status int NOT NULL, - error text, - metadata jsonb, - CONSTRAINT transfer_initiation_adjustments_pk PRIMARY KEY (id) - ); - - ALTER TABLE transfers.transfer_initiation_adjustments ADD CONSTRAINT adjusmtents_transfer_initiation_id_constraint - FOREIGN KEY (transfer_initiation_id) - REFERENCES transfers.transfer_initiation (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - - INSERT INTO transfers.transfer_initiation_adjustments (id, transfer_initiation_id, created_at, status, error, metadata) - SELECT gen_random_uuid(), id, updated_at, status, error, '{}'::jsonb FROM transfers.transfer_initiation; - - ALTER TABLE transfers.transfer_initiation DROP COLUMN IF EXISTS status; - ALTER TABLE transfers.transfer_initiation DROP COLUMN IF EXISTS error; - ALTER TABLE transfers.transfer_initiation DROP COLUMN IF EXISTS updated_at; - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - // Drop check constraint on created at since it's created by the code and - // not by the user. - _, err := tx.Exec(` - ALTER TABLE transfers.transfer_initiation_adjustments DROP CONSTRAINT transfer_initiation_adjustments_created_at_check; - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - // Drop check constraint on created at since it's created by the code and - // not by the user. - _, err := tx.Exec(` - ALTER TABLE transfers.transfer_initiation_payments DROP CONSTRAINT transfer_initiation_payments_created_at_check; - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - CREATE TABLE IF NOT EXISTS transfers.transfer_reversal ( - id character varying NOT NULL, - transfer_initiation_id character varying NOT NULL, - reference text, - created_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone NOT NULL, - description text NOT NULL, - connector_id CHARACTER VARYING NOT NULL, - amount numeric NOT NULL, - asset text NOT NULL, - status int NOT NULL, - error text, - metadata jsonb, - PRIMARY KEY (id) - ); - - -- UNIQUE constrait for processing only one reversal at a time. - CREATE UNIQUE INDEX transfer_reversal_processing_unique_constraint ON transfers.transfer_reversal (transfer_initiation_id) WHERE status = 1; - - ALTER TABLE transfers.transfer_reversal ADD CONSTRAINT transfer_reversal_connector_id - FOREIGN KEY (connector_id) - REFERENCES connectors.connector (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - - ALTER TABLE transfers.transfer_reversal ADD CONSTRAINT transfer_reversal_transfer_initiation_id - FOREIGN KEY (transfer_initiation_id) - REFERENCES transfers.transfer_initiation (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - - ALTER TABLE transfers.transfer_initiation ADD COLUMN IF NOT EXISTS initial_amount numeric NOT NULL DEFAULT 0; - UPDATE transfers.transfer_initiation SET initial_amount = amount; - - ALTER TABLE transfers.transfer_initiation ADD CONSTRAINT amount_non_negative CHECK (amount >= 0); - ALTER TABLE transfers.transfer_initiation ADD CONSTRAINT initial_amount_non_negative CHECK (initial_amount >= 0); - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - ALTER TYPE "public".payment_status ADD VALUE IF NOT EXISTS 'EXPIRED'; - ALTER TYPE "public".payment_status ADD VALUE IF NOT EXISTS 'REFUNDED'; - ALTER TYPE "public".payment_status ADD VALUE IF NOT EXISTS 'REFUNDED_FAILURE'; - ALTER TYPE "public".payment_status ADD VALUE IF NOT EXISTS 'DISPUTE'; - ALTER TYPE "public".payment_status ADD VALUE IF NOT EXISTS 'DISPUTE_WON'; - ALTER TYPE "public".payment_status ADD VALUE IF NOT EXISTS 'DISPUTE_LOST'; - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - CREATE TABLE IF NOT EXISTS accounts.bank_account_adjustments ( - id uuid NOT NULL, - created_at timestamp with time zone NOT NULL, - bank_account_id uuid NOT NULL, - connector_id CHARACTER VARYING NOT NULL, - account_id CHARACTER VARYING NOT NULL, - CONSTRAINT transfer_initiation_adjustments_pk PRIMARY KEY (id) - ); - - ALTER TABLE accounts.bank_account_adjustments ADD CONSTRAINT bank_account_adjustments_bank_account_id - FOREIGN KEY (bank_account_id) - REFERENCES accounts.bank_account (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - - ALTER TABLE accounts.bank_account_adjustments ADD CONSTRAINT bank_account_adjustments_connector_id - FOREIGN KEY (connector_id) - REFERENCES connectors.connector (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - - ALTER TABLE accounts.bank_account_adjustments ADD CONSTRAINT bank_account_adjustments_account_id - FOREIGN KEY (account_id) - REFERENCES accounts.account (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - - INSERT INTO accounts.bank_account_adjustments (id, created_at, bank_account_id, connector_id, account_id) - SELECT gen_random_uuid(), created_at, id, connector_id, account_id FROM accounts.bank_account WHERE account_id IS NOT NULL; - - ALTER TABLE accounts.bank_account DROP COLUMN IF EXISTS account_id; - ALTER TABLE accounts.bank_account DROP COLUMN IF EXISTS connector_id; - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - ALTER TABLE accounts.bank_account_adjustments RENAME TO bank_account_related_accounts; - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - _, err := tx.Exec(` - ALTER TYPE connector_provider ADD VALUE IF NOT EXISTS 'GENERIC'; - `) - if err != nil { - return err - } - - return nil - }, - }, - migrations.Migration{ - Up: func(tx bun.Tx) error { - return fixExpandingChangelogs(ctx, tx) - }, - }, - ) -} - -type PreviousConnector struct { - bun.BaseModel `bun:"connectors.connector"` - - ID uuid.UUID `bun:",pk,nullzero"` - CreatedAt time.Time `bun:",nullzero"` - Provider models.ConnectorProvider - - // EncryptedConfig is a PGP-encrypted JSON string. - EncryptedConfig []byte `bun:"config"` - - // Config is a decrypted config. It is not stored in the database. - Config json.RawMessage `bun:"decrypted_config,scanonly"` -} - -type Connector struct { - bun.BaseModel `bun:"connectors.connector_v2"` - - ID models.ConnectorID `bun:",pk,nullzero"` - Name string - CreatedAt time.Time `bun:",nullzero"` - Provider models.ConnectorProvider - - // EncryptedConfig is a PGP-encrypted JSON string. - EncryptedConfig []byte `bun:"config"` -} - -func migrateConnectors(ctx context.Context, tx bun.Tx) error { - _, err := tx.Exec(` - ALTER TABLE accounts.account ALTER COLUMN connector_id SET NOT NULL; - ALTER TABLE accounts.bank_account ALTER COLUMN connector_id SET NOT NULL; - - CREATE TABLE connectors.connector_v2 ( - id CHARACTER VARYING NOT NULL, - name text NOT NULL UNIQUE, - created_at timestamp with time zone NOT NULL DEFAULT NOW() CHECK (created_at<=NOW()), - provider connector_provider NOT NULL, - config bytea NULL, - CONSTRAINT connector_v2_pk PRIMARY KEY (id) - ); - `) - if err != nil { - return err - } - - var oldConnectors []*PreviousConnector - err = tx.NewSelect(). - Model(&oldConnectors). - Scan(ctx) - if err != nil { - return err - } - - newConnectors := make([]*Connector, 0, len(oldConnectors)) - for _, oldConnector := range oldConnectors { - newConnectors = append(newConnectors, &Connector{ - ID: models.ConnectorID{ - Reference: uuid.New(), - Provider: oldConnector.Provider, - }, - Name: oldConnector.Provider.String(), - CreatedAt: oldConnector.CreatedAt, - Provider: oldConnector.Provider, - EncryptedConfig: oldConnector.EncryptedConfig, - }) - } - - if len(newConnectors) > 0 { - _, err = tx.NewInsert(). - Model(&newConnectors). - Exec(ctx) - if err != nil { - return err - } - } - - _, err = tx.Exec(` - ALTER TABLE tasks.task ADD COLUMN IF NOT EXISTS provider connector_provider; - UPDATE tasks.task SET provider = (SELECT provider FROM connectors.connector WHERE id = task.connector_id); - ALTER TABLE payments.payment ADD COLUMN IF NOT EXISTS provider connector_provider; - UPDATE payments.payment SET provider = (SELECT provider FROM connectors.connector WHERE id = payment.connector_id); - ALTER TABLE tasks.task DROP CONSTRAINT IF EXISTS task_connector; - ALTER TABLE tasks.task ALTER COLUMN connector_id TYPE CHARACTER VARYING; - ALTER TABLE payments.payment DROP CONSTRAINT IF EXISTS payment_connector; - ALTER TABLE payments.payment ALTER COLUMN connector_id TYPE CHARACTER VARYING; - ALTER TABLE payments.transfers DROP CONSTRAINT IF EXISTS transfer_connector; - ALTER TABLE payments.transfers ALTER COLUMN connector_id TYPE CHARACTER VARYING; - ALTER TABLE accounts.account DROP CONSTRAINT IF EXISTS accounts_connector; - ALTER TABLE accounts.account ALTER COLUMN connector_id TYPE CHARACTER VARYING; - ALTER TABLE accounts.bank_account DROP CONSTRAINT IF EXISTS bank_accounts_connector; - ALTER TABLE accounts.bank_account ALTER COLUMN connector_id TYPE CHARACTER VARYING; - ALTER TABLE transfers.transfer_initiation DROP CONSTRAINT IF EXISTS transfer_initiation_connector_id; - - DROP TABLE connectors.connector; - ALTER TABLE connectors.connector_v2 RENAME TO connector; - - UPDATE tasks.task SET connector_id = (SELECT id FROM connectors.connector WHERE provider = task.provider); - UPDATE payments.payment SET connector_id = (SELECT id FROM connectors.connector WHERE provider = payment.provider); - UPDATE accounts.account SET connector_id = (SELECT id FROM connectors.connector WHERE provider::text = account.provider); - UPDATE accounts.bank_account SET connector_id = (SELECT id FROM connectors.connector WHERE provider = bank_account.provider); - - ALTER TABLE tasks.task DROP COLUMN IF EXISTS provider; - ALTER TABLE accounts.account DROP COLUMN IF EXISTS provider; - ALTER TABLE accounts.bank_account DROP COLUMN IF EXISTS provider; - ALTER TABLE payments.payment DROP COLUMN IF EXISTS provider; - - ALTER TABLE tasks.task ADD CONSTRAINT task_connector - FOREIGN KEY (connector_id) - REFERENCES connectors.connector (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - - ALTER TABLE payments.payment ADD CONSTRAINT payment_connector - FOREIGN KEY (connector_id) - REFERENCES connectors.connector (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - - ALTER TABLE accounts.account ADD CONSTRAINT accounts_connector - FOREIGN KEY (connector_id) - REFERENCES connectors.connector (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - - ALTER TABLE accounts.bank_account ADD CONSTRAINT bank_accounts_connector - FOREIGN KEY (connector_id) - REFERENCES connectors.connector (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - - ALTER TABLE transfers.transfer_initiation ADD COLUMN IF NOT EXISTS connector_id CHARACTER VARYING NOT NULL; - ALTER TABLE transfers.transfer_initiation ADD CONSTRAINT transfer_initiation_connector_id - FOREIGN KEY (connector_id) - REFERENCES connectors.connector (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - - UPDATE transfers.transfer_initiation SET connector_id = (SELECT id FROM connectors.connector WHERE provider = transfer_initiation.provider); - `) - if err != nil { - return err - } - - return nil -} - -type PreviousAccountID struct { - Reference string - Provider models.ConnectorProvider -} - -func PreviousAccountIDFromString(value string) (*PreviousAccountID, error) { - data, err := base64.URLEncoding.DecodeString(value) - if err != nil { - return nil, err - } - ret := PreviousAccountID{} - err = canonicaljson.Unmarshal(data, &ret) - if err != nil { - return nil, err - } - - return &ret, nil -} - -func (aid *PreviousAccountID) Scan(value interface{}) error { - if value == nil { - return errors.New("account id is nil") - } - - if s, err := driver.String.ConvertValue(value); err == nil { - - if v, ok := s.(string); ok { - - id, err := PreviousAccountIDFromString(v) - if err != nil { - return fmt.Errorf("failed to parse account id %s: %v", v, err) - } - - *aid = *id - return nil - } - } - - return fmt.Errorf("failed to scan account id: %v", value) -} - -func (aid PreviousAccountID) Value() (driver.Value, error) { - return aid.String(), nil -} - -func (aid *PreviousAccountID) String() string { - if aid == nil || aid.Reference == "" { - return "" - } - - data, err := canonicaljson.Marshal(aid) - if err != nil { - panic(err) - } - - return base64.URLEncoding.EncodeToString(data) -} - -func migrateAccountID(ctx context.Context, tx bun.Tx) error { - _, err := tx.Exec(` - ALTER TABLE accounts.balances DROP CONSTRAINT IF EXISTS balances_account; - ALTER TABLE accounts.bank_account DROP CONSTRAINT IF EXISTS bank_account_account_id; - ALTER TABLE transfers.transfer_initiation DROP CONSTRAINT IF EXISTS destination_account; - ALTER TABLE transfers.transfer_initiation DROP CONSTRAINT IF EXISTS source_account; - ALTER TABLE payments.payment DROP CONSTRAINT IF EXISTS payment_destination_account; - ALTER TABLE payments.payment DROP CONSTRAINT IF EXISTS payment_source_account; - `) - if err != nil { - return err - } - - var previousIDs []PreviousAccountID - var connectorIDs []models.ConnectorID - err = tx.NewSelect().Model((*models.Account)(nil)).Column("id", "connector_id").Scan(ctx, &previousIDs, &connectorIDs) - if err != nil { - return err - } - - if len(previousIDs) != len(connectorIDs) { - return fmt.Errorf("migrateAccountID: previousIDs and connectorIDs have different length") - } - - type AccoutIDMigration struct { - PreviousAccountID PreviousAccountID - NewAccountID models.AccountID - } - migrations := make([]AccoutIDMigration, 0, len(previousIDs)) - for i, previousID := range previousIDs { - migrations = append(migrations, AccoutIDMigration{ - PreviousAccountID: previousID, - NewAccountID: models.AccountID{ - Reference: previousID.Reference, - ConnectorID: connectorIDs[i], - }, - }) - } - - for _, migration := range migrations { - _, err := tx.NewUpdate(). - Model((*models.Account)(nil)). - Set("id = ?", migration.NewAccountID). - Where("id = ?", migration.PreviousAccountID). - Exec(ctx) - if err != nil { - return err - } - - _, err = tx.NewUpdate(). - Model((*models.Balance)(nil)). - Set("account_id = ?", migration.NewAccountID). - Where("account_id = ?", migration.PreviousAccountID). - Exec(ctx) - if err != nil { - return err - } - - _, err = tx.NewUpdate(). - Model((*models.BankAccount)(nil)). - Set("account_id = ?", migration.NewAccountID). - Where("account_id = ?", migration.PreviousAccountID). - Exec(ctx) - if err != nil { - return err - } - - _, err = tx.NewUpdate(). - Model((*models.TransferInitiation)(nil)). - Set("source_account_id = ?", migration.NewAccountID). - Where("source_account_id = ?", migration.PreviousAccountID). - Exec(ctx) - if err != nil { - return err - } - - _, err = tx.NewUpdate(). - Model((*models.TransferInitiation)(nil)). - Set("destination_account_id = ?", migration.NewAccountID). - Where("destination_account_id = ?", migration.PreviousAccountID). - Exec(ctx) - if err != nil { - return err - } - - _, err = tx.NewUpdate(). - Model((*models.Payment)(nil)). - Set("source_account_id = ?", migration.NewAccountID). - Where("source_account_id = ?", migration.PreviousAccountID). - Exec(ctx) - if err != nil { - return err - } - - _, err = tx.NewUpdate(). - Model((*models.Payment)(nil)). - Set("destination_account_id = ?", migration.NewAccountID). - Where("destination_account_id = ?", migration.PreviousAccountID). - Exec(ctx) - if err != nil { - return err - } - } - - _, err = tx.Exec(` - ALTER TABLE transfers.transfer_initiation ADD CONSTRAINT source_account - FOREIGN KEY (source_account_id) - REFERENCES accounts.account (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - - ALTER TABLE transfers.transfer_initiation ADD CONSTRAINT destination_account - FOREIGN KEY (destination_account_id) - REFERENCES accounts.account (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - - ALTER TABLE accounts.balances ADD CONSTRAINT balances_account - FOREIGN KEY (account_id) - REFERENCES accounts.account (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - - ALTER TABLE accounts.bank_account ADD CONSTRAINT bank_account_account_id - FOREIGN KEY (account_id) - REFERENCES accounts.account (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - - ALTER TABLE payments.payment ADD CONSTRAINT payment_source_account - FOREIGN KEY (source_account_id) - REFERENCES accounts.account (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - - ALTER TABLE payments.payment ADD CONSTRAINT payment_destination_account - FOREIGN KEY (destination_account_id) - REFERENCES accounts.account (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - `) - if err != nil { - return err - } - - return nil -} - -type PreviousPaymentID struct { - models.PaymentReference - Provider models.ConnectorProvider -} - -func (pid PreviousPaymentID) String() string { - data, err := canonicaljson.Marshal(pid) - if err != nil { - panic(err) - } - - return base64.URLEncoding.EncodeToString(data) -} - -func PaymentIDFromString(value string) (*PreviousPaymentID, error) { - data, err := base64.URLEncoding.DecodeString(value) - if err != nil { - return nil, err - } - ret := PreviousPaymentID{} - err = canonicaljson.Unmarshal(data, &ret) - if err != nil { - return nil, err - } - - return &ret, nil -} - -func (pid PreviousPaymentID) Value() (driver.Value, error) { - return pid.String(), nil -} - -func (pid *PreviousPaymentID) Scan(value interface{}) error { - if value == nil { - return errors.New("payment id is nil") - } - - if s, err := driver.String.ConvertValue(value); err == nil { - - if v, ok := s.(string); ok { - - id, err := PaymentIDFromString(v) - if err != nil { - return fmt.Errorf("failed to parse paymentid %s: %v", v, err) - } - - *pid = *id - return nil - } - } - - return fmt.Errorf("failed to scan paymentid: %v", value) -} - -func migratePaymentID(ctx context.Context, tx bun.Tx) error { - _, err := tx.Exec(` - ALTER TABLE payments.adjustment DROP CONSTRAINT IF EXISTS adjustment_payment; - ALTER TABLE payments.metadata DROP CONSTRAINT IF EXISTS metadata_payment; - `) - if err != nil { - return err - } - - var previousIDs []PreviousPaymentID - var connectorIDs []models.ConnectorID - err = tx.NewSelect().Model((*models.Payment)(nil)).Column("id", "connector_id").Scan(ctx, &previousIDs, &connectorIDs) - if err != nil { - return err - } - - if len(previousIDs) != len(connectorIDs) { - return fmt.Errorf("migrateAccountID: previousIDs and connectorIDs have different length") - } - - type PaymentIDMigration struct { - PreviousPaymentID PreviousPaymentID - NewPaymentID models.PaymentID - } - migrations := make([]PaymentIDMigration, 0, len(previousIDs)) - for i, previousID := range previousIDs { - migrations = append(migrations, PaymentIDMigration{ - PreviousPaymentID: previousID, - NewPaymentID: models.PaymentID{ - PaymentReference: previousID.PaymentReference, - ConnectorID: connectorIDs[i], - }, - }) - } - - for _, migration := range migrations { - _, err := tx.NewUpdate(). - Model((*models.Payment)(nil)). - Set("id = ?", migration.NewPaymentID). - Where("id = ?", migration.PreviousPaymentID). - Exec(ctx) - if err != nil { - return err - } - - _, err = tx.NewUpdate(). - Model((*models.PaymentAdjustment)(nil)). - Set("payment_id = ?", migration.NewPaymentID). - Where("payment_id = ?", migration.PreviousPaymentID). - Exec(ctx) - if err != nil { - return err - } - - _, err = tx.NewUpdate(). - Model((*models.PaymentMetadata)(nil)). - Set("payment_id = ?", migration.NewPaymentID). - Where("payment_id = ?", migration.PreviousPaymentID). - Exec(ctx) - if err != nil { - return err - } - } - - _, err = tx.Exec(` - ALTER TABLE payments.adjustment ADD CONSTRAINT adjustment_payment - FOREIGN KEY (payment_id) - REFERENCES payments.payment (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - - ALTER TABLE payments.metadata ADD CONSTRAINT metadata_payment - FOREIGN KEY (payment_id) - REFERENCES payments.payment (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - `) - if err != nil { - return err - } - - return nil -} - -type PreviousTransferInitiationID struct { - Reference string - Provider models.ConnectorProvider -} - -func (tid PreviousTransferInitiationID) String() string { - data, err := canonicaljson.Marshal(tid) - if err != nil { - panic(err) - } - - return base64.URLEncoding.EncodeToString(data) -} - -func TransferInitiationIDFromString(value string) (PreviousTransferInitiationID, error) { - data, err := base64.URLEncoding.DecodeString(value) - if err != nil { - return PreviousTransferInitiationID{}, err - } - ret := PreviousTransferInitiationID{} - err = canonicaljson.Unmarshal(data, &ret) - if err != nil { - return PreviousTransferInitiationID{}, err - } - - return ret, nil -} - -func (tid PreviousTransferInitiationID) Value() (driver.Value, error) { - return tid.String(), nil -} - -func (tid *PreviousTransferInitiationID) Scan(value interface{}) error { - if value == nil { - return errors.New("payment id is nil") - } - - if s, err := driver.String.ConvertValue(value); err == nil { - - if v, ok := s.(string); ok { - - id, err := TransferInitiationIDFromString(v) - if err != nil { - return fmt.Errorf("failed to parse paymentid %s: %v", v, err) - } - - *tid = id - return nil - } - } - - return fmt.Errorf("failed to scan paymentid: %v", value) -} - -func migrateTransferInitiationID(ctx context.Context, tx bun.Tx) error { - _, err := tx.Exec(` - ALTER TABLE transfers.transfer_initiation_payments DROP CONSTRAINT IF EXISTS transfer_initiation_id_constraint; - `) - if err != nil { - return err - } - - var previousIDs []PreviousTransferInitiationID - var connectorIDs []models.ConnectorID - err = tx.NewSelect().Model((*models.TransferInitiation)(nil)).Column("id", "connector_id").Scan(ctx, &previousIDs, &connectorIDs) - if err != nil { - return err - } - - if len(previousIDs) != len(connectorIDs) { - return fmt.Errorf("migrateAccountID: previousIDs and connectorIDs have different length") - } - - type TransferInitiationIDMigration struct { - PreviousTransferInitiationID PreviousTransferInitiationID - NewTransferInitiationID models.TransferInitiationID - } - - migrations := make([]TransferInitiationIDMigration, 0, len(previousIDs)) - for i, previousID := range previousIDs { - migrations = append(migrations, TransferInitiationIDMigration{ - PreviousTransferInitiationID: previousID, - NewTransferInitiationID: models.TransferInitiationID{ - Reference: previousID.Reference, - ConnectorID: connectorIDs[i], - }, - }) - } - - for _, migration := range migrations { - _, err := tx.NewUpdate(). - Model((*models.TransferInitiation)(nil)). - Set("id = ?", migration.NewTransferInitiationID). - Where("id = ?", migration.PreviousTransferInitiationID). - Exec(ctx) - if err != nil { - return err - } - - _, err = tx.NewUpdate(). - Model((*models.TransferInitiationPayment)(nil)). - Set("transfer_initiation_id = ?", migration.NewTransferInitiationID). - Where("transfer_initiation_id = ?", migration.PreviousTransferInitiationID). - Exec(ctx) - if err != nil { - return err - } - } - - _, err = tx.Exec(` - ALTER TABLE transfers.transfer_initiation_payments ADD CONSTRAINT transfer_initiation_id_constraint - FOREIGN KEY (transfer_initiation_id) - REFERENCES transfers.transfer_initiation (id) - ON DELETE CASCADE - NOT DEFERRABLE - INITIALLY IMMEDIATE - ; - `) - if err != nil { - return err - } - - return nil -} - -func fixExpandingChangelogs(ctx context.Context, tx bun.Tx) error { - var createdAt time.Time - for { - var metadata []models.PaymentMetadata - query := tx.NewSelect(). - Model(&metadata). - Order("created_at ASC"). - Limit(100) - - if !createdAt.IsZero() { - query.Where("created_at > ?", createdAt) - } - - err := query.Scan(ctx) - if err != nil { - return err - } - - if len(metadata) == 0 { - break - } - - for i, m := range metadata { - if m.Changelog == nil { - continue - } - - var newChangelogs []models.MetadataChangelog - for _, cl := range m.Changelog { - if len(newChangelogs) > 0 && cl.Value == newChangelogs[len(newChangelogs)-1].Value { - continue - } - - newChangelogs = append(newChangelogs, cl) - } - - metadata[i].Changelog = newChangelogs - createdAt = m.CreatedAt - } - - fmt.Println("Updating", len(metadata), "metadata") - - _, err = tx.NewInsert(). - Model(&metadata). - On("CONFLICT (payment_id, key) DO UPDATE"). - Set("changelog = EXCLUDED.changelog"). - Exec(ctx) - if err != nil { - return err - } - } - - return nil -} diff --git a/components/payments/internal/storage/module.go b/components/payments/internal/storage/module.go new file mode 100644 index 0000000000..721319a4b9 --- /dev/null +++ b/components/payments/internal/storage/module.go @@ -0,0 +1,18 @@ +package storage + +import ( + "github.com/formancehq/go-libs/bun/bunconnect" + "github.com/formancehq/go-libs/service" + "github.com/spf13/cobra" + "github.com/uptrace/bun" + "go.uber.org/fx" +) + +func Module(cmd *cobra.Command, connectionOptions bunconnect.ConnectionOptions, configEncryptionKey string) fx.Option { + return fx.Options( + bunconnect.Module(connectionOptions, service.IsDebug(cmd)), + fx.Provide(func(db *bun.DB) Storage { + return newStorage(db, configEncryptionKey) + }), + ) +} diff --git a/components/payments/internal/storage/paginate.go b/components/payments/internal/storage/paginate.go new file mode 100644 index 0000000000..3f56489705 --- /dev/null +++ b/components/payments/internal/storage/paginate.go @@ -0,0 +1,14 @@ +package storage + +import ( + "context" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/uptrace/bun" +) + +func paginateWithOffset[FILTERS any, RETURN any](s *store, ctx context.Context, + q *bunpaginate.OffsetPaginatedQuery[FILTERS], builders ...func(query *bun.SelectQuery) *bun.SelectQuery) (*bunpaginate.Cursor[RETURN], error) { + query := s.db.NewSelect() + return bunpaginate.UsingOffset[FILTERS, RETURN](ctx, query, *q, builders...) +} diff --git a/components/payments/internal/storage/payments.go b/components/payments/internal/storage/payments.go new file mode 100644 index 0000000000..25a0fd5765 --- /dev/null +++ b/components/payments/internal/storage/payments.go @@ -0,0 +1,339 @@ +package storage + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "math/big" + "time" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/query" + "github.com/formancehq/payments/internal/models" + "github.com/pkg/errors" + "github.com/uptrace/bun" +) + +type payment struct { + bun.BaseModel `bun:"table:payments"` + + // Mandatory fields + ID models.PaymentID `bun:"id,pk,type:character varying,notnull"` + ConnectorID models.ConnectorID `bun:"connector_id,type:character varying,notnull"` + Reference string `bun:"reference,type:text,notnull"` + CreatedAt time.Time `bun:"created_at,type:timestamp without time zone,notnull"` + Type models.PaymentType `bun:"type,type:text,notnull"` + InitialAmount *big.Int `bun:"initial_amount,type:numeric,notnull"` + Amount *big.Int `bun:"amount,type:numeric,notnull"` + Asset string `bun:"asset,type:text,notnull"` + Scheme models.PaymentScheme `bun:"scheme,type:text,notnull"` + + // Scan only fields + Status models.PaymentStatus `bun:"status,type:text,notnull,scanonly"` + + // Optional fields + // c.f.: https://bun.uptrace.dev/guide/models.html#nulls + SourceAccountID *models.AccountID `bun:"source_account_id,type:character varying,nullzero"` + DestinationAccountID *models.AccountID `bun:"destination_account_id,type:character varying,nullzero"` + + // Optional fields with default + // c.f. https://bun.uptrace.dev/guide/models.html#default + Metadata map[string]string `bun:"metadata,type:jsonb,nullzero,notnull,default:'{}'"` +} + +type paymentAdjustment struct { + bun.BaseModel `bun:"table:payment_adjustments"` + + // Mandatory fields + ID models.PaymentAdjustmentID `bun:"id,pk,type:character varying,notnull"` + PaymentID models.PaymentID `bun:"payment_id,type:character varying,notnull"` + CreatedAt time.Time `bun:"created_at,type:timestamp without time zone,notnull"` + Status models.PaymentStatus `bun:"status,type:text,notnull"` + Raw json.RawMessage `bun:"raw,type:json,notnull"` + + // Optional fields + // c.f.: https://bun.uptrace.dev/guide/models.html#nulls + Amount *big.Int `bun:"amount,type:numeric,nullzero"` + Asset *string `bun:"asset,type:text,nullzero"` + + // Optional fields with default + // c.f. https://bun.uptrace.dev/guide/models.html#default + Metadata map[string]string `bun:"metadata,type:jsonb,nullzero,notnull,default:'{}'"` +} + +func (s *store) PaymentsUpsert(ctx context.Context, payments []models.Payment) error { + paymentsToInsert := make([]payment, 0, len(payments)) + adjustmentsToInsert := make([]paymentAdjustment, 0) + for _, p := range payments { + paymentsToInsert = append(paymentsToInsert, fromPaymentModels(p)) + + for _, a := range p.Adjustments { + adjustmentsToInsert = append(adjustmentsToInsert, fromPaymentAdjustmentModels(a)) + } + } + + tx, err := s.db.BeginTx(ctx, &sql.TxOptions{}) + if err != nil { + return errors.Wrap(err, "failed to create transaction") + } + + _, err = tx.NewInsert(). + Model(&paymentsToInsert). + On("CONFLICT (id) DO NOTHING"). + Exec(ctx) + if err != nil { + return e("failed to insert payments", err) + } + + _, err = tx.NewInsert(). + Model(&adjustmentsToInsert). + On("CONFLICT (id) DO NOTHING"). + Exec(ctx) + if err != nil { + return e("failed to insert adjustments", err) + } + + return e("failed to commit transactions", tx.Commit()) +} + +func (s *store) PaymentsUpdateMetadata(ctx context.Context, id models.PaymentID, metadata map[string]string) error { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return e("update payment metadata", err) + } + defer tx.Rollback() + + var payment payment + err = tx.NewSelect(). + Model(&payment). + Column("id", "metadata"). + Where("id = ?", id). + Scan(ctx) + if err != nil { + return e("update payment metadata", err) + } + + if payment.Metadata == nil { + payment.Metadata = make(map[string]string) + } + + for k, v := range metadata { + payment.Metadata[k] = v + } + + _, err = tx.NewUpdate(). + Model(&payment). + Column("metadata"). + Where("id = ?", id). + Exec(ctx) + if err != nil { + return e("update payment metadata", err) + } + + return e("failed to commit transaction", tx.Commit()) +} + +func (s *store) PaymentsGet(ctx context.Context, id models.PaymentID) (*models.Payment, error) { + var payment payment + + err := s.db.NewSelect(). + Model(&payment). + Where("id = ?", id). + Scan(ctx) + if err != nil { + return nil, e("failed to get payment", err) + } + + var ajs []paymentAdjustment + err = s.db.NewSelect(). + Model(&ajs). + Where("payment_id = ?", id). + Order("created_at DESC"). + Scan(ctx) + if err != nil { + return nil, e("failed to get payment adjustments", err) + } + + adjustments := make([]models.PaymentAdjustment, 0, len(ajs)) + for _, a := range ajs { + adjustments = append(adjustments, toPaymentAdjustmentModels(a)) + } + + res := toPaymentModels(payment, adjustments[len(adjustments)-1].Status) + res.Adjustments = adjustments + return &res, nil +} + +func (s *store) PaymentsDeleteFromConnectorID(ctx context.Context, connectorID models.ConnectorID) error { + _, err := s.db.NewDelete(). + Model((*payment)(nil)). + Where("connector_id = ?", connectorID). + Exec(ctx) + + return e("failed to delete payments", err) +} + +type PaymentQuery struct{} + +type ListPaymentsQuery bunpaginate.OffsetPaginatedQuery[bunpaginate.PaginatedQueryOptions[PaymentQuery]] + +func NewListPaymentsQuery(opts bunpaginate.PaginatedQueryOptions[PaymentQuery]) ListPaymentsQuery { + return ListPaymentsQuery{ + PageSize: opts.PageSize, + Order: bunpaginate.OrderAsc, + Options: opts, + } +} + +func (s *store) paymentsQueryContext(qb query.Builder) (string, []any, error) { + where, args, err := qb.Build(query.ContextFn(func(key, operator string, value any) (string, []any, error) { + switch { + case key == "reference", + key == "connector_id", + key == "type", + key == "asset", + key == "scheme", + key == "status", + key == "source_account_id", + key == "destination_account_id": + if operator != "$match" { + return "", nil, errors.Wrap(ErrValidation, "'type' column can only be used with $match") + } + return fmt.Sprintf("%s = ?", key), []any{value}, nil + + case key == "initial_amount", + key == "amount": + return fmt.Sprintf("%s %s ?", key, query.DefaultComparisonOperatorsMapping[operator]), []any{value}, nil + case metadataRegex.Match([]byte(key)): + if operator != "$match" { + return "", nil, errors.Wrap(ErrValidation, "'metadata' column can only be used with $match") + } + match := metadataRegex.FindAllStringSubmatch(key, 3) + + key := "metadata" + return key + " @> ?", []any{map[string]any{ + match[0][1]: value, + }}, nil + default: + return "", nil, errors.Wrap(ErrValidation, fmt.Sprintf("unknown key '%s' when building query", key)) + } + })) + + return where, args, err +} + +func (s *store) PaymentsList(ctx context.Context, q ListPaymentsQuery) (*bunpaginate.Cursor[models.Payment], error) { + var ( + where string + args []any + err error + ) + if q.Options.QueryBuilder != nil { + where, args, err = s.paymentsQueryContext(q.Options.QueryBuilder) + if err != nil { + return nil, err + } + } + + // TODO(polo): should fetch the adjustments and get the last status and amount? + cursor, err := paginateWithOffset[bunpaginate.PaginatedQueryOptions[PaymentQuery], payment](s, ctx, + (*bunpaginate.OffsetPaginatedQuery[bunpaginate.PaginatedQueryOptions[PaymentQuery]])(&q), + func(query *bun.SelectQuery) *bun.SelectQuery { + if where != "" { + query = query.Where(where, args...) + } + + query.Column("payment.*", "apd.status"). + Join(`join lateral ( + select status + from payment_adjustments apd + where payment_id = payment.id + order by created_at desc + limit 1 + ) apd on true`) + + // TODO(polo): sorter ? + query = query.Order("created_at DESC") + + return query + }, + ) + if err != nil { + return nil, e("failed to fetch payments", err) + } + + payments := make([]models.Payment, 0, len(cursor.Data)) + for _, p := range cursor.Data { + payments = append(payments, toPaymentModels(p, p.Status)) + } + + return &bunpaginate.Cursor[models.Payment]{ + PageSize: cursor.PageSize, + HasMore: cursor.HasMore, + Previous: cursor.Previous, + Next: cursor.Next, + Data: payments, + }, nil +} + +func fromPaymentModels(from models.Payment) payment { + return payment{ + ID: from.ID, + ConnectorID: from.ConnectorID, + Reference: from.Reference, + CreatedAt: from.CreatedAt, + Type: from.Type, + InitialAmount: from.InitialAmount, + Amount: from.Amount, + Asset: from.Asset, + Scheme: from.Scheme, + SourceAccountID: from.SourceAccountID, + DestinationAccountID: from.DestinationAccountID, + Metadata: from.Metadata, + } +} + +func toPaymentModels(payment payment, status models.PaymentStatus) models.Payment { + return models.Payment{ + ID: payment.ID, + ConnectorID: payment.ConnectorID, + InitialAmount: payment.InitialAmount, + Reference: payment.Reference, + CreatedAt: payment.CreatedAt, + Type: payment.Type, + Amount: payment.Amount, + Asset: payment.Asset, + Scheme: payment.Scheme, + Status: status, + SourceAccountID: payment.SourceAccountID, + DestinationAccountID: payment.DestinationAccountID, + Metadata: payment.Metadata, + } +} + +func fromPaymentAdjustmentModels(from models.PaymentAdjustment) paymentAdjustment { + return paymentAdjustment{ + ID: from.ID, + PaymentID: from.PaymentID, + CreatedAt: from.CreatedAt, + Status: from.Status, + Amount: from.Amount, + Asset: from.Asset, + Metadata: from.Metadata, + Raw: from.Raw, + } +} + +func toPaymentAdjustmentModels(from paymentAdjustment) models.PaymentAdjustment { + return models.PaymentAdjustment{ + ID: from.ID, + PaymentID: from.PaymentID, + CreatedAt: from.CreatedAt, + Status: from.Status, + Amount: from.Amount, + Asset: from.Asset, + Metadata: from.Metadata, + Raw: from.Raw, + } +} diff --git a/components/payments/internal/storage/pools.go b/components/payments/internal/storage/pools.go new file mode 100644 index 0000000000..6655c9c21f --- /dev/null +++ b/components/payments/internal/storage/pools.go @@ -0,0 +1,235 @@ +package storage + +import ( + "context" + "fmt" + "time" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/go-libs/query" + "github.com/formancehq/payments/internal/models" + "github.com/google/uuid" + "github.com/pkg/errors" + "github.com/uptrace/bun" +) + +type pool struct { + bun.BaseModel `bun:"table:pools"` + + // Mandatory fields + ID uuid.UUID `bun:"id,pk,type:uuid,notnull"` + Name string `bun:"name,type:text,notnull"` + CreatedAt time.Time `bun:"created_at,type:timestamp without time zone,notnull"` + + PoolAccounts []*poolAccounts `bun:"rel:has-many,join:id=pool_id"` +} + +type poolAccounts struct { + bun.BaseModel `bun:"table:pool_accounts"` + + PoolID uuid.UUID `bun:"pool_id,pk,type:uuid,notnull"` + AccountID models.AccountID `bun:"account_id,pk,type:character varying,notnull"` +} + +func (s *store) PoolsUpsert(ctx context.Context, pool models.Pool) error { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return e("begin transaction: %w", err) + } + defer tx.Rollback() + + poolToInsert, accountsToInsert := fromPoolModel(pool) + + _, err = tx.NewInsert(). + Model(&poolToInsert). + On("CONFLICT (id) DO NOTHING"). + Exec(ctx) + if err != nil { + return e("insert pool: %w", err) + } + + _, err = tx.NewInsert(). + Model(&accountsToInsert). + On("CONFLICT (pool_id, account_id) DO NOTHING"). + Exec(ctx) + if err != nil { + return e("insert pool accounts: %w", err) + } + + return e("commit transaction: %w", tx.Commit()) +} + +func (s *store) PoolsGet(ctx context.Context, id uuid.UUID) (*models.Pool, error) { + var pool pool + err := s.db.NewSelect(). + Model(&pool). + Relation("PoolAccounts"). + Where("id = ?", id). + Scan(ctx) + if err != nil { + return nil, e("get pool: %w", err) + } + + return pointer.For(toPoolModel(pool)), nil +} + +func (s *store) PoolsDelete(ctx context.Context, id uuid.UUID) error { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return e("begin transaction: %w", err) + } + defer tx.Rollback() + + _, err = tx.NewDelete(). + Model((*pool)(nil)). + Where("id = ?", id). + Exec(ctx) + if err != nil { + return e("delete pool: %w", err) + } + + _, err = tx.NewDelete(). + Model((*poolAccounts)(nil)). + Where("pool_id = ?", id). + Exec(ctx) + if err != nil { + return e("delete pool accounts: %w", err) + } + + return e("commit transaction: %w", tx.Commit()) +} + +func (s *store) PoolsAddAccount(ctx context.Context, id uuid.UUID, accountID models.AccountID) error { + _, err := s.db.NewInsert(). + Model(&poolAccounts{ + PoolID: id, + AccountID: accountID, + }). + On("CONFLICT (pool_id, account_id) DO NOTHING"). + Exec(ctx) + if err != nil { + return e("insert pool account: %w", err) + } + return nil +} + +func (s *store) PoolsRemoveAccount(ctx context.Context, id uuid.UUID, accountID models.AccountID) error { + _, err := s.db.NewDelete(). + Model((*poolAccounts)(nil)). + Where("pool_id = ? AND account_id = ?", id, accountID). + Exec(ctx) + if err != nil { + return e("delete pool account: %w", err) + } + return nil +} + +type PoolQuery struct{} + +type ListPoolsQuery bunpaginate.OffsetPaginatedQuery[bunpaginate.PaginatedQueryOptions[PoolQuery]] + +func NewListPoolsQuery(opts bunpaginate.PaginatedQueryOptions[PoolQuery]) ListPoolsQuery { + return ListPoolsQuery{ + Order: bunpaginate.OrderAsc, + PageSize: opts.PageSize, + Options: opts, + } +} + +func (s *store) poolsQueryContext(qb query.Builder) (string, []any, error) { + return qb.Build(query.ContextFn(func(key, operator string, value any) (string, []any, error) { + switch { + case key == "name": + if operator != "$match" { + return "", nil, errors.Wrap(ErrValidation, "'metadata' column can only be used with $match") + } + + return fmt.Sprintf("%s = ?", key), []any{value}, nil + // TODO(polo): add filters for accounts ID + default: + return "", nil, errors.Wrap(ErrValidation, fmt.Sprintf("unknown key '%s' when building query", key)) + } + })) +} + +func (s *store) PoolsList(ctx context.Context, q ListPoolsQuery) (*bunpaginate.Cursor[models.Pool], error) { + var ( + where string + args []any + err error + ) + if q.Options.QueryBuilder != nil { + where, args, err = s.poolsQueryContext(q.Options.QueryBuilder) + if err != nil { + return nil, err + } + } + + cursor, err := paginateWithOffset[bunpaginate.PaginatedQueryOptions[PoolQuery], pool](s, ctx, + (*bunpaginate.OffsetPaginatedQuery[bunpaginate.PaginatedQueryOptions[PoolQuery]])(&q), + func(query *bun.SelectQuery) *bun.SelectQuery { + query = query. + Relation("PoolAccounts") + + if where != "" { + query = query.Where(where, args...) + } + + query = query.Order("created_at DESC") + + return query + }, + ) + if err != nil { + return nil, e("failed to fetch pools", err) + } + + pools := make([]models.Pool, 0, len(cursor.Data)) + for _, p := range cursor.Data { + pools = append(pools, toPoolModel(p)) + } + + return &bunpaginate.Cursor[models.Pool]{ + PageSize: cursor.PageSize, + HasMore: cursor.HasMore, + Previous: cursor.Previous, + Next: cursor.Next, + Data: pools, + }, nil +} + +func fromPoolModel(from models.Pool) (pool, []poolAccounts) { + p := pool{ + ID: from.ID, + Name: from.Name, + CreatedAt: from.CreatedAt, + } + + var accounts []poolAccounts + for _, pa := range from.PoolAccounts { + accounts = append(accounts, poolAccounts{ + PoolID: pa.PoolID, + AccountID: pa.AccountID, + }) + } + + return p, accounts +} + +func toPoolModel(from pool) models.Pool { + var accounts []models.PoolAccounts + for _, pa := range from.PoolAccounts { + accounts = append(accounts, models.PoolAccounts{ + PoolID: pa.PoolID, + AccountID: pa.AccountID, + }) + } + + return models.Pool{ + ID: from.ID, + Name: from.Name, + CreatedAt: from.CreatedAt, + PoolAccounts: accounts, + } +} diff --git a/components/payments/internal/storage/schedules.go b/components/payments/internal/storage/schedules.go new file mode 100644 index 0000000000..6509ed54b9 --- /dev/null +++ b/components/payments/internal/storage/schedules.go @@ -0,0 +1,141 @@ +package storage + +import ( + "context" + "fmt" + "time" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/go-libs/query" + "github.com/formancehq/payments/internal/models" + "github.com/pkg/errors" + "github.com/uptrace/bun" +) + +type schedule struct { + bun.BaseModel `bun:"table:schedules"` + + // Mandatory fields + ID string `bun:"id,pk,type:text,notnull"` + ConnectorID models.ConnectorID `bun:"connector_id,pk,type:character varying,notnull"` + CreatedAt time.Time `bun:"created_at,type:timestamp without time zone,notnull"` +} + +func (s *store) SchedulesUpsert(ctx context.Context, schedule models.Schedule) error { + toInsert := fromScheduleModel(schedule) + + _, err := s.db.NewInsert(). + Model(&toInsert). + On("CONFLICT (id, connector_id) DO NOTHING"). + Exec(ctx) + + return e("failed to insert schedule", err) +} + +func (s *store) SchedulesDeleteFromConnectorID(ctx context.Context, connectorID models.ConnectorID) error { + _, err := s.db.NewDelete(). + Model((*schedule)(nil)). + Where("connector_id = ?", connectorID). + Exec(ctx) + + return e("failed to delete schedule", err) +} + +func (s *store) SchedulesGet(ctx context.Context, id string, connectorID models.ConnectorID) (*models.Schedule, error) { + var schedule schedule + err := s.db.NewSelect(). + Model(&schedule). + Where("id = ? AND connector_id = ?", id, connectorID). + Scan(ctx) + + if err != nil { + return nil, e("failed to fetch schedule", err) + } + + return pointer.For(toScheduleModel(schedule)), nil +} + +type ScheduleQuery struct{} + +type ListSchedulesQuery bunpaginate.OffsetPaginatedQuery[bunpaginate.PaginatedQueryOptions[ScheduleQuery]] + +func NewListSchedulesQuery(opts bunpaginate.PaginatedQueryOptions[ScheduleQuery]) ListSchedulesQuery { + return ListSchedulesQuery{ + Order: bunpaginate.OrderAsc, + PageSize: opts.PageSize, + Options: opts, + } +} + +func (s *store) schedulesQueryContext(qb query.Builder) (string, []any, error) { + return qb.Build(query.ContextFn(func(key, operator string, value any) (string, []any, error) { + switch { + case key == "connector_id": + if operator != "$match" { + return "", nil, errors.Wrap(ErrValidation, "'connector_id' column can only be used with $match") + } + return fmt.Sprintf("%s = ?", key), []any{value}, nil + default: + return "", nil, errors.Wrap(ErrValidation, fmt.Sprintf("unknown key '%s' when building query", key)) + } + })) +} + +func (s *store) SchedulesList(ctx context.Context, q ListSchedulesQuery) (*bunpaginate.Cursor[models.Schedule], error) { + var ( + where string + args []any + err error + ) + if q.Options.QueryBuilder != nil { + where, args, err = s.schedulesQueryContext(q.Options.QueryBuilder) + if err != nil { + return nil, err + } + } + + cursor, err := paginateWithOffset[bunpaginate.PaginatedQueryOptions[ScheduleQuery], schedule](s, ctx, + (*bunpaginate.OffsetPaginatedQuery[bunpaginate.PaginatedQueryOptions[ScheduleQuery]])(&q), + func(query *bun.SelectQuery) *bun.SelectQuery { + if where != "" { + query = query.Where(where, args...) + } + + query = query.Order("created_at DESC") + + return query + }, + ) + if err != nil { + return nil, e("failed to fetch schedules", err) + } + + schedules := make([]models.Schedule, 0, len(cursor.Data)) + for _, s := range cursor.Data { + schedules = append(schedules, toScheduleModel(s)) + } + + return &bunpaginate.Cursor[models.Schedule]{ + Data: schedules, + HasMore: cursor.HasMore, + Previous: cursor.Previous, + Next: cursor.Next, + }, nil +} + +func fromScheduleModel(s models.Schedule) schedule { + return schedule{ + ID: s.ID, + ConnectorID: s.ConnectorID, + CreatedAt: s.CreatedAt, + } +} + +func toScheduleModel(s schedule) models.Schedule { + return models.Schedule{ + ID: s.ID, + ConnectorID: s.ConnectorID, + CreatedAt: s.CreatedAt, + } +} diff --git a/components/payments/internal/storage/states.go b/components/payments/internal/storage/states.go new file mode 100644 index 0000000000..5271c095d3 --- /dev/null +++ b/components/payments/internal/storage/states.go @@ -0,0 +1,68 @@ +package storage + +import ( + "context" + "encoding/json" + + "github.com/formancehq/payments/internal/models" + "github.com/uptrace/bun" +) + +type state struct { + bun.BaseModel `bun:"table:states"` + + ID models.StateID `bun:"id,pk,type:character varying,notnull"` + ConnectorID models.ConnectorID `bun:"connector_id,type:character varying,notnull"` + State json.RawMessage `bun:"state,type:json,notnull"` +} + +func (s *store) StatesUpsert(ctx context.Context, state models.State) error { + toInsert := fromStateModels(state) + + _, err := s.db.NewInsert(). + Model(&toInsert). + On("CONFLICT (id) DO UPDATE"). + Set("state = EXCLUDED.state"). + Exec(ctx) + return e("failed to upsert state", err) +} + +func (s *store) StatesGet(ctx context.Context, id models.StateID) (models.State, error) { + var state state + + err := s.db.NewSelect(). + Model(&state). + Where("id = ?", id). + Scan(ctx) + if err != nil { + return models.State{}, e("failed to get state", err) + } + + res := toStateModels(state) + return res, nil +} + +func (s *store) StatesDeleteFromConnectorID(ctx context.Context, connectorID models.ConnectorID) error { + _, err := s.db.NewDelete(). + Model((*state)(nil)). + Where("connector_id = ?", connectorID). + Exec(ctx) + + return e("failed to delete state", err) +} + +func fromStateModels(from models.State) state { + return state{ + ID: from.ID, + ConnectorID: from.ConnectorID, + State: from.State, + } +} + +func toStateModels(from state) models.State { + return models.State{ + ID: from.ID, + ConnectorID: from.ConnectorID, + State: from.State, + } +} diff --git a/components/payments/internal/storage/storage.go b/components/payments/internal/storage/storage.go new file mode 100644 index 0000000000..b9cf09474a --- /dev/null +++ b/components/payments/internal/storage/storage.go @@ -0,0 +1,95 @@ +package storage + +import ( + "context" + "time" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/payments/internal/models" + "github.com/google/uuid" + "github.com/uptrace/bun" +) + +type Storage interface { + // Accounts + AccountsUpsert(ctx context.Context, accounts []models.Account) error + AccountsGet(ctx context.Context, id models.AccountID) (*models.Account, error) + AccountsList(ctx context.Context, q ListAccountsQuery) (*bunpaginate.Cursor[models.Account], error) + AccountsDeleteFromConnectorID(ctx context.Context, connectorID models.ConnectorID) error + + // Balances + BalancesUpsert(ctx context.Context, balances []models.Balance) error + BalancesDeleteForConnectorID(ctx context.Context, connectorID models.ConnectorID) error + BalancesList(ctx context.Context, q ListBalancesQuery) (*bunpaginate.Cursor[models.Balance], error) + BalancesGetAt(ctx context.Context, accountID models.AccountID, at time.Time) ([]*models.Balance, error) + + // Bank Accounts + BankAccountsUpsert(ctx context.Context, bankAccount models.BankAccount) error + BankAccountsUpdateMetadata(ctx context.Context, id uuid.UUID, metadata map[string]string) error + BankAccountsGet(ctx context.Context, id uuid.UUID, expand bool) (*models.BankAccount, error) + BankAccountsList(ctx context.Context, q ListBankAccountsQuery) (*bunpaginate.Cursor[models.BankAccount], error) + BankAccountsAddRelatedAccount(ctx context.Context, relatedAccount models.BankAccountRelatedAccount) error + BankAccountsDeleteRelatedAccountFromConnectorID(ctx context.Context, connectorID models.ConnectorID) error + + // Connectors + ConnectorsInstall(ctx context.Context, c models.Connector) error + ConnectorsUninstall(ctx context.Context, id models.ConnectorID) error + ConnectorsGet(ctx context.Context, id models.ConnectorID) (*models.Connector, error) + ConnectorsList(ctx context.Context, q ListConnectorsQuery) (*bunpaginate.Cursor[models.Connector], error) + + // Payments + PaymentsUpsert(ctx context.Context, payments []models.Payment) error + PaymentsUpdateMetadata(ctx context.Context, id models.PaymentID, metadata map[string]string) error + PaymentsGet(ctx context.Context, id models.PaymentID) (*models.Payment, error) + PaymentsList(ctx context.Context, q ListPaymentsQuery) (*bunpaginate.Cursor[models.Payment], error) + PaymentsDeleteFromConnectorID(ctx context.Context, connectorID models.ConnectorID) error + + // Pools + PoolsUpsert(ctx context.Context, pool models.Pool) error + PoolsGet(ctx context.Context, id uuid.UUID) (*models.Pool, error) + PoolsDelete(ctx context.Context, id uuid.UUID) error + PoolsAddAccount(ctx context.Context, id uuid.UUID, accountID models.AccountID) error + PoolsRemoveAccount(ctx context.Context, id uuid.UUID, accountID models.AccountID) error + PoolsList(ctx context.Context, q ListPoolsQuery) (*bunpaginate.Cursor[models.Pool], error) + + // Schedules + SchedulesUpsert(ctx context.Context, schedule models.Schedule) error + SchedulesList(ctx context.Context, q ListSchedulesQuery) (*bunpaginate.Cursor[models.Schedule], error) + SchedulesGet(ctx context.Context, id string, connectorID models.ConnectorID) (*models.Schedule, error) + SchedulesDeleteFromConnectorID(ctx context.Context, connectorID models.ConnectorID) error + + // State + StatesUpsert(ctx context.Context, state models.State) error + StatesGet(ctx context.Context, id models.StateID) (models.State, error) + StatesDeleteFromConnectorID(ctx context.Context, connectorID models.ConnectorID) error + + // Tasks + TasksUpsert(ctx context.Context, connectorID models.ConnectorID, tasks models.Tasks) error + TasksGet(ctx context.Context, connectorID models.ConnectorID) (*models.Tasks, error) + TasksDeleteFromConnectorID(ctx context.Context, connectorID models.ConnectorID) error + + // Webhooks Configs + WebhooksConfigsUpsert(ctx context.Context, webhooksConfigs []models.WebhookConfig) error + WebhooksConfigsDeleteFromConnectorID(ctx context.Context, connectorID models.ConnectorID) error + + // Webhooks + WebhooksInsert(ctx context.Context, webhook models.Webhook) error + WebhooksDeleteFromConnectorID(ctx context.Context, connectorID models.ConnectorID) error + + // Workflow Instances + InstancesUpsert(ctx context.Context, instance models.Instance) error + InstancesUpdate(ctx context.Context, instance models.Instance) error + InstancesList(ctx context.Context, q ListInstancesQuery) (*bunpaginate.Cursor[models.Instance], error) + InstancesDeleteFromConnectorID(ctx context.Context, connectorID models.ConnectorID) error +} + +const encryptionOptions = "compress-algo=1, cipher-algo=aes256" + +type store struct { + db *bun.DB + configEncryptionKey string +} + +func newStorage(db *bun.DB, configEncryptionKey string) Storage { + return &store{db: db, configEncryptionKey: configEncryptionKey} +} diff --git a/components/payments/internal/storage/tasks.go b/components/payments/internal/storage/tasks.go new file mode 100644 index 0000000000..8ead2078eb --- /dev/null +++ b/components/payments/internal/storage/tasks.go @@ -0,0 +1,65 @@ +package storage + +import ( + "context" + "encoding/json" + + "github.com/formancehq/payments/internal/models" + "github.com/pkg/errors" + "github.com/uptrace/bun" +) + +type tasks struct { + bun.BaseModel `bun:"table:tasks"` + + // Mandatory fields + ConnectorID models.ConnectorID `bun:"connector_id,pk,type:character varying,notnull"` + Tasks json.RawMessage `bun:"tasks,type:json,notnull"` +} + +func (s *store) TasksUpsert(ctx context.Context, connectorID models.ConnectorID, ts models.Tasks) error { + payload, err := json.Marshal(&ts) + if err != nil { + return errors.Wrap(err, "failed to marshal tasks") + } + + tasks := tasks{ + ConnectorID: connectorID, + Tasks: payload, + } + + _, err = s.db.NewInsert(). + Model(&tasks). + On("CONFLICT (connector_id) DO UPDATE"). + Set("tasks = EXCLUDED.tasks"). + Exec(ctx) + return e("failed to insert tasks", err) +} + +func (s *store) TasksGet(ctx context.Context, connectorID models.ConnectorID) (*models.Tasks, error) { + var ts tasks + + err := s.db.NewSelect(). + Model(&ts). + Where("connector_id = ?", connectorID). + Scan(ctx) + if err != nil { + return nil, e("failed to fetch tasks", err) + } + + var tasks models.Tasks + if err := json.Unmarshal(ts.Tasks, &tasks); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal tasks") + } + + return &tasks, nil +} + +func (s *store) TasksDeleteFromConnectorID(ctx context.Context, connectorID models.ConnectorID) error { + _, err := s.db.NewDelete(). + Model((*tasks)(nil)). + Where("connector_id = ?", connectorID). + Exec(ctx) + + return e("failed to delete tasks", err) +} diff --git a/components/payments/cmd/api/internal/storage/utils.go b/components/payments/internal/storage/utils.go similarity index 100% rename from components/payments/cmd/api/internal/storage/utils.go rename to components/payments/internal/storage/utils.go diff --git a/components/payments/internal/storage/webhooks.go b/components/payments/internal/storage/webhooks.go new file mode 100644 index 0000000000..1eac74f47f --- /dev/null +++ b/components/payments/internal/storage/webhooks.go @@ -0,0 +1,57 @@ +package storage + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "github.com/uptrace/bun" +) + +type webhook struct { + bun.BaseModel `bun:"table:webhooks"` + + // Mandatory fields + ID string `bun:"id,pk,type:uuid,notnull"` + ConnectorID models.ConnectorID `bun:"connector_id,type:character varying,notnull"` + + // Optional fields + Headers map[string][]string `bun:"headers,type:json"` + QueryValues map[string][]string `bun:"query_values,type:json"` + Body []byte `bun:"body,type:bytea,nullzero"` +} + +func (s *store) WebhooksInsert(ctx context.Context, webhook models.Webhook) error { + toInsert := fromWebhookModels(webhook) + + _, err := s.db.NewInsert(). + Model(&toInsert). + On("CONFLICT (id) DO NOTHING"). + Exec(ctx) + if err != nil { + return e("insert webhook", err) + } + + return nil +} + +func (s *store) WebhooksDeleteFromConnectorID(ctx context.Context, connectorID models.ConnectorID) error { + _, err := s.db.NewDelete(). + Model((*webhook)(nil)). + Where("connector_id = ?", connectorID). + Exec(ctx) + if err != nil { + return e("delete webhook", err) + } + + return nil +} + +func fromWebhookModels(from models.Webhook) webhook { + return webhook{ + ID: from.ID, + ConnectorID: from.ConnectorID, + Headers: from.Headers, + QueryValues: from.QueryValues, + Body: from.Body, + } +} diff --git a/components/payments/internal/storage/webhooks_configs.go b/components/payments/internal/storage/webhooks_configs.go new file mode 100644 index 0000000000..e4a2f13614 --- /dev/null +++ b/components/payments/internal/storage/webhooks_configs.go @@ -0,0 +1,60 @@ +package storage + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "github.com/uptrace/bun" +) + +type webhookConfig struct { + bun.BaseModel `bun:"table:webhooks_configs"` + + // Mandatory fields + Name string `bun:"name,pk,type:text,notnull"` + ConnectorID models.ConnectorID `bun:"connector_id,pk,type:character varying,notnull"` + URLPath string `bun:"url_path,type:text,notnull"` +} + +func (s *store) WebhooksConfigsUpsert(ctx context.Context, webhooksConfigs []models.WebhookConfig) error { + toInsert := fromWebhooksConfigsModels(webhooksConfigs) + + _, err := s.db.NewInsert(). + Model(&toInsert). + On("CONFLICT (name, connector_id) DO NOTHING"). + Exec(ctx) + if err != nil { + return e("upsert webhook config", err) + } + + return nil +} + +func (s *store) WebhooksConfigsDeleteFromConnectorID(ctx context.Context, connectorID models.ConnectorID) error { + _, err := s.db.NewDelete(). + Model((*webhookConfig)(nil)). + Where("connector_id = ?", connectorID). + Exec(ctx) + if err != nil { + return e("delete webhook config", err) + } + + return nil +} + +func fromWebhookConfigModels(from models.WebhookConfig) webhookConfig { + return webhookConfig{ + Name: from.Name, + ConnectorID: from.ConnectorID, + URLPath: from.URLPath, + } +} + +func fromWebhooksConfigsModels(from []models.WebhookConfig) []webhookConfig { + to := make([]webhookConfig, 0, len(from)) + for _, webhookConfig := range from { + to = append(to, fromWebhookConfigModels(webhookConfig)) + } + + return to +} diff --git a/components/payments/internal/storage/workflow_instances.go b/components/payments/internal/storage/workflow_instances.go new file mode 100644 index 0000000000..e1ed817079 --- /dev/null +++ b/components/payments/internal/storage/workflow_instances.go @@ -0,0 +1,164 @@ +package storage + +import ( + "context" + "fmt" + "time" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/query" + "github.com/formancehq/payments/internal/models" + "github.com/pkg/errors" + "github.com/uptrace/bun" +) + +type instance struct { + bun.BaseModel `bun:"table:workflows_instances"` + + // Mandatory fields + ID string `bun:"id,pk,type:text,notnull"` + ScheduleID string `bun:"schedule_id,pk,type:text,notnull"` + ConnectorID models.ConnectorID `bun:"connector_id,pk,type:character varying,notnull"` + CreatedAt time.Time `bun:"created_at,type:timestamp without time zone,notnull"` + UpdatedAt time.Time `bun:"updated_at,type:timestamp without time zone,notnull"` + + // Optional fields with default + // c.f. https://bun.uptrace.dev/guide/models.html#default + Terminated bool `bun:"terminated,type:boolean,notnull,nullzero,default:false"` + + // Optional fields + // c.f.: https://bun.uptrace.dev/guide/models.html#nulls + TerminatedAt *time.Time `bun:"terminated_at,type:timestamp without time zone,nullzero"` + Error *string `bun:"error,type:text,nullzero"` +} + +func (s *store) InstancesUpsert(ctx context.Context, instance models.Instance) error { + toInsert := fromInstanceModel(instance) + + _, err := s.db.NewInsert(). + Model(&toInsert). + On("CONFLICT (id, schedule_id, connector_id) DO NOTHING"). + Exec(ctx) + + return e("failed to insert new instance", err) +} + +func (s *store) InstancesUpdate(ctx context.Context, instance models.Instance) error { + toUpdate := fromInstanceModel(instance) + + _, err := s.db.NewUpdate(). + Model(&toUpdate). + Set("updated_at = ?", instance.UpdatedAt). + Set("terminated = ?", instance.Terminated). + Set("terminated_at = ?", instance.TerminatedAt). + Set("error = ?", instance.Error). + WherePK(). + Exec(ctx) + + return e("failed to update instance", err) +} + +func (s *store) InstancesDeleteFromConnectorID(ctx context.Context, connectorID models.ConnectorID) error { + _, err := s.db.NewDelete(). + Model((*instance)(nil)). + Where("connector_id = ?", connectorID). + Exec(ctx) + + return e("failed to delete instances", err) +} + +type InstanceQuery struct{} + +type ListInstancesQuery bunpaginate.OffsetPaginatedQuery[bunpaginate.PaginatedQueryOptions[InstanceQuery]] + +func NewListInstancesQuery(opts bunpaginate.PaginatedQueryOptions[InstanceQuery]) ListInstancesQuery { + return ListInstancesQuery{ + Order: bunpaginate.OrderAsc, + PageSize: opts.PageSize, + Options: opts, + } +} + +func (s *store) instancesQueryContext(qb query.Builder) (string, []any, error) { + return qb.Build(query.ContextFn(func(key, operator string, value any) (string, []any, error) { + switch { + case key == "schedule_id", + key == "connector_id": + if operator != "$match" { + return "", nil, errors.Wrap(ErrValidation, "'connector_id' column can only be used with $match") + } + return fmt.Sprintf("%s = ?", key), []any{value}, nil + default: + return "", nil, errors.Wrap(ErrValidation, fmt.Sprintf("unknown key '%s' when building query", key)) + } + })) +} + +func (s *store) InstancesList(ctx context.Context, q ListInstancesQuery) (*bunpaginate.Cursor[models.Instance], error) { + var ( + where string + args []any + err error + ) + if q.Options.QueryBuilder != nil { + where, args, err = s.instancesQueryContext(q.Options.QueryBuilder) + if err != nil { + return nil, err + } + } + + cursor, err := paginateWithOffset[bunpaginate.PaginatedQueryOptions[InstanceQuery], instance](s, ctx, + (*bunpaginate.OffsetPaginatedQuery[bunpaginate.PaginatedQueryOptions[InstanceQuery]])(&q), + func(query *bun.SelectQuery) *bun.SelectQuery { + if where != "" { + query = query.Where(where, args...) + } + + query = query.Order("created_at DESC") + + return query + }, + ) + if err != nil { + return nil, e("failed to fetch instances", err) + } + + instances := make([]models.Instance, 0, len(cursor.Data)) + for _, i := range cursor.Data { + instances = append(instances, toInstanceModel(i)) + } + + return &bunpaginate.Cursor[models.Instance]{ + PageSize: cursor.PageSize, + HasMore: cursor.HasMore, + Previous: cursor.Previous, + Next: cursor.Next, + Data: instances, + }, nil +} + +func fromInstanceModel(from models.Instance) instance { + return instance{ + ID: from.ID, + ScheduleID: from.ScheduleID, + ConnectorID: from.ConnectorID, + CreatedAt: from.CreatedAt, + UpdatedAt: from.UpdatedAt, + Terminated: from.Terminated, + TerminatedAt: from.TerminatedAt, + Error: from.Error, + } +} + +func toInstanceModel(from instance) models.Instance { + return models.Instance{ + ID: from.ID, + ScheduleID: from.ScheduleID, + ConnectorID: from.ConnectorID, + CreatedAt: from.CreatedAt, + UpdatedAt: from.UpdatedAt, + Terminated: from.Terminated, + TerminatedAt: from.TerminatedAt, + Error: from.Error, + } +} diff --git a/components/payments/local_env/postgres/init.sql b/components/payments/local_env/postgres/init.sql deleted file mode 100644 index 2945c921e9..0000000000 --- a/components/payments/local_env/postgres/init.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER ROLE payments SET search_path = public; \ No newline at end of file diff --git a/ee/orchestration/cmd/root.go b/ee/orchestration/cmd/root.go index 1a162d6b41..27b79494f1 100644 --- a/ee/orchestration/cmd/root.go +++ b/ee/orchestration/cmd/root.go @@ -5,29 +5,25 @@ import ( "fmt" "net/http" + "github.com/formancehq/go-libs/auth" + "github.com/formancehq/go-libs/bun/bunconnect" "github.com/formancehq/go-libs/bun/bunmigrate" "github.com/formancehq/go-libs/licence" - "github.com/formancehq/orchestration/internal/storage" - "github.com/uptrace/bun" - - "github.com/formancehq/go-libs/bun/bunconnect" - - "github.com/formancehq/go-libs/auth" "github.com/formancehq/go-libs/otlp" - "golang.org/x/oauth2" - "golang.org/x/oauth2/clientcredentials" - - "github.com/formancehq/orchestration/internal/triggers" - "github.com/formancehq/orchestration/internal/workflow" - - "github.com/formancehq/go-libs/publish" - "github.com/formancehq/orchestration/internal/temporalclient" - "github.com/formancehq/go-libs/otlp/otlptraces" + "github.com/formancehq/go-libs/publish" "github.com/formancehq/go-libs/service" + "github.com/formancehq/go-libs/temporal" + "github.com/formancehq/orchestration/internal/storage" + "github.com/formancehq/orchestration/internal/temporalclient" + "github.com/formancehq/orchestration/internal/triggers" + "github.com/formancehq/orchestration/internal/workflow" _ "github.com/formancehq/orchestration/internal/workflow/stages/all" "github.com/spf13/cobra" + "github.com/uptrace/bun" "go.uber.org/fx" + "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" ) var ( @@ -38,20 +34,13 @@ var ( ) const ( - stackFlag = "stack" - stackURLFlag = "stack-url" - stackClientIDFlag = "stack-client-id" - stackClientSecretFlag = "stack-client-secret" - temporalAddressFlag = "temporal-address" - temporalNamespaceFlag = "temporal-namespace" - temporalSSLClientKeyFlag = "temporal-ssl-client-key" - temporalSSLClientCertFlag = "temporal-ssl-client-cert" - temporalTaskQueueFlag = "temporal-task-queue" - temporalInitSearchAttributes = "temporal-init-search-attributes" - temporalMaxParallelActivitiesFlag = "temporal-max-parallel-activities" - topicsFlag = "topics" - listenFlag = "listen" - workerFlag = "worker" + stackFlag = "stack" + stackURLFlag = "stack-url" + stackClientIDFlag = "stack-client-id" + stackClientSecretFlag = "stack-client-secret" + topicsFlag = "topics" + listenFlag = "listen" + workerFlag = "worker" ) func NewRootCommand() *cobra.Command { @@ -82,21 +71,16 @@ func commonOptions(cmd *cobra.Command) (fx.Option, error) { return nil, err } - temporalAddress, _ := cmd.Flags().GetString(temporalAddressFlag) - temporalNamespace, _ := cmd.Flags().GetString(temporalNamespaceFlag) - temporalSSLClientKey, _ := cmd.Flags().GetString(temporalSSLClientKeyFlag) - temporalSSLClientCert, _ := cmd.Flags().GetString(temporalSSLClientCertFlag) - temporalTaskQueue, _ := cmd.Flags().GetString(temporalTaskQueueFlag) - temporalInitSearchAttributes, _ := cmd.Flags().GetBool(temporalInitSearchAttributes) + temporalTaskQueue, _ := cmd.Flags().GetString(temporal.TemporalTaskQueueFlag) return fx.Options( otlptraces.FXModuleFromFlags(cmd), - temporalclient.NewModule( - temporalAddress, - temporalNamespace, - temporalSSLClientCert, - temporalSSLClientKey, - temporalInitSearchAttributes, + temporal.FXModuleFromFlags( + cmd, + workflow.Tracer, + temporal.SearchAttributes{ + SearchAttributes: temporalclient.SearchAttributes, + }, ), bunconnect.Module(*connectionOptions, service.IsDebug(cmd)), publish.FXModuleFromFlags(cmd, service.IsDebug(cmd)), diff --git a/ee/orchestration/cmd/serve.go b/ee/orchestration/cmd/serve.go index 4899339776..3c5e29eb15 100644 --- a/ee/orchestration/cmd/serve.go +++ b/ee/orchestration/cmd/serve.go @@ -3,21 +3,20 @@ package cmd import ( "context" - "github.com/go-chi/chi/v5" - "github.com/formancehq/go-libs/auth" "github.com/formancehq/go-libs/aws/iam" "github.com/formancehq/go-libs/bun/bunconnect" - "github.com/formancehq/go-libs/licence" - "github.com/formancehq/go-libs/publish" - "github.com/formancehq/go-libs/health" "github.com/formancehq/go-libs/httpserver" + "github.com/formancehq/go-libs/licence" + "github.com/formancehq/go-libs/publish" "github.com/formancehq/go-libs/service" + "github.com/formancehq/go-libs/temporal" "github.com/formancehq/orchestration/internal/api" v1 "github.com/formancehq/orchestration/internal/api/v1" v2 "github.com/formancehq/orchestration/internal/api/v2" "github.com/formancehq/orchestration/internal/storage" + "github.com/go-chi/chi/v5" "github.com/spf13/cobra" "github.com/uptrace/bun" "go.uber.org/fx" @@ -78,19 +77,13 @@ func newServeCommand() *cobra.Command { cmd.Flags().Bool(workerFlag, false, "Enable worker mode") cmd.Flags().String(listenFlag, ":8080", "Listening address") - cmd.Flags().Float64(temporalMaxParallelActivitiesFlag, 10, "Maximum number of parallel activities") cmd.Flags().String(stackURLFlag, "", "Stack url") cmd.Flags().String(stackClientIDFlag, "", "Stack client ID") cmd.Flags().String(stackClientSecretFlag, "", "Stack client secret") - cmd.Flags().String(temporalAddressFlag, "", "Temporal server address") - cmd.Flags().String(temporalNamespaceFlag, "default", "Temporal namespace") - cmd.Flags().String(temporalSSLClientKeyFlag, "", "Temporal client key") - cmd.Flags().String(temporalSSLClientCertFlag, "", "Temporal client cert") - cmd.Flags().String(temporalTaskQueueFlag, "default", "Temporal task queue name") - cmd.Flags().Bool(temporalInitSearchAttributes, false, "Init temporal search attributes") cmd.Flags().StringSlice(topicsFlag, []string{}, "Topics to listen") cmd.Flags().String(stackFlag, "", "Stack") + temporal.AddFlags(cmd.Flags()) service.AddFlags(cmd.Flags()) publish.AddFlags(ServiceName, cmd.Flags()) auth.AddFlags(cmd.Flags()) diff --git a/ee/orchestration/cmd/worker.go b/ee/orchestration/cmd/worker.go index 3873ffffe9..2cd27351db 100644 --- a/ee/orchestration/cmd/worker.go +++ b/ee/orchestration/cmd/worker.go @@ -3,19 +3,16 @@ package cmd import ( "net/http" + sdk "github.com/formancehq/formance-sdk-go/v2" "github.com/formancehq/go-libs/aws/iam" "github.com/formancehq/go-libs/bun/bunconnect" "github.com/formancehq/go-libs/licence" "github.com/formancehq/go-libs/publish" - - "go.temporal.io/sdk/worker" - - "github.com/formancehq/orchestration/internal/triggers" - - sdk "github.com/formancehq/formance-sdk-go/v2" "github.com/formancehq/go-libs/service" - "github.com/formancehq/orchestration/internal/temporalworker" + "github.com/formancehq/go-libs/temporal" + "github.com/formancehq/orchestration/internal/triggers" "github.com/spf13/cobra" + "go.temporal.io/sdk/worker" "go.uber.org/fx" ) @@ -33,15 +30,14 @@ func stackClientModule(cmd *cobra.Command) fx.Option { } func workerOptions(cmd *cobra.Command) fx.Option { - stack, _ := cmd.Flags().GetString(stackFlag) - temporalTaskQueue, _ := cmd.Flags().GetString(temporalTaskQueueFlag) - temporalMaxParallelActivities, _ := cmd.Flags().GetInt(temporalMaxParallelActivitiesFlag) + temporalTaskQueue, _ := cmd.Flags().GetString(temporal.TemporalTaskQueueFlag) + temporalMaxParallelActivities, _ := cmd.Flags().GetInt(temporal.TemporalMaxParallelActivitiesFlag) topics, _ := cmd.Flags().GetStringSlice(topicsFlag) return fx.Options( stackClientModule(cmd), - temporalworker.NewWorkerModule(temporalTaskQueue, worker.Options{ + temporal.NewWorkerModule(cmd.Context(), temporalTaskQueue, worker.Options{ TaskQueueActivitiesPerSecond: float64(temporalMaxParallelActivities), }), triggers.NewListenerModule( @@ -64,19 +60,13 @@ func newWorkerCommand() *cobra.Command { return service.New(cmd.OutOrStdout(), commonOptions, workerOptions(cmd)).Run(cmd) }, } - ret.Flags().Float64(temporalMaxParallelActivitiesFlag, 10, "Maximum number of parallel activities") ret.Flags().String(stackURLFlag, "", "Stack url") ret.Flags().String(stackClientIDFlag, "", "Stack client ID") ret.Flags().String(stackClientSecretFlag, "", "Stack client secret") - ret.Flags().String(temporalAddressFlag, "", "Temporal server address") - ret.Flags().String(temporalNamespaceFlag, "default", "Temporal namespace") - ret.Flags().String(temporalSSLClientKeyFlag, "", "Temporal client key") - ret.Flags().String(temporalSSLClientCertFlag, "", "Temporal client cert") - ret.Flags().String(temporalTaskQueueFlag, "default", "Temporal task queue name") - ret.Flags().Bool(temporalInitSearchAttributes, false, "Init temporal search attributes") ret.Flags().StringSlice(topicsFlag, []string{}, "Topics to listen") ret.Flags().String(stackFlag, "", "Stack") + temporal.AddFlags(ret.Flags()) publish.AddFlags(ServiceName, ret.Flags()) bunconnect.AddFlags(ret.Flags()) iam.AddFlags(ret.Flags()) diff --git a/ee/orchestration/go.mod b/ee/orchestration/go.mod index 6d3034bba2..c1242627b2 100644 --- a/ee/orchestration/go.mod +++ b/ee/orchestration/go.mod @@ -24,7 +24,6 @@ require ( go.opentelemetry.io/otel/trace v1.30.0 go.temporal.io/api v1.39.0 go.temporal.io/sdk v1.29.1 - go.temporal.io/sdk/contrib/opentelemetry v0.6.0 go.uber.org/fx v1.22.2 go.uber.org/mock v0.4.0 golang.org/x/oauth2 v0.23.0 @@ -167,6 +166,7 @@ require ( go.opentelemetry.io/otel/metric v1.30.0 // indirect go.opentelemetry.io/otel/sdk v1.30.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect + go.temporal.io/sdk/contrib/opentelemetry v0.6.0 // indirect go.uber.org/dig v1.18.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect diff --git a/ee/orchestration/internal/api/v1/handler_delete_workflow_test.go b/ee/orchestration/internal/api/v1/handler_delete_workflow_test.go index bfa09517a3..cff8d8b55a 100644 --- a/ee/orchestration/internal/api/v1/handler_delete_workflow_test.go +++ b/ee/orchestration/internal/api/v1/handler_delete_workflow_test.go @@ -7,14 +7,11 @@ import ( "net/http/httptest" "testing" - "github.com/go-chi/chi/v5" - sharedapi "github.com/formancehq/go-libs/testing/api" - "github.com/formancehq/orchestration/internal/api" "github.com/formancehq/orchestration/internal/workflow" + "github.com/go-chi/chi/v5" "github.com/stretchr/testify/require" - "github.com/uptrace/bun" ) diff --git a/ee/orchestration/internal/api/v1/main_test.go b/ee/orchestration/internal/api/v1/main_test.go index 224c7a08b8..265ff9b300 100644 --- a/ee/orchestration/internal/api/v1/main_test.go +++ b/ee/orchestration/internal/api/v1/main_test.go @@ -6,33 +6,27 @@ import ( "net/http" "testing" - "github.com/formancehq/go-libs/testing/docker" - "github.com/formancehq/go-libs/testing/utils" - + "github.com/formancehq/go-libs/auth" + "github.com/formancehq/go-libs/bun/bunconnect" "github.com/formancehq/go-libs/bun/bundebug" - - "go.temporal.io/sdk/worker" - "github.com/formancehq/go-libs/logging" "github.com/formancehq/go-libs/publish" - "github.com/formancehq/orchestration/internal/temporalworker" - "github.com/formancehq/orchestration/internal/workflow/stages" - chi "github.com/go-chi/chi/v5" - "github.com/google/uuid" - "go.temporal.io/sdk/testsuite" - - "github.com/formancehq/go-libs/bun/bunconnect" - - "github.com/formancehq/orchestration/internal/api" - "github.com/formancehq/orchestration/internal/triggers" - - "github.com/formancehq/go-libs/auth" + "github.com/formancehq/go-libs/temporal" + "github.com/formancehq/go-libs/testing/docker" "github.com/formancehq/go-libs/testing/platform/pgtesting" + "github.com/formancehq/go-libs/testing/utils" + "github.com/formancehq/orchestration/internal/api" "github.com/formancehq/orchestration/internal/storage" + "github.com/formancehq/orchestration/internal/triggers" "github.com/formancehq/orchestration/internal/workflow" + "github.com/formancehq/orchestration/internal/workflow/stages" + chi "github.com/go-chi/chi/v5" + "github.com/google/uuid" flag "github.com/spf13/pflag" "github.com/stretchr/testify/require" "github.com/uptrace/bun" + "go.temporal.io/sdk/testsuite" + "go.temporal.io/sdk/worker" ) func test(t *testing.T, fn func(router *chi.Mux, backend api.Backend, db *bun.DB)) { @@ -53,15 +47,15 @@ func test(t *testing.T, fn func(router *chi.Mux, backend api.Backend, db *bun.DB }) taskQueue := uuid.NewString() - worker := temporalworker.New(logging.Testing(), devServer.Client(), taskQueue, - []temporalworker.DefinitionSet{ + worker := temporal.New(context.Background(), logging.Testing(), devServer.Client(), taskQueue, + []temporal.DefinitionSet{ workflow.NewWorkflows(false).DefinitionSet(), - temporalworker.NewDefinitionSet().Append(temporalworker.Definition{ + temporal.NewDefinitionSet().Append(temporal.Definition{ Name: "NoOp", Func: (&stages.NoOp{}).GetWorkflow(), }), }, - []temporalworker.DefinitionSet{ + []temporal.DefinitionSet{ workflow.NewActivities(publish.NoOpPublisher, db).DefinitionSet(), }, worker.Options{}, diff --git a/ee/orchestration/internal/api/v2/main_test.go b/ee/orchestration/internal/api/v2/main_test.go index 57fd66fc0d..aea18296ad 100644 --- a/ee/orchestration/internal/api/v2/main_test.go +++ b/ee/orchestration/internal/api/v2/main_test.go @@ -6,33 +6,26 @@ import ( "net/http" "testing" - "github.com/go-chi/chi/v5" - - "github.com/formancehq/go-libs/testing/docker" - "github.com/formancehq/go-libs/testing/utils" - + "github.com/formancehq/go-libs/auth" + "github.com/formancehq/go-libs/bun/bunconnect" "github.com/formancehq/go-libs/bun/bundebug" - - "go.temporal.io/sdk/worker" - "github.com/formancehq/go-libs/logging" "github.com/formancehq/go-libs/publish" - "github.com/formancehq/orchestration/internal/temporalworker" - "github.com/formancehq/orchestration/internal/workflow/stages" - "github.com/google/uuid" - "go.temporal.io/sdk/testsuite" - - "github.com/formancehq/go-libs/bun/bunconnect" - - "github.com/formancehq/orchestration/internal/api" - "github.com/formancehq/orchestration/internal/triggers" - - "github.com/formancehq/go-libs/auth" + "github.com/formancehq/go-libs/temporal" + "github.com/formancehq/go-libs/testing/docker" "github.com/formancehq/go-libs/testing/platform/pgtesting" + "github.com/formancehq/go-libs/testing/utils" + "github.com/formancehq/orchestration/internal/api" "github.com/formancehq/orchestration/internal/storage" + "github.com/formancehq/orchestration/internal/triggers" "github.com/formancehq/orchestration/internal/workflow" + "github.com/formancehq/orchestration/internal/workflow/stages" + "github.com/go-chi/chi/v5" + "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/uptrace/bun" + "go.temporal.io/sdk/testsuite" + "go.temporal.io/sdk/worker" ) func test(t *testing.T, fn func(router *chi.Mux, backend api.Backend, db *bun.DB)) { @@ -53,15 +46,15 @@ func test(t *testing.T, fn func(router *chi.Mux, backend api.Backend, db *bun.DB }) taskQueue := uuid.NewString() - worker := temporalworker.New(logging.Testing(), devServer.Client(), taskQueue, - []temporalworker.DefinitionSet{ + worker := temporal.New(context.Background(), logging.Testing(), devServer.Client(), taskQueue, + []temporal.DefinitionSet{ workflow.NewWorkflows(false).DefinitionSet(), - temporalworker.NewDefinitionSet().Append(temporalworker.Definition{ + temporal.NewDefinitionSet().Append(temporal.Definition{ Name: "NoOp", Func: (&stages.NoOp{}).GetWorkflow(), }), }, - []temporalworker.DefinitionSet{ + []temporal.DefinitionSet{ workflow.NewActivities(publish.NoOpPublisher, db).DefinitionSet(), }, worker.Options{}, diff --git a/ee/orchestration/internal/temporalclient/client_module.go b/ee/orchestration/internal/temporalclient/client_module.go deleted file mode 100644 index 021de2e5c7..0000000000 --- a/ee/orchestration/internal/temporalclient/client_module.go +++ /dev/null @@ -1,105 +0,0 @@ -package temporalclient - -import ( - "context" - "crypto/tls" - "time" - - "go.temporal.io/api/enums/v1" - "go.temporal.io/api/operatorservice/v1" - - "github.com/formancehq/go-libs/logging" - "github.com/formancehq/orchestration/internal/triggers" - "github.com/formancehq/orchestration/internal/workflow" - "go.temporal.io/api/serviceerror" - "go.temporal.io/sdk/client" - "go.temporal.io/sdk/contrib/opentelemetry" - "go.temporal.io/sdk/interceptor" - "go.uber.org/fx" -) - -func NewModule(address, namespace string, certStr string, key string, initSearchAttributes bool) fx.Option { - return fx.Options( - fx.Provide(func(logger logging.Logger) (client.Options, error) { - - var cert *tls.Certificate - if key != "" && certStr != "" { - clientCert, err := tls.X509KeyPair([]byte(certStr), []byte(key)) - if err != nil { - return client.Options{}, err - } - cert = &clientCert - } - - tracingInterceptor, err := opentelemetry.NewTracingInterceptor(opentelemetry.TracerOptions{ - Tracer: workflow.Tracer, - }) - if err != nil { - return client.Options{}, err - } - - options := client.Options{ - Namespace: namespace, - HostPort: address, - Interceptors: []interceptor.ClientInterceptor{tracingInterceptor}, - Logger: newLogger(logger), - } - if cert != nil { - options.ConnectionOptions = client.ConnectionOptions{ - TLS: &tls.Config{Certificates: []tls.Certificate{*cert}}, - } - } - return options, nil - }), - fx.Provide(client.Dial), - fx.Invoke(func(lifecycle fx.Lifecycle, c client.Client) { - lifecycle.Append(fx.Hook{ - OnStart: func(ctx context.Context) error { - if initSearchAttributes { - return createSearchAttributes(ctx, c, namespace) - } - return nil - }, - OnStop: func(ctx context.Context) error { - c.Close() - return nil - }, - }) - }), - ) -} - -func createSearchAttributes(ctx context.Context, c client.Client, namespace string) error { - _, err := c.OperatorService().AddSearchAttributes(logging.TestingContext(), &operatorservice.AddSearchAttributesRequest{ - SearchAttributes: map[string]enums.IndexedValueType{ - workflow.SearchAttributeWorkflowID: enums.INDEXED_VALUE_TYPE_TEXT, - triggers.SearchAttributeTriggerID: enums.INDEXED_VALUE_TYPE_TEXT, - }, - Namespace: namespace, - }) - if err != nil { - if _, ok := err.(*serviceerror.AlreadyExists); !ok { - return err - } - } - // Search attributes are created asynchronously, so poll the list, until it is ready - for { - ret, err := c.OperatorService().ListSearchAttributes(ctx, &operatorservice.ListSearchAttributesRequest{ - Namespace: namespace, - }) - if err != nil { - panic(err) - } - - if ret.CustomAttributes[workflow.SearchAttributeWorkflowID] != enums.INDEXED_VALUE_TYPE_UNSPECIFIED && - ret.CustomAttributes[triggers.SearchAttributeTriggerID] != enums.INDEXED_VALUE_TYPE_UNSPECIFIED { - return nil - } - - select { - case <-ctx.Done(): - return ctx.Err() - case <-time.After(500 * time.Millisecond): - } - } -} diff --git a/ee/orchestration/internal/temporalclient/logger.go b/ee/orchestration/internal/temporalclient/logger.go deleted file mode 100644 index 5d27fbd0c5..0000000000 --- a/ee/orchestration/internal/temporalclient/logger.go +++ /dev/null @@ -1,42 +0,0 @@ -package temporalclient - -import ( - "github.com/formancehq/go-libs/logging" - "go.temporal.io/sdk/log" -) - -func keyvalsToMap(keyvals ...interface{}) map[string]any { - ret := make(map[string]any) - for i := 0; i < len(keyvals); i += 2 { - ret[keyvals[i].(string)] = keyvals[i+1] - } - return ret -} - -type logger struct { - logger logging.Logger -} - -func (l logger) Debug(msg string, keyvals ...interface{}) { - l.logger.WithFields(keyvalsToMap(keyvals...)).Debugf(msg) -} - -func (l logger) Info(msg string, keyvals ...interface{}) { - l.logger.WithFields(keyvalsToMap(keyvals...)).Infof(msg) -} - -func (l logger) Warn(msg string, keyvals ...interface{}) { - l.logger.WithFields(keyvalsToMap(keyvals...)).Errorf(msg) -} - -func (l logger) Error(msg string, keyvals ...interface{}) { - l.logger.WithFields(keyvalsToMap(keyvals...)).Errorf(msg) -} - -var _ log.Logger = (*logger)(nil) - -func newLogger(l logging.Logger) *logger { - return &logger{ - logger: l, - } -} diff --git a/ee/orchestration/internal/temporalclient/search_attributes.go b/ee/orchestration/internal/temporalclient/search_attributes.go new file mode 100644 index 0000000000..c5502bbd78 --- /dev/null +++ b/ee/orchestration/internal/temporalclient/search_attributes.go @@ -0,0 +1,14 @@ +package temporalclient + +import ( + "github.com/formancehq/orchestration/internal/triggers" + "github.com/formancehq/orchestration/internal/workflow" + "go.temporal.io/api/enums/v1" +) + +var ( + SearchAttributes = map[string]enums.IndexedValueType{ + workflow.SearchAttributeWorkflowID: enums.INDEXED_VALUE_TYPE_TEXT, + triggers.SearchAttributeTriggerID: enums.INDEXED_VALUE_TYPE_TEXT, + } +) diff --git a/ee/orchestration/internal/temporalworker/module.go b/ee/orchestration/internal/temporalworker/module.go deleted file mode 100644 index e36227db6f..0000000000 --- a/ee/orchestration/internal/temporalworker/module.go +++ /dev/null @@ -1,86 +0,0 @@ -package temporalworker - -import ( - "context" - - "github.com/formancehq/go-libs/logging" - - temporalworkflow "go.temporal.io/sdk/workflow" - - "go.temporal.io/sdk/activity" - "go.temporal.io/sdk/client" - "go.temporal.io/sdk/worker" - "go.uber.org/fx" -) - -type Definition struct { - Func any - Name string -} - -type DefinitionSet []Definition - -func NewDefinitionSet() DefinitionSet { - return DefinitionSet{} -} - -func (d DefinitionSet) Append(definition Definition) DefinitionSet { - d = append(d, definition) - - return d -} - -func New(logger logging.Logger, c client.Client, taskQueue string, workflows, activities []DefinitionSet, options worker.Options) worker.Worker { - options.BackgroundActivityContext = logging.ContextWithLogger(context.Background(), logger) - worker := worker.New(c, taskQueue, options) - - for _, set := range workflows { - for _, workflow := range set { - worker.RegisterWorkflowWithOptions(workflow.Func, temporalworkflow.RegisterOptions{ - Name: workflow.Name, - }) - } - } - - for _, set := range activities { - for _, act := range set { - worker.RegisterActivityWithOptions(act.Func, activity.RegisterOptions{ - Name: act.Name, - }) - } - } - - return worker -} - -func NewWorkerModule(taskQueue string, options worker.Options) fx.Option { - return fx.Options( - fx.Provide( - fx.Annotate(func(logger logging.Logger, c client.Client, workflows, activities []DefinitionSet) worker.Worker { - return New(logger, c, taskQueue, workflows, activities, options) - }, fx.ParamTags(``, ``, `group:"workflows"`, `group:"activities"`)), - ), - fx.Invoke(func(lc fx.Lifecycle, w worker.Worker) { - willStop := false - lc.Append(fx.Hook{ - OnStart: func(ctx context.Context) error { - go func() { - err := w.Run(worker.InterruptCh()) - if err != nil { - // If the worker is started/stopped fast, the Run method can return an error - if !willStop { - panic(err) - } - } - }() - return nil - }, - OnStop: func(ctx context.Context) error { - willStop = true - w.Stop() - return nil - }, - }) - }), - ) -} diff --git a/ee/orchestration/internal/triggers/activities.go b/ee/orchestration/internal/triggers/activities.go index c1d0b23bd1..9be2f1f8a4 100644 --- a/ee/orchestration/internal/triggers/activities.go +++ b/ee/orchestration/internal/triggers/activities.go @@ -4,11 +4,10 @@ import ( "context" "strings" - "github.com/formancehq/orchestration/internal/temporalworker" - "github.com/ThreeDotsLabs/watermill/message" "github.com/formancehq/go-libs/collectionutils" "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/go-libs/temporal" "github.com/formancehq/orchestration/internal/workflow" "github.com/formancehq/orchestration/pkg/events" "github.com/uptrace/bun" @@ -104,21 +103,21 @@ func (a Activities) SendEventForTriggerTermination(ctx context.Context, occurren } } -func (a Activities) DefinitionSet() temporalworker.DefinitionSet { - return temporalworker.NewDefinitionSet(). - Append(temporalworker.Definition{ +func (a Activities) DefinitionSet() temporal.DefinitionSet { + return temporal.NewDefinitionSet(). + Append(temporal.Definition{ Func: a.EvalTriggerVariables, Name: "EvalTriggerVariables", }). - Append(temporalworker.Definition{ + Append(temporal.Definition{ Func: a.InsertTriggerOccurrence, Name: "InsertTriggerOccurrence", }). - Append(temporalworker.Definition{ + Append(temporal.Definition{ Func: a.ListTriggers, Name: "ListTriggers", }). - Append(temporalworker.Definition{ + Append(temporal.Definition{ Func: a.SendEventForTriggerTermination, Name: "SendEventForTriggerTermination", }) diff --git a/ee/orchestration/internal/triggers/module.go b/ee/orchestration/internal/triggers/module.go index f2f440bd0f..c21251c0fd 100644 --- a/ee/orchestration/internal/triggers/module.go +++ b/ee/orchestration/internal/triggers/module.go @@ -4,10 +4,9 @@ import ( "net/http" "strings" - "github.com/formancehq/orchestration/internal/temporalworker" - "github.com/ThreeDotsLabs/watermill/message" "github.com/formancehq/go-libs/logging" + "github.com/formancehq/go-libs/temporal" "github.com/formancehq/orchestration/internal/workflow" "github.com/uptrace/bun" "go.temporal.io/sdk/client" @@ -23,14 +22,14 @@ func NewModule(taskQueue string) fx.Option { fx.Provide(func() *triggerWorkflow { return NewWorkflow(taskQueue, true) }), - fx.Provide(fx.Annotate(func(workflow *triggerWorkflow) temporalworker.DefinitionSet { + fx.Provide(fx.Annotate(func(workflow *triggerWorkflow) temporal.DefinitionSet { return workflow.DefinitionSet() }, fx.ResultTags(`group:"workflows"`))), fx.Provide(func(db *bun.DB, manager *workflow.WorkflowManager, expressionEvaluator *expressionEvaluator, publisher message.Publisher) Activities { return NewActivities(db, manager, expressionEvaluator, publisher) }), - fx.Provide(fx.Annotate(func(activities Activities) temporalworker.DefinitionSet { + fx.Provide(fx.Annotate(func(activities Activities) temporal.DefinitionSet { return activities.DefinitionSet() }, fx.ResultTags(`group:"activities"`))), ) diff --git a/ee/orchestration/internal/triggers/workflow_trigger.go b/ee/orchestration/internal/triggers/workflow_trigger.go index bc58b21c87..8d90f52172 100644 --- a/ee/orchestration/internal/triggers/workflow_trigger.go +++ b/ee/orchestration/internal/triggers/workflow_trigger.go @@ -3,14 +3,11 @@ package triggers import ( "time" - "go.temporal.io/api/enums/v1" - - "github.com/formancehq/orchestration/internal/temporalworker" - "github.com/formancehq/go-libs/pointer" - "github.com/formancehq/orchestration/internal/workflow" - "github.com/formancehq/go-libs/publish" + "github.com/formancehq/go-libs/temporal" + "github.com/formancehq/orchestration/internal/workflow" + "go.temporal.io/api/enums/v1" temporalworkflow "go.temporal.io/sdk/workflow" ) @@ -117,13 +114,13 @@ func (w triggerWorkflow) ExecuteTrigger(ctx temporalworkflow.Context, req Proces return nil } -func (w triggerWorkflow) DefinitionSet() temporalworker.DefinitionSet { - return temporalworker.NewDefinitionSet(). - Append(temporalworker.Definition{ +func (w triggerWorkflow) DefinitionSet() temporal.DefinitionSet { + return temporal.NewDefinitionSet(). + Append(temporal.Definition{ Func: w.RunTrigger, Name: "RunTrigger", }). - Append(temporalworker.Definition{ + Append(temporal.Definition{ Func: w.ExecuteTrigger, Name: "ExecuteTrigger", }) diff --git a/ee/orchestration/internal/triggers/workflow_trigger_test.go b/ee/orchestration/internal/triggers/workflow_trigger_test.go index 98336f7f52..8684a9d146 100644 --- a/ee/orchestration/internal/triggers/workflow_trigger_test.go +++ b/ee/orchestration/internal/triggers/workflow_trigger_test.go @@ -1,25 +1,23 @@ package triggers import ( + "context" "testing" "time" - worker "go.temporal.io/sdk/worker" - - "github.com/formancehq/go-libs/bun/bundebug" - "github.com/uptrace/bun" - "github.com/formancehq/go-libs/bun/bunconnect" + "github.com/formancehq/go-libs/bun/bundebug" "github.com/formancehq/go-libs/logging" + "github.com/formancehq/go-libs/publish" + "github.com/formancehq/go-libs/temporal" "github.com/formancehq/orchestration/internal/storage" - "github.com/formancehq/orchestration/internal/temporalworker" "github.com/formancehq/orchestration/internal/workflow" "github.com/formancehq/orchestration/internal/workflow/stages" - "go.temporal.io/sdk/client" - - "github.com/formancehq/go-libs/publish" "github.com/google/uuid" "github.com/stretchr/testify/require" + "github.com/uptrace/bun" + "go.temporal.io/sdk/client" + worker "go.temporal.io/sdk/worker" ) func TestWorkflow(t *testing.T) { @@ -43,16 +41,16 @@ func TestWorkflow(t *testing.T) { taskQueue := uuid.NewString() workflowManager := workflow.NewManager(db, devServer.Client(), taskQueue, false) - worker := temporalworker.New(logging.Testing(), devServer.Client(), taskQueue, - []temporalworker.DefinitionSet{ + worker := temporal.New(context.Background(), logging.Testing(), devServer.Client(), taskQueue, + []temporal.DefinitionSet{ NewWorkflow(taskQueue, false).DefinitionSet(), workflow.NewWorkflows(false).DefinitionSet(), - temporalworker.NewDefinitionSet().Append(temporalworker.Definition{ + temporal.NewDefinitionSet().Append(temporal.Definition{ Name: "NoOp", Func: (&stages.NoOp{}).GetWorkflow(), }), }, - []temporalworker.DefinitionSet{ + []temporal.DefinitionSet{ workflow.NewActivities(publish.NoOpPublisher, db).DefinitionSet(), NewActivities(db, workflowManager, NewDefaultExpressionEvaluator(), publish.NoOpPublisher).DefinitionSet(), }, diff --git a/ee/orchestration/internal/workflow/activities.go b/ee/orchestration/internal/workflow/activities.go index b06d487cec..7fee6faa12 100644 --- a/ee/orchestration/internal/workflow/activities.go +++ b/ee/orchestration/internal/workflow/activities.go @@ -4,7 +4,7 @@ import ( "context" "github.com/ThreeDotsLabs/watermill/message" - "github.com/formancehq/orchestration/internal/temporalworker" + "github.com/formancehq/go-libs/temporal" "github.com/formancehq/orchestration/pkg/events" "github.com/uptrace/bun" "go.temporal.io/sdk/activity" @@ -107,30 +107,30 @@ func (a Activities) UpdateStage(ctx context.Context, stage Stage) error { return err } -func (a Activities) DefinitionSet() temporalworker.DefinitionSet { - return temporalworker.NewDefinitionSet(). - Append(temporalworker.Definition{ +func (a Activities) DefinitionSet() temporal.DefinitionSet { + return temporal.NewDefinitionSet(). + Append(temporal.Definition{ Func: a.InsertNewInstance, Name: "InsertNewInstance", - }).Append(temporalworker.Definition{ + }).Append(temporal.Definition{ Func: a.InsertNewStage, Name: "InsertNewStage", - }).Append(temporalworker.Definition{ + }).Append(temporal.Definition{ Func: a.SendWorkflowStageStartedEvent, Name: "SendWorkflowStageStartedEvent", - }).Append(temporalworker.Definition{ + }).Append(temporal.Definition{ Func: a.SendWorkflowStageTerminationEvent, Name: "SendWorkflowStageTerminationEvent", - }).Append(temporalworker.Definition{ + }).Append(temporal.Definition{ Func: a.SendWorkflowStartedEvent, Name: "SendWorkflowStartedEvent", - }).Append(temporalworker.Definition{ + }).Append(temporal.Definition{ Func: a.SendWorkflowTerminationEvent, Name: "SendWorkflowTerminationEvent", - }).Append(temporalworker.Definition{ + }).Append(temporal.Definition{ Func: a.UpdateStage, Name: "UpdateStage", - }).Append(temporalworker.Definition{ + }).Append(temporal.Definition{ Func: a.UpdateInstance, Name: "UpdateInstance", }) diff --git a/ee/orchestration/internal/workflow/activities/activity.go b/ee/orchestration/internal/workflow/activities/activity.go index 1e035bf30b..2bf2808547 100644 --- a/ee/orchestration/internal/workflow/activities/activity.go +++ b/ee/orchestration/internal/workflow/activities/activity.go @@ -6,7 +6,7 @@ import ( sdk "github.com/formancehq/formance-sdk-go/v2" "github.com/formancehq/go-libs/pointer" - "github.com/formancehq/orchestration/internal/temporalworker" + temporalworker "github.com/formancehq/go-libs/temporal" "github.com/pkg/errors" "go.temporal.io/sdk/activity" "go.temporal.io/sdk/temporal" diff --git a/ee/orchestration/internal/workflow/manager_test.go b/ee/orchestration/internal/workflow/manager_test.go index edaf2f6403..c36ebb4af5 100644 --- a/ee/orchestration/internal/workflow/manager_test.go +++ b/ee/orchestration/internal/workflow/manager_test.go @@ -1,23 +1,21 @@ package workflow import ( + "context" "testing" "time" + "github.com/formancehq/go-libs/bun/bunconnect" "github.com/formancehq/go-libs/bun/bundebug" - "github.com/uptrace/bun" - "go.temporal.io/sdk/worker" - "github.com/formancehq/go-libs/logging" "github.com/formancehq/go-libs/publish" - "github.com/formancehq/orchestration/internal/temporalworker" + "github.com/formancehq/go-libs/temporal" + "github.com/formancehq/orchestration/internal/storage" "github.com/formancehq/orchestration/internal/workflow/stages" "github.com/google/uuid" - - "github.com/formancehq/go-libs/bun/bunconnect" - - "github.com/formancehq/orchestration/internal/storage" "github.com/stretchr/testify/require" + "github.com/uptrace/bun" + "go.temporal.io/sdk/worker" ) func TestConfig(t *testing.T) { @@ -39,15 +37,15 @@ func TestConfig(t *testing.T) { require.NoError(t, storage.Migrate(logging.TestingContext(), db)) taskQueue := uuid.NewString() - worker := temporalworker.New(logging.Testing(), devServer.Client(), taskQueue, - []temporalworker.DefinitionSet{ + worker := temporal.New(context.Background(), logging.Testing(), devServer.Client(), taskQueue, + []temporal.DefinitionSet{ NewWorkflows(false).DefinitionSet(), - temporalworker.NewDefinitionSet().Append(temporalworker.Definition{ + temporal.NewDefinitionSet().Append(temporal.Definition{ Name: "NoOp", Func: (&stages.NoOp{}).GetWorkflow(), }), }, - []temporalworker.DefinitionSet{ + []temporal.DefinitionSet{ NewActivities(publish.NoOpPublisher, db).DefinitionSet(), }, worker.Options{}, diff --git a/ee/orchestration/internal/workflow/module.go b/ee/orchestration/internal/workflow/module.go index bb2a7e88eb..106fb01839 100644 --- a/ee/orchestration/internal/workflow/module.go +++ b/ee/orchestration/internal/workflow/module.go @@ -1,7 +1,7 @@ package workflow import ( - "github.com/formancehq/orchestration/internal/temporalworker" + "github.com/formancehq/go-libs/temporal" "github.com/formancehq/orchestration/internal/workflow/activities" "github.com/formancehq/orchestration/internal/workflow/stages" "github.com/iancoleman/strcase" @@ -20,20 +20,20 @@ func NewModule(taskQueue string) fx.Option { }), fx.Provide(activities.New), fx.Provide(NewActivities), - fx.Provide(fx.Annotate(func(a activities.Activities) temporalworker.DefinitionSet { + fx.Provide(fx.Annotate(func(a activities.Activities) temporal.DefinitionSet { return a.DefinitionSet() }, fx.ResultTags(`group:"activities"`))), - fx.Provide(fx.Annotate(func(a Activities) temporalworker.DefinitionSet { + fx.Provide(fx.Annotate(func(a Activities) temporal.DefinitionSet { return a.DefinitionSet() }, fx.ResultTags(`group:"activities"`))), - fx.Provide(fx.Annotate(func(workflow *Workflows) temporalworker.DefinitionSet { + fx.Provide(fx.Annotate(func(workflow *Workflows) temporal.DefinitionSet { return workflow.DefinitionSet() }, fx.ResultTags(`group:"workflows"`))), } - set := temporalworker.NewDefinitionSet() + set := temporal.NewDefinitionSet() for name, schema := range stages.All() { - set = set.Append(temporalworker.Definition{ + set = set.Append(temporal.Definition{ Name: "Run" + strcase.ToCamel(name), Func: schema.GetWorkflow(), }) diff --git a/ee/orchestration/internal/workflow/run.go b/ee/orchestration/internal/workflow/run.go index 6eb9034dc7..5a7ef69396 100644 --- a/ee/orchestration/internal/workflow/run.go +++ b/ee/orchestration/internal/workflow/run.go @@ -3,8 +3,7 @@ package workflow import ( "time" - "github.com/formancehq/orchestration/internal/temporalworker" - + "github.com/formancehq/go-libs/temporal" "go.temporal.io/api/enums/v1" "go.temporal.io/sdk/workflow" ) @@ -79,12 +78,12 @@ func (w Workflows) Run(ctx workflow.Context, i Input, instance Instance) error { return nil } -func (w Workflows) DefinitionSet() temporalworker.DefinitionSet { - return temporalworker.NewDefinitionSet(). - Append(temporalworker.Definition{ +func (w Workflows) DefinitionSet() temporal.DefinitionSet { + return temporal.NewDefinitionSet(). + Append(temporal.Definition{ Func: w.Run, Name: "Run", - }).Append(temporalworker.Definition{ + }).Append(temporal.Definition{ Func: w.Initiate, Name: "Initiate", }) diff --git a/tests/integration/go.mod b/tests/integration/go.mod index 8c9c4ce672..3b57db6cce 100644 --- a/tests/integration/go.mod +++ b/tests/integration/go.mod @@ -9,7 +9,7 @@ require ( github.com/egymgmbh/go-prefix-writer v0.0.0-20180609083313-7326ea162eca github.com/formancehq/auth v0.0.0-00010101000000-000000000000 github.com/formancehq/formance-sdk-go/v2 v2.0.0-00010101000000-000000000000 - github.com/formancehq/go-libs v1.7.1 + github.com/formancehq/go-libs v1.7.2-0.20240925132527-7627842ea9b5 github.com/formancehq/ledger v0.0.0-00010101000000-000000000000 github.com/formancehq/orchestration v0.0.0-00010101000000-000000000000 github.com/formancehq/payments v0.0.0-00010101000000-000000000000 @@ -50,16 +50,14 @@ require ( github.com/ThreeDotsLabs/watermill-http/v2 v2.3.1 // indirect github.com/ThreeDotsLabs/watermill-kafka/v3 v3.0.5 // indirect github.com/ThreeDotsLabs/watermill-nats/v2 v2.1.1 // indirect - github.com/adyen/adyen-go-api-library/v7 v7.3.1 // indirect github.com/ajg/form v1.5.1 // indirect github.com/alitto/pond v1.9.2 // indirect github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect github.com/aquasecurity/esquery v0.2.0 // indirect - github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aws/aws-msk-iam-sasl-signer-go v1.0.0 // indirect github.com/aws/aws-sdk-go-v2 v1.31.0 // indirect - github.com/aws/aws-sdk-go-v2/config v1.27.36 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.17.34 // indirect + github.com/aws/aws-sdk-go-v2/config v1.27.37 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.35 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14 // indirect github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.4.18 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.18 // indirect @@ -67,9 +65,9 @@ require ( github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.5 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.23.0 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.31.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.23.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.31.1 // indirect github.com/aws/smithy-go v1.21.0 // indirect github.com/bluele/gcache v0.0.2 // indirect github.com/bombsimon/logrusr/v3 v3.1.0 // indirect @@ -93,9 +91,7 @@ require ( github.com/fatih/color v1.17.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/formancehq/payments/genericclient v0.0.0-00010101000000-000000000000 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect - github.com/get-momo/atlar-v1-go-client v1.2.1 // indirect github.com/gibson042/canonicaljson-go v1.0.3 // indirect github.com/go-chi/chi v4.1.2+incompatible // indirect github.com/go-chi/chi/v5 v5.1.0 // indirect @@ -104,16 +100,6 @@ require ( github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect - github.com/go-openapi/analysis v0.21.4 // indirect - github.com/go-openapi/errors v0.20.4 // indirect - github.com/go-openapi/jsonpointer v0.20.0 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/loads v0.21.2 // indirect - github.com/go-openapi/runtime v0.26.0 // indirect - github.com/go-openapi/spec v0.20.9 // indirect - github.com/go-openapi/strfmt v0.21.8 // indirect - github.com/go-openapi/swag v0.22.4 // indirect - github.com/go-openapi/validate v0.22.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.22.1 // indirect @@ -124,6 +110,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/mock v1.6.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect @@ -136,10 +123,12 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-plugin v1.6.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect - github.com/hashicorp/golang-lru/v2 v2.0.4 // indirect + github.com/hashicorp/yamux v0.1.1 // indirect github.com/iancoleman/strcase v0.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect @@ -155,7 +144,6 @@ require ( github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/jinzhu/inflection v1.0.0 // indirect - github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect @@ -168,10 +156,9 @@ require ( github.com/lithammer/shortuuid/v3 v3.0.7 // indirect github.com/logrusorgru/aurora v2.0.3+incompatible // indirect github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect - github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/term v0.5.0 // indirect github.com/morikuni/aec v1.0.0 // indirect @@ -180,13 +167,13 @@ require ( github.com/nats-io/nkeys v0.4.7 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/nexus-rpc/sdk-go v0.0.10 // indirect + github.com/oklog/run v1.0.0 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opencontainers/runc v1.1.14 // indirect github.com/opensearch-project/opensearch-go v1.1.0 // indirect github.com/opensearch-project/opensearch-go/v2 v2.3.0 // indirect - github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect @@ -197,18 +184,15 @@ require ( github.com/rs/cors v1.11.1 // indirect github.com/shirou/gopsutil/v4 v4.24.8 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect - github.com/spf13/afero v1.11.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/testify v1.9.0 // indirect - github.com/stripe/stripe-go/v72 v72.122.0 // indirect github.com/tidwall/gjson v1.14.4 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tklauser/go-sysconf v0.3.14 // indirect github.com/tklauser/numcpus v0.8.0 // indirect github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect - github.com/uptrace/bun/extra/bundebug v1.2.3 // indirect github.com/uptrace/bun/extra/bunotel v1.2.3 // indirect github.com/uptrace/opentelemetry-go-extra/otellogrus v0.3.2 // indirect github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect @@ -223,7 +207,6 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/zitadel/logging v0.3.4 // indirect - go.mongodb.org/mongo-driver v1.12.0 // indirect go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.44.0 // indirect go.opentelemetry.io/contrib/instrumentation/host v0.55.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect @@ -276,7 +259,7 @@ replace ( github.com/formancehq/ledger => ../../components/ledger github.com/formancehq/orchestration => ../../ee/orchestration github.com/formancehq/payments => ../../components/payments - github.com/formancehq/payments/genericclient => ../../components/payments/cmd/connectors/internal/connectors/generic/client/generated + // github.com/formancehq/payments/genericclient => ../../components/payments/cmd/connectors/internal/connectors/generic/client/generated github.com/formancehq/reconciliation => ../../ee/reconciliation github.com/formancehq/search => ../../ee/search github.com/formancehq/stack/libs/events => ../../libs/events diff --git a/tests/integration/go.sum b/tests/integration/go.sum index 754ef699b0..ab2f06a83d 100644 --- a/tests/integration/go.sum +++ b/tests/integration/go.sum @@ -1,618 +1,17 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= -cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= -cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= -cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= -cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= -cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= -cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= -cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= -cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= -cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= -cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= -cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= -cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= -cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= -cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= -cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= -cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw= -cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= -cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= -cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= -cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o= -cloud.google.com/go/accesscontextmanager v1.4.0/go.mod h1:/Kjh7BBu/Gh83sv+K60vN9QE5NJcd80sU33vIe2IFPE= -cloud.google.com/go/accesscontextmanager v1.6.0/go.mod h1:8XCvZWfYw3K/ji0iVnp+6pu7huxoQTLmxAbVjbloTtM= -cloud.google.com/go/accesscontextmanager v1.7.0/go.mod h1:CEGLewx8dwa33aDAZQujl7Dx+uYhS0eay198wB/VumQ= -cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= -cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= -cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg= -cloud.google.com/go/aiplatform v1.35.0/go.mod h1:7MFT/vCaOyZT/4IIFfxH4ErVg/4ku6lKv3w0+tFTgXQ= -cloud.google.com/go/aiplatform v1.36.1/go.mod h1:WTm12vJRPARNvJ+v6P52RDHCNe4AhvjcIZ/9/RRHy/k= -cloud.google.com/go/aiplatform v1.37.0/go.mod h1:IU2Cv29Lv9oCn/9LkFiiuKfwrRTq+QQMbW+hPCxJGZw= -cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= -cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= -cloud.google.com/go/analytics v0.17.0/go.mod h1:WXFa3WSym4IZ+JiKmavYdJwGG/CvpqiqczmL59bTD9M= -cloud.google.com/go/analytics v0.18.0/go.mod h1:ZkeHGQlcIPkw0R/GW+boWHhCOR43xz9RN/jn7WcqfIE= -cloud.google.com/go/analytics v0.19.0/go.mod h1:k8liqf5/HCnOUkbawNtrWWc+UAzyDlW89doe8TtoDsE= -cloud.google.com/go/apigateway v1.3.0/go.mod h1:89Z8Bhpmxu6AmUxuVRg/ECRGReEdiP3vQtk4Z1J9rJk= -cloud.google.com/go/apigateway v1.4.0/go.mod h1:pHVY9MKGaH9PQ3pJ4YLzoj6U5FUDeDFBllIz7WmzJoc= -cloud.google.com/go/apigateway v1.5.0/go.mod h1:GpnZR3Q4rR7LVu5951qfXPJCHquZt02jf7xQx7kpqN8= -cloud.google.com/go/apigeeconnect v1.3.0/go.mod h1:G/AwXFAKo0gIXkPTVfZDd2qA1TxBXJ3MgMRBQkIi9jc= -cloud.google.com/go/apigeeconnect v1.4.0/go.mod h1:kV4NwOKqjvt2JYR0AoIWo2QGfoRtn/pkS3QlHp0Ni04= -cloud.google.com/go/apigeeconnect v1.5.0/go.mod h1:KFaCqvBRU6idyhSNyn3vlHXc8VMDJdRmwDF6JyFRqZ8= -cloud.google.com/go/apigeeregistry v0.4.0/go.mod h1:EUG4PGcsZvxOXAdyEghIdXwAEi/4MEaoqLMLDMIwKXY= -cloud.google.com/go/apigeeregistry v0.5.0/go.mod h1:YR5+s0BVNZfVOUkMa5pAR2xGd0A473vA5M7j247o1wM= -cloud.google.com/go/apigeeregistry v0.6.0/go.mod h1:BFNzW7yQVLZ3yj0TKcwzb8n25CFBri51GVGOEUcgQsc= -cloud.google.com/go/apikeys v0.4.0/go.mod h1:XATS/yqZbaBK0HOssf+ALHp8jAlNHUgyfprvNcBIszU= -cloud.google.com/go/apikeys v0.5.0/go.mod h1:5aQfwY4D+ewMMWScd3hm2en3hCj+BROlyrt3ytS7KLI= -cloud.google.com/go/apikeys v0.6.0/go.mod h1:kbpXu5upyiAlGkKrJgQl8A0rKNNJ7dQ377pdroRSSi8= -cloud.google.com/go/appengine v1.4.0/go.mod h1:CS2NhuBuDXM9f+qscZ6V86m1MIIqPj3WC/UoEuR1Sno= -cloud.google.com/go/appengine v1.5.0/go.mod h1:TfasSozdkFI0zeoxW3PTBLiNqRmzraodCWatWI9Dmak= -cloud.google.com/go/appengine v1.6.0/go.mod h1:hg6i0J/BD2cKmDJbaFSYHFyZkgBEfQrDg/X0V5fJn84= -cloud.google.com/go/appengine v1.7.0/go.mod h1:eZqpbHFCqRGa2aCdope7eC0SWLV1j0neb/QnMJVWx6A= -cloud.google.com/go/appengine v1.7.1/go.mod h1:IHLToyb/3fKutRysUlFO0BPt5j7RiQ45nrzEJmKTo6E= -cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= -cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= -cloud.google.com/go/area120 v0.7.0/go.mod h1:a3+8EUD1SX5RUcCs3MY5YasiO1z6yLiNLRiFrykbynY= -cloud.google.com/go/area120 v0.7.1/go.mod h1:j84i4E1RboTWjKtZVWXPqvK5VHQFJRF2c1Nm69pWm9k= -cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= -cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= -cloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0= -cloud.google.com/go/artifactregistry v1.9.0/go.mod h1:2K2RqvA2CYvAeARHRkLDhMDJ3OXy26h3XW+3/Jh2uYc= -cloud.google.com/go/artifactregistry v1.11.1/go.mod h1:lLYghw+Itq9SONbCa1YWBoWs1nOucMH0pwXN1rOBZFI= -cloud.google.com/go/artifactregistry v1.11.2/go.mod h1:nLZns771ZGAwVLzTX/7Al6R9ehma4WUEhZGWV6CeQNQ= -cloud.google.com/go/artifactregistry v1.12.0/go.mod h1:o6P3MIvtzTOnmvGagO9v/rOjjA0HmhJ+/6KAXrmYDCI= -cloud.google.com/go/artifactregistry v1.13.0/go.mod h1:uy/LNfoOIivepGhooAUpL1i30Hgee3Cu0l4VTWHUC08= -cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= -cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= -cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= -cloud.google.com/go/asset v1.9.0/go.mod h1:83MOE6jEJBMqFKadM9NLRcs80Gdw76qGuHn8m3h8oHQ= -cloud.google.com/go/asset v1.10.0/go.mod h1:pLz7uokL80qKhzKr4xXGvBQXnzHn5evJAEAtZiIb0wY= -cloud.google.com/go/asset v1.11.1/go.mod h1:fSwLhbRvC9p9CXQHJ3BgFeQNM4c9x10lqlrdEUYXlJo= -cloud.google.com/go/asset v1.12.0/go.mod h1:h9/sFOa4eDIyKmH6QMpm4eUK3pDojWnUhTgJlk762Hg= -cloud.google.com/go/asset v1.13.0/go.mod h1:WQAMyYek/b7NBpYq/K4KJWcRqzoalEsxz/t/dTk4THw= -cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= -cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= -cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= -cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= -cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= -cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= -cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= -cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= -cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= -cloud.google.com/go/automl v1.8.0/go.mod h1:xWx7G/aPEe/NP+qzYXktoBSDfjO+vnKMGgsApGJJquM= -cloud.google.com/go/automl v1.12.0/go.mod h1:tWDcHDp86aMIuHmyvjuKeeHEGq76lD7ZqfGLN6B0NuU= -cloud.google.com/go/baremetalsolution v0.3.0/go.mod h1:XOrocE+pvK1xFfleEnShBlNAXf+j5blPPxrhjKgnIFc= -cloud.google.com/go/baremetalsolution v0.4.0/go.mod h1:BymplhAadOO/eBa7KewQ0Ppg4A4Wplbn+PsFKRLo0uI= -cloud.google.com/go/baremetalsolution v0.5.0/go.mod h1:dXGxEkmR9BMwxhzBhV0AioD0ULBmuLZI8CdwalUxuss= -cloud.google.com/go/batch v0.3.0/go.mod h1:TR18ZoAekj1GuirsUsR1ZTKN3FC/4UDnScjT8NXImFE= -cloud.google.com/go/batch v0.4.0/go.mod h1:WZkHnP43R/QCGQsZ+0JyG4i79ranE2u8xvjq/9+STPE= -cloud.google.com/go/batch v0.7.0/go.mod h1:vLZN95s6teRUqRQ4s3RLDsH8PvboqBK+rn1oevL159g= -cloud.google.com/go/beyondcorp v0.2.0/go.mod h1:TB7Bd+EEtcw9PCPQhCJtJGjk/7TC6ckmnSFS+xwTfm4= -cloud.google.com/go/beyondcorp v0.3.0/go.mod h1:E5U5lcrcXMsCuoDNyGrpyTm/hn7ne941Jz2vmksAxW8= -cloud.google.com/go/beyondcorp v0.4.0/go.mod h1:3ApA0mbhHx6YImmuubf5pyW8srKnCEPON32/5hj+RmM= -cloud.google.com/go/beyondcorp v0.5.0/go.mod h1:uFqj9X+dSfrheVp7ssLTaRHd2EHqSL4QZmH4e8WXGGU= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= -cloud.google.com/go/bigquery v1.43.0/go.mod h1:ZMQcXHsl+xmU1z36G2jNGZmKp9zNY5BUua5wDgmNCfw= -cloud.google.com/go/bigquery v1.44.0/go.mod h1:0Y33VqXTEsbamHJvJHdFmtqHvMIY28aK1+dFsvaChGc= -cloud.google.com/go/bigquery v1.47.0/go.mod h1:sA9XOgy0A8vQK9+MWhEQTY6Tix87M/ZurWFIxmF9I/E= -cloud.google.com/go/bigquery v1.48.0/go.mod h1:QAwSz+ipNgfL5jxiaK7weyOhzdoAy1zFm0Nf1fysJac= -cloud.google.com/go/bigquery v1.49.0/go.mod h1:Sv8hMmTFFYBlt/ftw2uN6dFdQPzBlREY9yBh7Oy7/4Q= -cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU= -cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= -cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= -cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI= -cloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y= -cloud.google.com/go/billing v1.12.0/go.mod h1:yKrZio/eu+okO/2McZEbch17O5CB5NpZhhXG6Z766ss= -cloud.google.com/go/billing v1.13.0/go.mod h1:7kB2W9Xf98hP9Sr12KfECgfGclsH3CQR0R08tnRlRbc= -cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= -cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= -cloud.google.com/go/binaryauthorization v1.3.0/go.mod h1:lRZbKgjDIIQvzYQS1p99A7/U1JqvqeZg0wiI5tp6tg0= -cloud.google.com/go/binaryauthorization v1.4.0/go.mod h1:tsSPQrBd77VLplV70GUhBf/Zm3FsKmgSqgm4UmiDItk= -cloud.google.com/go/binaryauthorization v1.5.0/go.mod h1:OSe4OU1nN/VswXKRBmciKpo9LulY41gch5c68htf3/Q= -cloud.google.com/go/certificatemanager v1.3.0/go.mod h1:n6twGDvcUBFu9uBgt4eYvvf3sQ6My8jADcOVwHmzadg= -cloud.google.com/go/certificatemanager v1.4.0/go.mod h1:vowpercVFyqs8ABSmrdV+GiFf2H/ch3KyudYQEMM590= -cloud.google.com/go/certificatemanager v1.6.0/go.mod h1:3Hh64rCKjRAX8dXgRAyOcY5vQ/fE1sh8o+Mdd6KPgY8= -cloud.google.com/go/channel v1.8.0/go.mod h1:W5SwCXDJsq/rg3tn3oG0LOxpAo6IMxNa09ngphpSlnk= -cloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL8yBMbKUk= -cloud.google.com/go/channel v1.11.0/go.mod h1:IdtI0uWGqhEeatSB62VOoJ8FSUhJ9/+iGkJVqp74CGE= -cloud.google.com/go/channel v1.12.0/go.mod h1:VkxCGKASi4Cq7TbXxlaBezonAYpp1GCnKMY6tnMQnLU= -cloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U= -cloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA= -cloud.google.com/go/cloudbuild v1.6.0/go.mod h1:UIbc/w9QCbH12xX+ezUsgblrWv+Cv4Tw83GiSMHOn9M= -cloud.google.com/go/cloudbuild v1.7.0/go.mod h1:zb5tWh2XI6lR9zQmsm1VRA+7OCuve5d8S+zJUul8KTg= -cloud.google.com/go/cloudbuild v1.9.0/go.mod h1:qK1d7s4QlO0VwfYn5YuClDGg2hfmLZEb4wQGAbIgL1s= -cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM= -cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk= -cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA= -cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= -cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= -cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4= -cloud.google.com/go/cloudtasks v1.8.0/go.mod h1:gQXUIwCSOI4yPVK7DgTVFiiP0ZW/eQkydWzwVMdHxrI= -cloud.google.com/go/cloudtasks v1.9.0/go.mod h1:w+EyLsVkLWHcOaqNEyvcKAsWp9p29dL6uL9Nst1cI7Y= -cloud.google.com/go/cloudtasks v1.10.0/go.mod h1:NDSoTLkZ3+vExFEWu2UJV1arUyzVDAiZtdWcsUyNwBs= -cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= -cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= -cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= -cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= -cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= -cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= -cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= -cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= -cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= -cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE= -cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= -cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= -cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= -cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= -cloud.google.com/go/compute v1.19.3/go.mod h1:qxvISKp/gYnXkSAD1ppcSOveRAmzxicEv/JlizULFrI= -cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= -cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= -cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= -cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= -cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= -cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= -cloud.google.com/go/container v1.6.0/go.mod h1:Xazp7GjJSeUYo688S+6J5V+n/t+G5sKBTFkKNudGRxg= -cloud.google.com/go/container v1.7.0/go.mod h1:Dp5AHtmothHGX3DwwIHPgq45Y8KmNsgN3amoYfxVkLo= -cloud.google.com/go/container v1.13.1/go.mod h1:6wgbMPeQRw9rSnKBCAJXnds3Pzj03C4JHamr8asWKy4= -cloud.google.com/go/container v1.14.0/go.mod h1:3AoJMPhHfLDxLvrlVWaK57IXzaPnLaZq63WX59aQBfM= -cloud.google.com/go/container v1.15.0/go.mod h1:ft+9S0WGjAyjDggg5S06DXj+fHJICWg8L7isCQe9pQA= -cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= -cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= -cloud.google.com/go/containeranalysis v0.7.0/go.mod h1:9aUL+/vZ55P2CXfuZjS4UjQ9AgXoSw8Ts6lemfmxBxI= -cloud.google.com/go/containeranalysis v0.9.0/go.mod h1:orbOANbwk5Ejoom+s+DUCTTJ7IBdBQJDcSylAx/on9s= -cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= -cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= -cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= -cloud.google.com/go/datacatalog v1.7.0/go.mod h1:9mEl4AuDYWw81UGc41HonIHH7/sn52H0/tc8f8ZbZIE= -cloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOXxZoKYF5wdISM= -cloud.google.com/go/datacatalog v1.8.1/go.mod h1:RJ58z4rMp3gvETA465Vg+ag8BGgBdnRPEMMSTr5Uv+M= -cloud.google.com/go/datacatalog v1.12.0/go.mod h1:CWae8rFkfp6LzLumKOnmVh4+Zle4A3NXLzVJ1d1mRm0= -cloud.google.com/go/datacatalog v1.13.0/go.mod h1:E4Rj9a5ZtAxcQJlEBTLgMTphfP11/lNaAshpoBgemX8= -cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= -cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= -cloud.google.com/go/dataflow v0.8.0/go.mod h1:Rcf5YgTKPtQyYz8bLYhFoIV/vP39eL7fWNcSOyFfLJE= -cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= -cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= -cloud.google.com/go/dataform v0.5.0/go.mod h1:GFUYRe8IBa2hcomWplodVmUx/iTL0FrsauObOM3Ipr0= -cloud.google.com/go/dataform v0.6.0/go.mod h1:QPflImQy33e29VuapFdf19oPbE4aYTJxr31OAPV+ulA= -cloud.google.com/go/dataform v0.7.0/go.mod h1:7NulqnVozfHvWUBpMDfKMUESr+85aJsC/2O0o3jWPDE= -cloud.google.com/go/datafusion v1.4.0/go.mod h1:1Zb6VN+W6ALo85cXnM1IKiPw+yQMKMhB9TsTSRDo/38= -cloud.google.com/go/datafusion v1.5.0/go.mod h1:Kz+l1FGHB0J+4XF2fud96WMmRiq/wj8N9u007vyXZ2w= -cloud.google.com/go/datafusion v1.6.0/go.mod h1:WBsMF8F1RhSXvVM8rCV3AeyWVxcC2xY6vith3iw3S+8= -cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= -cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= -cloud.google.com/go/datalabeling v0.7.0/go.mod h1:WPQb1y08RJbmpM3ww0CSUAGweL0SxByuW2E+FU+wXcM= -cloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX8gdmFcZzA= -cloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A= -cloud.google.com/go/dataplex v1.5.2/go.mod h1:cVMgQHsmfRoI5KFYq4JtIBEUbYwc3c7tXmIDhRmNNVQ= -cloud.google.com/go/dataplex v1.6.0/go.mod h1:bMsomC/aEJOSpHXdFKFGQ1b0TDPIeL28nJObeO1ppRs= -cloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s= -cloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI= -cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4= -cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= -cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= -cloud.google.com/go/dataqna v0.7.0/go.mod h1:Lx9OcIIeqCrw1a6KdO3/5KMP1wAmTc0slZWwP12Qq3c= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/datastore v1.10.0/go.mod h1:PC5UzAmDEkAmkfaknstTYbNpgE49HAgW2J1gcgUfmdM= -cloud.google.com/go/datastore v1.11.0/go.mod h1:TvGxBIHCS50u8jzG+AW/ppf87v1of8nwzFNgEZU1D3c= -cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= -cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= -cloud.google.com/go/datastream v1.4.0/go.mod h1:h9dpzScPhDTs5noEMQVWP8Wx8AFBRyS0s8KWPx/9r0g= -cloud.google.com/go/datastream v1.5.0/go.mod h1:6TZMMNPwjUqZHBKPQ1wwXpb0d5VDVPl2/XoS5yi88q4= -cloud.google.com/go/datastream v1.6.0/go.mod h1:6LQSuswqLa7S4rPAOZFVjHIG3wJIjZcZrw8JDEDJuIs= -cloud.google.com/go/datastream v1.7.0/go.mod h1:uxVRMm2elUSPuh65IbZpzJNMbuzkcvu5CjMqVIUHrww= -cloud.google.com/go/deploy v1.4.0/go.mod h1:5Xghikd4VrmMLNaF6FiRFDlHb59VM59YoDQnOUdsH/c= -cloud.google.com/go/deploy v1.5.0/go.mod h1:ffgdD0B89tToyW/U/D2eL0jN2+IEV/3EMuXHA0l4r+s= -cloud.google.com/go/deploy v1.6.0/go.mod h1:f9PTHehG/DjCom3QH0cntOVRm93uGBDt2vKzAPwpXQI= -cloud.google.com/go/deploy v1.8.0/go.mod h1:z3myEJnA/2wnB4sgjqdMfgxCA0EqC3RBTNcVPs93mtQ= -cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= -cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= -cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= -cloud.google.com/go/dialogflow v1.18.0/go.mod h1:trO7Zu5YdyEuR+BhSNOqJezyFQ3aUzz0njv7sMx/iek= -cloud.google.com/go/dialogflow v1.19.0/go.mod h1:JVmlG1TwykZDtxtTXujec4tQ+D8SBFMoosgy+6Gn0s0= -cloud.google.com/go/dialogflow v1.29.0/go.mod h1:b+2bzMe+k1s9V+F2jbJwpHPzrnIyHihAdRFMtn2WXuM= -cloud.google.com/go/dialogflow v1.31.0/go.mod h1:cuoUccuL1Z+HADhyIA7dci3N5zUssgpBJmCzI6fNRB4= -cloud.google.com/go/dialogflow v1.32.0/go.mod h1:jG9TRJl8CKrDhMEcvfcfFkkpp8ZhgPz3sBGmAUYJ2qE= -cloud.google.com/go/dlp v1.6.0/go.mod h1:9eyB2xIhpU0sVwUixfBubDoRwP+GjeUoxxeueZmqvmM= -cloud.google.com/go/dlp v1.7.0/go.mod h1:68ak9vCiMBjbasxeVD17hVPxDEck+ExiHavX8kiHG+Q= -cloud.google.com/go/dlp v1.9.0/go.mod h1:qdgmqgTyReTz5/YNSSuueR8pl7hO0o9bQ39ZhtgkWp4= -cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= -cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= -cloud.google.com/go/documentai v1.9.0/go.mod h1:FS5485S8R00U10GhgBC0aNGrJxBP8ZVpEeJ7PQDZd6k= -cloud.google.com/go/documentai v1.10.0/go.mod h1:vod47hKQIPeCfN2QS/jULIvQTugbmdc0ZvxxfQY1bg4= -cloud.google.com/go/documentai v1.16.0/go.mod h1:o0o0DLTEZ+YnJZ+J4wNfTxmDVyrkzFvttBXXtYRMHkM= -cloud.google.com/go/documentai v1.18.0/go.mod h1:F6CK6iUH8J81FehpskRmhLq/3VlwQvb7TvwOceQ2tbs= -cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= -cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= -cloud.google.com/go/domains v0.8.0/go.mod h1:M9i3MMDzGFXsydri9/vW+EWz9sWb4I6WyHqdlAk0idE= -cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= -cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= -cloud.google.com/go/edgecontainer v0.3.0/go.mod h1:FLDpP4nykgwwIfcLt6zInhprzw0lEi2P1fjO6Ie0qbc= -cloud.google.com/go/edgecontainer v1.0.0/go.mod h1:cttArqZpBB2q58W/upSG++ooo6EsblxDIolxa3jSjbY= -cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= -cloud.google.com/go/essentialcontacts v1.3.0/go.mod h1:r+OnHa5jfj90qIfZDO/VztSFqbQan7HV75p8sA+mdGI= -cloud.google.com/go/essentialcontacts v1.4.0/go.mod h1:8tRldvHYsmnBCHdFpvU+GL75oWiBKl80BiqlFh9tp+8= -cloud.google.com/go/essentialcontacts v1.5.0/go.mod h1:ay29Z4zODTuwliK7SnX8E86aUF2CTzdNtvv42niCX0M= -cloud.google.com/go/eventarc v1.7.0/go.mod h1:6ctpF3zTnaQCxUjHUdcfgcA1A2T309+omHZth7gDfmc= -cloud.google.com/go/eventarc v1.8.0/go.mod h1:imbzxkyAU4ubfsaKYdQg04WS1NvncblHEup4kvF+4gw= -cloud.google.com/go/eventarc v1.10.0/go.mod h1:u3R35tmZ9HvswGRBnF48IlYgYeBcPUCjkr4BTdem2Kw= -cloud.google.com/go/eventarc v1.11.0/go.mod h1:PyUjsUKPWoRBCHeOxZd/lbOOjahV41icXyUY5kSTvVY= -cloud.google.com/go/filestore v1.3.0/go.mod h1:+qbvHGvXU1HaKX2nD0WEPo92TP/8AQuCVEBXNY9z0+w= -cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLYoINEK8XQAI= -cloud.google.com/go/filestore v1.5.0/go.mod h1:FqBXDWBp4YLHqRnVGveOkHDf8svj9r5+mUDLupOWEDs= -cloud.google.com/go/filestore v1.6.0/go.mod h1:di5unNuss/qfZTw2U9nhFqo8/ZDSc466dre85Kydllg= -cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= -cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= -cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= -cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY= -cloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5UwtlpzoyquQ08= -cloud.google.com/go/functions v1.10.0/go.mod h1:0D3hEOe3DbEvCXtYOZHQZmD+SzYsi1YbI7dGvHfldXw= -cloud.google.com/go/functions v1.12.0/go.mod h1:AXWGrF3e2C/5ehvwYo/GH6O5s09tOPksiKhz+hH8WkA= -cloud.google.com/go/functions v1.13.0/go.mod h1:EU4O007sQm6Ef/PwRsI8N2umygGqPBS/IZQKBQBcJ3c= -cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= -cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= -cloud.google.com/go/gaming v1.7.0/go.mod h1:LrB8U7MHdGgFG851iHAfqUdLcKBdQ55hzXy9xBJz0+w= -cloud.google.com/go/gaming v1.8.0/go.mod h1:xAqjS8b7jAVW0KFYeRUxngo9My3f33kFmua++Pi+ggM= -cloud.google.com/go/gaming v1.9.0/go.mod h1:Fc7kEmCObylSWLO334NcO+O9QMDyz+TKC4v1D7X+Bc0= -cloud.google.com/go/gkebackup v0.2.0/go.mod h1:XKvv/4LfG829/B8B7xRkk8zRrOEbKtEam6yNfuQNH60= -cloud.google.com/go/gkebackup v0.3.0/go.mod h1:n/E671i1aOQvUxT541aTkCwExO/bTer2HDlj4TsBRAo= -cloud.google.com/go/gkebackup v0.4.0/go.mod h1:byAyBGUwYGEEww7xsbnUTBHIYcOPy/PgUWUtOeRm9Vg= -cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= -cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= -cloud.google.com/go/gkeconnect v0.7.0/go.mod h1:SNfmVqPkaEi3bF/B3CNZOAYPYdg7sU+obZ+QTky2Myw= -cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= -cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= -cloud.google.com/go/gkehub v0.11.0/go.mod h1:JOWHlmN+GHyIbuWQPl47/C2RFhnFKH38jH9Ascu3n0E= -cloud.google.com/go/gkehub v0.12.0/go.mod h1:djiIwwzTTBrF5NaXCGv3mf7klpEMcST17VBTVVDcuaw= -cloud.google.com/go/gkemulticloud v0.3.0/go.mod h1:7orzy7O0S+5kq95e4Hpn7RysVA7dPs8W/GgfUtsPbrA= -cloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI= -cloud.google.com/go/gkemulticloud v0.5.0/go.mod h1:W0JDkiyi3Tqh0TJr//y19wyb1yf8llHVto2Htf2Ja3Y= -cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= -cloud.google.com/go/gsuiteaddons v1.3.0/go.mod h1:EUNK/J1lZEZO8yPtykKxLXI6JSVN2rg9bN8SXOa0bgM= -cloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSushomRlg+3ua1o= -cloud.google.com/go/gsuiteaddons v1.5.0/go.mod h1:TFCClYLd64Eaa12sFVmUyG62tk4mdIsI7pAnSXRkcFo= -cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= -cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= -cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= -cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= -cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= -cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= -cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY= -cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY= -cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= -cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= -cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= -cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk= -cloud.google.com/go/iap v1.7.0/go.mod h1:beqQx56T9O1G1yNPph+spKpNibDlYIiIixiqsQXxLIo= -cloud.google.com/go/iap v1.7.1/go.mod h1:WapEwPc7ZxGt2jFGB/C/bm+hP0Y6NXzOYGjpPnmMS74= -cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM= -cloud.google.com/go/ids v1.2.0/go.mod h1:5WXvp4n25S0rA/mQWAg1YEEBBq6/s+7ml1RDCW1IrcY= -cloud.google.com/go/ids v1.3.0/go.mod h1:JBdTYwANikFKaDP6LtW5JAi4gubs57SVNQjemdt6xV4= -cloud.google.com/go/iot v1.3.0/go.mod h1:r7RGh2B61+B8oz0AGE+J72AhA0G7tdXItODWsaA2oLs= -cloud.google.com/go/iot v1.4.0/go.mod h1:dIDxPOn0UvNDUMD8Ger7FIaTuvMkj+aGk94RPP0iV+g= -cloud.google.com/go/iot v1.5.0/go.mod h1:mpz5259PDl3XJthEmh9+ap0affn/MqNSP4My77Qql9o= -cloud.google.com/go/iot v1.6.0/go.mod h1:IqdAsmE2cTYYNO1Fvjfzo9po179rAtJeVGUvkLN3rLE= -cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= -cloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg= -cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0= -cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4jMAg= -cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w= -cloud.google.com/go/kms v1.10.0/go.mod h1:ng3KTUtQQU9bPX3+QGLsflZIHlkbn8amFAMY63m8d24= -cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI= -cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= -cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= -cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= -cloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8= -cloud.google.com/go/language v1.9.0/go.mod h1:Ns15WooPM5Ad/5no/0n81yUetis74g3zrbeJBE+ptUY= -cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= -cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= -cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= -cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= -cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= -cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= -cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= -cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= -cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= -cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= -cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= -cloud.google.com/go/maps v0.1.0/go.mod h1:BQM97WGyfw9FWEmQMpZ5T6cpovXXSd1cGmFma94eubI= -cloud.google.com/go/maps v0.6.0/go.mod h1:o6DAMMfb+aINHz/p/jbcY+mYeXBoZoxTfdSQ8VAJaCw= -cloud.google.com/go/maps v0.7.0/go.mod h1:3GnvVl3cqeSvgMcpRlQidXsPYuDGQ8naBis7MVzpXsY= -cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= -cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= -cloud.google.com/go/mediatranslation v0.7.0/go.mod h1:LCnB/gZr90ONOIQLgSXagp8XUW1ODs2UmUMvcgMfI2I= -cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= -cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= -cloud.google.com/go/memcache v1.6.0/go.mod h1:XS5xB0eQZdHtTuTF9Hf8eJkKtR3pVRCcvJwtm68T3rA= -cloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY= -cloud.google.com/go/memcache v1.9.0/go.mod h1:8oEyzXCu+zo9RzlEaEjHl4KkgjlNDaXbCQeQWlzNFJM= -cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= -cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= -cloud.google.com/go/metastore v1.7.0/go.mod h1:s45D0B4IlsINu87/AsWiEVYbLaIMeUSoxlKKDqBGFS8= -cloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI= -cloud.google.com/go/metastore v1.10.0/go.mod h1:fPEnH3g4JJAk+gMRnrAnoqyv2lpUCqJPWOodSaf45Eo= -cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk= -cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= -cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w= -cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw= -cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= -cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= -cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= -cloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8= -cloud.google.com/go/networkconnectivity v1.10.0/go.mod h1:UP4O4sWXJG13AqrTdQCD9TnLGEbtNRqjuaaA7bNjF5E= -cloud.google.com/go/networkconnectivity v1.11.0/go.mod h1:iWmDD4QF16VCDLXUqvyspJjIEtBR/4zq5hwnY2X3scM= -cloud.google.com/go/networkmanagement v1.4.0/go.mod h1:Q9mdLLRn60AsOrPc8rs8iNV6OHXaGcDdsIQe1ohekq8= -cloud.google.com/go/networkmanagement v1.5.0/go.mod h1:ZnOeZ/evzUdUsnvRt792H0uYEnHQEMaz+REhhzJRcf4= -cloud.google.com/go/networkmanagement v1.6.0/go.mod h1:5pKPqyXjB/sgtvB5xqOemumoQNB7y95Q7S+4rjSOPYY= -cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= -cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= -cloud.google.com/go/networksecurity v0.7.0/go.mod h1:mAnzoxx/8TBSyXEeESMy9OOYwo1v+gZ5eMRnsT5bC8k= -cloud.google.com/go/networksecurity v0.8.0/go.mod h1:B78DkqsxFG5zRSVuwYFRZ9Xz8IcQ5iECsNrPn74hKHU= -cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= -cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= -cloud.google.com/go/notebooks v1.4.0/go.mod h1:4QPMngcwmgb6uw7Po99B2xv5ufVoIQ7nOGDyL4P8AgA= -cloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0= -cloud.google.com/go/notebooks v1.7.0/go.mod h1:PVlaDGfJgj1fl1S3dUwhFMXFgfYGhYQt2164xOMONmE= -cloud.google.com/go/notebooks v1.8.0/go.mod h1:Lq6dYKOYOWUCTvw5t2q1gp1lAp0zxAxRycayS0iJcqQ= -cloud.google.com/go/optimization v1.1.0/go.mod h1:5po+wfvX5AQlPznyVEZjGJTMr4+CAkJf2XSTQOOl9l4= -cloud.google.com/go/optimization v1.2.0/go.mod h1:Lr7SOHdRDENsh+WXVmQhQTrzdu9ybg0NecjHidBq6xs= -cloud.google.com/go/optimization v1.3.1/go.mod h1:IvUSefKiwd1a5p0RgHDbWCIbDFgKuEdB+fPPuP0IDLI= -cloud.google.com/go/orchestration v1.3.0/go.mod h1:Sj5tq/JpWiB//X/q3Ngwdl5K7B7Y0KZ7bfv0wL6fqVA= -cloud.google.com/go/orchestration v1.4.0/go.mod h1:6W5NLFWs2TlniBphAViZEVhrXRSMgUGDfW7vrWKvsBk= -cloud.google.com/go/orchestration v1.6.0/go.mod h1:M62Bevp7pkxStDfFfTuCOaXgaaqRAga1yKyoMtEoWPQ= -cloud.google.com/go/orgpolicy v1.4.0/go.mod h1:xrSLIV4RePWmP9P3tBl8S93lTmlAxjm06NSm2UTmKvE= -cloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc= -cloud.google.com/go/orgpolicy v1.10.0/go.mod h1:w1fo8b7rRqlXlIJbVhOMPrwVljyuW5mqssvBtU18ONc= -cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= -cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= -cloud.google.com/go/osconfig v1.9.0/go.mod h1:Yx+IeIZJ3bdWmzbQU4fxNl8xsZ4amB+dygAwFPlvnNo= -cloud.google.com/go/osconfig v1.10.0/go.mod h1:uMhCzqC5I8zfD9zDEAfvgVhDS8oIjySWh+l4WK6GnWw= -cloud.google.com/go/osconfig v1.11.0/go.mod h1:aDICxrur2ogRd9zY5ytBLV89KEgT2MKB2L/n6x1ooPw= -cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= -cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= -cloud.google.com/go/oslogin v1.6.0/go.mod h1:zOJ1O3+dTU8WPlGEkFSh7qeHPPSoxrcMbbK1Nm2iX70= -cloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo= -cloud.google.com/go/oslogin v1.9.0/go.mod h1:HNavntnH8nzrn8JCTT5fj18FuJLFJc4NaZJtBnQtKFs= -cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= -cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= -cloud.google.com/go/phishingprotection v0.7.0/go.mod h1:8qJI4QKHoda/sb/7/YmMQ2omRLSLYSu9bU0EKCNI+Lk= -cloud.google.com/go/policytroubleshooter v1.3.0/go.mod h1:qy0+VwANja+kKrjlQuOzmlvscn4RNsAc0e15GGqfMxg= -cloud.google.com/go/policytroubleshooter v1.4.0/go.mod h1:DZT4BcRw3QoO8ota9xw/LKtPa8lKeCByYeKTIf/vxdE= -cloud.google.com/go/policytroubleshooter v1.5.0/go.mod h1:Rz1WfV+1oIpPdN2VvvuboLVRsB1Hclg3CKQ53j9l8vw= -cloud.google.com/go/policytroubleshooter v1.6.0/go.mod h1:zYqaPTsmfvpjm5ULxAyD/lINQxJ0DDsnWOP/GZ7xzBc= -cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= -cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= -cloud.google.com/go/privatecatalog v0.7.0/go.mod h1:2s5ssIFO69F5csTXcwBP7NPFTZvps26xGzvQ2PQaBYg= -cloud.google.com/go/privatecatalog v0.8.0/go.mod h1:nQ6pfaegeDAq/Q5lrfCQzQLhubPiZhSaNhIgfJlnIXs= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/pubsub v1.26.0/go.mod h1:QgBH3U/jdJy/ftjPhTkyXNj543Tin1pRYcdcPRnFIRI= -cloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhzp5O6qsW0= -cloud.google.com/go/pubsub v1.28.0/go.mod h1:vuXFpwaVoIPQMGXqRyUQigu/AX1S3IWugR9xznmcXX8= -cloud.google.com/go/pubsub v1.30.0/go.mod h1:qWi1OPS0B+b5L+Sg6Gmc9zD1Y+HaM0MdUr7LsupY1P4= -cloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg= -cloud.google.com/go/pubsublite v1.6.0/go.mod h1:1eFCS0U11xlOuMFV/0iBqw3zP12kddMeCbj/F3FSj9k= -cloud.google.com/go/pubsublite v1.7.0/go.mod h1:8hVMwRXfDfvGm3fahVbtDbiLePT3gpoiJYJY+vxWxVM= -cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= -cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= -cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= -cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= -cloud.google.com/go/recaptchaenterprise/v2 v2.4.0/go.mod h1:Am3LHfOuBstrLrNCBrlI5sbwx9LBg3te2N6hGvHn2mE= -cloud.google.com/go/recaptchaenterprise/v2 v2.5.0/go.mod h1:O8LzcHXN3rz0j+LBC91jrwI3R+1ZSZEWrfL7XHgNo9U= -cloud.google.com/go/recaptchaenterprise/v2 v2.6.0/go.mod h1:RPauz9jeLtB3JVzg6nCbe12qNoaa8pXc4d/YukAmcnA= -cloud.google.com/go/recaptchaenterprise/v2 v2.7.0/go.mod h1:19wVj/fs5RtYtynAPJdDTb69oW0vNHYDBTbB4NvMD9c= -cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= -cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= -cloud.google.com/go/recommendationengine v0.7.0/go.mod h1:1reUcE3GIu6MeBz/h5xZJqNLuuVjNg1lmWMPyjatzac= -cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= -cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= -cloud.google.com/go/recommender v1.7.0/go.mod h1:XLHs/W+T8olwlGOgfQenXBTbIseGclClff6lhFVe9Bs= -cloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70= -cloud.google.com/go/recommender v1.9.0/go.mod h1:PnSsnZY7q+VL1uax2JWkt/UegHssxjUVVCrX52CuEmQ= -cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= -cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= -cloud.google.com/go/redis v1.9.0/go.mod h1:HMYQuajvb2D0LvMgZmLDZW8V5aOC/WxstZHiy4g8OiA= -cloud.google.com/go/redis v1.10.0/go.mod h1:ThJf3mMBQtW18JzGgh41/Wld6vnDDc/F/F35UolRZPM= -cloud.google.com/go/redis v1.11.0/go.mod h1:/X6eicana+BWcUda5PpwZC48o37SiFVTFSs0fWAJ7uQ= -cloud.google.com/go/resourcemanager v1.3.0/go.mod h1:bAtrTjZQFJkiWTPDb1WBjzvc6/kifjj4QBYuKCCoqKA= -cloud.google.com/go/resourcemanager v1.4.0/go.mod h1:MwxuzkumyTX7/a3n37gmsT3py7LIXwrShilPh3P1tR0= -cloud.google.com/go/resourcemanager v1.5.0/go.mod h1:eQoXNAiAvCf5PXxWxXjhKQoTMaUSNrEfg+6qdf/wots= -cloud.google.com/go/resourcemanager v1.6.0/go.mod h1:YcpXGRs8fDzcUl1Xw8uOVmI8JEadvhRIkoXXUNVYcVo= -cloud.google.com/go/resourcemanager v1.7.0/go.mod h1:HlD3m6+bwhzj9XCouqmeiGuni95NTrExfhoSrkC/3EI= -cloud.google.com/go/resourcesettings v1.3.0/go.mod h1:lzew8VfESA5DQ8gdlHwMrqZs1S9V87v3oCnKCWoOuQU= -cloud.google.com/go/resourcesettings v1.4.0/go.mod h1:ldiH9IJpcrlC3VSuCGvjR5of/ezRrOxFtpJoJo5SmXg= -cloud.google.com/go/resourcesettings v1.5.0/go.mod h1:+xJF7QSG6undsQDfsCJyqWXyBwUoJLhetkRMDRnIoXA= -cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= -cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= -cloud.google.com/go/retail v1.10.0/go.mod h1:2gDk9HsL4HMS4oZwz6daui2/jmKvqShXKQuB2RZ+cCc= -cloud.google.com/go/retail v1.11.0/go.mod h1:MBLk1NaWPmh6iVFSz9MeKG/Psyd7TAgm6y/9L2B4x9Y= -cloud.google.com/go/retail v1.12.0/go.mod h1:UMkelN/0Z8XvKymXFbD4EhFJlYKRx1FGhQkVPU5kF14= -cloud.google.com/go/run v0.2.0/go.mod h1:CNtKsTA1sDcnqqIFR3Pb5Tq0usWxJJvsWOCPldRU3Do= -cloud.google.com/go/run v0.3.0/go.mod h1:TuyY1+taHxTjrD0ZFk2iAR+xyOXEA0ztb7U3UNA0zBo= -cloud.google.com/go/run v0.8.0/go.mod h1:VniEnuBwqjigv0A7ONfQUaEItaiCRVujlMqerPPiktM= -cloud.google.com/go/run v0.9.0/go.mod h1:Wwu+/vvg8Y+JUApMwEDfVfhetv30hCG4ZwDR/IXl2Qg= -cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= -cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= -cloud.google.com/go/scheduler v1.6.0/go.mod h1:SgeKVM7MIwPn3BqtcBntpLyrIJftQISRrYB5ZtT+KOk= -cloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44= -cloud.google.com/go/scheduler v1.8.0/go.mod h1:TCET+Y5Gp1YgHT8py4nlg2Sew8nUHMqcpousDgXJVQc= -cloud.google.com/go/scheduler v1.9.0/go.mod h1:yexg5t+KSmqu+njTIh3b7oYPheFtBWGcbVUYF1GGMIc= -cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= -cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= -cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= -cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= -cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= -cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= -cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= -cloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq/t9dzI+2Q= -cloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA= -cloud.google.com/go/security v1.12.0/go.mod h1:rV6EhrpbNHrrxqlvW0BWAIawFWq3X90SduMJdFwtLB8= -cloud.google.com/go/security v1.13.0/go.mod h1:Q1Nvxl1PAgmeW0y3HTt54JYIvUdtcpYKVfIB8AOMZ+0= -cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= -cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= -cloud.google.com/go/securitycenter v1.15.0/go.mod h1:PeKJ0t8MoFmmXLXWm41JidyzI3PJjd8sXWaVqg43WWk= -cloud.google.com/go/securitycenter v1.16.0/go.mod h1:Q9GMaLQFUD+5ZTabrbujNWLtSLZIZF7SAR0wWECrjdk= -cloud.google.com/go/securitycenter v1.18.1/go.mod h1:0/25gAzCM/9OL9vVx4ChPeM/+DlfGQJDwBy/UC8AKK0= -cloud.google.com/go/securitycenter v1.19.0/go.mod h1:LVLmSg8ZkkyaNy4u7HCIshAngSQ8EcIRREP3xBnyfag= -cloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU= -cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s= -cloud.google.com/go/servicecontrol v1.10.0/go.mod h1:pQvyvSRh7YzUF2efw7H87V92mxU8FnFDawMClGCNuAA= -cloud.google.com/go/servicecontrol v1.11.0/go.mod h1:kFmTzYzTUIuZs0ycVqRHNaNhgR+UMUpw9n02l/pY+mc= -cloud.google.com/go/servicecontrol v1.11.1/go.mod h1:aSnNNlwEFBY+PWGQ2DoM0JJ/QUXqV5/ZD9DOLB7SnUk= -cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= -cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= -cloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPjsRs1RlmJ4pqiNjVL4= -cloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U= -cloud.google.com/go/servicedirectory v1.8.0/go.mod h1:srXodfhY1GFIPvltunswqXpVxFPpZjf8nkKQT7XcXaY= -cloud.google.com/go/servicedirectory v1.9.0/go.mod h1:29je5JjiygNYlmsGz8k6o+OZ8vd4f//bQLtvzkPPT/s= -cloud.google.com/go/servicemanagement v1.4.0/go.mod h1:d8t8MDbezI7Z2R1O/wu8oTggo3BI2GKYbdG4y/SJTco= -cloud.google.com/go/servicemanagement v1.5.0/go.mod h1:XGaCRe57kfqu4+lRxaFEAuqmjzF0r+gWHjWqKqBvKFo= -cloud.google.com/go/servicemanagement v1.6.0/go.mod h1:aWns7EeeCOtGEX4OvZUWCCJONRZeFKiptqKf1D0l/Jc= -cloud.google.com/go/servicemanagement v1.8.0/go.mod h1:MSS2TDlIEQD/fzsSGfCdJItQveu9NXnUniTrq/L8LK4= -cloud.google.com/go/serviceusage v1.3.0/go.mod h1:Hya1cozXM4SeSKTAgGXgj97GlqUvF5JaoXacR1JTP/E= -cloud.google.com/go/serviceusage v1.4.0/go.mod h1:SB4yxXSaYVuUBYUml6qklyONXNLt83U0Rb+CXyhjEeU= -cloud.google.com/go/serviceusage v1.5.0/go.mod h1:w8U1JvqUqwJNPEOTQjrMHkw3IaIFLoLsPLvsE3xueec= -cloud.google.com/go/serviceusage v1.6.0/go.mod h1:R5wwQcbOWsyuOfbP9tGdAnCAc6B9DRwPG1xtWMDeuPA= -cloud.google.com/go/shell v1.3.0/go.mod h1:VZ9HmRjZBsjLGXusm7K5Q5lzzByZmJHf1d0IWHEN5X4= -cloud.google.com/go/shell v1.4.0/go.mod h1:HDxPzZf3GkDdhExzD/gs8Grqk+dmYcEjGShZgYa9URw= -cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+qE2f9A= -cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos= -cloud.google.com/go/spanner v1.44.0/go.mod h1:G8XIgYdOK+Fbcpbs7p2fiprDw4CaZX63whnSMLVBxjk= -cloud.google.com/go/spanner v1.45.0/go.mod h1:FIws5LowYz8YAE1J8fOS7DJup8ff7xJeetWEo5REA2M= -cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= -cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= -cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= -cloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco= -cloud.google.com/go/speech v1.14.1/go.mod h1:gEosVRPJ9waG7zqqnsHpYTOoAS4KouMRLDFMekpJ0J0= -cloud.google.com/go/speech v1.15.0/go.mod h1:y6oH7GhqCaZANH7+Oe0BhgIogsNInLlz542tg3VqeYI= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= -cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= -cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= -cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= -cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= -cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= -cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= -cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= -cloud.google.com/go/storagetransfer v1.8.0/go.mod h1:JpegsHHU1eXg7lMHkvf+KE5XDJ7EQu0GwNJbbVGanEw= -cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= -cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= -cloud.google.com/go/talent v1.3.0/go.mod h1:CmcxwJ/PKfRgd1pBjQgU6W3YBwiewmUzQYH5HHmSCmM= -cloud.google.com/go/talent v1.4.0/go.mod h1:ezFtAgVuRf8jRsvyE6EwmbTK5LKciD4KVnHuDEFmOOA= -cloud.google.com/go/talent v1.5.0/go.mod h1:G+ODMj9bsasAEJkQSzO2uHQWXHHXUomArjWQQYkqK6c= -cloud.google.com/go/texttospeech v1.4.0/go.mod h1:FX8HQHA6sEpJ7rCMSfXuzBcysDAuWusNNNvN9FELDd8= -cloud.google.com/go/texttospeech v1.5.0/go.mod h1:oKPLhR4n4ZdQqWKURdwxMy0uiTS1xU161C8W57Wkea4= -cloud.google.com/go/texttospeech v1.6.0/go.mod h1:YmwmFT8pj1aBblQOI3TfKmwibnsfvhIBzPXcW4EBovc= -cloud.google.com/go/tpu v1.3.0/go.mod h1:aJIManG0o20tfDQlRIej44FcwGGl/cD0oiRyMKG19IQ= -cloud.google.com/go/tpu v1.4.0/go.mod h1:mjZaX8p0VBgllCzF6wcU2ovUXN9TONFLd7iz227X2Xg= -cloud.google.com/go/tpu v1.5.0/go.mod h1:8zVo1rYDFuW2l4yZVY0R0fb/v44xLh3llq7RuV61fPM= -cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28= -cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= -cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= -cloud.google.com/go/trace v1.9.0/go.mod h1:lOQqpE5IaWY0Ixg7/r2SjixMuc6lfTFeO4QGM4dQWOk= -cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= -cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= -cloud.google.com/go/translate v1.5.0/go.mod h1:29YDSYveqqpA1CQFD7NQuP49xymq17RXNaUDdc0mNu0= -cloud.google.com/go/translate v1.6.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= -cloud.google.com/go/translate v1.7.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= -cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk= -cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw= -cloud.google.com/go/video v1.12.0/go.mod h1:MLQew95eTuaNDEGriQdcYn0dTwf9oWiA4uYebxM5kdg= -cloud.google.com/go/video v1.13.0/go.mod h1:ulzkYlYgCp15N2AokzKjy7MQ9ejuynOJdf1tR5lGthk= -cloud.google.com/go/video v1.14.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= -cloud.google.com/go/video v1.15.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= -cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= -cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= -cloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M= -cloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU= -cloud.google.com/go/videointelligence v1.10.0/go.mod h1:LHZngX1liVtUhZvi2uNS0VQuOzNi2TkY1OakiuoUOjU= -cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= -cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= -cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= -cloud.google.com/go/vision/v2 v2.4.0/go.mod h1:VtI579ll9RpVTrdKdkMzckdnwMyX2JILb+MhPqRbPsY= -cloud.google.com/go/vision/v2 v2.5.0/go.mod h1:MmaezXOOE+IWa+cS7OhRRLK2cNv1ZL98zhqFFZaaH2E= -cloud.google.com/go/vision/v2 v2.6.0/go.mod h1:158Hes0MvOS9Z/bDMSFpjwsUrZ5fPrdwuyyvKSGAGMY= -cloud.google.com/go/vision/v2 v2.7.0/go.mod h1:H89VysHy21avemp6xcf9b9JvZHVehWbET0uT/bcuY/0= -cloud.google.com/go/vmmigration v1.2.0/go.mod h1:IRf0o7myyWFSmVR1ItrBSFLFD/rJkfDCUTO4vLlJvsE= -cloud.google.com/go/vmmigration v1.3.0/go.mod h1:oGJ6ZgGPQOFdjHuocGcLqX4lc98YQ7Ygq8YQwHh9A7g= -cloud.google.com/go/vmmigration v1.5.0/go.mod h1:E4YQ8q7/4W9gobHjQg4JJSgXXSgY21nA5r8swQV+Xxc= -cloud.google.com/go/vmmigration v1.6.0/go.mod h1:bopQ/g4z+8qXzichC7GW1w2MjbErL54rk3/C843CjfY= -cloud.google.com/go/vmwareengine v0.1.0/go.mod h1:RsdNEf/8UDvKllXhMz5J40XxDrNJNN4sagiox+OI208= -cloud.google.com/go/vmwareengine v0.2.2/go.mod h1:sKdctNJxb3KLZkE/6Oui94iw/xs9PRNC2wnNLXsHvH8= -cloud.google.com/go/vmwareengine v0.3.0/go.mod h1:wvoyMvNWdIzxMYSpH/R7y2h5h3WFkx6d+1TIsP39WGY= -cloud.google.com/go/vpcaccess v1.4.0/go.mod h1:aQHVbTWDYUR1EbTApSVvMq1EnT57ppDmQzZ3imqIk4w= -cloud.google.com/go/vpcaccess v1.5.0/go.mod h1:drmg4HLk9NkZpGfCmZ3Tz0Bwnm2+DKqViEpeEpOq0m8= -cloud.google.com/go/vpcaccess v1.6.0/go.mod h1:wX2ILaNhe7TlVa4vC5xce1bCnqE3AeH27RV31lnmZes= -cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= -cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= -cloud.google.com/go/webrisk v1.6.0/go.mod h1:65sW9V9rOosnc9ZY7A7jsy1zoHS5W9IAXv6dGqhMQMc= -cloud.google.com/go/webrisk v1.7.0/go.mod h1:mVMHgEYH0r337nmt1JyLthzMr6YxwN1aAIEc2fTcq7A= -cloud.google.com/go/webrisk v1.8.0/go.mod h1:oJPDuamzHXgUc+b8SiHRcVInZQuybnvEW72PqTc7sSg= -cloud.google.com/go/websecurityscanner v1.3.0/go.mod h1:uImdKm2wyeXQevQJXeh8Uun/Ym1VqworNDlBXQevGMo= -cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ= -cloud.google.com/go/websecurityscanner v1.5.0/go.mod h1:Y6xdCPy81yi0SQnDY1xdNTNpfY1oAgXUlcfN3B3eSng= -cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= -cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= -cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= -cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= -cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= -git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/IBM/sarama v1.43.3 h1:Yj6L2IaNvb2mRBop39N7mmJAHBVY3dTPncr3qGVkxPA= github.com/IBM/sarama v1.43.3/go.mod h1:FVIRaLrhK3Cla/9FfRF5X9Zua2KpS3SYIXxhac1H+FQ= -github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/ThreeDotsLabs/watermill v1.3.7 h1:NV0PSTmuACVEOV4dMxRnmGXrmbz8U83LENOvpHekN7o= github.com/ThreeDotsLabs/watermill v1.3.7/go.mod h1:lBnrLbxOjeMRgcJbv+UiZr8Ylz8RkJ4m6i/VN/Nk+to= github.com/ThreeDotsLabs/watermill-http/v2 v2.3.1 h1:M0iYM5HsGcoxtiQqprRlYZNZnGk3w5LsE9RbC2R8myQ= @@ -621,28 +20,14 @@ github.com/ThreeDotsLabs/watermill-kafka/v3 v3.0.5 h1:ud+4txnRgtr3kZXfXZ5+C7kVQE github.com/ThreeDotsLabs/watermill-kafka/v3 v3.0.5/go.mod h1:t4o+4A6GB+XC8WL3DandhzPwd265zQuyWMQC/I+WIOU= github.com/ThreeDotsLabs/watermill-nats/v2 v2.1.1 h1:afAkAFzeooBRQvxElR+6xoigXKCukcZXnE9ACxhwlPI= github.com/ThreeDotsLabs/watermill-nats/v2 v2.1.1/go.mod h1:stjbT+s4u/s5ime5jdIyvPyjBGwGeJewIN7jxH8gp4k= -github.com/adyen/adyen-go-api-library/v7 v7.3.1 h1:NToWy5oZDH5Juz45h9GTlidGFldW10xvaihCJIOWZcw= -github.com/adyen/adyen-go-api-library/v7 v7.3.1/go.mod h1:z9oHJsUpqgCkBhKa8hpBgQvTU8ObRfvO0NKEYUoocx0= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= -github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= -github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= -github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= github.com/alitto/pond v1.9.2 h1:9Qb75z/scEZVCoSU+osVmQ0I0JOeLfdTDafrbcJ8CLs= github.com/alitto/pond v1.9.2/go.mod h1:xQn3P/sHTYcU/1BR3i86IGIrilcrGC2LiS+E2+CJWsI= -github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 h1:yL7+Jz0jTC6yykIK/Wh74gnTJnrGr5AyrNMXuA0gves= github.com/antlr/antlr4/runtime/Go/antlr v1.4.10/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= -github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= -github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= -github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= github.com/aquasecurity/esquery v0.2.0 h1:9WWXve95TE8hbm3736WB7nS6Owl8UGDeu+0jiyE9ttA= github.com/aquasecurity/esquery v0.2.0/go.mod h1:VU+CIFR6C+H142HHZf9RUkp4Eedpo9UrEKeCQHWf9ao= -github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-msk-iam-sasl-signer-go v1.0.0 h1:UyjtGmO0Uwl/K+zpzPwLoXzMhcN9xmnR2nrqJoBrg3c= github.com/aws/aws-msk-iam-sasl-signer-go v1.0.0/go.mod h1:TJAXuFs2HcMib3sN5L0gUC+Q01Qvy3DemvA55WuC+iA= github.com/aws/aws-sdk-go v1.42.27/go.mod h1:OGr6lGMAKGlG9CVrYnWYDKIyb829c6EVBRjxqjmPepc= @@ -651,11 +36,11 @@ github.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3eP github.com/aws/aws-sdk-go-v2 v1.31.0 h1:3V05LbxTSItI5kUqNwhJrrrY1BAXxXt0sN0l72QmG5U= github.com/aws/aws-sdk-go-v2 v1.31.0/go.mod h1:ztolYtaEUtdpf9Wftr31CJfLVjOnD/CVRkKOOYgF8hA= github.com/aws/aws-sdk-go-v2/config v1.18.25/go.mod h1:dZnYpD5wTW/dQF0rRNLVypB396zWCcPiBIvdvSWHEg4= -github.com/aws/aws-sdk-go-v2/config v1.27.36 h1:4IlvHh6Olc7+61O1ktesh0jOcqmq/4WG6C2Aj5SKXy0= -github.com/aws/aws-sdk-go-v2/config v1.27.36/go.mod h1:IiBpC0HPAGq9Le0Xxb1wpAKzEfAQ3XlYgJLYKEVYcfw= +github.com/aws/aws-sdk-go-v2/config v1.27.37 h1:xaoIwzHVuRWRHFI0jhgEdEGc8xE1l91KaeRDsWEIncU= +github.com/aws/aws-sdk-go-v2/config v1.27.37/go.mod h1:S2e3ax9/8KnMSyRVNd3sWTKs+1clJ2f1U6nE0lpvQRg= github.com/aws/aws-sdk-go-v2/credentials v1.13.24/go.mod h1:jYPYi99wUOPIFi0rhiOvXeSEReVOzBqFNOX5bXYoG2o= -github.com/aws/aws-sdk-go-v2/credentials v1.17.34 h1:gmkk1l/cDGSowPRzkdxYi8edw+gN4HmVK151D/pqGNc= -github.com/aws/aws-sdk-go-v2/credentials v1.17.34/go.mod h1:4R9OEV3tgFMsok4ZeFpExn7zQaZRa9MRGFYnI/xC/vs= +github.com/aws/aws-sdk-go-v2/credentials v1.17.35 h1:7QknrZhYySEB1lEXJxGAmuD5sWwys5ZXNr4m5oEz0IE= +github.com/aws/aws-sdk-go-v2/credentials v1.17.35/go.mod h1:8Vy4kk7at4aPSmibr7K+nLTzG6qUQAUO4tW49fzUV4E= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3/go.mod h1:4Q0UFP0YJf0NrsEuEYHpM9fTSEVnD16Z3uyEF7J9JGM= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14 h1:C/d03NAmh8C4BZXhuRNboF/DqhBkBCeDiJDcaqIT5pA= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.14/go.mod h1:7I0Ju7p9mCIdlrfS+JCgqcYD0VXz/N4yozsox+0o078= @@ -676,14 +61,14 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27/go.mod h1:EO github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20 h1:Xbwbmk44URTiHNx6PNo0ujDE6ERlsCKJD3u1zfnzAPg= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.20/go.mod h1:oAfOFzUB14ltPZj1rWwRc3d/6OgD76R8KlvU3EqM9Fg= github.com/aws/aws-sdk-go-v2/service/sso v1.12.10/go.mod h1:ouy2P4z6sJN70fR3ka3wD3Ro3KezSxU6eKGQI2+2fjI= -github.com/aws/aws-sdk-go-v2/service/sso v1.23.0 h1:fHySkG0IGj2nepgGJPmmhZYL9ndnsq1Tvc6MeuVQCaQ= -github.com/aws/aws-sdk-go-v2/service/sso v1.23.0/go.mod h1:XRlMvmad0ZNL+75C5FYdMvbbLkd6qiqz6foR1nA1PXY= +github.com/aws/aws-sdk-go-v2/service/sso v1.23.1 h1:2jrVsMHqdLD1+PA4BA6Nh1eZp0Gsy3mFSB5MxDvcJtU= +github.com/aws/aws-sdk-go-v2/service/sso v1.23.1/go.mod h1:XRlMvmad0ZNL+75C5FYdMvbbLkd6qiqz6foR1nA1PXY= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10/go.mod h1:AFvkxc8xfBe8XA+5St5XIHHrQQtkxqrRincx4hmMHOk= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.0 h1:cU/OeQPNReyMj1JEBgjE29aclYZYtXcsPMXbTkVGMFk= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.0/go.mod h1:FnvDM4sfa+isJ3kDXIzAB9GAwVSzFzSy97uZ3IsHo4E= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.1 h1:0L7yGCg3Hb3YQqnSgBTZM5wepougtL1aEccdcdYhHME= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.27.1/go.mod h1:FnvDM4sfa+isJ3kDXIzAB9GAwVSzFzSy97uZ3IsHo4E= github.com/aws/aws-sdk-go-v2/service/sts v1.19.0/go.mod h1:BgQOMsg8av8jset59jelyPW7NoZcZXLVpDsXunGDrk8= -github.com/aws/aws-sdk-go-v2/service/sts v1.31.0 h1:GNVxIHBTi2EgwCxpNiozhNasMOK+ROUA2Z3X+cSBX58= -github.com/aws/aws-sdk-go-v2/service/sts v1.31.0/go.mod h1:yMWe0F+XG0DkRZK5ODZhG7BEFYhLXi2dqGsv6tX0cgI= +github.com/aws/aws-sdk-go-v2/service/sts v1.31.1 h1:8K0UNOkZiK9Uh3HIF6Bx0rcNCftqGCeKmOaR7Gp5BSo= +github.com/aws/aws-sdk-go-v2/service/sts v1.31.1/go.mod h1:yMWe0F+XG0DkRZK5ODZhG7BEFYhLXi2dqGsv6tX0cgI= github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/aws/smithy-go v1.21.0 h1:H7L8dtDRk0P1Qm6y0ji7MCYMQObJ5R9CRpyPhRUkLYA= github.com/aws/smithy-go v1.21.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= @@ -692,39 +77,18 @@ github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw= github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0= github.com/bombsimon/logrusr/v3 v3.1.0 h1:zORbLM943D+hDMGgyjMhSAz/iDz86ZV72qaak/CA0zQ= github.com/bombsimon/logrusr/v3 v3.1.0/go.mod h1:PksPPgSFEL2I52pla2glgCyyd2OqOHAnFF5E+g8Ixco= -github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= +github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20230310173818-32f1caf87195/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -745,8 +109,6 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA= github.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws= @@ -761,24 +123,14 @@ github.com/elastic/go-elasticsearch/v7 v7.17.10/go.mod h1:OJ4wdbtDNk5g503kvlHLyE github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= -github.com/envoyproxy/go-control-plane v0.11.0/go.mod h1:VnHyVMpzcLvCFt9yUz1UnCwHLhwx1WguiVDV7pTG/tI= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= -github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= -github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= github.com/ericlagergren/decimal v0.0.0-20221120152707-495c53812d05 h1:S92OBrGuLLZsyM5ybUzgc/mPjIYk2AZqufieooe98uw= github.com/ericlagergren/decimal v0.0.0-20221120152707-495c53812d05/go.mod h1:M9R1FoZ3y//hwwnJtO51ypFGwm8ZfpxPT/ZLtO1mcgQ= github.com/expr-lang/expr v1.16.9 h1:WUAzmR0JNI9JCiF0/ewwHB1gmcGw5wW7nWt8gc6PpCI= github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= @@ -786,17 +138,12 @@ github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/formancehq/go-libs v1.7.1 h1:9D5cxKWFlVtdX5AYDXeUz1Nb9PdoEfQX0f/yeLsU324= -github.com/formancehq/go-libs v1.7.1/go.mod h1:pWTScpoyieF7OoJ6WVmXNG9NhDjbZbAmFqd7UOw85iI= +github.com/formancehq/go-libs v1.7.2-0.20240925132527-7627842ea9b5 h1:6UcoXXm5hzFk7c2aOonPUPO3bNrU7CvTiE/nZQWMvY4= +github.com/formancehq/go-libs v1.7.2-0.20240925132527-7627842ea9b5/go.mod h1:ynmWBbsdhVyjE+MxneMErtgd/RnNAk892VuIhZE2fps= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= -github.com/get-momo/atlar-v1-go-client v1.2.1 h1:sKWd0maMshxBErGXsYVGhGIB+zFxynrWLNHnegB4lXs= -github.com/get-momo/atlar-v1-go-client v1.2.1/go.mod h1:qcLoXEhjTCOeBqAzG2tucpvxGJS2LYNwaU7WnJYnO64= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gibson042/canonicaljson-go v1.0.3 h1:EAyF8L74AWabkyUmrvEFHEt/AGFQeD6RfwbAuf0j1bI= github.com/gibson042/canonicaljson-go v1.0.3/go.mod h1:DsLpJTThXyGNO+KZlI85C1/KDcImpP67k/RKVjcaEqo= github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= @@ -807,17 +154,7 @@ github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= -github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= -github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= -github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= -github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= -github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= -github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= @@ -827,39 +164,6 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-openapi/analysis v0.21.4 h1:ZDFLvSNxpDaomuCueM0BlSXxpANBlFYiBvr+GXrvIHc= -github.com/go-openapi/analysis v0.21.4/go.mod h1:4zQ35W4neeZTqh3ol0rv/O8JBbka9QyAgQRPp9y3pfo= -github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= -github.com/go-openapi/errors v0.20.4 h1:unTcVm6PispJsMECE3zWgvG4xTiKda1LIR5rCRWLG6M= -github.com/go-openapi/errors v0.20.4/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuAOhlsB1FSgk= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ= -github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA= -github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= -github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= -github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/loads v0.21.2 h1:r2a/xFIYeZ4Qd2TnGpWDIQNcP80dIaZgf704za8enro= -github.com/go-openapi/loads v0.21.2/go.mod h1:Jq58Os6SSGz0rzh62ptiu8Z31I+OTHqmULx5e/gJbNw= -github.com/go-openapi/runtime v0.26.0 h1:HYOFtG00FM1UvqrcxbEJg/SwvDRvYLQKGhw2zaQjTcc= -github.com/go-openapi/runtime v0.26.0/go.mod h1:QgRGeZwrUcSHdeh4Ka9Glvo0ug1LC5WyE+EV88plZrQ= -github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= -github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8= -github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= -github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= -github.com/go-openapi/strfmt v0.21.8 h1:VYBUoKYRLAlgKDrIxR/I0lKrztDQ0tuTDrbhLVP8Erg= -github.com/go-openapi/strfmt v0.21.8/go.mod h1:adeGTkxE44sPyLk0JV235VQAO/ZXUr8KAzYjclFs3ew= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= -github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-openapi/validate v0.22.2 h1:Lda8nadL/5kIvS5mdXCAIuZ7IVXvKFIppLnw+EZh+n0= -github.com/go-openapi/validate v0.22.2/go.mod h1:kVxh31KbfsxU8ZyoHaDbLBWU5CnMdqBUEtadQ2G4d5M= -github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= -github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -875,7 +179,6 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -884,127 +187,32 @@ github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keL github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= -github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= -github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 h1:c5FlPPgxOn7kJz3VoPLkQYQXGBS3EklQ4Zfi57uOuqQ= github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/s2a-go v0.1.0/go.mod h1:OJpEgntRZo8ugHpF9hkoLJbS5dSI20XZeXJ9JVywLlM= -github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= -github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= -github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= -github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= -github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= -github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= -github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= -github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= -github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= -github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= -github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= -github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= -github.com/googleapis/gax-go/v2 v2.10.0/go.mod h1:4UOEnMCrxsSqQ940WnTiD6qJ63le2ev3xfyagutxiPw= -github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI= -github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= @@ -1017,9 +225,6 @@ github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pw github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -1031,20 +236,17 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1 github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-plugin v1.6.1 h1:P7MR2UP6gNKGPp+y7EZw2kOiq4IR9WiqLvp0XOsVdwI= +github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1hZyMK0PWAw0= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru/v2 v2.0.4 h1:7GHuZcgid37q8o5i3QI9KMT4nCWQQ3Kx3Ov6bb9MfK0= -github.com/hashicorp/golang-lru/v2 v2.0.4/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= +github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= @@ -1084,32 +286,18 @@ github.com/jgroeneveld/schema v1.0.0 h1:J0E10CrOkiSEsw6dfb1IfrDJD14pf6QLVJ3tRPl/ github.com/jgroeneveld/schema v1.0.0/go.mod h1:M14lv7sNMtGvo3ops1MwslaSYgDYxrSmbzWIQ0Mr5rs= github.com/jgroeneveld/trial v2.0.0+incompatible h1:d59ctdgor+VqdZCAiUfVN8K13s0ALDioG5DWwZNtRuQ= github.com/jgroeneveld/trial v2.0.0+incompatible/go.mod h1:I6INLW96EN8WysNBXUFI3M4RIC8ePg9ntAc/Wy+U/+M= +github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= +github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -1139,34 +327,23 @@ github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczG github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= -github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= -github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= -github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= -github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q= github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= -github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 h1:7GoSOOW2jpsfkntVKaS2rAr1TJqfcxotyaUcuxoZSzg= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= -github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= @@ -1185,9 +362,10 @@ github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nexus-rpc/sdk-go v0.0.10 h1:7jEPUlsghxoD4OJ2H8YbFJ1t4wbxsUef7yZgBfyY3uA= github.com/nexus-rpc/sdk-go v0.0.10/go.mod h1:TpfkM2Cw0Rlk9drGkoiSMpFqflKTiQLWUNyKJjF8mKQ= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oauth2-proxy/mockoidc v0.0.0-20220308204021-b9169deeb282 h1:TQMyrpijtkFyXpNI3rY5hsZQZw+paiH+BfAlsb81HBY= github.com/oauth2-proxy/mockoidc v0.0.0-20220308204021-b9169deeb282/go.mod h1:rW25Kyd08Wdn3UVn0YBsDTSvReu0jqpmJKzxITPSjks= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= @@ -1205,52 +383,34 @@ github.com/opensearch-project/opensearch-go v1.1.0/go.mod h1:+6/XHCuTH+fwsMJikZE github.com/opensearch-project/opensearch-go/v2 v2.3.0 h1:nQIEMr+A92CkhHrZgUhcfsrZjibvB3APXf2a1VwCmMQ= github.com/opensearch-project/opensearch-go/v2 v2.3.0/go.mod h1:8LDr9FCgUTVoT+5ESjc2+iaZuldqE+23Iq0r1XeNue8= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= -github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA= github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI= github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= -github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= -github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= -github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= -github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= -github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4= github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/riandyrn/otelchi v0.10.0 h1:QMbR/FMDWBOkej6dfyWteYefUKqIFxnyrpaoWRJ9RPQ= github.com/riandyrn/otelchi v0.10.0/go.mod h1:zBaX2FavWMlsvq4GqHit+QXxF1c5wIMZZFaYyW4+7FA= github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= -github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= github.com/shirou/gopsutil/v4 v4.24.8 h1:pVQjIenQkIhqO81mwTaXjTzOMT7d3TZkf43PlVFHENI= github.com/shirou/gopsutil/v4 v4.24.8/go.mod h1:wE0OrJtj4dG+hYkxqDH3QiBICdKSf04/npcvLLc/oRg= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= @@ -1261,12 +421,6 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -1280,22 +434,19 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stripe/stripe-go/v72 v72.122.0 h1:eRXWqnEwGny6dneQ5BsxGzUCED5n180u8n665JHlut8= -github.com/stripe/stripe-go/v72 v72.122.0/go.mod h1:QwqJQtduHubZht9mek5sds9CtQcKFdsykV9ZepRWwo0= github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= @@ -1327,10 +478,8 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= -github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= -github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -1342,33 +491,16 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xo/dburl v0.23.2 h1:Fl88cvayrgE56JA/sqhNMLljCW/b7RmG1mMkKMZUFgA= github.com/xo/dburl v0.23.2/go.mod h1:uazlaAQxj4gkshhfuuYyvwCBouOmNnG2aDxTCFZpmL4= -github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= -github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= github.com/zitadel/logging v0.3.4 h1:9hZsTjMMTE3X2LUi0xcF9Q9EdLo+FAezeu52ireBbHM= github.com/zitadel/logging v0.3.4/go.mod h1:aPpLQhE+v6ocNK0TWrBrd363hZ95KcI17Q1ixAQwZF0= github.com/zitadel/oidc/v2 v2.12.2 h1:3kpckg4rurgw7w7aLJrq7yvRxb2pkNOtD08RH42vPEs= github.com/zitadel/oidc/v2 v2.12.2/go.mod h1:vhP26g1g4YVntcTi0amMYW3tJuid70nxqxf+kb6XKgg= -go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= -go.mongodb.org/mongo-driver v1.12.0 h1:aPx33jmn/rQuJXPQLZQ8NtfPQG8CaqgLThFtqRb0PiE= -go.mongodb.org/mongo-driver v1.12.0/go.mod h1:AZkxhPnFJUoH7kZlFkVKucV20K387miPfm7oimrSmK0= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.44.0 h1:QaNUlLvmettd1vnmFHrgBYQHearxWP3uO4h4F3pVtkM= go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.44.0/go.mod h1:cJu+5jZwoZfkBOECSFtBZK/O7h/pY5djn0fwnIGnQ4A= go.opentelemetry.io/contrib/instrumentation/host v0.55.0 h1:V/Cy5A2ydwvyED4ewwXJ441R3QllG+U8tXXVOjPeX4Y= @@ -1405,9 +537,6 @@ go.opentelemetry.io/otel/sdk/metric v1.30.0 h1:QJLT8Pe11jyHBHfSAgYH7kEmT24eX792j go.opentelemetry.io/otel/sdk/metric v1.30.0/go.mod h1:waS6P3YqFNzeP01kuo/MBBYqaoBJl7efRQHOaydhy1Y= go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= -go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.temporal.io/api v1.39.0 h1:pbhcfvNDB7mllb8lIBqPcg+m6LMG/IhTpdiFxe+0mYk= @@ -1433,759 +562,156 @@ go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= -golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= -golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= -golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= -golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= -golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= -golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= -golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220207234003-57398862261d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= -gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= -gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= -gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= -gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= -gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= -gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= -gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= -google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= -google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= -google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= -google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= -google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= -google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= -google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= -google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= -google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= -google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= -google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= -google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= -google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= -google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= -google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= -google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= -google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= -google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= -google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= -google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= -google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91A08= -google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= -google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo= -google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= -google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= -google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= -google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= -google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI= -google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0= -google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= -google.golang.org/api v0.118.0/go.mod h1:76TtD3vkgmZ66zZzp72bUUklpmQmKlhh6sYtIjYK+5E= -google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms= -google.golang.org/api v0.124.0/go.mod h1:xu2HQurE5gi/3t1aFCvhPD781p0a3p11sdunTJ2BlP4= -google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= -google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= -google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= -google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= -google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= -google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= -google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= -google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= -google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= -google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= -google.golang.org/genproto v0.0.0-20221024153911-1573dae28c9c/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= -google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= -google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo= -google.golang.org/genproto v0.0.0-20221109142239-94d6d90a7d66/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221117204609-8f9c96812029/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE= -google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230112194545-e10362b5ecf9/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230123190316-2c411cf9d197/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230124163310-31e0e69b6fc2/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230127162408-596548ed4efa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA= -google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= -google.golang.org/genproto v0.0.0-20230223222841-637eb2293923/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= -google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488/go.mod h1:TvhZT5f700eVlTNwND1xoEZQeWTB2RY/65kplwl/bFA= -google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= -google.golang.org/genproto v0.0.0-20230320184635-7606e756e683/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= -google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= -google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= -google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= -google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= -google.golang.org/genproto v0.0.0-20230525234025-438c736192d0/go.mod h1:9ExIQyXL5hZrHzQceCwuSYwZZ5QZBazOcprJ5rgs3lY= -google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64= -google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go.mod h1:ts19tUU+Z0ZShN1y3aPyq2+O3d5FUNNgT6FtOzmrNn8= -google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= -google.golang.org/genproto/googleapis/api v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= -google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= -google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234015-3fc162c6f38a/go.mod h1:xURIpW9ES5+/GZhnV6beoEtxQrnkRGIfP5VQG2tCBLc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= -google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= -google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= -google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= -google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs= gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= -lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= -lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo= mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw= -modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= -modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= -modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= -modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= -modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= -modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= -modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= -modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws= -modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= -modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= -modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= -modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= -modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= -modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= -modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= -modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= -modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0= -modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s= -modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= -modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= -modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= -modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= -modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= -modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= -modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= -modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/tests/integration/suite/payments-connectors-dummy-pay.go b/tests/integration/suite/payments-connectors-dummy-pay.go index e83bdb2e70..e017a93425 100644 --- a/tests/integration/suite/payments-connectors-dummy-pay.go +++ b/tests/integration/suite/payments-connectors-dummy-pay.go @@ -1,110 +1,110 @@ package suite -import ( - webhooks "github.com/formancehq/webhooks/pkg" - "io" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "time" +// import ( +// webhooks "github.com/formancehq/webhooks/pkg" +// "io" +// "net/http" +// "net/http/httptest" +// "os" +// "path/filepath" +// "time" - "github.com/formancehq/stack/tests/integration/internal/modules" +// "github.com/formancehq/stack/tests/integration/internal/modules" - "github.com/formancehq/formance-sdk-go/v2/pkg/models/operations" - "github.com/formancehq/formance-sdk-go/v2/pkg/models/shared" - paymentEvents "github.com/formancehq/payments/pkg/events" - "github.com/formancehq/stack/libs/events" - . "github.com/formancehq/stack/tests/integration/internal" - "github.com/google/uuid" - "github.com/nats-io/nats.go" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) +// "github.com/formancehq/formance-sdk-go/v2/pkg/models/operations" +// "github.com/formancehq/formance-sdk-go/v2/pkg/models/shared" +// paymentEvents "github.com/formancehq/payments/pkg/events" +// "github.com/formancehq/stack/libs/events" +// . "github.com/formancehq/stack/tests/integration/internal" +// "github.com/google/uuid" +// "github.com/nats-io/nats.go" +// . "github.com/onsi/ginkgo/v2" +// . "github.com/onsi/gomega" +// ) -var _ = WithModules([]*Module{modules.Payments}, func() { - When("configuring dummy pay connector", func() { - var ( - msgs chan *nats.Msg - cancelSubscription func() - ) - JustBeforeEach(func() { - cancelSubscription, msgs = SubscribePayments() +// var _ = WithModules([]*Module{modules.Payments}, func() { +// When("configuring dummy pay connector", func() { +// var ( +// msgs chan *nats.Msg +// cancelSubscription func() +// ) +// JustBeforeEach(func() { +// cancelSubscription, msgs = SubscribePayments() - paymentsDir := filepath.Join(os.TempDir(), uuid.NewString()) - Expect(os.MkdirAll(paymentsDir, 0o777)).To(Succeed()) - response, err := Client().Payments.V1.InstallConnector( - TestContext(), - operations.InstallConnectorRequest{ - ConnectorConfig: shared.ConnectorConfig{ - DummyPayConfig: &shared.DummyPayConfig{ - FilePollingPeriod: ptr("1s"), - Directory: paymentsDir, - Name: "test", - NumberOfAccountsPreGenerated: ptr(int64(0)), - NumberOfPaymentsPreGenerated: ptr(int64(1)), - }, - }, - Connector: shared.ConnectorDummyPay, - }, - ) - Expect(err).ToNot(HaveOccurred()) - Expect(response.StatusCode).To(Equal(201)) - Expect(response.ConnectorResponse).ToNot(BeNil()) - }) - JustAfterEach(func() { - cancelSubscription() - }) - It("should trigger some events", func() { - msg := WaitOnChanWithTimeout(msgs, 20*time.Second) - Expect(events.Check(msg.Data, "payments", paymentEvents.EventTypeSavedPayments)).Should(Succeed()) - }) - It("should generate some payments", func() { - Eventually(func(g Gomega) []shared.Payment { - response, err := Client().Payments.V1.ListPayments( - TestContext(), - operations.ListPaymentsRequest{}, - ) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(response.StatusCode).To(Equal(200)) +// paymentsDir := filepath.Join(os.TempDir(), uuid.NewString()) +// Expect(os.MkdirAll(paymentsDir, 0o777)).To(Succeed()) +// response, err := Client().Payments.V1.InstallConnector( +// TestContext(), +// operations.InstallConnectorRequest{ +// ConnectorConfig: shared.ConnectorConfig{ +// DummyPayConfig: &shared.DummyPayConfig{ +// FilePollingPeriod: ptr("1s"), +// Directory: paymentsDir, +// Name: "test", +// NumberOfAccountsPreGenerated: ptr(int64(0)), +// NumberOfPaymentsPreGenerated: ptr(int64(1)), +// }, +// }, +// Connector: shared.ConnectorDummyPay, +// }, +// ) +// Expect(err).ToNot(HaveOccurred()) +// Expect(response.StatusCode).To(Equal(201)) +// Expect(response.ConnectorResponse).ToNot(BeNil()) +// }) +// JustAfterEach(func() { +// cancelSubscription() +// }) +// It("should trigger some events", func() { +// msg := WaitOnChanWithTimeout(msgs, 20*time.Second) +// Expect(events.Check(msg.Data, "payments", paymentEvents.EventTypeSavedPayments)).Should(Succeed()) +// }) +// It("should generate some payments", func() { +// Eventually(func(g Gomega) []shared.Payment { +// response, err := Client().Payments.V1.ListPayments( +// TestContext(), +// operations.ListPaymentsRequest{}, +// ) +// g.Expect(err).ToNot(HaveOccurred()) +// g.Expect(response.StatusCode).To(Equal(200)) - return response.PaymentsCursor.Cursor.Data - }).WithTimeout(10 * time.Second).ShouldNot(BeEmpty()) // TODO: Check other fields - }) - WithModules([]*Module{modules.Webhooks}, func() { - var ( - httpServer *httptest.Server - called chan []byte - secret = webhooks.NewSecret() - ) - BeforeEach(func() { - called = make(chan []byte) - httpServer = httptest.NewServer( - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer close(called) - data, _ := io.ReadAll(r.Body) - called <- data - })) - DeferCleanup(func() { - httpServer.Close() - }) +// return response.PaymentsCursor.Cursor.Data +// }).WithTimeout(10 * time.Second).ShouldNot(BeEmpty()) // TODO: Check other fields +// }) +// WithModules([]*Module{modules.Webhooks}, func() { +// var ( +// httpServer *httptest.Server +// called chan []byte +// secret = webhooks.NewSecret() +// ) +// BeforeEach(func() { +// called = make(chan []byte) +// httpServer = httptest.NewServer( +// http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// defer close(called) +// data, _ := io.ReadAll(r.Body) +// called <- data +// })) +// DeferCleanup(func() { +// httpServer.Close() +// }) - response, err := Client().Webhooks.V1.InsertConfig( - TestContext(), - shared.ConfigUser{ - Endpoint: httpServer.URL, - Secret: &secret, - EventTypes: []string{ - "payments.saved_payment", - }, - }, - ) - Expect(err).ToNot(HaveOccurred()) - Expect(response.StatusCode).To(Equal(http.StatusOK)) - }) - It("Should trigger a webhook", func() { - Eventually(called).Should(ReceiveEvent("payments", paymentEvents.EventTypeSavedPayments)) - }) - }) - }) -}) +// response, err := Client().Webhooks.V1.InsertConfig( +// TestContext(), +// shared.ConfigUser{ +// Endpoint: httpServer.URL, +// Secret: &secret, +// EventTypes: []string{ +// "payments.saved_payment", +// }, +// }, +// ) +// Expect(err).ToNot(HaveOccurred()) +// Expect(response.StatusCode).To(Equal(http.StatusOK)) +// }) +// It("Should trigger a webhook", func() { +// Eventually(called).Should(ReceiveEvent("payments", paymentEvents.EventTypeSavedPayments)) +// }) +// }) +// }) +// }) diff --git a/tests/integration/suite/payments-connectors-generic.go b/tests/integration/suite/payments-connectors-generic.go index 60f0c40d7f..66e4be2e6e 100644 --- a/tests/integration/suite/payments-connectors-generic.go +++ b/tests/integration/suite/payments-connectors-generic.go @@ -1,193 +1,193 @@ package suite -import ( - "math/big" - "time" +// import ( +// "math/big" +// "time" - "github.com/formancehq/formance-sdk-go/v2/pkg/models/operations" - "github.com/formancehq/formance-sdk-go/v2/pkg/models/sdkerrors" - "github.com/formancehq/formance-sdk-go/v2/pkg/models/shared" - . "github.com/formancehq/stack/tests/integration/internal" - "github.com/formancehq/stack/tests/integration/internal/modules" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) +// "github.com/formancehq/formance-sdk-go/v2/pkg/models/operations" +// "github.com/formancehq/formance-sdk-go/v2/pkg/models/sdkerrors" +// "github.com/formancehq/formance-sdk-go/v2/pkg/models/shared" +// . "github.com/formancehq/stack/tests/integration/internal" +// "github.com/formancehq/stack/tests/integration/internal/modules" +// . "github.com/onsi/ginkgo/v2" +// . "github.com/onsi/gomega" +// ) -const ( - fakeConnectorID = "eyJQcm92aWRlciI6Ik1PRFVMUiIsIlJlZmVyZW5jZSI6IjQyNDY5MjEwLTQ5YTMtNGQ0NS04OWQ1LWVmNWI3YWI4OTUwNyJ9" -) +// const ( +// fakeConnectorID = "eyJQcm92aWRlciI6Ik1PRFVMUiIsIlJlZmVyZW5jZSI6IjQyNDY5MjEwLTQ5YTMtNGQ0NS04OWQ1LWVmNWI3YWI4OTUwNyJ9" +// ) -var _ = WithModules([]*Module{modules.Payments}, func() { - When("trying to create accounts of an non existent connector", func() { - BeforeEach(func() { - _, err := Client().Payments.V1.CreateAccount( - TestContext(), - shared.AccountRequest{ - AccountName: ptr("test"), - ConnectorID: fakeConnectorID, // not installed - CreatedAt: time.Now(), - Reference: "test1", - Type: shared.AccountTypeInternal, - }, - ) - Expect(err).NotTo(BeNil()) - Expect(err.(*sdkerrors.PaymentsErrorResponse).ErrorCode).To(Equal(shared.PaymentsErrorsEnumValidation)) - }) - It("Should fail with a 400", func() { - }) - }) -}) +// var _ = WithModules([]*Module{modules.Payments}, func() { +// When("trying to create accounts of an non existent connector", func() { +// BeforeEach(func() { +// _, err := Client().Payments.V1.CreateAccount( +// TestContext(), +// shared.AccountRequest{ +// AccountName: ptr("test"), +// ConnectorID: fakeConnectorID, // not installed +// CreatedAt: time.Now(), +// Reference: "test1", +// Type: shared.AccountTypeInternal, +// }, +// ) +// Expect(err).NotTo(BeNil()) +// Expect(err.(*sdkerrors.PaymentsErrorResponse).ErrorCode).To(Equal(shared.PaymentsErrorsEnumValidation)) +// }) +// It("Should fail with a 400", func() { +// }) +// }) +// }) -var _ = WithModules([]*Module{modules.Payments}, func() { - When("trying to create payments of an non existent connector", func() { - BeforeEach(func() { - _, err := Client().Payments.V1.CreatePayment( - TestContext(), - shared.PaymentRequest{ - Amount: big.NewInt(100), - Asset: "EUR/2", - ConnectorID: fakeConnectorID, - CreatedAt: time.Now(), - Reference: "test", - Scheme: shared.PaymentSchemeOther, - Status: shared.PaymentStatusSucceeded, - Type: shared.PaymentTypeTransfer, - }, - ) - Expect(err).NotTo(BeNil()) - Expect(err.(*sdkerrors.PaymentsErrorResponse).ErrorCode).To(Equal(shared.PaymentsErrorsEnumValidation)) - }) - It("Should fail with a 400", func() { - }) - }) -}) +// var _ = WithModules([]*Module{modules.Payments}, func() { +// When("trying to create payments of an non existent connector", func() { +// BeforeEach(func() { +// _, err := Client().Payments.V1.CreatePayment( +// TestContext(), +// shared.PaymentRequest{ +// Amount: big.NewInt(100), +// Asset: "EUR/2", +// ConnectorID: fakeConnectorID, +// CreatedAt: time.Now(), +// Reference: "test", +// Scheme: shared.PaymentSchemeOther, +// Status: shared.PaymentStatusSucceeded, +// Type: shared.PaymentTypeTransfer, +// }, +// ) +// Expect(err).NotTo(BeNil()) +// Expect(err.(*sdkerrors.PaymentsErrorResponse).ErrorCode).To(Equal(shared.PaymentsErrorsEnumValidation)) +// }) +// It("Should fail with a 400", func() { +// }) +// }) +// }) -var _ = WithModules([]*Module{modules.Payments}, func() { - var ( - connectorID string - ) - BeforeEach(func() { - response, err := Client().Payments.V1.InstallConnector( - TestContext(), - operations.InstallConnectorRequest{ - ConnectorConfig: shared.ConnectorConfig{ - GenericConfig: &shared.GenericConfig{ - Name: "test", - }, - }, - Connector: shared.ConnectorGeneric, - }, - ) - Expect(err).ToNot(HaveOccurred()) - Expect(response.StatusCode).To(Equal(201)) - Expect(response.ConnectorResponse).ToNot(BeNil()) +// var _ = WithModules([]*Module{modules.Payments}, func() { +// var ( +// connectorID string +// ) +// BeforeEach(func() { +// response, err := Client().Payments.V1.InstallConnector( +// TestContext(), +// operations.InstallConnectorRequest{ +// ConnectorConfig: shared.ConnectorConfig{ +// GenericConfig: &shared.GenericConfig{ +// Name: "test", +// }, +// }, +// Connector: shared.ConnectorGeneric, +// }, +// ) +// Expect(err).ToNot(HaveOccurred()) +// Expect(response.StatusCode).To(Equal(201)) +// Expect(response.ConnectorResponse).ToNot(BeNil()) - connectorID = response.ConnectorResponse.Data.ConnectorID - }) - When("creating accounts and payments", func() { - var ( - accountIDInternal1 string - accountIDInternal2 string - ) - BeforeEach(func() { - createAccountResponse, err := Client().Payments.V1.CreateAccount( - TestContext(), - shared.AccountRequest{ - AccountName: ptr("test 1"), - ConnectorID: connectorID, - CreatedAt: time.Now(), - Reference: "test1", - Type: shared.AccountTypeInternal, - }, - ) - Expect(err).ToNot(HaveOccurred()) - Expect(createAccountResponse.StatusCode).To(Equal(200)) +// connectorID = response.ConnectorResponse.Data.ConnectorID +// }) +// When("creating accounts and payments", func() { +// var ( +// accountIDInternal1 string +// accountIDInternal2 string +// ) +// BeforeEach(func() { +// createAccountResponse, err := Client().Payments.V1.CreateAccount( +// TestContext(), +// shared.AccountRequest{ +// AccountName: ptr("test 1"), +// ConnectorID: connectorID, +// CreatedAt: time.Now(), +// Reference: "test1", +// Type: shared.AccountTypeInternal, +// }, +// ) +// Expect(err).ToNot(HaveOccurred()) +// Expect(createAccountResponse.StatusCode).To(Equal(200)) - accountIDInternal1 = createAccountResponse.PaymentsAccountResponse.Data.ID +// accountIDInternal1 = createAccountResponse.PaymentsAccountResponse.Data.ID - createAccountResponse, err = Client().Payments.V1.CreateAccount( - TestContext(), - shared.AccountRequest{ - AccountName: ptr("test 2"), - ConnectorID: connectorID, - CreatedAt: time.Now(), - Reference: "test2", - Type: shared.AccountTypeInternal, - }, - ) - Expect(err).ToNot(HaveOccurred()) - Expect(createAccountResponse.StatusCode).To(Equal(200)) +// createAccountResponse, err = Client().Payments.V1.CreateAccount( +// TestContext(), +// shared.AccountRequest{ +// AccountName: ptr("test 2"), +// ConnectorID: connectorID, +// CreatedAt: time.Now(), +// Reference: "test2", +// Type: shared.AccountTypeInternal, +// }, +// ) +// Expect(err).ToNot(HaveOccurred()) +// Expect(createAccountResponse.StatusCode).To(Equal(200)) - accountIDInternal2 = createAccountResponse.PaymentsAccountResponse.Data.ID +// accountIDInternal2 = createAccountResponse.PaymentsAccountResponse.Data.ID - createPaymentResponse, err := Client().Payments.V1.CreatePayment( - TestContext(), - shared.PaymentRequest{ - Amount: big.NewInt(100), - Asset: "EUR/2", - ConnectorID: connectorID, - CreatedAt: time.Now(), - DestinationAccountID: &accountIDInternal2, - Reference: "p1", - Scheme: shared.PaymentSchemeOther, - SourceAccountID: &accountIDInternal1, - Status: shared.PaymentStatusSucceeded, - Type: shared.PaymentTypeTransfer, - }, - ) - Expect(err).ToNot(HaveOccurred()) - Expect(createPaymentResponse.StatusCode).To(Equal(200)) +// createPaymentResponse, err := Client().Payments.V1.CreatePayment( +// TestContext(), +// shared.PaymentRequest{ +// Amount: big.NewInt(100), +// Asset: "EUR/2", +// ConnectorID: connectorID, +// CreatedAt: time.Now(), +// DestinationAccountID: &accountIDInternal2, +// Reference: "p1", +// Scheme: shared.PaymentSchemeOther, +// SourceAccountID: &accountIDInternal1, +// Status: shared.PaymentStatusSucceeded, +// Type: shared.PaymentTypeTransfer, +// }, +// ) +// Expect(err).ToNot(HaveOccurred()) +// Expect(createPaymentResponse.StatusCode).To(Equal(200)) - createPaymentResponse, err = Client().Payments.V1.CreatePayment( - TestContext(), - shared.PaymentRequest{ - Amount: big.NewInt(200), - Asset: "EUR/2", - ConnectorID: connectorID, - CreatedAt: time.Now(), - Reference: "p2", - DestinationAccountID: &accountIDInternal1, - Scheme: shared.PaymentSchemeOther, - Status: shared.PaymentStatusSucceeded, - Type: shared.PaymentTypePayIn, - }, - ) - Expect(err).ToNot(HaveOccurred()) - Expect(createPaymentResponse.StatusCode).To(Equal(200)) +// createPaymentResponse, err = Client().Payments.V1.CreatePayment( +// TestContext(), +// shared.PaymentRequest{ +// Amount: big.NewInt(200), +// Asset: "EUR/2", +// ConnectorID: connectorID, +// CreatedAt: time.Now(), +// Reference: "p2", +// DestinationAccountID: &accountIDInternal1, +// Scheme: shared.PaymentSchemeOther, +// Status: shared.PaymentStatusSucceeded, +// Type: shared.PaymentTypePayIn, +// }, +// ) +// Expect(err).ToNot(HaveOccurred()) +// Expect(createPaymentResponse.StatusCode).To(Equal(200)) - createPaymentResponse, err = Client().Payments.V1.CreatePayment( - TestContext(), - shared.PaymentRequest{ - Amount: big.NewInt(300), - Asset: "EUR/2", - ConnectorID: connectorID, - CreatedAt: time.Now(), - Reference: "p3", - SourceAccountID: &accountIDInternal1, - Scheme: shared.PaymentSchemeOther, - Status: shared.PaymentStatusFailed, - Type: shared.PaymentTypePayout, - }, - ) - Expect(err).ToNot(HaveOccurred()) - Expect(createPaymentResponse.StatusCode).To(Equal(200)) - }) - It("should be available on api", func() { - listAccountsResponse, err := Client().Payments.V1.PaymentslistAccounts( - TestContext(), - operations.PaymentslistAccountsRequest{}, - ) - Expect(err).ToNot(HaveOccurred()) - Expect(listAccountsResponse.StatusCode).To(Equal(200)) - Expect(listAccountsResponse.AccountsCursor.Cursor.Data).To(HaveLen(2)) +// createPaymentResponse, err = Client().Payments.V1.CreatePayment( +// TestContext(), +// shared.PaymentRequest{ +// Amount: big.NewInt(300), +// Asset: "EUR/2", +// ConnectorID: connectorID, +// CreatedAt: time.Now(), +// Reference: "p3", +// SourceAccountID: &accountIDInternal1, +// Scheme: shared.PaymentSchemeOther, +// Status: shared.PaymentStatusFailed, +// Type: shared.PaymentTypePayout, +// }, +// ) +// Expect(err).ToNot(HaveOccurred()) +// Expect(createPaymentResponse.StatusCode).To(Equal(200)) +// }) +// It("should be available on api", func() { +// listAccountsResponse, err := Client().Payments.V1.PaymentslistAccounts( +// TestContext(), +// operations.PaymentslistAccountsRequest{}, +// ) +// Expect(err).ToNot(HaveOccurred()) +// Expect(listAccountsResponse.StatusCode).To(Equal(200)) +// Expect(listAccountsResponse.AccountsCursor.Cursor.Data).To(HaveLen(2)) - listPaymentsResponse, err := Client().Payments.V1.ListPayments( - TestContext(), - operations.ListPaymentsRequest{}, - ) - Expect(err).ToNot(HaveOccurred()) - Expect(listPaymentsResponse.StatusCode).To(Equal(200)) - Expect(listPaymentsResponse.PaymentsCursor.Cursor.Data).To(HaveLen(3)) - }) - }) -}) +// listPaymentsResponse, err := Client().Payments.V1.ListPayments( +// TestContext(), +// operations.ListPaymentsRequest{}, +// ) +// Expect(err).ToNot(HaveOccurred()) +// Expect(listPaymentsResponse.StatusCode).To(Equal(200)) +// Expect(listPaymentsResponse.PaymentsCursor.Cursor.Data).To(HaveLen(3)) +// }) +// }) +// }) diff --git a/tests/integration/suite/payments-connectors-reset.go b/tests/integration/suite/payments-connectors-reset.go index ea29a5ff5d..b1b863f239 100644 --- a/tests/integration/suite/payments-connectors-reset.go +++ b/tests/integration/suite/payments-connectors-reset.go @@ -1,116 +1,116 @@ package suite -import ( - "encoding/json" - "os" - "path/filepath" - "time" +// import ( +// "encoding/json" +// "os" +// "path/filepath" +// "time" - "github.com/formancehq/stack/tests/integration/internal/modules" +// "github.com/formancehq/stack/tests/integration/internal/modules" - "github.com/formancehq/formance-sdk-go/v2/pkg/models/operations" - "github.com/formancehq/formance-sdk-go/v2/pkg/models/shared" - paymentEvents "github.com/formancehq/payments/pkg/events" - "github.com/formancehq/stack/libs/events" - . "github.com/formancehq/stack/tests/integration/internal" - "github.com/google/uuid" - "github.com/nats-io/nats.go" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) +// "github.com/formancehq/formance-sdk-go/v2/pkg/models/operations" +// "github.com/formancehq/formance-sdk-go/v2/pkg/models/shared" +// paymentEvents "github.com/formancehq/payments/pkg/events" +// "github.com/formancehq/stack/libs/events" +// . "github.com/formancehq/stack/tests/integration/internal" +// "github.com/google/uuid" +// "github.com/nats-io/nats.go" +// . "github.com/onsi/ginkgo/v2" +// . "github.com/onsi/gomega" +// ) -var _ = WithModules([]*Module{modules.Payments, modules.Search}, func() { - var ( - connectorID string - existingPaymentID string - msgs chan *nats.Msg - cancelSubscription func() - ) - BeforeEach(func() { - cancelSubscription, msgs = SubscribePayments() +// var _ = WithModules([]*Module{modules.Payments, modules.Search}, func() { +// var ( +// connectorID string +// existingPaymentID string +// msgs chan *nats.Msg +// cancelSubscription func() +// ) +// BeforeEach(func() { +// cancelSubscription, msgs = SubscribePayments() - paymentsDir := filepath.Join(os.TempDir(), uuid.NewString()) - Expect(os.MkdirAll(paymentsDir, 0o777)).To(Succeed()) - response, err := Client().Payments.V1.InstallConnector( - TestContext(), - operations.InstallConnectorRequest{ - ConnectorConfig: shared.ConnectorConfig{ - DummyPayConfig: &shared.DummyPayConfig{ - FilePollingPeriod: ptr("1s"), - Directory: paymentsDir, - Name: "test", - NumberOfPaymentsPreGenerated: ptr(int64(1)), - }, - }, - Connector: shared.ConnectorDummyPay, - }, - ) - Expect(err).ToNot(HaveOccurred()) - Expect(response.StatusCode).To(Equal(201)) - Expect(response.ConnectorResponse).ToNot(BeNil()) - Expect(response.ConnectorResponse.Data).ToNot(BeNil()) - connectorID = response.ConnectorResponse.Data.ConnectorID +// paymentsDir := filepath.Join(os.TempDir(), uuid.NewString()) +// Expect(os.MkdirAll(paymentsDir, 0o777)).To(Succeed()) +// response, err := Client().Payments.V1.InstallConnector( +// TestContext(), +// operations.InstallConnectorRequest{ +// ConnectorConfig: shared.ConnectorConfig{ +// DummyPayConfig: &shared.DummyPayConfig{ +// FilePollingPeriod: ptr("1s"), +// Directory: paymentsDir, +// Name: "test", +// NumberOfPaymentsPreGenerated: ptr(int64(1)), +// }, +// }, +// Connector: shared.ConnectorDummyPay, +// }, +// ) +// Expect(err).ToNot(HaveOccurred()) +// Expect(response.StatusCode).To(Equal(201)) +// Expect(response.ConnectorResponse).ToNot(BeNil()) +// Expect(response.ConnectorResponse.Data).ToNot(BeNil()) +// connectorID = response.ConnectorResponse.Data.ConnectorID - Eventually(func(g Gomega) bool { - response, err := Client().Search.V1.Search( - TestContext(), - shared.Query{ - Target: ptr("PAYMENT"), - }, - ) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(response.StatusCode).To(Equal(200)) - if len(response.Response.Cursor.Data) == 0 { - return false - } - existingPaymentID = response.Response.Cursor.Data[0]["id"].(string) - return true - }).Should(BeTrue()) - }) - AfterEach(func() { - cancelSubscription() - }) - When("resetting connector", func() { - BeforeEach(func() { - response, err := Client().Payments.V1.ResetConnectorV1( - TestContext(), - operations.ResetConnectorV1Request{ - Connector: shared.ConnectorDummyPay, - ConnectorID: connectorID, - }, - ) - Expect(err).ToNot(HaveOccurred()) - Expect(response.StatusCode).To(Equal(204)) - }) - It("should trigger some events", func() { - var msg *nats.Msg - Eventually(func(g Gomega) bool { - msg = WaitOnChanWithTimeout(msgs, 5*time.Second) - type typedMessage struct { - Type string `json:"type"` - } +// Eventually(func(g Gomega) bool { +// response, err := Client().Search.V1.Search( +// TestContext(), +// shared.Query{ +// Target: ptr("PAYMENT"), +// }, +// ) +// g.Expect(err).ToNot(HaveOccurred()) +// g.Expect(response.StatusCode).To(Equal(200)) +// if len(response.Response.Cursor.Data) == 0 { +// return false +// } +// existingPaymentID = response.Response.Cursor.Data[0]["id"].(string) +// return true +// }).Should(BeTrue()) +// }) +// AfterEach(func() { +// cancelSubscription() +// }) +// When("resetting connector", func() { +// BeforeEach(func() { +// response, err := Client().Payments.V1.ResetConnectorV1( +// TestContext(), +// operations.ResetConnectorV1Request{ +// Connector: shared.ConnectorDummyPay, +// ConnectorID: connectorID, +// }, +// ) +// Expect(err).ToNot(HaveOccurred()) +// Expect(response.StatusCode).To(Equal(204)) +// }) +// It("should trigger some events", func() { +// var msg *nats.Msg +// Eventually(func(g Gomega) bool { +// msg = WaitOnChanWithTimeout(msgs, 5*time.Second) +// type typedMessage struct { +// Type string `json:"type"` +// } - tm := &typedMessage{} - g.Expect(json.Unmarshal(msg.Data, tm)).To(Succeed()) - return tm.Type == paymentEvents.EventTypeConnectorReset - }).Should(BeTrue()) +// tm := &typedMessage{} +// g.Expect(json.Unmarshal(msg.Data, tm)).To(Succeed()) +// return tm.Type == paymentEvents.EventTypeConnectorReset +// }).Should(BeTrue()) - Expect(events.Check(msg.Data, "payments", paymentEvents.EventTypeConnectorReset)).Should(Succeed()) - }) - It("should delete payments on search service", func() { - Eventually(func(g Gomega) []map[string]any { - response, err := Client().Search.V1.Search( - TestContext(), - shared.Query{ - Target: ptr("PAYMENT"), - Terms: []string{"id=" + existingPaymentID}, - }, - ) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(response.StatusCode).To(Equal(200)) +// Expect(events.Check(msg.Data, "payments", paymentEvents.EventTypeConnectorReset)).Should(Succeed()) +// }) +// It("should delete payments on search service", func() { +// Eventually(func(g Gomega) []map[string]any { +// response, err := Client().Search.V1.Search( +// TestContext(), +// shared.Query{ +// Target: ptr("PAYMENT"), +// Terms: []string{"id=" + existingPaymentID}, +// }, +// ) +// g.Expect(err).ToNot(HaveOccurred()) +// g.Expect(response.StatusCode).To(Equal(200)) - return response.Response.Cursor.Data - }).Should(BeEmpty()) - }) - }) -}) +// return response.Response.Cursor.Data +// }).Should(BeEmpty()) +// }) +// }) +// }) diff --git a/tests/integration/suite/payments-connectors-stripe.go b/tests/integration/suite/payments-connectors-stripe.go index 0aa259bb30..9708cd43b4 100644 --- a/tests/integration/suite/payments-connectors-stripe.go +++ b/tests/integration/suite/payments-connectors-stripe.go @@ -1,84 +1,84 @@ package suite -import ( - "os" - "time" +// import ( +// "os" +// "time" - "github.com/formancehq/stack/tests/integration/internal/modules" +// "github.com/formancehq/stack/tests/integration/internal/modules" - "github.com/formancehq/formance-sdk-go/v2/pkg/models/operations" - "github.com/formancehq/formance-sdk-go/v2/pkg/models/shared" - paymentEvents "github.com/formancehq/payments/pkg/events" - "github.com/formancehq/stack/libs/events" - . "github.com/formancehq/stack/tests/integration/internal" - "github.com/nats-io/nats.go" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) +// "github.com/formancehq/formance-sdk-go/v2/pkg/models/operations" +// "github.com/formancehq/formance-sdk-go/v2/pkg/models/shared" +// paymentEvents "github.com/formancehq/payments/pkg/events" +// "github.com/formancehq/stack/libs/events" +// . "github.com/formancehq/stack/tests/integration/internal" +// "github.com/nats-io/nats.go" +// . "github.com/onsi/ginkgo/v2" +// . "github.com/onsi/gomega" +// ) -var _ = WithModules([]*Module{modules.Payments, modules.Search}, func() { - When("configuring stripe connector", func() { - var ( - msgs chan *nats.Msg - cancelSubscription func() - ) - BeforeEach(func() { - apiKey := os.Getenv("STRIPE_API_KEY") - if apiKey == "" { - Skip("No stripe api key provided") - } +// var _ = WithModules([]*Module{modules.Payments, modules.Search}, func() { +// When("configuring stripe connector", func() { +// var ( +// msgs chan *nats.Msg +// cancelSubscription func() +// ) +// BeforeEach(func() { +// apiKey := os.Getenv("STRIPE_API_KEY") +// if apiKey == "" { +// Skip("No stripe api key provided") +// } - cancelSubscription, msgs = SubscribePayments() +// cancelSubscription, msgs = SubscribePayments() - response, err := Client().Payments.V1.InstallConnector( - TestContext(), - operations.InstallConnectorRequest{ - ConnectorConfig: shared.ConnectorConfig{ - StripeConfig: &shared.StripeConfig{ - APIKey: apiKey, - Name: "stripe-test", - }, - }, - Connector: shared.ConnectorStripe, - }, - ) - Expect(err).ToNot(HaveOccurred()) - Expect(response.StatusCode).To(Equal(201)) - Expect(response.ConnectorResponse).ToNot(BeNil()) - Expect(response.ConnectorResponse.Data).ToNot(BeNil()) - }) - AfterEach(func() { - cancelSubscription() - }) - It("should trigger some events", func() { - msg := WaitOnChanWithTimeout(msgs, 5*time.Second) - Expect(events.Check(msg.Data, "payments", paymentEvents.EventTypeSavedPayments)).Should(Succeed()) - }) - It("should generate some payments", func() { - Eventually(func(g Gomega) []shared.Payment { - response, err := Client().Payments.V1.ListPayments( - TestContext(), - operations.ListPaymentsRequest{}, - ) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(response.StatusCode).To(Equal(200)) - return response.PaymentsCursor.Cursor.Data - }).ShouldNot(BeEmpty()) // TODO: Check other fields - }) - It("should be ingested on search", func() { - Eventually(func(g Gomega) bool { - response, err := Client().Search.V1.Search( - TestContext(), - shared.Query{ - Target: ptr("PAYMENT"), - }, - ) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(response.StatusCode).To(Equal(200)) - g.Expect(response.Response.Cursor.Data).NotTo(BeEmpty()) +// response, err := Client().Payments.V1.InstallConnector( +// TestContext(), +// operations.InstallConnectorRequest{ +// ConnectorConfig: shared.ConnectorConfig{ +// StripeConfig: &shared.StripeConfig{ +// APIKey: apiKey, +// Name: "stripe-test", +// }, +// }, +// Connector: shared.ConnectorStripe, +// }, +// ) +// Expect(err).ToNot(HaveOccurred()) +// Expect(response.StatusCode).To(Equal(201)) +// Expect(response.ConnectorResponse).ToNot(BeNil()) +// Expect(response.ConnectorResponse.Data).ToNot(BeNil()) +// }) +// AfterEach(func() { +// cancelSubscription() +// }) +// It("should trigger some events", func() { +// msg := WaitOnChanWithTimeout(msgs, 5*time.Second) +// Expect(events.Check(msg.Data, "payments", paymentEvents.EventTypeSavedPayments)).Should(Succeed()) +// }) +// It("should generate some payments", func() { +// Eventually(func(g Gomega) []shared.Payment { +// response, err := Client().Payments.V1.ListPayments( +// TestContext(), +// operations.ListPaymentsRequest{}, +// ) +// g.Expect(err).ToNot(HaveOccurred()) +// g.Expect(response.StatusCode).To(Equal(200)) +// return response.PaymentsCursor.Cursor.Data +// }).ShouldNot(BeEmpty()) // TODO: Check other fields +// }) +// It("should be ingested on search", func() { +// Eventually(func(g Gomega) bool { +// response, err := Client().Search.V1.Search( +// TestContext(), +// shared.Query{ +// Target: ptr("PAYMENT"), +// }, +// ) +// g.Expect(err).ToNot(HaveOccurred()) +// g.Expect(response.StatusCode).To(Equal(200)) +// g.Expect(response.Response.Cursor.Data).NotTo(BeEmpty()) - return true - }).Should(BeTrue()) - }) - }) -}) +// return true +// }).Should(BeTrue()) +// }) +// }) +// }) diff --git a/tests/integration/suite/reconciliation-policy-create-list.go b/tests/integration/suite/reconciliation-policy-create-list.go index 53b54a0820..11bec01b47 100644 --- a/tests/integration/suite/reconciliation-policy-create-list.go +++ b/tests/integration/suite/reconciliation-policy-create-list.go @@ -1,77 +1,77 @@ package suite -import ( - "github.com/formancehq/formance-sdk-go/v2/pkg/models/operations" - "github.com/formancehq/formance-sdk-go/v2/pkg/models/shared" - . "github.com/formancehq/stack/tests/integration/internal" - "github.com/formancehq/stack/tests/integration/internal/modules" - "github.com/google/uuid" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) +// import ( +// "github.com/formancehq/formance-sdk-go/v2/pkg/models/operations" +// "github.com/formancehq/formance-sdk-go/v2/pkg/models/shared" +// . "github.com/formancehq/stack/tests/integration/internal" +// "github.com/formancehq/stack/tests/integration/internal/modules" +// "github.com/google/uuid" +// . "github.com/onsi/ginkgo/v2" +// . "github.com/onsi/gomega" +// ) -var _ = WithModules([]*Module{modules.Auth, modules.Ledger, modules.Payments, modules.Reconciliation}, func() { - When("1 - reconciliation list policies", func() { - var ( - policies []shared.Policy - ) - JustBeforeEach(func() { - policiesResponse, err := Client().Reconciliation.V1.ListPolicies( - TestContext(), - operations.ListPoliciesRequest{}, - ) - Expect(err).ToNot(HaveOccurred()) - Expect(policiesResponse.StatusCode).To(Equal(200)) +// var _ = WithModules([]*Module{modules.Auth, modules.Ledger, modules.Payments, modules.Reconciliation}, func() { +// When("1 - reconciliation list policies", func() { +// var ( +// policies []shared.Policy +// ) +// JustBeforeEach(func() { +// policiesResponse, err := Client().Reconciliation.V1.ListPolicies( +// TestContext(), +// operations.ListPoliciesRequest{}, +// ) +// Expect(err).ToNot(HaveOccurred()) +// Expect(policiesResponse.StatusCode).To(Equal(200)) - policies = policiesResponse.PoliciesCursorResponse.Cursor.Data - }) - It("should respond with empty lists", func() { - Expect(policies).To(BeEmpty()) - }) - }) - When("2 - reconciliation create 2 policies", func() { - JustBeforeEach(func() { - response, err := Client().Reconciliation.V1.CreatePolicy( - TestContext(), - shared.PolicyRequest{ - LedgerName: "default", - LedgerQuery: map[string]interface{}{}, - Name: "test 1", - PaymentsPoolID: uuid.New().String(), - }, - ) - Expect(err).ToNot(HaveOccurred()) - Expect(response.StatusCode).To(Equal(201)) +// policies = policiesResponse.PoliciesCursorResponse.Cursor.Data +// }) +// It("should respond with empty lists", func() { +// Expect(policies).To(BeEmpty()) +// }) +// }) +// When("2 - reconciliation create 2 policies", func() { +// JustBeforeEach(func() { +// response, err := Client().Reconciliation.V1.CreatePolicy( +// TestContext(), +// shared.PolicyRequest{ +// LedgerName: "default", +// LedgerQuery: map[string]interface{}{}, +// Name: "test 1", +// PaymentsPoolID: uuid.New().String(), +// }, +// ) +// Expect(err).ToNot(HaveOccurred()) +// Expect(response.StatusCode).To(Equal(201)) - response, err = Client().Reconciliation.V1.CreatePolicy( - TestContext(), - shared.PolicyRequest{ - LedgerName: "default", - LedgerQuery: map[string]interface{}{}, - Name: "test 2", - PaymentsPoolID: uuid.New().String(), - }, - ) - Expect(err).ToNot(HaveOccurred()) - Expect(response.StatusCode).To(Equal(201)) - }) - Then("should list 2 policies", func() { - var ( - policies []shared.Policy - ) - JustBeforeEach(func() { - policiesResponse, err := Client().Reconciliation.V1.ListPolicies( - TestContext(), - operations.ListPoliciesRequest{}, - ) - Expect(err).ToNot(HaveOccurred()) - Expect(policiesResponse.StatusCode).To(Equal(200)) +// response, err = Client().Reconciliation.V1.CreatePolicy( +// TestContext(), +// shared.PolicyRequest{ +// LedgerName: "default", +// LedgerQuery: map[string]interface{}{}, +// Name: "test 2", +// PaymentsPoolID: uuid.New().String(), +// }, +// ) +// Expect(err).ToNot(HaveOccurred()) +// Expect(response.StatusCode).To(Equal(201)) +// }) +// Then("should list 2 policies", func() { +// var ( +// policies []shared.Policy +// ) +// JustBeforeEach(func() { +// policiesResponse, err := Client().Reconciliation.V1.ListPolicies( +// TestContext(), +// operations.ListPoliciesRequest{}, +// ) +// Expect(err).ToNot(HaveOccurred()) +// Expect(policiesResponse.StatusCode).To(Equal(200)) - policies = policiesResponse.PoliciesCursorResponse.Cursor.Data - }) - It("should return 2 items", func() { - Expect(policies).To(HaveLen(2)) - }) - }) - }) -}) +// policies = policiesResponse.PoliciesCursorResponse.Cursor.Data +// }) +// It("should return 2 items", func() { +// Expect(policies).To(HaveLen(2)) +// }) +// }) +// }) +// })