Files
fgueryandGitHub 58271b3544 feat(CONN-92): qonto connector (#427)
* doc(conn-92): fix doc link

* doc(CONN-92): clarify the new connector creation instructions

* feat(CONN-92): qonto plugin, account retrieval

* feat(CONN-92): implement pagination and fix timezone

* feat(CONN-92): add metadata

* feat(CONN-92): fetchBalance implemented

* feat(CONN-92): remove unsupported capabilities

* feat(CONN-92): bank account type differ from different endpoint

* feat(CONN-92): added external account + beneficiaries endpoint

* feat(CONN-92): invalid beneficiary's accounts actually happen all the time

* feat(CONN-92): allow multiple currencies for external accounts

* feat(CONN-92): temp commit to debug payment save

* feat(CONN-92): remove loggers

* feat(CONN-92): better job at omitempty

* feat(CONN-92): fix payment destination account to match external account

* feat(CONN-92): added test for no from payload

* feat(CONN-92): test on raw where relevant

* feat(CONN-92): added payment_test

* feat(CONN-92): use jsonNumber for incoming floats

* feat(CONN_92): remove page number from request, instead use last_updated + pagesize

* feat(CONN-92): fetch all transactions by statuses, in order (not only completed ones)

* feat(CONN-92): always add the parentReference

* feat(CONN-92): extract couple of method to improve readability

* feat(CONN-92): use constants for transaction status

* feat(CONN-92): added comment on transaction status

* feat(CONN-92): add transfer endpoint

* feat(CONN-92): unify account metadata syntax between internal & external accounts

* feat(CONN-92): make plugin inherit from basePlugin

* feat(CONN-92): add transfer capability

* feat(CONN-92): fix internal transfert -- doc is lying on the response format

* feat(CONN-92): avoid sending update_at_from == zero time (as Qonto refuses the request)

* feat(CONN-92): coderabbit suggestions:

* fix error formatting
* more leniency in transfer response status check (allwing completed)
* more validaiton when creating a transfer

* feat(CONN-92): more coderabbit suggestions, improve error reporting

* feat(CONN-92): more coderabbit suggestions, more errors on invalid state

* feat(CONN-92): more coderabbit reviews, more coverage and bits of refacto

* feat(CONN-92): bit more coverage

* feat(CONN-92): bit more coverage

* feat(CONN-92): added more coverage for config & errors; also improved Qonto's error reporting

* feat(CONN-92): precommit run

* feat(CONN-92): remove needMore functionality as its mostly irrelevant here

* feat(CONN-92): codeRabbit is right, Qonto has a max page size -- my bad

* feat(CONN-92): correct Account pagination

* feat(CONN-92): account pagination with equal lastUpdatedAt

* feat(CONN-92): review pagination for beneficiaries and payments

* feat(CONN-92): update speakeasy lock

* feat(CONN-92): added a bit of comments on the surprising bits

* feat(CONN-92): codeRabbit comment

* feat(CONN-92): transfer don't always return currency -- we can default to EUR as its the only one supported

* feat(CONN-92): return error when account does not have currency set

* feat(CONN-92): remove initialization of lastUpdatedAt

* feat(CONN-92): remove all pagination from accounts, as we fetch all anyway

* feat(CONN-92): cleaned somewhat the http connection logic

* feat(CONN-92): add uuid in transfer as reference to pick it up for transaction matching later on

* feat(CONN-92): detect transactions created via transfer and set parentId appropriately

* feat(CONN-92): truncate too long reference

* feat(CONN-92): remove create internal transfer endpoint
2025-05-20 13:59:57 +00:00

177 lines
5.4 KiB
Go

package client
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/formancehq/payments/internal/connectors/metrics"
errorsutils "github.com/formancehq/payments/internal/utils/errors"
)
type EventType string
const (
// Transfer
EventTypeTransferNormalCreated EventType = "TRANSFER_NORMAL_CREATED"
EventTypeTransferNormalFailed EventType = "TRANSFER_NORMAL_FAILED"
EventTypeTransferNormalSucceeded EventType = "TRANSFER_NORMAL_SUCCEEDED"
// PayOut
EventTypePayoutNormalCreated EventType = "PAYOUT_NORMAL_CREATED"
EventTypePayoutNormalFailed EventType = "PAYOUT_NORMAL_FAILED"
EventTypePayoutNormalSucceeded EventType = "PAYOUT_NORMAL_SUCCEEDED"
EventTypePayoutInstantFailed EventType = "INSTANT_PAYOUT_FAILED"
EventTypePayoutInstantSucceeded EventType = "INSTANT_PAYOUT_SUCCEEDED"
// PayIn
EventTypePayinNormalCreated EventType = "PAYIN_NORMAL_CREATED"
EventTypePayinNormalFailed EventType = "PAYIN_NORMAL_FAILED"
EventTypePayinNormalSucceeded EventType = "PAYIN_NORMAL_SUCCEEDED"
// Refund
EventTypeTransferRefundCreated EventType = "TRANSFER_REFUND_CREATED"
EventTypeTransferRefundFailed EventType = "TRANSFER_REFUND_FAILED"
EventTypeTransferRefundSucceeded EventType = "TRANSFER_REFUND_SUCCEEDED"
EventTypePayinRefundCreated EventType = "PAYIN_REFUND_CREATED"
EventTypePayinRefundFailed EventType = "PAYIN_REFUND_FAILED"
EventTypePayinRefundSucceeded EventType = "PAYIN_REFUND_SUCCEEDED"
EventTypePayOutRefundCreated EventType = "PAYOUT_REFUND_CREATED"
EventTypePayOutRefundFailed EventType = "PAYOUT_REFUND_FAILED"
EventTypePayOutRefundSucceeded EventType = "PAYOUT_REFUND_SUCCEEDED"
)
var (
AllEventTypes = []EventType{
EventTypeTransferNormalCreated,
EventTypeTransferNormalFailed,
EventTypeTransferNormalSucceeded,
EventTypePayoutNormalCreated,
EventTypePayoutNormalFailed,
EventTypePayoutNormalSucceeded,
EventTypePayoutInstantFailed,
EventTypePayoutInstantSucceeded,
EventTypePayinNormalCreated,
EventTypePayinNormalFailed,
EventTypePayinNormalSucceeded,
EventTypeTransferRefundCreated,
EventTypeTransferRefundFailed,
EventTypeTransferRefundSucceeded,
EventTypePayinRefundCreated,
EventTypePayinRefundFailed,
EventTypePayinRefundSucceeded,
EventTypePayOutRefundCreated,
EventTypePayOutRefundFailed,
EventTypePayOutRefundSucceeded,
}
)
type Webhook struct {
ResourceID string `json:"ResourceId"`
Date int64 `json:"Date"`
EventType EventType `json:"EventType"`
}
type Hook struct {
ID string `json:"Id"`
URL string `json:"Url"`
Status string `json:"Status"`
Validity string `json:"Validity"`
EventType EventType `json:"EventType"`
}
func (c *client) ListAllHooks(ctx context.Context) ([]*Hook, error) {
ctx = context.WithValue(ctx, metrics.MetricOperationContextKey, "list_hooks")
endpoint := fmt.Sprintf("%s/v2.01/%s/hooks", c.endpoint, c.clientID)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, http.NoBody)
if err != nil {
return nil, fmt.Errorf("failed to create list hooks request: %w", err)
}
q := req.URL.Query()
q.Add("per_page", "100") // Should be enough, since we're creating only a few
q.Add("Sort", "CreationDate:ASC")
req.URL.RawQuery = q.Encode()
var hooks []*Hook
statusCode, err := c.httpClient.Do(ctx, req, &hooks, nil)
if err != nil {
return nil, errorsutils.NewWrappedError(
fmt.Errorf("failed to list hooks: status code %d", statusCode),
err,
)
}
return hooks, nil
}
type CreateHookRequest struct {
EventType EventType `json:"EventType"`
URL string `json:"Url"`
}
func (c *client) CreateHook(ctx context.Context, eventType EventType, URL string) error {
ctx = context.WithValue(ctx, metrics.MetricOperationContextKey, "create_hook")
body, err := json.Marshal(&CreateHookRequest{
EventType: eventType,
URL: URL,
})
if err != nil {
return fmt.Errorf("failed to marshal create hook request: %w", err)
}
endpoint := fmt.Sprintf("%s/v2.01/%s/hooks", c.endpoint, c.clientID)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(body))
if err != nil {
return fmt.Errorf("failed to create hooks request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
var errRes mangopayError
_, err = c.httpClient.Do(ctx, req, nil, &errRes)
if err != nil {
return errorsutils.NewWrappedError(
fmt.Errorf("failed to create hook: %v", errRes.Error()),
err,
)
}
return nil
}
type UpdateHookRequest struct {
URL string `json:"Url"`
Status string `json:"Status"`
}
func (c *client) UpdateHook(ctx context.Context, hookID string, URL string) error {
ctx = context.WithValue(ctx, metrics.MetricOperationContextKey, "update_hook")
body, err := json.Marshal(&UpdateHookRequest{
URL: URL,
Status: "ENABLED",
})
if err != nil {
return fmt.Errorf("failed to marshal udpate hook request: %w", err)
}
endpoint := fmt.Sprintf("%s/v2.01/%s/hooks/%s", c.endpoint, c.clientID, hookID)
req, err := http.NewRequestWithContext(ctx, http.MethodPut, endpoint, bytes.NewBuffer(body))
if err != nil {
return fmt.Errorf("failed to create update hooks request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
var errRes mangopayError
_, err = c.httpClient.Do(ctx, req, nil, &errRes)
if err != nil {
return errorsutils.NewWrappedError(
fmt.Errorf("failed to update hook: %v", errRes.Error()),
err,
)
}
return nil
}