From bfeb357089ee3df75fca3d385778a55773d48117 Mon Sep 17 00:00:00 2001 From: Paul Nicolas Date: Fri, 22 Dec 2023 16:19:29 +0100 Subject: [PATCH] feat(fctl): add reconciliation (#1077) --- components/fctl/cmd/reconciliation/list.go | 154 ++++++++++++++++ .../cmd/reconciliation/policies/create.go | 92 ++++++++++ .../cmd/reconciliation/policies/delete.go | 97 ++++++++++ .../fctl/cmd/reconciliation/policies/list.go | 162 +++++++++++++++++ .../reconciliation/policies/reconciliation.go | 169 ++++++++++++++++++ .../fctl/cmd/reconciliation/policies/root.go | 20 +++ .../fctl/cmd/reconciliation/policies/show.go | 112 ++++++++++++ components/fctl/cmd/reconciliation/root.go | 18 ++ components/fctl/cmd/reconciliation/show.go | 154 ++++++++++++++++ components/fctl/cmd/root.go | 2 + components/fctl/pkg/utils.go | 8 + ee/gateway/pkg/plugins/auth.go | 21 ++- ee/reconciliation/openapi.yaml | 12 +- releases/sdks/go/docs/models/shared/policy.md | 16 +- .../go/docs/models/shared/policyrequest.md | 12 +- .../go/docs/models/shared/policyresponse.md | 6 +- .../models/shared/reconciliationresponse.md | 6 +- .../go/docs/sdks/reconciliation/README.md | 4 +- releases/sdks/go/pkg/models/shared/policy.go | 16 +- .../go/pkg/models/shared/policyrequest.go | 12 +- .../go/pkg/models/shared/policyresponse.go | 8 +- .../models/shared/reconciliationresponse.go | 8 +- 22 files changed, 1051 insertions(+), 58 deletions(-) create mode 100644 components/fctl/cmd/reconciliation/list.go create mode 100644 components/fctl/cmd/reconciliation/policies/create.go create mode 100644 components/fctl/cmd/reconciliation/policies/delete.go create mode 100644 components/fctl/cmd/reconciliation/policies/list.go create mode 100644 components/fctl/cmd/reconciliation/policies/reconciliation.go create mode 100644 components/fctl/cmd/reconciliation/policies/root.go create mode 100644 components/fctl/cmd/reconciliation/policies/show.go create mode 100644 components/fctl/cmd/reconciliation/root.go create mode 100644 components/fctl/cmd/reconciliation/show.go diff --git a/components/fctl/cmd/reconciliation/list.go b/components/fctl/cmd/reconciliation/list.go new file mode 100644 index 0000000000..935ad2041a --- /dev/null +++ b/components/fctl/cmd/reconciliation/list.go @@ -0,0 +1,154 @@ +package reconciliation + +import ( + "fmt" + "time" + + fctl "github.com/formancehq/fctl/pkg" + "github.com/formancehq/formance-sdk-go/pkg/models/operations" + "github.com/formancehq/formance-sdk-go/pkg/models/shared" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +type ListStore struct { + Cursor *shared.ReconciliationsCursorResponseCursor `json:"cursor"` +} + +type ListController struct { + store *ListStore + + cursorFlag string + pageSizeFlag string +} + +var _ fctl.Controller[*ListStore] = (*ListController)(nil) + +func NewListStore() *ListStore { + return &ListStore{ + Cursor: &shared.ReconciliationsCursorResponseCursor{}, + } +} + +func NewListController() *ListController { + return &ListController{ + store: NewListStore(), + + cursorFlag: "cursor", + pageSizeFlag: "page-size", + } +} + +func (c *ListController) GetStore() *ListStore { + return c.store +} + +func (c *ListController) Run(cmd *cobra.Command, args []string) (fctl.Renderable, error) { + cfg, err := fctl.GetConfig(cmd) + if err != nil { + return nil, err + } + + organizationID, err := fctl.ResolveOrganizationID(cmd, cfg) + if err != nil { + return nil, err + } + + stack, err := fctl.ResolveStack(cmd, cfg, organizationID) + if err != nil { + return nil, err + } + + client, err := fctl.NewStackClient(cmd, cfg, stack) + if err != nil { + return nil, err + } + + var cursor *string + if c := fctl.GetString(cmd, c.cursorFlag); c != "" { + cursor = &c + } + + var pageSize *int64 + if ps := fctl.GetInt(cmd, c.pageSizeFlag); ps > 0 { + pageSize = fctl.Ptr(int64(ps)) + } + + response, err := client.Reconciliation.ListReconciliations( + cmd.Context(), + operations.ListReconciliationsRequest{ + Cursor: cursor, + PageSize: pageSize, + }, + ) + if err != nil { + return nil, err + } + + if response.StatusCode >= 300 { + return nil, fmt.Errorf("unexpected status code: %d", response.StatusCode) + } + + c.store.Cursor = &response.ReconciliationsCursorResponse.Cursor + + return c, nil +} + +func (c *ListController) Render(cmd *cobra.Command, args []string) error { + tableData := fctl.Map(c.store.Cursor.Data, func(p shared.Reconciliation) []string { + return []string{ + p.ID, + p.PolicyID, + p.CreatedAt.Format(time.RFC3339), + p.ReconciledAtLedger.Format(time.RFC3339), + p.ReconciledAtPayments.Format(time.RFC3339), + p.Status, + } + }) + tableData = fctl.Prepend(tableData, []string{"ID", "PolicyID", "CreatedAt", "ReconciledAtLedger", + "ReconciledAtPayments", "Status"}) + if err := pterm.DefaultTable. + WithHasHeader(). + WithWriter(cmd.OutOrStdout()). + WithData(tableData). + Render(); err != nil { + return err + } + + tableData = pterm.TableData{} + tableData = append(tableData, []string{pterm.LightCyan("HasMore"), fmt.Sprintf("%v", c.store.Cursor.HasMore)}) + tableData = append(tableData, []string{pterm.LightCyan("PageSize"), fmt.Sprintf("%d", c.store.Cursor.PageSize)}) + tableData = append(tableData, []string{pterm.LightCyan("Next"), func() string { + if c.store.Cursor.Next == nil { + return "" + } + return *c.store.Cursor.Next + }()}) + tableData = append(tableData, []string{pterm.LightCyan("Previous"), func() string { + if c.store.Cursor.Previous == nil { + return "" + } + return *c.store.Cursor.Previous + }()}) + + if err := pterm.DefaultTable. + WithWriter(cmd.OutOrStdout()). + WithData(tableData). + Render(); err != nil { + return err + } + + return nil +} + +func NewListCommand() *cobra.Command { + c := NewListController() + return fctl.NewCommand("list", + fctl.WithAliases("ls", "l"), + fctl.WithArgs(cobra.ExactArgs(0)), + fctl.WithShortDescription("List reconciliations"), + fctl.WithStringFlag(c.cursorFlag, "", "Cursor"), + fctl.WithIntFlag(c.pageSizeFlag, 0, "PageSize"), + fctl.WithController[*ListStore](c), + ) +} diff --git a/components/fctl/cmd/reconciliation/policies/create.go b/components/fctl/cmd/reconciliation/policies/create.go new file mode 100644 index 0000000000..14e0510e8c --- /dev/null +++ b/components/fctl/cmd/reconciliation/policies/create.go @@ -0,0 +1,92 @@ +package policies + +import ( + "encoding/json" + "fmt" + + fctl "github.com/formancehq/fctl/pkg" + "github.com/formancehq/formance-sdk-go/pkg/models/shared" + "github.com/pkg/errors" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +type CreateStore struct { + PolicyID string `json:"policyID"` +} +type CreateController struct { + store *CreateStore +} + +var _ fctl.Controller[*CreateStore] = (*CreateController)(nil) + +func NewCreateStore() *CreateStore { + return &CreateStore{} +} + +func NewCreateController() *CreateController { + return &CreateController{ + store: NewCreateStore(), + } +} + +func NewCreateCommand() *cobra.Command { + c := NewCreateController() + return fctl.NewCommand("create |-", + fctl.WithConfirmFlag(), + fctl.WithShortDescription("Create a policy"), + fctl.WithAliases("cr", "c"), + fctl.WithArgs(cobra.ExactArgs(1)), + fctl.WithController[*CreateStore](c), + ) +} + +func (c *CreateController) GetStore() *CreateStore { + return c.store +} + +func (c *CreateController) Run(cmd *cobra.Command, args []string) (fctl.Renderable, error) { + soc, err := fctl.GetStackOrganizationConfig(cmd) + if err != nil { + return nil, err + } + + if !fctl.CheckStackApprobation(cmd, soc.Stack, "You are about to create a new policy") { + return nil, fctl.ErrMissingApproval + } + + client, err := fctl.NewStackClient(cmd, soc.Config, soc.Stack) + if err != nil { + return nil, errors.Wrap(err, "creating stack client") + } + + script, err := fctl.ReadFile(cmd, soc.Stack, args[0]) + if err != nil { + return nil, err + } + + request := shared.PolicyRequest{} + if err := json.Unmarshal([]byte(script), &request); err != nil { + return nil, err + } + + //nolint:gosimple + response, err := client.Reconciliation.CreatePolicy(cmd.Context(), request) + if err != nil { + return nil, err + } + + if response.StatusCode >= 300 { + return nil, fmt.Errorf("unexpected status code: %d", response.StatusCode) + } + + c.store.PolicyID = response.PolicyResponse.Data.ID + + return c, nil +} + +func (c *CreateController) Render(cmd *cobra.Command, args []string) error { + pterm.Success.WithWriter(cmd.OutOrStdout()).Printfln("Policy created with ID: %s", c.store.PolicyID) + + return nil +} diff --git a/components/fctl/cmd/reconciliation/policies/delete.go b/components/fctl/cmd/reconciliation/policies/delete.go new file mode 100644 index 0000000000..c845eb9682 --- /dev/null +++ b/components/fctl/cmd/reconciliation/policies/delete.go @@ -0,0 +1,97 @@ +package policies + +import ( + "fmt" + + fctl "github.com/formancehq/fctl/pkg" + "github.com/formancehq/formance-sdk-go/pkg/models/operations" + "github.com/pkg/errors" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +type DeleteStore struct { + PolicyID string `json:"policyID"` + Success bool `json:"success"` +} + +type DeleteController struct { + store *DeleteStore +} + +var _ fctl.Controller[*DeleteStore] = (*DeleteController)(nil) + +func NewDeleteStore() *DeleteStore { + return &DeleteStore{} +} + +func NewDeleteController() *DeleteController { + return &DeleteController{ + store: NewDeleteStore(), + } +} +func NewDeleteCommand() *cobra.Command { + c := NewDeleteController() + return fctl.NewCommand("delete ", + fctl.WithConfirmFlag(), + fctl.WithAliases("d"), + fctl.WithShortDescription("Delete a policy"), + fctl.WithArgs(cobra.ExactArgs(1)), + fctl.WithController[*DeleteStore](c), + ) +} + +func (c *DeleteController) GetStore() *DeleteStore { + return c.store +} + +func (c *DeleteController) Run(cmd *cobra.Command, args []string) (fctl.Renderable, error) { + cfg, err := fctl.GetConfig(cmd) + if err != nil { + return nil, errors.Wrap(err, "retrieving config") + } + + organizationID, err := fctl.ResolveOrganizationID(cmd, cfg) + if err != nil { + return nil, err + } + + stack, err := fctl.ResolveStack(cmd, cfg, organizationID) + if err != nil { + return nil, err + } + + if !fctl.CheckStackApprobation(cmd, stack, "You are about to delete '%s'", args[0]) { + return nil, fctl.ErrMissingApproval + } + + client, err := fctl.NewStackClient(cmd, cfg, stack) + if err != nil { + return nil, errors.Wrap(err, "creating stack client") + } + + response, err := client.Reconciliation.DeletePolicy( + cmd.Context(), + operations.DeletePolicyRequest{ + PolicyID: args[0], + }, + ) + + if err != nil { + return nil, err + } + + if response.StatusCode >= 300 { + return nil, fmt.Errorf("unexpected status code: %d", response.StatusCode) + } + + c.store.PolicyID = args[0] + c.store.Success = true + + return c, nil +} + +func (c *DeleteController) Render(cmd *cobra.Command, args []string) error { + pterm.Success.WithWriter(cmd.OutOrStdout()).Printfln("Policy %s Deleted!", c.store.PolicyID) + return nil +} diff --git a/components/fctl/cmd/reconciliation/policies/list.go b/components/fctl/cmd/reconciliation/policies/list.go new file mode 100644 index 0000000000..dfac2c0c5f --- /dev/null +++ b/components/fctl/cmd/reconciliation/policies/list.go @@ -0,0 +1,162 @@ +package policies + +import ( + "encoding/json" + "fmt" + "time" + + fctl "github.com/formancehq/fctl/pkg" + "github.com/formancehq/formance-sdk-go/pkg/models/operations" + "github.com/formancehq/formance-sdk-go/pkg/models/shared" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +type ListStore struct { + Cursor *shared.PoliciesCursorResponseCursor `json:"cursor"` +} + +type ListController struct { + store *ListStore + + cursorFlag string + pageSizeFlag string +} + +var _ fctl.Controller[*ListStore] = (*ListController)(nil) + +func NewListStore() *ListStore { + return &ListStore{ + Cursor: &shared.PoliciesCursorResponseCursor{}, + } +} + +func NewListController() *ListController { + return &ListController{ + store: NewListStore(), + + cursorFlag: "cursor", + pageSizeFlag: "page-size", + } +} + +func (c *ListController) GetStore() *ListStore { + return c.store +} + +func (c *ListController) Run(cmd *cobra.Command, args []string) (fctl.Renderable, error) { + cfg, err := fctl.GetConfig(cmd) + if err != nil { + return nil, err + } + + organizationID, err := fctl.ResolveOrganizationID(cmd, cfg) + if err != nil { + return nil, err + } + + stack, err := fctl.ResolveStack(cmd, cfg, organizationID) + if err != nil { + return nil, err + } + + client, err := fctl.NewStackClient(cmd, cfg, stack) + if err != nil { + return nil, err + } + + var cursor *string + if c := fctl.GetString(cmd, c.cursorFlag); c != "" { + cursor = &c + } + + var pageSize *int64 + if ps := fctl.GetInt(cmd, c.pageSizeFlag); ps > 0 { + pageSize = fctl.Ptr(int64(ps)) + } + + response, err := client.Reconciliation.ListPolicies( + cmd.Context(), + operations.ListPoliciesRequest{ + Cursor: cursor, + PageSize: pageSize, + }, + ) + if err != nil { + return nil, err + } + + if response.StatusCode >= 300 { + return nil, fmt.Errorf("unexpected status code: %d", response.StatusCode) + } + + c.store.Cursor = &response.PoliciesCursorResponse.Cursor + + return c, nil +} + +func (c *ListController) Render(cmd *cobra.Command, args []string) error { + tableData := fctl.Map(c.store.Cursor.Data, func(p shared.Policy) []string { + return []string{ + p.ID, + p.Name, + p.CreatedAt.Format(time.RFC3339), + p.LedgerName, + func() string { + if p.LedgerQuery == nil { + return "" + } + + raw, _ := json.Marshal(p.LedgerQuery) + return string(raw) + }(), + p.PaymentsPoolID, + } + }) + tableData = fctl.Prepend(tableData, []string{"ID", "Name", "CreatedAt", "LedgerName", + "LedgerQuery", "PaymentsPoolID"}) + if err := pterm.DefaultTable. + WithHasHeader(). + WithWriter(cmd.OutOrStdout()). + WithData(tableData). + Render(); err != nil { + return err + } + + tableData = pterm.TableData{} + tableData = append(tableData, []string{pterm.LightCyan("HasMore"), fmt.Sprintf("%v", c.store.Cursor.HasMore)}) + tableData = append(tableData, []string{pterm.LightCyan("PageSize"), fmt.Sprintf("%d", c.store.Cursor.PageSize)}) + tableData = append(tableData, []string{pterm.LightCyan("Next"), func() string { + if c.store.Cursor.Next == nil { + return "" + } + return *c.store.Cursor.Next + }()}) + tableData = append(tableData, []string{pterm.LightCyan("Previous"), func() string { + if c.store.Cursor.Previous == nil { + return "" + } + return *c.store.Cursor.Previous + }()}) + + if err := pterm.DefaultTable. + WithWriter(cmd.OutOrStdout()). + WithData(tableData). + Render(); err != nil { + return err + } + + return nil +} + +func NewListCommand() *cobra.Command { + c := NewListController() + return fctl.NewCommand("list", + fctl.WithAliases("ls", "l"), + fctl.WithArgs(cobra.ExactArgs(0)), + fctl.WithShortDescription("List policies"), + fctl.WithStringFlag(c.cursorFlag, "", "Cursor"), + fctl.WithIntFlag(c.pageSizeFlag, 0, "PageSize"), + fctl.WithController[*ListStore](c), + ) +} diff --git a/components/fctl/cmd/reconciliation/policies/reconciliation.go b/components/fctl/cmd/reconciliation/policies/reconciliation.go new file mode 100644 index 0000000000..06e9659fd6 --- /dev/null +++ b/components/fctl/cmd/reconciliation/policies/reconciliation.go @@ -0,0 +1,169 @@ +package policies + +import ( + "fmt" + "math/big" + "time" + + fctl "github.com/formancehq/fctl/pkg" + "github.com/formancehq/formance-sdk-go/pkg/models/operations" + "github.com/formancehq/formance-sdk-go/pkg/models/shared" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +type ReconciliationStore struct { + Reconciliation shared.Reconciliation `json:"reconciliation"` +} +type ReconciliationController struct { + store *ReconciliationStore +} + +var _ fctl.Controller[*ReconciliationStore] = (*ReconciliationController)(nil) + +func NewReconciliationStore() *ReconciliationStore { + return &ReconciliationStore{} +} + +func NewReconciliationController() *ReconciliationController { + return &ReconciliationController{ + store: NewReconciliationStore(), + } +} + +func NewReconciliationCommand() *cobra.Command { + return fctl.NewCommand("reconcile ", + fctl.WithShortDescription("Launch a reconciliation from a policy"), + fctl.WithArgs(cobra.ExactArgs(3)), + fctl.WithAliases("r"), + fctl.WithController[*ReconciliationStore](NewReconciliationController()), + ) +} + +func (c *ReconciliationController) GetStore() *ReconciliationStore { + return c.store +} + +func (c *ReconciliationController) Run(cmd *cobra.Command, args []string) (fctl.Renderable, error) { + cfg, err := fctl.GetConfig(cmd) + if err != nil { + return nil, err + } + + organizationID, err := fctl.ResolveOrganizationID(cmd, cfg) + if err != nil { + return nil, err + } + + stack, err := fctl.ResolveStack(cmd, cfg, organizationID) + if err != nil { + return nil, err + } + + ledgerClient, err := fctl.NewStackClient(cmd, cfg, stack) + if err != nil { + return nil, err + } + + atLedger, err := time.Parse(time.RFC3339, args[1]) + if err != nil { + return nil, err + } + + atPayments, err := time.Parse(time.RFC3339, args[2]) + if err != nil { + return nil, err + } + + fmt.Println(args[0], atLedger, atPayments) + response, err := ledgerClient.Reconciliation.Reconcile(cmd.Context(), operations.ReconcileRequest{ + PolicyID: args[0], + ReconciliationRequest: shared.ReconciliationRequest{ + ReconciledAtLedger: atLedger, + ReconciledAtPayments: atPayments, + }, + }) + if err != nil { + return nil, err + } + + if response.StatusCode >= 300 { + return nil, fmt.Errorf("unexpected status code: %d", response.StatusCode) + } + + if response.ReconciliationResponse == nil { + return nil, fmt.Errorf("policy not found") + } + + c.store.Reconciliation = response.ReconciliationResponse.Data + + return c, nil +} + +func (c *ReconciliationController) Render(cmd *cobra.Command, args []string) error { + fctl.Section.WithWriter(cmd.OutOrStdout()).Println("Information") + tableData := pterm.TableData{} + tableData = append(tableData, []string{pterm.LightCyan("ID"), c.store.Reconciliation.ID}) + tableData = append(tableData, []string{pterm.LightCyan("PolicyID"), c.store.Reconciliation.PolicyID}) + tableData = append(tableData, []string{pterm.LightCyan("CreatedAt"), c.store.Reconciliation.CreatedAt.Format(time.RFC3339)}) + tableData = append(tableData, []string{pterm.LightCyan("ReconciledAtLedger"), c.store.Reconciliation.ReconciledAtLedger.Format(time.RFC3339)}) + tableData = append(tableData, []string{pterm.LightCyan("ReconciledAtPayments"), c.store.Reconciliation.ReconciledAtPayments.Format(time.RFC3339)}) + tableData = append(tableData, []string{pterm.LightCyan("Status"), c.store.Reconciliation.Status}) + + if err := pterm.DefaultTable. + WithWriter(cmd.OutOrStdout()). + WithData(tableData). + Render(); err != nil { + return err + } + + fctl.Section.WithWriter(cmd.OutOrStdout()).Println("Ledger Balances") + tableData = fctl.MapMap(c.store.Reconciliation.LedgerBalances, func(key string, value *big.Int) []string { + return []string{ + key, + value.String(), + } + }) + tableData = fctl.Prepend(tableData, []string{"Asset", "Amount"}) + if err := pterm.DefaultTable. + WithHasHeader(). + WithWriter(cmd.OutOrStdout()). + WithData(tableData). + Render(); err != nil { + return err + } + + fctl.Section.WithWriter(cmd.OutOrStdout()).Println("Payments Balances") + tableData = fctl.MapMap(c.store.Reconciliation.PaymentsBalances, func(key string, value *big.Int) []string { + return []string{ + key, + value.String(), + } + }) + tableData = fctl.Prepend(tableData, []string{"Asset", "Amount"}) + if err := pterm.DefaultTable. + WithHasHeader(). + WithWriter(cmd.OutOrStdout()). + WithData(tableData). + Render(); err != nil { + return err + } + + fctl.Section.WithWriter(cmd.OutOrStdout()).Println("Drift Balances") + tableData = fctl.MapMap(c.store.Reconciliation.DriftBalances, func(key string, value *big.Int) []string { + return []string{ + key, + value.String(), + } + }) + tableData = fctl.Prepend(tableData, []string{"Asset", "Amount"}) + if err := pterm.DefaultTable. + WithHasHeader(). + WithWriter(cmd.OutOrStdout()). + WithData(tableData). + Render(); err != nil { + return err + } + + return nil +} diff --git a/components/fctl/cmd/reconciliation/policies/root.go b/components/fctl/cmd/reconciliation/policies/root.go new file mode 100644 index 0000000000..094c6a8f3f --- /dev/null +++ b/components/fctl/cmd/reconciliation/policies/root.go @@ -0,0 +1,20 @@ +package policies + +import ( + fctl "github.com/formancehq/fctl/pkg" + "github.com/spf13/cobra" +) + +func NewPoliciesCommand() *cobra.Command { + return fctl.NewCommand("policies", + fctl.WithAliases("p"), + fctl.WithShortDescription("Policies management"), + fctl.WithChildCommands( + NewListCommand(), + NewShowCommand(), + NewCreateCommand(), + NewDeleteCommand(), + NewReconciliationCommand(), + ), + ) +} diff --git a/components/fctl/cmd/reconciliation/policies/show.go b/components/fctl/cmd/reconciliation/policies/show.go new file mode 100644 index 0000000000..10e266b663 --- /dev/null +++ b/components/fctl/cmd/reconciliation/policies/show.go @@ -0,0 +1,112 @@ +package policies + +import ( + "encoding/json" + "fmt" + "time" + + fctl "github.com/formancehq/fctl/pkg" + "github.com/formancehq/formance-sdk-go/pkg/models/operations" + "github.com/formancehq/formance-sdk-go/pkg/models/shared" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +type ShowStore struct { + Policy shared.Policy `json:"policy"` +} +type ShowController struct { + store *ShowStore +} + +var _ fctl.Controller[*ShowStore] = (*ShowController)(nil) + +func NewShowStore() *ShowStore { + return &ShowStore{} +} + +func NewShowController() *ShowController { + return &ShowController{ + store: NewShowStore(), + } +} + +func NewShowCommand() *cobra.Command { + return fctl.NewCommand("get ", + fctl.WithShortDescription("Get policy"), + fctl.WithArgs(cobra.ExactArgs(1)), + fctl.WithAliases("sh", "s"), + fctl.WithController[*ShowStore](NewShowController()), + ) +} + +func (c *ShowController) GetStore() *ShowStore { + return c.store +} + +func (c *ShowController) Run(cmd *cobra.Command, args []string) (fctl.Renderable, error) { + cfg, err := fctl.GetConfig(cmd) + if err != nil { + return nil, err + } + + organizationID, err := fctl.ResolveOrganizationID(cmd, cfg) + if err != nil { + return nil, err + } + + stack, err := fctl.ResolveStack(cmd, cfg, organizationID) + if err != nil { + return nil, err + } + + ledgerClient, err := fctl.NewStackClient(cmd, cfg, stack) + if err != nil { + return nil, err + } + + response, err := ledgerClient.Reconciliation.GetPolicy(cmd.Context(), operations.GetPolicyRequest{ + PolicyID: args[0], + }) + if err != nil { + return nil, err + } + + if response.StatusCode >= 300 { + return nil, fmt.Errorf("unexpected status code: %d", response.StatusCode) + } + + if response.PolicyResponse == nil { + return nil, fmt.Errorf("policy not found") + } + + c.store.Policy = response.PolicyResponse.Data + + return c, nil +} + +func (c *ShowController) Render(cmd *cobra.Command, args []string) error { + fctl.Section.WithWriter(cmd.OutOrStdout()).Println("Information") + tableData := pterm.TableData{} + tableData = append(tableData, []string{pterm.LightCyan("ID"), c.store.Policy.ID}) + tableData = append(tableData, []string{pterm.LightCyan("Name"), c.store.Policy.Name}) + tableData = append(tableData, []string{pterm.LightCyan("CreatedAt"), c.store.Policy.CreatedAt.Format(time.RFC3339)}) + tableData = append(tableData, []string{pterm.LightCyan("LedgerName"), c.store.Policy.LedgerName}) + tableData = append(tableData, []string{pterm.LightCyan("LedgerQuey"), func() string { + if c.store.Policy.LedgerQuery == nil { + return "" + } + raw, _ := json.Marshal(c.store.Policy.LedgerQuery) + return string(raw) + }()}) + tableData = append(tableData, []string{pterm.LightCyan("PaymentsPoolID"), c.store.Policy.PaymentsPoolID}) + + if err := pterm.DefaultTable. + WithWriter(cmd.OutOrStdout()). + WithData(tableData). + Render(); err != nil { + return err + } + + return nil +} diff --git a/components/fctl/cmd/reconciliation/root.go b/components/fctl/cmd/reconciliation/root.go new file mode 100644 index 0000000000..1ac375f075 --- /dev/null +++ b/components/fctl/cmd/reconciliation/root.go @@ -0,0 +1,18 @@ +package reconciliation + +import ( + "github.com/formancehq/fctl/cmd/reconciliation/policies" + fctl "github.com/formancehq/fctl/pkg" + "github.com/spf13/cobra" +) + +func NewCommand() *cobra.Command { + return fctl.NewStackCommand("reconciliation", + fctl.WithShortDescription("Reconciliation management"), + fctl.WithChildCommands( + policies.NewPoliciesCommand(), + NewListCommand(), + NewShowCommand(), + ), + ) +} diff --git a/components/fctl/cmd/reconciliation/show.go b/components/fctl/cmd/reconciliation/show.go new file mode 100644 index 0000000000..79b9a8dc3e --- /dev/null +++ b/components/fctl/cmd/reconciliation/show.go @@ -0,0 +1,154 @@ +package reconciliation + +import ( + "fmt" + "math/big" + "time" + + fctl "github.com/formancehq/fctl/pkg" + "github.com/formancehq/formance-sdk-go/pkg/models/operations" + "github.com/formancehq/formance-sdk-go/pkg/models/shared" + "github.com/pterm/pterm" + "github.com/spf13/cobra" +) + +type ShowStore struct { + Reconciliation shared.Reconciliation `json:"policy"` +} +type ShowController struct { + store *ShowStore +} + +var _ fctl.Controller[*ShowStore] = (*ShowController)(nil) + +func NewShowStore() *ShowStore { + return &ShowStore{} +} + +func NewShowController() *ShowController { + return &ShowController{ + store: NewShowStore(), + } +} + +func NewShowCommand() *cobra.Command { + return fctl.NewCommand("get ", + fctl.WithShortDescription("Get reconciliation"), + fctl.WithArgs(cobra.ExactArgs(1)), + fctl.WithAliases("sh", "s"), + fctl.WithController[*ShowStore](NewShowController()), + ) +} + +func (c *ShowController) GetStore() *ShowStore { + return c.store +} + +func (c *ShowController) Run(cmd *cobra.Command, args []string) (fctl.Renderable, error) { + cfg, err := fctl.GetConfig(cmd) + if err != nil { + return nil, err + } + + organizationID, err := fctl.ResolveOrganizationID(cmd, cfg) + if err != nil { + return nil, err + } + + stack, err := fctl.ResolveStack(cmd, cfg, organizationID) + if err != nil { + return nil, err + } + + ledgerClient, err := fctl.NewStackClient(cmd, cfg, stack) + if err != nil { + return nil, err + } + + response, err := ledgerClient.Reconciliation.GetReconciliation(cmd.Context(), operations.GetReconciliationRequest{ + ReconciliationID: args[0], + }) + if err != nil { + return nil, err + } + + if response.StatusCode >= 300 { + return nil, fmt.Errorf("unexpected status code: %d", response.StatusCode) + } + + if response.ReconciliationResponse == nil { + return nil, fmt.Errorf("policy not found") + } + + c.store.Reconciliation = response.ReconciliationResponse.Data + + return c, nil +} + +func (c *ShowController) Render(cmd *cobra.Command, args []string) error { + fctl.Section.WithWriter(cmd.OutOrStdout()).Println("Information") + tableData := pterm.TableData{} + tableData = append(tableData, []string{pterm.LightCyan("ID"), c.store.Reconciliation.ID}) + tableData = append(tableData, []string{pterm.LightCyan("PolicyID"), c.store.Reconciliation.PolicyID}) + tableData = append(tableData, []string{pterm.LightCyan("CreatedAt"), c.store.Reconciliation.CreatedAt.Format(time.RFC3339)}) + tableData = append(tableData, []string{pterm.LightCyan("ReconciledAtLedger"), c.store.Reconciliation.ReconciledAtLedger.Format(time.RFC3339)}) + tableData = append(tableData, []string{pterm.LightCyan("ReconciledAtPayments"), c.store.Reconciliation.ReconciledAtPayments.Format(time.RFC3339)}) + tableData = append(tableData, []string{pterm.LightCyan("Status"), c.store.Reconciliation.Status}) + + if err := pterm.DefaultTable. + WithWriter(cmd.OutOrStdout()). + WithData(tableData). + Render(); err != nil { + return err + } + + fctl.Section.WithWriter(cmd.OutOrStdout()).Println("Ledger Balances") + tableData = fctl.MapMap(c.store.Reconciliation.LedgerBalances, func(key string, value *big.Int) []string { + return []string{ + key, + value.String(), + } + }) + tableData = fctl.Prepend(tableData, []string{"Asset", "Amount"}) + if err := pterm.DefaultTable. + WithHasHeader(). + WithWriter(cmd.OutOrStdout()). + WithData(tableData). + Render(); err != nil { + return err + } + + fctl.Section.WithWriter(cmd.OutOrStdout()).Println("Payments Balances") + tableData = fctl.MapMap(c.store.Reconciliation.PaymentsBalances, func(key string, value *big.Int) []string { + return []string{ + key, + value.String(), + } + }) + tableData = fctl.Prepend(tableData, []string{"Asset", "Amount"}) + if err := pterm.DefaultTable. + WithHasHeader(). + WithWriter(cmd.OutOrStdout()). + WithData(tableData). + Render(); err != nil { + return err + } + + fctl.Section.WithWriter(cmd.OutOrStdout()).Println("Drift Balances") + tableData = fctl.MapMap(c.store.Reconciliation.DriftBalances, func(key string, value *big.Int) []string { + return []string{ + key, + value.String(), + } + }) + tableData = fctl.Prepend(tableData, []string{"Asset", "Amount"}) + if err := pterm.DefaultTable. + WithHasHeader(). + WithWriter(cmd.OutOrStdout()). + WithData(tableData). + Render(); err != nil { + return err + } + + return nil +} diff --git a/components/fctl/cmd/root.go b/components/fctl/cmd/root.go index 3c56be5c4f..3b2c03b86c 100644 --- a/components/fctl/cmd/root.go +++ b/components/fctl/cmd/root.go @@ -16,6 +16,7 @@ import ( "github.com/formancehq/fctl/cmd/orchestration" "github.com/formancehq/fctl/cmd/payments" "github.com/formancehq/fctl/cmd/profiles" + "github.com/formancehq/fctl/cmd/reconciliation" "github.com/formancehq/fctl/cmd/search" "github.com/formancehq/fctl/cmd/stack" "github.com/formancehq/fctl/cmd/ui" @@ -47,6 +48,7 @@ func NewRootCommand() *cobra.Command { NewPromptCommand(), ledger.NewCommand(), payments.NewCommand(), + reconciliation.NewCommand(), profiles.NewCommand(), stack.NewCommand(), auth.NewCommand(), diff --git a/components/fctl/pkg/utils.go b/components/fctl/pkg/utils.go index d7ad47c9b9..ad8c7ad4aa 100644 --- a/components/fctl/pkg/utils.go +++ b/components/fctl/pkg/utils.go @@ -14,6 +14,14 @@ func Map[SRC any, DST any](srcs []SRC, mapper func(SRC) DST) []DST { return ret } +func MapMap[KEY comparable, VALUE any, DST any](srcs map[KEY]VALUE, mapper func(KEY, VALUE) DST) []DST { + ret := make([]DST, 0) + for k, v := range srcs { + ret = append(ret, mapper(k, v)) + } + return ret +} + func MapKeys[K comparable, V any](m map[K]V) []K { ret := make([]K, 0) for k := range m { diff --git a/ee/gateway/pkg/plugins/auth.go b/ee/gateway/pkg/plugins/auth.go index da2ffe8d41..b9230c75f7 100644 --- a/ee/gateway/pkg/plugins/auth.go +++ b/ee/gateway/pkg/plugins/auth.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "os" "strconv" "strings" @@ -15,7 +16,6 @@ import ( "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp/caddyauth" "github.com/hashicorp/go-retryablehttp" - "github.com/zitadel/oidc/v2/pkg/client" "github.com/zitadel/oidc/v2/pkg/client/rp" "github.com/zitadel/oidc/v2/pkg/oidc" "github.com/zitadel/oidc/v2/pkg/op" @@ -158,16 +158,19 @@ func (ja *JWTAuth) Authenticate(w http.ResponseWriter, r *http.Request) (caddyau // Helpers //------------------------------------------------------------------------------ -func (ja *JWTAuth) getAccessTokenVerifier( - ctx context.Context, -) (op.AccessTokenVerifier, error) { +func (ja *JWTAuth) getAccessTokenVerifier(ctx context.Context) (op.AccessTokenVerifier, error) { if ja.accessTokenVerifier == nil { - discoveryConfiguration, err := client.Discover(ja.Issuer, ja.httpClient) - if err != nil { - return nil, err + //discoveryConfiguration, err := client.Discover(ja.Issuer, ja.httpClient) + //if err != nil { + // return nil, err + //} + + // todo: ugly quick fix + authServicePort := "8080" + if fromEnv := os.Getenv("AUTH_SERVICE_PORT"); fromEnv != "" { + authServicePort = fromEnv } - - keySet := rp.NewRemoteKeySet(ja.httpClient, discoveryConfiguration.JwksURI) + keySet := rp.NewRemoteKeySet(ja.httpClient, fmt.Sprintf("http://auth:%s/keys", authServicePort)) ja.accessTokenVerifier = op.NewAccessTokenVerifier( ja.Issuer, diff --git a/ee/reconciliation/openapi.yaml b/ee/reconciliation/openapi.yaml index d7c97c993b..4ad5733354 100644 --- a/ee/reconciliation/openapi.yaml +++ b/ee/reconciliation/openapi.yaml @@ -304,8 +304,8 @@ components: type: string example: "default" ledgerQuery: - type: string - example: "{\"$match\": {\"metadata[reconciliation]\": \"pool:main\"}}" + type: object + additionalProperties: true paymentsPoolID: type: string example: "XXX" @@ -314,7 +314,7 @@ components: required: - data properties: - policy: + data: $ref: '#/components/schemas/Policy' ReconciliationRequest: type: object @@ -335,7 +335,7 @@ components: required: - data properties: - reconciliation: + data: $ref: '#/components/schemas/Reconciliation' Policy: type: object @@ -361,8 +361,8 @@ components: type: string example: "default" ledgerQuery: - type: string - example: "{\"$match\": {\"metadata[reconciliation]\": \"pool:main\"}}" + type: object + additionalProperties: true paymentsPoolID: type: string example: "XXX" diff --git a/releases/sdks/go/docs/models/shared/policy.md b/releases/sdks/go/docs/models/shared/policy.md index fad571b950..a8d88c3916 100755 --- a/releases/sdks/go/docs/models/shared/policy.md +++ b/releases/sdks/go/docs/models/shared/policy.md @@ -3,11 +3,11 @@ ## Fields -| Field | Type | Required | Description | Example | -| ----------------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------- | -| `CreatedAt` | [time.Time](https://pkg.go.dev/time#Time) | :heavy_check_mark: | N/A | 2021-01-01T00:00:00.000Z | -| `ID` | *string* | :heavy_check_mark: | N/A | XXX | -| `LedgerName` | *string* | :heavy_check_mark: | N/A | default | -| `LedgerQuery` | *string* | :heavy_check_mark: | N/A | {"$match": {"metadata[reconciliation]": "pool:main"}} | -| `Name` | *string* | :heavy_check_mark: | N/A | XXX | -| `PaymentsPoolID` | *string* | :heavy_check_mark: | N/A | XXX | \ No newline at end of file +| Field | Type | Required | Description | Example | +| ----------------------------------------- | ----------------------------------------- | ----------------------------------------- | ----------------------------------------- | ----------------------------------------- | +| `CreatedAt` | [time.Time](https://pkg.go.dev/time#Time) | :heavy_check_mark: | N/A | 2021-01-01T00:00:00.000Z | +| `ID` | *string* | :heavy_check_mark: | N/A | XXX | +| `LedgerName` | *string* | :heavy_check_mark: | N/A | default | +| `LedgerQuery` | map[string]*interface{}* | :heavy_check_mark: | N/A | | +| `Name` | *string* | :heavy_check_mark: | N/A | XXX | +| `PaymentsPoolID` | *string* | :heavy_check_mark: | N/A | XXX | \ No newline at end of file diff --git a/releases/sdks/go/docs/models/shared/policyrequest.md b/releases/sdks/go/docs/models/shared/policyrequest.md index 07f663552a..ca85250f52 100755 --- a/releases/sdks/go/docs/models/shared/policyrequest.md +++ b/releases/sdks/go/docs/models/shared/policyrequest.md @@ -3,9 +3,9 @@ ## Fields -| Field | Type | Required | Description | Example | -| ----------------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------- | -| `LedgerName` | *string* | :heavy_check_mark: | N/A | default | -| `LedgerQuery` | *string* | :heavy_check_mark: | N/A | {"$match": {"metadata[reconciliation]": "pool:main"}} | -| `Name` | *string* | :heavy_check_mark: | N/A | XXX | -| `PaymentsPoolID` | *string* | :heavy_check_mark: | N/A | XXX | \ No newline at end of file +| Field | Type | Required | Description | Example | +| ------------------------ | ------------------------ | ------------------------ | ------------------------ | ------------------------ | +| `LedgerName` | *string* | :heavy_check_mark: | N/A | default | +| `LedgerQuery` | map[string]*interface{}* | :heavy_check_mark: | N/A | | +| `Name` | *string* | :heavy_check_mark: | N/A | XXX | +| `PaymentsPoolID` | *string* | :heavy_check_mark: | N/A | XXX | \ No newline at end of file diff --git a/releases/sdks/go/docs/models/shared/policyresponse.md b/releases/sdks/go/docs/models/shared/policyresponse.md index 6f4d9ab641..26ba75e826 100755 --- a/releases/sdks/go/docs/models/shared/policyresponse.md +++ b/releases/sdks/go/docs/models/shared/policyresponse.md @@ -3,6 +3,6 @@ ## Fields -| Field | Type | Required | Description | -| ---------------------------------------- | ---------------------------------------- | ---------------------------------------- | ---------------------------------------- | -| `Policy` | [*Policy](../../models/shared/policy.md) | :heavy_minus_sign: | N/A | \ No newline at end of file +| Field | Type | Required | Description | +| --------------------------------------- | --------------------------------------- | --------------------------------------- | --------------------------------------- | +| `Data` | [Policy](../../models/shared/policy.md) | :heavy_check_mark: | N/A | \ No newline at end of file diff --git a/releases/sdks/go/docs/models/shared/reconciliationresponse.md b/releases/sdks/go/docs/models/shared/reconciliationresponse.md index 10c2f49f10..9643d8915a 100755 --- a/releases/sdks/go/docs/models/shared/reconciliationresponse.md +++ b/releases/sdks/go/docs/models/shared/reconciliationresponse.md @@ -3,6 +3,6 @@ ## Fields -| Field | Type | Required | Description | -| -------------------------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------- | -------------------------------------------------------- | -| `Reconciliation` | [*Reconciliation](../../models/shared/reconciliation.md) | :heavy_minus_sign: | N/A | \ No newline at end of file +| Field | Type | Required | Description | +| ------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------- | +| `Data` | [Reconciliation](../../models/shared/reconciliation.md) | :heavy_check_mark: | N/A | \ No newline at end of file diff --git a/releases/sdks/go/docs/sdks/reconciliation/README.md b/releases/sdks/go/docs/sdks/reconciliation/README.md index 7002fed12c..801dd586a7 100755 --- a/releases/sdks/go/docs/sdks/reconciliation/README.md +++ b/releases/sdks/go/docs/sdks/reconciliation/README.md @@ -34,7 +34,9 @@ func main() { ctx := context.Background() res, err := s.Reconciliation.CreatePolicy(ctx, shared.PolicyRequest{ LedgerName: "default", - LedgerQuery: "{\"$match\": {\"metadata[reconciliation]\": \"pool:main\"}}", + LedgerQuery: map[string]interface{}{ + "key": "string", + }, Name: "XXX", PaymentsPoolID: "XXX", }) diff --git a/releases/sdks/go/pkg/models/shared/policy.go b/releases/sdks/go/pkg/models/shared/policy.go index 8251b0c5bb..a33f1e1478 100755 --- a/releases/sdks/go/pkg/models/shared/policy.go +++ b/releases/sdks/go/pkg/models/shared/policy.go @@ -8,12 +8,12 @@ import ( ) type Policy struct { - CreatedAt time.Time `json:"createdAt"` - ID string `json:"id"` - LedgerName string `json:"ledgerName"` - LedgerQuery string `json:"ledgerQuery"` - Name string `json:"name"` - PaymentsPoolID string `json:"paymentsPoolID"` + CreatedAt time.Time `json:"createdAt"` + ID string `json:"id"` + LedgerName string `json:"ledgerName"` + LedgerQuery map[string]interface{} `json:"ledgerQuery"` + Name string `json:"name"` + PaymentsPoolID string `json:"paymentsPoolID"` } func (p Policy) MarshalJSON() ([]byte, error) { @@ -48,9 +48,9 @@ func (o *Policy) GetLedgerName() string { return o.LedgerName } -func (o *Policy) GetLedgerQuery() string { +func (o *Policy) GetLedgerQuery() map[string]interface{} { if o == nil { - return "" + return map[string]interface{}{} } return o.LedgerQuery } diff --git a/releases/sdks/go/pkg/models/shared/policyrequest.go b/releases/sdks/go/pkg/models/shared/policyrequest.go index ab88883df0..24aa41fc77 100755 --- a/releases/sdks/go/pkg/models/shared/policyrequest.go +++ b/releases/sdks/go/pkg/models/shared/policyrequest.go @@ -3,10 +3,10 @@ package shared type PolicyRequest struct { - LedgerName string `json:"ledgerName"` - LedgerQuery string `json:"ledgerQuery"` - Name string `json:"name"` - PaymentsPoolID string `json:"paymentsPoolID"` + LedgerName string `json:"ledgerName"` + LedgerQuery map[string]interface{} `json:"ledgerQuery"` + Name string `json:"name"` + PaymentsPoolID string `json:"paymentsPoolID"` } func (o *PolicyRequest) GetLedgerName() string { @@ -16,9 +16,9 @@ func (o *PolicyRequest) GetLedgerName() string { return o.LedgerName } -func (o *PolicyRequest) GetLedgerQuery() string { +func (o *PolicyRequest) GetLedgerQuery() map[string]interface{} { if o == nil { - return "" + return map[string]interface{}{} } return o.LedgerQuery } diff --git a/releases/sdks/go/pkg/models/shared/policyresponse.go b/releases/sdks/go/pkg/models/shared/policyresponse.go index e40e174e28..ae1a90df00 100755 --- a/releases/sdks/go/pkg/models/shared/policyresponse.go +++ b/releases/sdks/go/pkg/models/shared/policyresponse.go @@ -3,12 +3,12 @@ package shared type PolicyResponse struct { - Policy *Policy `json:"policy,omitempty"` + Data Policy `json:"data"` } -func (o *PolicyResponse) GetPolicy() *Policy { +func (o *PolicyResponse) GetData() Policy { if o == nil { - return nil + return Policy{} } - return o.Policy + return o.Data } diff --git a/releases/sdks/go/pkg/models/shared/reconciliationresponse.go b/releases/sdks/go/pkg/models/shared/reconciliationresponse.go index 366fef95e3..e2ad4540d7 100755 --- a/releases/sdks/go/pkg/models/shared/reconciliationresponse.go +++ b/releases/sdks/go/pkg/models/shared/reconciliationresponse.go @@ -3,12 +3,12 @@ package shared type ReconciliationResponse struct { - Reconciliation *Reconciliation `json:"reconciliation,omitempty"` + Data Reconciliation `json:"data"` } -func (o *ReconciliationResponse) GetReconciliation() *Reconciliation { +func (o *ReconciliationResponse) GetData() Reconciliation { if o == nil { - return nil + return Reconciliation{} } - return o.Reconciliation + return o.Data }