-
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.
Merge pull request #122 from formancehq/feat/currency-cloud-transfer-…
…payouts feat(currencycloud): add transfer/payout creation
- Loading branch information
Showing
12 changed files
with
769 additions
and
4 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
30 changes: 30 additions & 0 deletions
30
internal/connectors/plugins/public/currencycloud/client/client_generated.go
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
81 changes: 81 additions & 0 deletions
81
internal/connectors/plugins/public/currencycloud/client/payouts.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,81 @@ | ||
package client | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"net/http" | ||
"net/url" | ||
"strings" | ||
"time" | ||
) | ||
|
||
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 json.Number `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 time.Time `json:"created_at"` | ||
UpdatedAt time.Time `json:"updated_at"` | ||
UniqueRequestID string `json:"unique_request_id"` | ||
} | ||
|
||
func (c *client) InitiatePayout(ctx context.Context, payoutRequest *PayoutRequest) (*PayoutResponse, error) { | ||
// TODO(polo): metrics | ||
// f := connectors.ClientMetrics(ctx, "currencycloud", "initiate_payout") | ||
// now := time.Now() | ||
// defer f(ctx, now) | ||
|
||
if err := c.ensureLogin(ctx); err != nil { | ||
return nil, err | ||
} | ||
|
||
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") | ||
|
||
var payoutResponse PayoutResponse | ||
var errRes currencyCloudError | ||
_, err = c.httpClient.Do(req, &payoutResponse, &errRes) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to create payout: %w, %w", err, errRes.Error()) | ||
} | ||
|
||
return &payoutResponse, nil | ||
} |
81 changes: 81 additions & 0 deletions
81
internal/connectors/plugins/public/currencycloud/client/transfers.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,81 @@ | ||
package client | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"net/http" | ||
"net/url" | ||
"strings" | ||
"time" | ||
) | ||
|
||
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 json.Number `json:"amount"` | ||
Status string `json:"status"` | ||
CreatedAt time.Time `json:"created_at"` | ||
UpdatedAt time.Time `json:"updated_at"` | ||
CompletedAt time.Time `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) { | ||
// TODO(polo): metrics | ||
// f := connectors.ClientMetrics(ctx, "currencycloud", "initiate_transfer") | ||
// now := time.Now() | ||
// defer f(ctx, now) | ||
|
||
if err := c.ensureLogin(ctx); err != nil { | ||
return nil, err | ||
} | ||
|
||
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") | ||
|
||
var res TransferResponse | ||
var errRes currencyCloudError | ||
_, err = c.httpClient.Do(req, &res, &errRes) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to create transfer: %w, %w", err, errRes.Error()) | ||
} | ||
|
||
return &res, 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
88 changes: 88 additions & 0 deletions
88
internal/connectors/plugins/public/currencycloud/payouts.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,88 @@ | ||
package currencycloud | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
|
||
"github.com/formancehq/payments/internal/connectors/plugins/currency" | ||
"github.com/formancehq/payments/internal/connectors/plugins/public/currencycloud/client" | ||
"github.com/formancehq/payments/internal/models" | ||
) | ||
|
||
func (p *Plugin) validatePayoutRequest(pi models.PSPPaymentInitiation) error { | ||
if pi.SourceAccount == nil { | ||
return fmt.Errorf("source account is required: %w", models.ErrInvalidRequest) | ||
} | ||
|
||
if pi.DestinationAccount == nil { | ||
return fmt.Errorf("destination account is required: %w", models.ErrInvalidRequest) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (p *Plugin) createPayout(ctx context.Context, pi models.PSPPaymentInitiation) (models.PSPPayment, error) { | ||
if err := p.validatePayoutRequest(pi); err != nil { | ||
return models.PSPPayment{}, err | ||
} | ||
|
||
curr, precision, err := currency.GetCurrencyAndPrecisionFromAsset(supportedCurrenciesWithDecimal, pi.Asset) | ||
if err != nil { | ||
return models.PSPPayment{}, fmt.Errorf("failed to get currency and precision from asset: %v: %w", err, models.ErrInvalidRequest) | ||
} | ||
|
||
amount, err := currency.GetStringAmountFromBigIntWithPrecision(pi.Amount, precision) | ||
if err != nil { | ||
return models.PSPPayment{}, fmt.Errorf("failed to get string amount from big int: %v: %w", err, models.ErrInvalidRequest) | ||
} | ||
|
||
contact, err := p.client.GetContactID(ctx, pi.SourceAccount.Reference) | ||
if err != nil { | ||
return models.PSPPayment{}, err | ||
} | ||
|
||
resp, err := p.client.InitiatePayout(ctx, &client.PayoutRequest{ | ||
OnBehalfOf: contact.ID, | ||
BeneficiaryID: pi.DestinationAccount.Reference, | ||
Currency: curr, | ||
Amount: json.Number(amount), | ||
Reference: pi.Description, | ||
UniqueRequestID: pi.Reference, | ||
}) | ||
if err != nil { | ||
return models.PSPPayment{}, err | ||
} | ||
|
||
return translatePayoutToPayment(resp, pi.SourceAccount.Reference) | ||
} | ||
|
||
func translatePayoutToPayment(from *client.PayoutResponse, sourceAccountReference string) (models.PSPPayment, error) { | ||
raw, err := json.Marshal(from) | ||
if err != nil { | ||
return models.PSPPayment{}, err | ||
} | ||
|
||
precision, ok := supportedCurrenciesWithDecimal[from.Currency] | ||
if !ok { | ||
return models.PSPPayment{}, nil | ||
} | ||
|
||
amount, err := currency.GetAmountWithPrecisionFromString(from.Amount.String(), precision) | ||
if err != nil { | ||
return models.PSPPayment{}, err | ||
} | ||
|
||
return models.PSPPayment{ | ||
Reference: from.ID, | ||
CreatedAt: from.CreatedAt, | ||
Type: models.PAYMENT_TYPE_PAYOUT, | ||
Amount: amount, | ||
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, from.Currency), | ||
Scheme: models.PAYMENT_SCHEME_OTHER, | ||
Status: matchTransactionStatus(from.Status), | ||
SourceAccountReference: &sourceAccountReference, | ||
DestinationAccountReference: &from.BeneficiaryID, | ||
Raw: raw, | ||
}, nil | ||
} |
Oops, something went wrong.