mirror of
https://github.com/hanzo-fi/payments.git
synced 2026-10-30 20:31:20 +00:00
218 lines
6.3 KiB
Go
218 lines
6.3 KiB
Go
package moneycorp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/hanzo-fi/go-libs/v5/pkg/types/currency"
|
|
"github.com/hanzo-fi/go-libs/v5/pkg/types/pointer"
|
|
"github.com/hanzo-fi/payments/ce/plugins/moneycorp/client"
|
|
"github.com/hanzo-fi/payments/pkg/domain/models"
|
|
"github.com/hanzo-fi/payments/pkg/domain/pagination"
|
|
"github.com/hanzo-fi/payments/pkg/domain/plugins"
|
|
)
|
|
|
|
type paymentsState struct {
|
|
LastCreatedAt time.Time `json:"lastCreatedAt"`
|
|
}
|
|
|
|
func (p *Plugin) fetchNextPayments(ctx context.Context, req models.FetchNextPaymentsRequest) (models.FetchNextPaymentsResponse, error) {
|
|
var oldState paymentsState
|
|
if req.State != nil {
|
|
if err := json.Unmarshal(req.State, &oldState); err != nil {
|
|
return models.FetchNextPaymentsResponse{}, err
|
|
}
|
|
}
|
|
|
|
var from models.PSPAccount
|
|
if req.FromPayload == nil {
|
|
return models.FetchNextPaymentsResponse{}, models.ErrMissingFromPayloadInRequest
|
|
}
|
|
if err := json.Unmarshal(req.FromPayload, &from); err != nil {
|
|
return models.FetchNextPaymentsResponse{}, err
|
|
}
|
|
|
|
newState := paymentsState{
|
|
LastCreatedAt: oldState.LastCreatedAt,
|
|
}
|
|
|
|
payments := make([]models.PSPPayment, 0, req.PageSize)
|
|
needMore := false
|
|
hasMore := false
|
|
for page := 0; ; page++ {
|
|
pagedTransactions, err := p.client.GetTransactions(ctx, from.Reference, page, req.PageSize, oldState.LastCreatedAt)
|
|
if err != nil {
|
|
return models.FetchNextPaymentsResponse{}, err
|
|
}
|
|
|
|
payments, err = p.toPSPPayments(ctx, oldState.LastCreatedAt, payments, pagedTransactions)
|
|
if err != nil {
|
|
return models.FetchNextPaymentsResponse{}, err
|
|
}
|
|
|
|
needMore, hasMore = pagination.ShouldFetchMore(payments, pagedTransactions, req.PageSize)
|
|
if !needMore || !hasMore {
|
|
break
|
|
}
|
|
}
|
|
|
|
if !needMore {
|
|
payments = payments[:req.PageSize]
|
|
}
|
|
|
|
if len(payments) > 0 {
|
|
newState.LastCreatedAt = payments[len(payments)-1].CreatedAt
|
|
}
|
|
|
|
payload, err := json.Marshal(newState)
|
|
if err != nil {
|
|
return models.FetchNextPaymentsResponse{}, err
|
|
}
|
|
|
|
return models.FetchNextPaymentsResponse{
|
|
Payments: payments,
|
|
NewState: payload,
|
|
HasMore: hasMore,
|
|
}, nil
|
|
}
|
|
|
|
func (p *Plugin) toPSPPayments(
|
|
ctx context.Context,
|
|
lastCreatedAt time.Time,
|
|
payments []models.PSPPayment,
|
|
transactions []*client.Transaction,
|
|
) ([]models.PSPPayment, error) {
|
|
for _, transaction := range transactions {
|
|
createdAt, err := time.Parse("2006-01-02T15:04:05.999999999", transaction.Attributes.CreatedAt)
|
|
if err != nil {
|
|
return payments, fmt.Errorf("failed to parse transaction date: %v", err)
|
|
}
|
|
|
|
switch createdAt.Compare(lastCreatedAt) {
|
|
case -1, 0:
|
|
continue
|
|
default:
|
|
}
|
|
|
|
payment, err := p.transactionToPayment(ctx, transaction)
|
|
if err != nil {
|
|
if errors.Is(err, plugins.ErrCurrencyNotSupported) {
|
|
// Skip unsupported currencies rather than failing: a retryable
|
|
// error here would freeze ingestion for the whole account.
|
|
p.logger.WithField("transaction_id", transaction.ID).Info("skipping transaction with unsupported currency")
|
|
continue
|
|
}
|
|
return payments, err
|
|
}
|
|
if payment == nil {
|
|
continue
|
|
}
|
|
|
|
payments = append(payments, *payment)
|
|
}
|
|
return payments, nil
|
|
}
|
|
|
|
func (p *Plugin) transactionToPayment(ctx context.Context, transaction *client.Transaction) (*models.PSPPayment, error) {
|
|
switch transaction.Attributes.Type {
|
|
case "Transfer":
|
|
if transaction.Attributes.Direction == "Debit" {
|
|
return p.fetchAndTranslateTransfer(ctx, transaction)
|
|
} else {
|
|
// Do not fetch the transfer, it does not exists if we're trying to
|
|
// fetch it with the destination account.
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
rawData, err := json.Marshal(transaction)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal transaction: %w", err)
|
|
}
|
|
|
|
paymentType, shouldBeRecorded := matchPaymentType(transaction.Attributes.Type, transaction.Attributes.Direction)
|
|
if !shouldBeRecorded {
|
|
return nil, nil
|
|
}
|
|
|
|
createdAt, err := time.Parse("2006-01-02T15:04:05.999999999", transaction.Attributes.CreatedAt)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse transaction date: %w", err)
|
|
}
|
|
|
|
c, err := currency.GetPrecision(supportedCurrenciesWithDecimal, transaction.Attributes.Currency)
|
|
if err != nil {
|
|
if errors.Is(err, currency.ErrMissingCurrencies) {
|
|
return nil, fmt.Errorf("%w: %w", plugins.ErrCurrencyNotSupported, err)
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
amount, err := currency.GetAmountWithPrecisionFromString(transaction.Attributes.Amount.String(), c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
reference := transaction.ID
|
|
if transaction.Attributes.Type == "Payment" {
|
|
// In case of payments (related to payouts), we want to take the real
|
|
// object id as a reference
|
|
reference = transaction.Relationships.Data.ID
|
|
}
|
|
|
|
payment := models.PSPPayment{
|
|
Reference: reference,
|
|
CreatedAt: createdAt,
|
|
Type: paymentType,
|
|
Amount: amount,
|
|
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transaction.Attributes.Currency),
|
|
Scheme: models.PAYMENT_SCHEME_OTHER,
|
|
Status: models.PAYMENT_STATUS_SUCCEEDED,
|
|
Raw: rawData,
|
|
}
|
|
|
|
switch paymentType {
|
|
case models.PAYMENT_TYPE_PAYIN:
|
|
payment.DestinationAccountReference = pointer.For(strconv.Itoa(int(transaction.Attributes.AccountID)))
|
|
case models.PAYMENT_TYPE_PAYOUT:
|
|
payment.SourceAccountReference = pointer.For(strconv.Itoa(int(transaction.Attributes.AccountID)))
|
|
default:
|
|
if transaction.Attributes.Direction == "Debit" {
|
|
payment.SourceAccountReference = pointer.For(strconv.Itoa(int(transaction.Attributes.AccountID)))
|
|
} else {
|
|
payment.DestinationAccountReference = pointer.For(strconv.Itoa(int(transaction.Attributes.AccountID)))
|
|
}
|
|
}
|
|
|
|
return &payment, nil
|
|
}
|
|
|
|
func (p *Plugin) fetchAndTranslateTransfer(ctx context.Context, transaction *client.Transaction) (*models.PSPPayment, error) {
|
|
transfer, err := p.client.GetTransfer(ctx, fmt.Sprint(transaction.Attributes.AccountID), transaction.Relationships.Data.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return transferToPayment(transfer)
|
|
}
|
|
|
|
func matchPaymentType(transactionType string, transactionDirection string) (models.PaymentType, bool) {
|
|
switch transactionType {
|
|
case "Transfer":
|
|
return models.PAYMENT_TYPE_TRANSFER, true
|
|
case "Payment", "Exchange", "Charge", "Refund":
|
|
switch transactionDirection {
|
|
case "Debit":
|
|
return models.PAYMENT_TYPE_PAYOUT, true
|
|
case "Credit":
|
|
return models.PAYMENT_TYPE_PAYIN, true
|
|
}
|
|
}
|
|
|
|
return models.PAYMENT_TYPE_OTHER, false
|
|
}
|