Skip to content

feat: dynamically resolve connector types and their configs in v3 payments#155

Merged
laouji merged 6 commits into
mainfrom
EN-1018
May 21, 2026
Merged

feat: dynamically resolve connector types and their configs in v3 payments#155
laouji merged 6 commits into
mainfrom
EN-1018

Conversation

@laouji

@laouji laouji commented May 20, 2026

Copy link
Copy Markdown
Contributor

As long as the stack supports it you can now find and use new connector types without updating fctl.

naturally stacks that have payments v2 still use the SDK and only support old connector types

@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR consolidates per-connector CLI handlers into unified controllers. It removes ~40 connector-specific config/install files and adds generic controllers that handle both Payments API v3 and v1/v2 flows. Infrastructure improvements include raw HTTP client bundling, v3 config retrieval refactoring, and version normalization.

Changes

Unified Connector Management

Layer / File(s) Summary
HTTP client bundling and version normalization
pkg/clients.go, cmd/payments/versions/versions.go
New StackClients struct bundles typed SDK and raw HTTP clients sharing oauth2 token source with User-Agent injection. Version parsing normalizes v-prefixes from server response.
Unified connector config schema listing
cmd/payments/connectors/configs.go
New list-available command fetches connector schemas from v1/v3 APIs, iterates fields with sorting/normalization, and renders as grouped table by connector name.
V3 config retrieval refactoring to raw HTTP
cmd/payments/connectors/configs/getconfig.go
V3 branch now uses raw HTTP GET instead of SDK, parses response envelope, stores config as generic map, and renders as generic key/value table instead of connector-specific views.
Unified connector config update controller
cmd/payments/connectors/configs/updateconfig.go
Single controller replaces 13 per-connector update files, supporting v3 (canonical provider lookup + PATCH) and v1/v0/v2 (connector-specific typed config unmarshalling + typed API calls).
Unified connector installation controller
cmd/payments/connectors/install/install.go
Single controller replaces 17 per-connector install files, supporting v3 (provider canonicalization + POST) and v1/v2 (connector-specific typed config + typed API calls).
Command routing consolidation
cmd/payments/connectors/root.go, cmd/payments/connectors/install/root.go
Routes updated to wire unified controllers; install refactored from per-connector subcommands to positional-arg pattern install <connector> <file>.
Minor documentation updates
cmd/payments/connectors/list.go, Justfile
List command description changed from "enabled" to "installed"; Justfile adds install recipe.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • formancehq/fctl#129: Original PR that introduced per-connector config and install modules now being consolidated by this PR.

Poem

🐰 Whiskers twitching, tunnels clear,
Twenty connectors? No need, dear!
One smart handler fits them all,
Generic flows answer the call!
From v1 to v3, hop and bound,
Consolidated code all around! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: dynamic resolution of connector types and configs in v3 payments, which is the core objective of this PR.
Description check ✅ Passed The description relates to the changeset by explaining the benefit of dynamic connector type resolution for v3 payments and acknowledging v2 backward compatibility.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch EN-1018

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@laouji laouji marked this pull request as ready for review May 20, 2026 16:45
@laouji laouji requested a review from a team as a code owner May 20, 2026 16:45

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
cmd/payments/connectors/configs/updateconfig.go (1)

158-165: ⚡ Quick win

Redundant authentication call.

LoadAndAuthenticateCurrentProfile is already called in Run() at lines 65-68. Calling it again here is wasteful and could trigger unnecessary token refreshes. Pass the already-resolved profile, profileName, and relyingParty as parameters to runV1Typed instead.

