-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Lawrence Zawila <113581282+darkmatterpool@users.noreply.github.com>
- Loading branch information
1 parent
2c8ec51
commit 58a2bca
Showing
11 changed files
with
507 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package client | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"net/http" | ||
"net/url" | ||
"strings" | ||
) | ||
|
||
func (c *Client) authenticate(ctx context.Context) (string, error) { | ||
form := make(url.Values) | ||
|
||
form.Add("login_id", c.loginID) | ||
form.Add("api_key", c.apiKey) | ||
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, | ||
c.buildEndpoint("v2/authenticate/api"), strings.NewReader(form.Encode())) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to create request: %w", err) | ||
} | ||
|
||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded") | ||
req.Header.Add("Accept", "application/json") | ||
|
||
resp, err := c.httpClient.Do(req) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to do get request: %w", err) | ||
} | ||
|
||
defer resp.Body.Close() | ||
|
||
if resp.StatusCode != http.StatusOK { | ||
return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode) | ||
} | ||
|
||
//nolint:tagliatelle // allow for client code | ||
type response struct { | ||
AuthToken string `json:"auth_token"` | ||
} | ||
|
||
var res response | ||
|
||
if err = json.NewDecoder(resp.Body).Decode(&res); err != nil { | ||
return "", fmt.Errorf("failed to decode response body: %w", err) | ||
} | ||
|
||
return res.AuthToken, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package client | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"net/http" | ||
) | ||
|
||
type apiTransport struct { | ||
authToken string | ||
} | ||
|
||
func (t *apiTransport) RoundTrip(req *http.Request) (*http.Response, error) { | ||
req.Header.Add("X-Auth-Token", t.authToken) | ||
|
||
return http.DefaultTransport.RoundTrip(req) | ||
} | ||
|
||
type Client struct { | ||
httpClient *http.Client | ||
endpoint string | ||
loginID string | ||
apiKey string | ||
} | ||
|
||
func (c *Client) buildEndpoint(path string, args ...interface{}) string { | ||
return fmt.Sprintf("%s/%s", c.endpoint, fmt.Sprintf(path, args...)) | ||
} | ||
|
||
const devAPIEndpoint = "https://devapi.currencycloud.com" | ||
|
||
// NewClient creates a new client for the CurrencyCloud API. | ||
func NewClient(ctx context.Context, loginID, apiKey, endpoint string) (*Client, error) { | ||
if endpoint == "" { | ||
endpoint = devAPIEndpoint | ||
} | ||
|
||
c := &Client{ | ||
httpClient: &http.Client{}, | ||
endpoint: endpoint, | ||
loginID: loginID, | ||
apiKey: apiKey, | ||
} | ||
|
||
authToken, err := c.authenticate(ctx) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
c.httpClient.Transport = &apiTransport{authToken: authToken} | ||
|
||
return c, nil | ||
} |
60 changes: 60 additions & 0 deletions
60
internal/pkg/connectors/currencycloud/client/transactions.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
package client | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"net/http" | ||
) | ||
|
||
//nolint:tagliatelle // allow different styled tags in client | ||
type Transaction struct { | ||
ID string `json:"id"` | ||
Currency string `json:"currency"` | ||
Type string `json:"type"` | ||
Status string `json:"status"` | ||
CreatedAt string `json:"created_at"` | ||
Action string `json:"action"` | ||
|
||
Amount string `json:"amount"` | ||
} | ||
|
||
func (c *Client) GetTransactions(ctx context.Context, page int) ([]Transaction, int, error) { | ||
if page < 1 { | ||
return nil, 0, fmt.Errorf("page must be greater than 0") | ||
} | ||
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, | ||
c.buildEndpoint("v2/transactions/find?page=%d", page), http.NoBody) | ||
if err != nil { | ||
return nil, 0, 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, 0, err | ||
} | ||
|
||
defer resp.Body.Close() | ||
|
||
if resp.StatusCode != http.StatusOK { | ||
return nil, 0, fmt.Errorf("unexpected status code: %d", resp.StatusCode) | ||
} | ||
|
||
//nolint:tagliatelle // allow for client code | ||
type response struct { | ||
Transactions []Transaction `json:"transactions"` | ||
Pagination struct { | ||
NextPage int `json:"next_page"` | ||
} | ||
} | ||
|
||
var res response | ||
if err = json.NewDecoder(resp.Body).Decode(&res); err != nil { | ||
return nil, 0, err | ||
} | ||
|
||
return res.Transactions, res.Pagination.NextPage, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package currencycloud | ||
|
||
import ( | ||
"encoding/json" | ||
"time" | ||
) | ||
|
||
type Config struct { | ||
LoginID string `json:"loginID" bson:"loginID"` | ||
APIKey string `json:"apiKey" bson:"apiKey"` | ||
Endpoint string `json:"endpoint" bson:"endpoint"` | ||
PollingPeriod Duration `json:"pollingPeriod" bson:"pollingPeriod"` | ||
} | ||
|
||
func (c Config) Validate() error { | ||
if c.APIKey == "" { | ||
return ErrMissingAPIKey | ||
} | ||
|
||
if c.LoginID == "" { | ||
return ErrMissingLoginID | ||
} | ||
|
||
if c.PollingPeriod == 0 { | ||
return ErrMissingPollingPeriod | ||
} | ||
|
||
return nil | ||
} | ||
|
||
type Duration time.Duration | ||
|
||
func (d *Duration) String() string { | ||
return time.Duration(*d).String() | ||
} | ||
|
||
func (d *Duration) Duration() time.Duration { | ||
return time.Duration(*d) | ||
} | ||
|
||
func (d *Duration) MarshalJSON() ([]byte, error) { | ||
return json.Marshal(time.Duration(*d).String()) | ||
} | ||
|
||
func (d *Duration) UnmarshalJSON(b []byte) error { | ||
var durationValue interface{} | ||
|
||
if err := json.Unmarshal(b, &durationValue); err != nil { | ||
return err | ||
} | ||
|
||
switch value := durationValue.(type) { | ||
case float64: | ||
*d = Duration(time.Duration(value)) | ||
|
||
return nil | ||
case string: | ||
tmp, err := time.ParseDuration(value) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
*d = Duration(tmp) | ||
|
||
return nil | ||
default: | ||
return ErrDurationInvalid | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
package currencycloud | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/numary/go-libs/sharedlogging" | ||
"github.com/numary/payments/internal/pkg/integration" | ||
"github.com/numary/payments/internal/pkg/task" | ||
) | ||
|
||
const connectorName = "currencycloud" | ||
|
||
type Connector struct { | ||
logger sharedlogging.Logger | ||
cfg Config | ||
} | ||
|
||
func (c *Connector) Install(ctx task.ConnectorContext[TaskDescriptor]) error { | ||
return ctx.Scheduler().Schedule(TaskDescriptor{Name: taskNameFetchTransactions}, true) | ||
} | ||
|
||
func (c *Connector) Uninstall(ctx context.Context) error { | ||
return nil | ||
} | ||
|
||
func (c *Connector) Resolve(descriptor TaskDescriptor) task.Task { | ||
return resolveTasks(c.logger, c.cfg) | ||
} | ||
|
||
var _ integration.Connector[TaskDescriptor] = &Connector{} | ||
|
||
func newConnector(logger sharedlogging.Logger, cfg Config) *Connector { | ||
return &Connector{ | ||
logger: logger.WithFields(map[string]any{ | ||
"component": "connector", | ||
}), | ||
cfg: cfg, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
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") | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package currencycloud | ||
|
||
import ( | ||
"github.com/numary/go-libs/sharedlogging" | ||
"github.com/numary/payments/internal/pkg/integration" | ||
) | ||
|
||
type Loader struct{} | ||
|
||
const allowedTasks = 50 | ||
|
||
func (l *Loader) AllowTasks() int { | ||
return allowedTasks | ||
} | ||
|
||
func (l *Loader) Name() string { | ||
return connectorName | ||
} | ||
|
||
func (l *Loader) Load(logger sharedlogging.Logger, config Config) integration.Connector[TaskDescriptor] { | ||
return newConnector(logger, config) | ||
} | ||
|
||
func (l *Loader) ApplyDefaults(cfg Config) Config { | ||
return cfg | ||
} | ||
|
||
// NewLoader creates a new loader. | ||
func NewLoader() *Loader { | ||
return &Loader{} | ||
} |
Oops, something went wrong.