mirror of
https://github.com/hanzo-fi/payments.git
synced 2026-10-30 20:31:20 +00:00
603 lines
25 KiB
Go
603 lines
25 KiB
Go
package stripe
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"math/big"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/hanzo-fi/go-libs/v5/pkg/types/currency"
|
||
"github.com/hanzo-fi/payments/ce/plugins/stripe/client"
|
||
"github.com/hanzo-fi/payments/pkg/domain/models"
|
||
"github.com/hanzo-fi/payments/pkg/domain/plugins"
|
||
"github.com/pkg/errors"
|
||
stripesdk "github.com/stripe/stripe-go/v80"
|
||
)
|
||
|
||
var (
|
||
ErrUnsupportedTransactionType = errors.New("unsupported TransactionType")
|
||
ErrUnsupportedAdjustment = errors.New("unsupported adjustment")
|
||
// ErrUnsupportedCurrency wraps plugins.ErrCurrencyNotSupported so the fetch
|
||
// loop can detect it via errors.Is and skip the transaction instead of
|
||
// failing — a returned error here would freeze ingestion on endless retries.
|
||
ErrUnsupportedCurrency = fmt.Errorf("unsupported currency: %w", plugins.ErrCurrencyNotSupported)
|
||
)
|
||
|
||
type paymentsState struct {
|
||
Timeline client.Timeline `json:"timeline"`
|
||
}
|
||
|
||
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{}, errors.New("missing from payload when fetching payments")
|
||
}
|
||
if err := json.Unmarshal(req.FromPayload, &from); err != nil {
|
||
return models.FetchNextPaymentsResponse{}, err
|
||
}
|
||
|
||
var payments []models.PSPPayment
|
||
newState := oldState
|
||
rawPayments, timeline, hasMore, err := p.client.GetPayments(
|
||
ctx,
|
||
resolveAccount(from.Reference),
|
||
oldState.Timeline,
|
||
int64(req.PageSize),
|
||
)
|
||
if err != nil {
|
||
return models.FetchNextPaymentsResponse{}, err
|
||
}
|
||
newState.Timeline = timeline
|
||
|
||
for _, rawPayment := range rawPayments {
|
||
payment, err := p.translatePayment(&from.Reference, rawPayment)
|
||
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("reference", rawPayment.ID).Info("skipping payment with unsupported currency")
|
||
continue
|
||
}
|
||
return models.FetchNextPaymentsResponse{}, fmt.Errorf("failed to translate payment: %w", err)
|
||
}
|
||
|
||
// skip unsupported payments
|
||
if payment == nil {
|
||
continue
|
||
}
|
||
payments = append(payments, *payment)
|
||
}
|
||
|
||
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) translatePayment(accountRef *string, balanceTransaction *stripesdk.BalanceTransaction) (payment *models.PSPPayment, err error) {
|
||
if balanceTransaction.Source == nil {
|
||
p.logger.WithFields(map[string]any{
|
||
"type": balanceTransaction.Type,
|
||
"id": balanceTransaction.ID,
|
||
}).Info("skipping balance transaction with nil source element")
|
||
return nil, nil
|
||
}
|
||
|
||
rawData, err := json.Marshal(balanceTransaction)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to marshal raw data: %w", err)
|
||
}
|
||
metadata := make(map[string]string)
|
||
|
||
switch balanceTransaction.Type {
|
||
case stripesdk.BalanceTransactionTypeCharge:
|
||
if balanceTransaction.Source.Charge == nil {
|
||
p.logger.WithFields(map[string]any{
|
||
"type": balanceTransaction.Type,
|
||
"id": balanceTransaction.ID,
|
||
}).Info("skipping balance transaction with nil charge element")
|
||
return nil, nil
|
||
}
|
||
transactionCurrency := strings.ToUpper(string(balanceTransaction.Source.Charge.Currency))
|
||
_, ok := supportedCurrenciesWithDecimal[transactionCurrency]
|
||
if !ok {
|
||
return nil, fmt.Errorf("%w %q", ErrUnsupportedCurrency, transactionCurrency)
|
||
}
|
||
|
||
appendMetadata(metadata, balanceTransaction.Source.Charge.Metadata)
|
||
if balanceTransaction.Source.Charge.PaymentIntent != nil {
|
||
appendMetadata(metadata, balanceTransaction.Source.Charge.PaymentIntent.Metadata)
|
||
}
|
||
|
||
payment = &models.PSPPayment{
|
||
Reference: balanceTransaction.ID,
|
||
Type: models.PAYMENT_TYPE_PAYIN,
|
||
Status: models.PAYMENT_STATUS_SUCCEEDED,
|
||
Amount: big.NewInt(balanceTransaction.Source.Charge.Amount),
|
||
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transactionCurrency),
|
||
Scheme: detailsToPaymentScheme(balanceTransaction.Source.Charge.PaymentMethodDetails),
|
||
CreatedAt: time.Unix(balanceTransaction.Created, 0),
|
||
DestinationAccountReference: accountRef,
|
||
Metadata: metadata,
|
||
Raw: rawData,
|
||
}
|
||
|
||
case stripesdk.BalanceTransactionTypeRefund:
|
||
// Refund a charge
|
||
// Created when a credit card charge refund is initiated.
|
||
// If you authorize and capture separately and the capture amount is
|
||
// less than the initial authorization, you see a balance transaction
|
||
// of type charge for the full authorization amount and another balance
|
||
// transaction of type refund for the uncaptured portion.
|
||
// cf https://stripe.com/docs/reports/balance-transaction-types
|
||
|
||
transactionCurrency := strings.ToUpper(string(balanceTransaction.Source.Refund.Currency))
|
||
_, ok := supportedCurrenciesWithDecimal[transactionCurrency]
|
||
if !ok {
|
||
return nil, fmt.Errorf("%w %q", ErrUnsupportedCurrency, transactionCurrency)
|
||
}
|
||
|
||
if balanceTransaction.Source.Refund.Charge == nil {
|
||
return nil, fmt.Errorf("refund charge missing from refund payload: %q", balanceTransaction.ID)
|
||
}
|
||
|
||
appendMetadata(metadata, balanceTransaction.Source.Refund.Metadata)
|
||
if balanceTransaction.Source.Refund.PaymentIntent != nil {
|
||
appendMetadata(metadata, balanceTransaction.Source.Refund.PaymentIntent.Metadata)
|
||
}
|
||
|
||
payment = &models.PSPPayment{
|
||
Reference: balanceTransaction.ID,
|
||
ParentReference: balanceTransaction.Source.Refund.Charge.BalanceTransaction.ID,
|
||
Type: models.PAYMENT_TYPE_PAYIN,
|
||
Status: models.PAYMENT_STATUS_REFUNDED,
|
||
Amount: big.NewInt(balanceTransaction.Source.Refund.Amount),
|
||
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transactionCurrency),
|
||
Scheme: detailsToPaymentScheme(balanceTransaction.Source.Refund.Charge.PaymentMethodDetails),
|
||
CreatedAt: time.Unix(balanceTransaction.Created, 0),
|
||
DestinationAccountReference: accountRef,
|
||
Raw: rawData,
|
||
Metadata: metadata,
|
||
}
|
||
|
||
case stripesdk.BalanceTransactionTypeRefundFailure:
|
||
// Refund a charge
|
||
// Created when a credit card charge refund fails, and Stripe returns the funds to your balance.
|
||
// This may occur if your customer’s bank or card issuer is unable to correctly process a refund
|
||
// (e.g., due to a closed bank account or a problem with the card).
|
||
// cf https://stripe.com/docs/reports/balance-transaction-types
|
||
transactionCurrency := strings.ToUpper(string(balanceTransaction.Source.Refund.Currency))
|
||
_, ok := supportedCurrenciesWithDecimal[transactionCurrency]
|
||
if !ok {
|
||
return nil, fmt.Errorf("%w %q", ErrUnsupportedCurrency, transactionCurrency)
|
||
}
|
||
|
||
if balanceTransaction.Source.Refund.Charge == nil {
|
||
return nil, fmt.Errorf("refund charge missing from refund payload: %q", balanceTransaction.ID)
|
||
}
|
||
|
||
appendMetadata(metadata, balanceTransaction.Source.Refund.Metadata)
|
||
if balanceTransaction.Source.Refund.PaymentIntent != nil {
|
||
appendMetadata(metadata, balanceTransaction.Source.Refund.PaymentIntent.Metadata)
|
||
}
|
||
|
||
payment = &models.PSPPayment{
|
||
Reference: balanceTransaction.ID,
|
||
ParentReference: balanceTransaction.Source.Refund.Charge.BalanceTransaction.ID,
|
||
Type: models.PAYMENT_TYPE_PAYIN,
|
||
Status: models.PAYMENT_STATUS_REFUNDED_FAILURE,
|
||
Amount: big.NewInt(balanceTransaction.Source.Refund.Amount),
|
||
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transactionCurrency),
|
||
Scheme: detailsToPaymentScheme(balanceTransaction.Source.Refund.Charge.PaymentMethodDetails),
|
||
CreatedAt: time.Unix(balanceTransaction.Created, 0),
|
||
DestinationAccountReference: accountRef,
|
||
Raw: rawData,
|
||
Metadata: metadata,
|
||
}
|
||
|
||
case stripesdk.BalanceTransactionTypePayment:
|
||
transactionCurrency := strings.ToUpper(string(balanceTransaction.Source.Charge.Currency))
|
||
_, ok := supportedCurrenciesWithDecimal[transactionCurrency]
|
||
if !ok {
|
||
return nil, fmt.Errorf("%w %q", ErrUnsupportedCurrency, transactionCurrency)
|
||
}
|
||
|
||
appendMetadata(metadata, balanceTransaction.Source.Charge.Metadata)
|
||
if balanceTransaction.Source.Charge.PaymentIntent != nil {
|
||
appendMetadata(metadata, balanceTransaction.Source.Charge.PaymentIntent.Metadata)
|
||
}
|
||
|
||
payment = &models.PSPPayment{
|
||
Reference: balanceTransaction.ID,
|
||
Type: models.PAYMENT_TYPE_PAYIN,
|
||
Status: models.PAYMENT_STATUS_SUCCEEDED,
|
||
Amount: big.NewInt(balanceTransaction.Source.Charge.Amount - balanceTransaction.Source.Charge.AmountRefunded),
|
||
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transactionCurrency),
|
||
Scheme: models.PAYMENT_SCHEME_OTHER,
|
||
CreatedAt: time.Unix(balanceTransaction.Created, 0),
|
||
DestinationAccountReference: accountRef,
|
||
Raw: rawData,
|
||
Metadata: metadata,
|
||
}
|
||
|
||
case stripesdk.BalanceTransactionTypePaymentRefund:
|
||
// Refund a payment
|
||
// Created when a local payment method refund is initiated.
|
||
// Additionally, if your customer’s bank or card issuer is unable to correctly process a refund
|
||
// (e.g., due to a closed bank account or a problem with the card) Stripe returns the funds to your balance.
|
||
// The returned funds are represented as a Balance transaction with the type payment_refund.
|
||
// cf https://stripe.com/docs/reports/balance-transaction-types
|
||
transactionCurrency := strings.ToUpper(string(balanceTransaction.Source.Refund.Currency))
|
||
_, ok := supportedCurrenciesWithDecimal[transactionCurrency]
|
||
if !ok {
|
||
return nil, fmt.Errorf("%w %q", ErrUnsupportedCurrency, transactionCurrency)
|
||
}
|
||
|
||
if balanceTransaction.Source.Refund.Charge == nil {
|
||
return nil, fmt.Errorf("refund charge missing from payment refund payload: %q", balanceTransaction.ID)
|
||
}
|
||
|
||
appendMetadata(metadata, balanceTransaction.Source.Refund.Charge.Metadata)
|
||
if balanceTransaction.Source.Refund.Charge.PaymentIntent != nil {
|
||
appendMetadata(metadata, balanceTransaction.Source.Refund.Charge.PaymentIntent.Metadata)
|
||
}
|
||
|
||
payment = &models.PSPPayment{
|
||
Reference: balanceTransaction.ID,
|
||
ParentReference: balanceTransaction.Source.Refund.Charge.BalanceTransaction.ID,
|
||
Type: models.PAYMENT_TYPE_PAYIN,
|
||
Status: models.PAYMENT_STATUS_REFUNDED,
|
||
Amount: big.NewInt(balanceTransaction.Source.Refund.Amount),
|
||
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transactionCurrency),
|
||
Scheme: models.PAYMENT_SCHEME_OTHER,
|
||
CreatedAt: time.Unix(balanceTransaction.Created, 0),
|
||
DestinationAccountReference: accountRef,
|
||
Raw: rawData,
|
||
Metadata: metadata,
|
||
}
|
||
|
||
case stripesdk.BalanceTransactionTypePaymentFailureRefund:
|
||
// Refund a payment
|
||
// ACH, direct debit, and other delayed notification payment methods remain in a pending state
|
||
// until they either succeed or fail. You’ll see a pending Balance transaction of type payment
|
||
// when the payment is created. Another Balance transaction of type payment_failure_refund appears
|
||
// if the pending payment later fails.
|
||
// cf https://stripe.com/docs/reports/balance-transaction-types
|
||
|
||
var transactionCurrency string
|
||
var amount int64
|
||
var parentReference string
|
||
|
||
if balanceTransaction.Source.Refund == nil {
|
||
p.logger.WithFields(map[string]any{"type": balanceTransaction.Type, "reference": balanceTransaction.ID}).Infof("Handling a payment_failure_refund balance transaction without a refund source, we will store it as a single payment as we cannot trace to the original payment")
|
||
transactionCurrency = strings.ToUpper(string(balanceTransaction.Currency))
|
||
amount = balanceTransaction.Amount
|
||
parentReference = ""
|
||
} else {
|
||
if balanceTransaction.Source.Refund.Charge == nil {
|
||
return nil, fmt.Errorf("refund charge missing from payment refund failure payload: %q", balanceTransaction.ID)
|
||
}
|
||
transactionCurrency = strings.ToUpper(string(balanceTransaction.Source.Refund.Currency))
|
||
amount = balanceTransaction.Source.Refund.Amount
|
||
parentReference = balanceTransaction.Source.Refund.Charge.BalanceTransaction.ID
|
||
|
||
appendMetadata(metadata, balanceTransaction.Source.Refund.Charge.Metadata)
|
||
if balanceTransaction.Source.Refund.Charge.PaymentIntent != nil {
|
||
appendMetadata(metadata, balanceTransaction.Source.Refund.Charge.PaymentIntent.Metadata)
|
||
}
|
||
}
|
||
|
||
_, ok := supportedCurrenciesWithDecimal[transactionCurrency]
|
||
if !ok {
|
||
return nil, fmt.Errorf("%w %q", ErrUnsupportedCurrency, transactionCurrency)
|
||
}
|
||
|
||
payment = &models.PSPPayment{
|
||
Reference: balanceTransaction.ID,
|
||
ParentReference: parentReference,
|
||
Type: models.PAYMENT_TYPE_PAYIN,
|
||
Status: models.PAYMENT_STATUS_REFUNDED_FAILURE,
|
||
Amount: big.NewInt(amount),
|
||
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transactionCurrency),
|
||
Scheme: models.PAYMENT_SCHEME_OTHER,
|
||
CreatedAt: time.Unix(balanceTransaction.Created, 0),
|
||
DestinationAccountReference: accountRef,
|
||
Raw: rawData,
|
||
Metadata: metadata,
|
||
}
|
||
|
||
case stripesdk.BalanceTransactionTypePayout:
|
||
transactionCurrency := strings.ToUpper(string(balanceTransaction.Source.Payout.Currency))
|
||
_, ok := supportedCurrenciesWithDecimal[transactionCurrency]
|
||
if !ok {
|
||
return nil, fmt.Errorf("%w %q", ErrUnsupportedCurrency, transactionCurrency)
|
||
}
|
||
|
||
appendMetadata(metadata, balanceTransaction.Source.Payout.Metadata)
|
||
|
||
payment = &models.PSPPayment{
|
||
Reference: balanceTransaction.ID,
|
||
Type: models.PAYMENT_TYPE_PAYOUT,
|
||
Status: convertPayoutStatus(balanceTransaction.Source.Payout.Status),
|
||
Amount: big.NewInt(balanceTransaction.Source.Payout.Amount),
|
||
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transactionCurrency),
|
||
Scheme: func() models.PaymentScheme {
|
||
switch balanceTransaction.Source.Payout.Type {
|
||
case stripesdk.PayoutTypeBank:
|
||
return models.PAYMENT_SCHEME_SEPA_CREDIT
|
||
case stripesdk.PayoutTypeCard:
|
||
return destinationToPaymentScheme(balanceTransaction.Source.Payout.Destination)
|
||
}
|
||
return models.PAYMENT_SCHEME_UNKNOWN
|
||
}(),
|
||
CreatedAt: time.Unix(balanceTransaction.Created, 0),
|
||
SourceAccountReference: accountRef,
|
||
Raw: rawData,
|
||
Metadata: metadata,
|
||
}
|
||
|
||
case stripesdk.BalanceTransactionTypePayoutFailure, stripesdk.BalanceTransactionTypePayoutCancel:
|
||
transactionCurrency := strings.ToUpper(string(balanceTransaction.Source.Payout.Currency))
|
||
_, ok := supportedCurrenciesWithDecimal[transactionCurrency]
|
||
if !ok {
|
||
return nil, fmt.Errorf("%w %q", ErrUnsupportedCurrency, transactionCurrency)
|
||
}
|
||
|
||
status := models.PAYMENT_STATUS_FAILED
|
||
if balanceTransaction.Type == stripesdk.BalanceTransactionTypePayoutCancel {
|
||
status = models.PAYMENT_STATUS_CANCELLED
|
||
}
|
||
|
||
appendMetadata(metadata, balanceTransaction.Source.Payout.Metadata)
|
||
payment = &models.PSPPayment{
|
||
Reference: balanceTransaction.ID,
|
||
ParentReference: balanceTransaction.Source.Payout.BalanceTransaction.ID,
|
||
Type: models.PAYMENT_TYPE_PAYOUT,
|
||
Status: status,
|
||
Amount: big.NewInt(balanceTransaction.Source.Payout.Amount),
|
||
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transactionCurrency),
|
||
Scheme: func() models.PaymentScheme {
|
||
switch balanceTransaction.Source.Payout.Type {
|
||
case stripesdk.PayoutTypeBank:
|
||
return models.PAYMENT_SCHEME_SEPA_CREDIT
|
||
case stripesdk.PayoutTypeCard:
|
||
return destinationToPaymentScheme(balanceTransaction.Source.Payout.Destination)
|
||
}
|
||
return models.PAYMENT_SCHEME_UNKNOWN
|
||
}(),
|
||
CreatedAt: time.Unix(balanceTransaction.Created, 0),
|
||
SourceAccountReference: accountRef,
|
||
Raw: rawData,
|
||
Metadata: metadata,
|
||
}
|
||
|
||
case stripesdk.BalanceTransactionTypeTransfer:
|
||
transactionCurrency := strings.ToUpper(string(balanceTransaction.Source.Transfer.Currency))
|
||
_, ok := supportedCurrenciesWithDecimal[transactionCurrency]
|
||
if !ok {
|
||
return nil, fmt.Errorf("%w %q", ErrUnsupportedCurrency, transactionCurrency)
|
||
}
|
||
|
||
appendMetadata(metadata, balanceTransaction.Source.Transfer.Metadata)
|
||
payment = &models.PSPPayment{
|
||
Reference: balanceTransaction.ID,
|
||
Type: models.PAYMENT_TYPE_TRANSFER,
|
||
Status: models.PAYMENT_STATUS_SUCCEEDED,
|
||
Amount: big.NewInt(balanceTransaction.Source.Transfer.Amount - balanceTransaction.Source.Transfer.AmountReversed),
|
||
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transactionCurrency),
|
||
Scheme: models.PAYMENT_SCHEME_OTHER,
|
||
CreatedAt: time.Unix(balanceTransaction.Created, 0),
|
||
SourceAccountReference: accountRef,
|
||
Raw: rawData,
|
||
Metadata: metadata,
|
||
}
|
||
|
||
if balanceTransaction.Source.Transfer.Destination != nil {
|
||
payment.DestinationAccountReference = &balanceTransaction.Source.Transfer.Destination.ID
|
||
}
|
||
|
||
case stripesdk.BalanceTransactionTypeTransferRefund:
|
||
transactionCurrency := strings.ToUpper(string(balanceTransaction.Source.Transfer.Currency))
|
||
_, ok := supportedCurrenciesWithDecimal[transactionCurrency]
|
||
if !ok {
|
||
return nil, fmt.Errorf("%w %q", ErrUnsupportedCurrency, transactionCurrency)
|
||
}
|
||
|
||
appendMetadata(metadata, balanceTransaction.Source.Transfer.Metadata)
|
||
payment = &models.PSPPayment{
|
||
Reference: balanceTransaction.ID,
|
||
ParentReference: balanceTransaction.Source.Transfer.BalanceTransaction.ID,
|
||
Type: models.PAYMENT_TYPE_TRANSFER,
|
||
Status: models.PAYMENT_STATUS_REFUNDED,
|
||
Amount: big.NewInt(balanceTransaction.Amount),
|
||
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transactionCurrency),
|
||
Scheme: models.PAYMENT_SCHEME_OTHER,
|
||
SourceAccountReference: accountRef,
|
||
CreatedAt: time.Unix(balanceTransaction.Created, 0),
|
||
Raw: rawData,
|
||
Metadata: metadata,
|
||
}
|
||
|
||
if balanceTransaction.Source.Transfer.Destination != nil {
|
||
payment.DestinationAccountReference = &balanceTransaction.Source.Transfer.Destination.ID
|
||
}
|
||
|
||
case stripesdk.BalanceTransactionTypeTransferCancel, stripesdk.BalanceTransactionTypeTransferFailure:
|
||
transactionCurrency := strings.ToUpper(string(balanceTransaction.Source.Transfer.Currency))
|
||
_, ok := supportedCurrenciesWithDecimal[transactionCurrency]
|
||
if !ok {
|
||
return nil, fmt.Errorf("%w %q", ErrUnsupportedCurrency, transactionCurrency)
|
||
}
|
||
|
||
status := models.PAYMENT_STATUS_FAILED
|
||
if balanceTransaction.Type == stripesdk.BalanceTransactionTypeTransferCancel {
|
||
status = models.PAYMENT_STATUS_CANCELLED
|
||
}
|
||
|
||
appendMetadata(metadata, balanceTransaction.Source.Transfer.Metadata)
|
||
payment = &models.PSPPayment{
|
||
Reference: balanceTransaction.ID,
|
||
ParentReference: balanceTransaction.Source.Transfer.BalanceTransaction.ID,
|
||
Type: models.PAYMENT_TYPE_TRANSFER,
|
||
Status: status,
|
||
Amount: big.NewInt(balanceTransaction.Amount),
|
||
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transactionCurrency),
|
||
Scheme: models.PAYMENT_SCHEME_OTHER,
|
||
SourceAccountReference: accountRef,
|
||
CreatedAt: time.Unix(balanceTransaction.Created, 0),
|
||
Raw: rawData,
|
||
Metadata: metadata,
|
||
}
|
||
|
||
if balanceTransaction.Source.Transfer.Destination != nil {
|
||
payment.DestinationAccountReference = &balanceTransaction.Source.Transfer.Destination.ID
|
||
}
|
||
|
||
case stripesdk.BalanceTransactionTypeAdjustment:
|
||
var transactionCurrency string
|
||
var amount int64
|
||
var parentReference string
|
||
var scheme models.PaymentScheme
|
||
var paymentStatus models.PaymentStatus
|
||
|
||
if balanceTransaction.Source.Dispute == nil {
|
||
p.logger.WithField("type", balanceTransaction.Type).WithField("reference", balanceTransaction.ID).Infof("skipping unsupported balance transaction: %v", ErrUnsupportedAdjustment)
|
||
return nil, nil
|
||
}
|
||
|
||
if balanceTransaction.Source.Dispute.Charge == nil {
|
||
return nil, fmt.Errorf("refund charge missing from payment dispute payload: %q", balanceTransaction.ID)
|
||
}
|
||
transactionCurrency = strings.ToUpper(string(balanceTransaction.Source.Dispute.Charge.Currency))
|
||
amount = balanceTransaction.Source.Dispute.Charge.Amount
|
||
parentReference = balanceTransaction.Source.Dispute.Charge.BalanceTransaction.ID
|
||
scheme = detailsToPaymentScheme(balanceTransaction.Source.Dispute.Charge.PaymentMethodDetails)
|
||
disputeStatus := convertDisputeStatus(balanceTransaction.Source.Dispute.Status)
|
||
paymentStatus = models.PAYMENT_STATUS_DISPUTE
|
||
switch disputeStatus {
|
||
case models.PAYMENT_STATUS_DISPUTE_WON:
|
||
paymentStatus = models.PAYMENT_STATUS_SUCCEEDED
|
||
case models.PAYMENT_STATUS_DISPUTE_LOST:
|
||
paymentStatus = models.PAYMENT_STATUS_FAILED
|
||
}
|
||
|
||
appendMetadata(metadata, balanceTransaction.Source.Dispute.Charge.Metadata)
|
||
if balanceTransaction.Source.Dispute.Charge.PaymentIntent != nil {
|
||
appendMetadata(metadata, balanceTransaction.Source.Dispute.Charge.PaymentIntent.Metadata)
|
||
}
|
||
|
||
_, ok := supportedCurrenciesWithDecimal[transactionCurrency]
|
||
if !ok {
|
||
return nil, fmt.Errorf("%w %q", ErrUnsupportedCurrency, transactionCurrency)
|
||
}
|
||
|
||
payment = &models.PSPPayment{
|
||
Reference: balanceTransaction.ID,
|
||
ParentReference: parentReference,
|
||
Type: models.PAYMENT_TYPE_PAYIN,
|
||
Status: paymentStatus, // Dispute is occurring, we don't know the outcome yet
|
||
Amount: big.NewInt(amount),
|
||
Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, transactionCurrency),
|
||
Scheme: scheme,
|
||
CreatedAt: time.Unix(balanceTransaction.Created, 0),
|
||
DestinationAccountReference: accountRef,
|
||
Raw: rawData,
|
||
Metadata: metadata,
|
||
}
|
||
|
||
default:
|
||
p.logger.WithField("type", balanceTransaction.Type).WithField("reference", balanceTransaction.ID).Errorf("unsupported balance transaction type: %v", ErrUnsupportedTransactionType)
|
||
return nil, nil
|
||
}
|
||
|
||
return payment, err
|
||
}
|
||
|
||
func convertDisputeStatus(status stripesdk.DisputeStatus) models.PaymentStatus {
|
||
switch status {
|
||
case stripesdk.DisputeStatusNeedsResponse, stripesdk.DisputeStatusUnderReview:
|
||
return models.PAYMENT_STATUS_DISPUTE
|
||
case stripesdk.DisputeStatusLost:
|
||
return models.PAYMENT_STATUS_DISPUTE_LOST
|
||
case stripesdk.DisputeStatusWon:
|
||
return models.PAYMENT_STATUS_DISPUTE_WON
|
||
default:
|
||
return models.PAYMENT_STATUS_DISPUTE
|
||
}
|
||
}
|
||
|
||
func convertPayoutStatus(status stripesdk.PayoutStatus) models.PaymentStatus {
|
||
switch status {
|
||
case stripesdk.PayoutStatusCanceled:
|
||
return models.PAYMENT_STATUS_CANCELLED
|
||
case stripesdk.PayoutStatusFailed:
|
||
return models.PAYMENT_STATUS_FAILED
|
||
case stripesdk.PayoutStatusInTransit, stripesdk.PayoutStatusPending:
|
||
return models.PAYMENT_STATUS_PENDING
|
||
case stripesdk.PayoutStatusPaid:
|
||
return models.PAYMENT_STATUS_SUCCEEDED
|
||
}
|
||
|
||
return models.PAYMENT_STATUS_OTHER
|
||
}
|
||
|
||
func appendMetadata(s map[string]string, vs ...map[string]string) {
|
||
for _, in := range vs {
|
||
if in == nil {
|
||
continue
|
||
}
|
||
for k, v := range in {
|
||
s[k] = v
|
||
}
|
||
}
|
||
}
|
||
|
||
func detailsToPaymentScheme(details *stripesdk.ChargePaymentMethodDetails) models.PaymentScheme {
|
||
if details == nil || details.Card == nil {
|
||
return models.PAYMENT_SCHEME_UNKNOWN
|
||
}
|
||
scheme, _ := models.PaymentSchemeFromString(
|
||
fmt.Sprintf("CARD_%s", strings.ToUpper(string(details.Card.Brand))),
|
||
)
|
||
return scheme
|
||
}
|
||
|
||
func destinationToPaymentScheme(dest *stripesdk.PayoutDestination) models.PaymentScheme {
|
||
if dest == nil || dest.Card == nil {
|
||
return models.PAYMENT_SCHEME_UNKNOWN
|
||
}
|
||
|
||
switch dest.Card.Brand {
|
||
case stripesdk.CardBrandAmericanExpress:
|
||
return models.PAYMENT_SCHEME_CARD_AMEX
|
||
case stripesdk.CardBrandDiscover:
|
||
return models.PAYMENT_SCHEME_CARD_DISCOVER
|
||
case stripesdk.CardBrandDinersClub:
|
||
return models.PAYMENT_SCHEME_CARD_DINERS
|
||
case stripesdk.CardBrandJCB:
|
||
return models.PAYMENT_SCHEME_CARD_JCB
|
||
case stripesdk.CardBrandMasterCard:
|
||
return models.PAYMENT_SCHEME_CARD_MASTERCARD
|
||
case stripesdk.CardBrandUnionPay:
|
||
return models.PAYMENT_SCHEME_CARD_UNION_PAY
|
||
case stripesdk.CardBrandVisa:
|
||
return models.PAYMENT_SCHEME_CARD_VISA
|
||
default:
|
||
return models.PAYMENT_SCHEME_UNKNOWN
|
||
}
|
||
}
|