♻️ Proposed refactor
-func (c *ConnectorUpdateConfigController) runV1Typed(
+func (c *ConnectorUpdateConfigController) runV1Typed(
 	cmd *cobra.Command,
+	relyingParty client.RelyingParty,
+	profileName string,
+	profile fctl.Profile,
 	connectorName, connectorID, script string,
 ) (fctl.Renderable, error) {
-	_, profile, profileName, relyingParty, err := fctl.LoadAndAuthenticateCurrentProfile(cmd)
-	if err != nil {
-		return nil, err
-	}
-	sc, err := fctl.NewStackClientFromFlags(cmd, relyingParty, fctl.NewPTermDialog(), profileName, *profile)
+	sc, err := fctl.NewStackClientFromFlags(cmd, relyingParty, fctl.NewPTermDialog(), profileName, profile)
 	if err != nil {
 		return nil, err
 	}

And update the call site in Run():

 	default:
 		if connectorID == "" {
 			return nil, fmt.Errorf("--connector-id is required")
 		}
-		return c.runV1Typed(cmd, connectorName, connectorID, script)
+		return c.runV1Typed(cmd, relyingParty, profileName, *profile, connectorName, connectorID, script)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/payments/connectors/configs/updateconfig.go` around lines 158 - 165, The
code calls LoadAndAuthenticateCurrentProfile again inside runV1Typed which is
redundant; change runV1Typed to accept the already-resolved profile,
profileName, and relyingParty (from Run where LoadAndAuthenticateCurrentProfile
is first called) and remove the second LoadAndAuthenticateCurrentProfile
invocation, then use those passed-in values when creating the stack client via
fctl.NewStackClientFromFlags; finally update the Run() call site to pass
profile, profileName, and relyingParty into runV1Typed so the function reuses
the authenticated context instead of re-authenticating.
cmd/payments/connectors/install/install.go (1)

150-158: ⚡ Quick win

Redundant authentication call.

Same issue as in updateconfig.go - LoadAndAuthenticateCurrentProfile is already called in Run() at lines 52-55. Pass the resolved values as parameters instead.

♻️ Proposed refactor
-func (c *ConnectorInstallController) runV1Typed(
+func (c *ConnectorInstallController) runV1Typed(
 	cmd *cobra.Command,
+	relyingParty client.RelyingParty,
+	profileName string,
+	profile fctl.Profile,
 	connectorName, script string,
 ) (fctl.Renderable, error) {
-	_, profile, profileName, relyingParty, err := fctl.LoadAndAuthenticateCurrentProfile(cmd)
-	if err != nil {
-		return nil, err
-	}
-
-	stackClient, err := fctl.NewStackClientFromFlags(cmd, relyingParty, fctl.NewPTermDialog(), profileName, *profile)
+	stackClient, err := fctl.NewStackClientFromFlags(cmd, relyingParty, fctl.NewPTermDialog(), profileName, profile)
 	if err != nil {
 		return nil, err
 	}

And update the call site in Run():

 	default:
-		return c.runV1Typed(cmd, connectorName, script)
+		return c.runV1Typed(cmd, relyingParty, profileName, *profile, connectorName, script)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/payments/connectors/install/install.go` around lines 150 - 158, Remove
the redundant LoadAndAuthenticateCurrentProfile call inside the installer
function and instead use the already-resolved values from Run(); specifically,
delete the extra call that assigns _, profile, profileName, relyingParty, err
and pass the existing profile, profileName and relyingParty into
fctl.NewStackClientFromFlags where it's invoked now. Update the function
signature or call site in Run() so the function receives (or closes over)
profile, profileName and relyingParty and then call
fctl.NewStackClientFromFlags(cmd, relyingParty, fctl.NewPTermDialog(),
profileName, *profile) without re-authenticating.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@cmd/payments/connectors/configs/updateconfig.go`:
- Around line 158-165: The code calls LoadAndAuthenticateCurrentProfile again
inside runV1Typed which is redundant; change runV1Typed to accept the
already-resolved profile, profileName, and relyingParty (from Run where
LoadAndAuthenticateCurrentProfile is first called) and remove the second
LoadAndAuthenticateCurrentProfile invocation, then use those passed-in values
when creating the stack client via fctl.NewStackClientFromFlags; finally update
the Run() call site to pass profile, profileName, and relyingParty into
runV1Typed so the function reuses the authenticated context instead of
re-authenticating.

In `@cmd/payments/connectors/install/install.go`:
- Around line 150-158: Remove the redundant LoadAndAuthenticateCurrentProfile
call inside the installer function and instead use the already-resolved values
from Run(); specifically, delete the extra call that assigns _, profile,
profileName, relyingParty, err and pass the existing profile, profileName and
relyingParty into fctl.NewStackClientFromFlags where it's invoked now. Update
the function signature or call site in Run() so the function receives (or closes
over) profile, profileName and relyingParty and then call
fctl.NewStackClientFromFlags(cmd, relyingParty, fctl.NewPTermDialog(),
profileName, *profile) without re-authenticating.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e81cb2a4-aa14-4788-b804-91ddbf97e046

📥 Commits

Reviewing files that changed from the base of the PR and between 2c531a2 and 593166e.

📒 Files selected for processing (46)
  • Justfile
  • cmd/payments/connectors/configs.go
  • cmd/payments/connectors/configs/adyen.go
  • cmd/payments/connectors/configs/atlar.go
  • cmd/payments/connectors/configs/bankingcircle.go
  • cmd/payments/connectors/configs/coinbaseprime.go
  • cmd/payments/connectors/configs/column.go
  • cmd/payments/connectors/configs/currencycloud.go
  • cmd/payments/connectors/configs/fireblocks.go
  • cmd/payments/connectors/configs/getconfig.go
  • cmd/payments/connectors/configs/increase.go
  • cmd/payments/connectors/configs/mangopay.go
  • cmd/payments/connectors/configs/modulr.go
  • cmd/payments/connectors/configs/moneycorp.go
  • cmd/payments/connectors/configs/plaid.go
  • cmd/payments/connectors/configs/powens.go
  • cmd/payments/connectors/configs/qonto.go
  • cmd/payments/connectors/configs/root.go
  • cmd/payments/connectors/configs/stripe.go
  • cmd/payments/connectors/configs/tink.go
  • cmd/payments/connectors/configs/updateconfig.go
  • cmd/payments/connectors/configs/wise.go
  • cmd/payments/connectors/install/adyen.go
  • cmd/payments/connectors/install/atlar.go
  • cmd/payments/connectors/install/bankingcircle.go
  • cmd/payments/connectors/install/coinbaseprime.go
  • cmd/payments/connectors/install/column.go
  • cmd/payments/connectors/install/currencycloud.go
  • cmd/payments/connectors/install/fireblocks.go
  • cmd/payments/connectors/install/generic.go
  • cmd/payments/connectors/install/increase.go
  • cmd/payments/connectors/install/install.go
  • cmd/payments/connectors/install/mangopay.go
  • cmd/payments/connectors/install/modulr.go
  • cmd/payments/connectors/install/moneycorp.go
  • cmd/payments/connectors/install/plaid.go
  • cmd/payments/connectors/install/powens.go
  • cmd/payments/connectors/install/qonto.go
  • cmd/payments/connectors/install/root.go
  • cmd/payments/connectors/install/stripe.go
  • cmd/payments/connectors/install/tink.go
  • cmd/payments/connectors/install/wise.go
  • cmd/payments/connectors/list.go
  • cmd/payments/connectors/root.go
  • cmd/payments/versions/versions.go
  • pkg/clients.go
💤 Files with no reviewable changes (36)
  • cmd/payments/connectors/install/bankingcircle.go
  • cmd/payments/connectors/install/modulr.go
  • cmd/payments/connectors/configs/fireblocks.go
  • cmd/payments/connectors/configs/column.go
  • cmd/payments/connectors/configs/atlar.go
  • cmd/payments/connectors/install/generic.go
  • cmd/payments/connectors/install/mangopay.go
  • cmd/payments/connectors/install/fireblocks.go
  • cmd/payments/connectors/install/qonto.go
  • cmd/payments/connectors/configs/qonto.go
  • cmd/payments/connectors/install/tink.go
  • cmd/payments/connectors/configs/bankingcircle.go
  • cmd/payments/connectors/install/wise.go
  • cmd/payments/connectors/install/moneycorp.go
  • cmd/payments/connectors/configs/powens.go
  • cmd/payments/connectors/configs/increase.go
  • cmd/payments/connectors/configs/root.go
  • cmd/payments/connectors/configs/stripe.go
  • cmd/payments/connectors/configs/modulr.go
  • cmd/payments/connectors/configs/adyen.go
  • cmd/payments/connectors/install/increase.go
  • cmd/payments/connectors/configs/mangopay.go
  • cmd/payments/connectors/configs/tink.go
  • cmd/payments/connectors/configs/currencycloud.go
  • cmd/payments/connectors/configs/plaid.go
  • cmd/payments/connectors/install/currencycloud.go
  • cmd/payments/connectors/install/plaid.go
  • cmd/payments/connectors/install/stripe.go
  • cmd/payments/connectors/install/column.go
  • cmd/payments/connectors/install/atlar.go
  • cmd/payments/connectors/configs/coinbaseprime.go
  • cmd/payments/connectors/configs/wise.go
  • cmd/payments/connectors/install/powens.go
  • cmd/payments/connectors/install/adyen.go
  • cmd/payments/connectors/install/coinbaseprime.go
  • cmd/payments/connectors/configs/moneycorp.go

@fguery fguery left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how easy it'd be, but ti'd be nice to add some tests. I know we don't have much in fctl though so up to you.

Comment thread pkg/clients.go

rawHTTPClient := oauth2.NewClient(baseCtx, tokenSource)
rawHTTPClient.Transport = newInjectHTTPHeadersRoundTripper(
http.Header{"User-Agent": []string{fmt.Sprintf("fctl/%s", getVersion(cmd))}},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shoudl we add otel?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok so the effect of that is that we would set the traceparent header and the otel collectors on the receiving end of those requests will try to set those trace ids as the parent spans. However we have no way to collect traces from fctl, so all requests made from this fctl client will have missing root spans in Signoz.

I'm pretty sure we don't want that.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RIght, fair enough!

NewTinkCommand(),
NewPlaidCommand(),
),
fctl.WithShortDescription("Install a connector"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly worth adding a link to the doc for the list?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a command list-available that dynamically fetches the connectors and their configuration template

name := strings.ToLower(connectorName)
switch name {
case internal.AdyenConnector:
var config shared.AdyenConfig

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we can do a refactor to avoid duplicating this that much? Even if we need to use a weaker type, it might be justified here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is legacy code should we go to such an effort? I'd rather not spend hours debugging every single connector to ensure we made it compatible (the strongly typed SDK at least spares me that effort)

name := strings.ToLower(connectorName)
switch name {
case internal.AdyenConnector:
config := &shared.AdyenConfig{}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as on the install conector, it'd be nice to refactor that a bit

sort.Strings(connectorNames)
for _, connectorName := range connectorNames {
fieldMap := data[connectorName]
fieldNames := make([]string, 0, len(fieldMap))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can use data[connectorName].Keys, depending of the go version.
Not a big deal

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but I'm old and set in my ways 😂

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imagine my surprise, as an old Java guy, when I saw no generics, no streaming API, etc 😂
Fine :)

@laouji laouji merged commit 90e8aa2 into main May 21, 2026
5 checks passed
@laouji laouji deleted the EN-1018 branch May 21, 2026 10:34
@altitude

Copy link
Copy Markdown
Member

great stuff @laouji

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants