Files
zeekay 7d727874d5 chore: rebrand Formance Payments -> Hanzo Payments (hanzo-fi)
- go.mod module path + all self-imports: github.com/formancehq/payments -> github.com/hanzo-fi/payments
  (1590 .go files / 3152 refs; 20 modules: root, pkg/client, pkg/domain, 18 ce/plugins).
  External dep kept: formancehq/go-libs.
- plugins: each ce/plugins/* consumed a *published* pkg/domain; added local
  'replace .../pkg/domain => ../../../pkg/domain' (consistent with root) so every
  standalone plugin module builds in-monorepo. compile-plugin.sh MOD var rebranded too.
- branding: README, LICENSE (dual MIT+EE; upstream copyright retained + Hanzo added),
  goreleaser (release images -> registry.hanzo.ai/hanzo-fi, OCI labels, homepage),
  proto go_package, renovate, speakeasy gen.yaml, connector-template *.gotpl.
- CI: removed Numary/Formance workflows + composite action; added canonical hanzo.yml +
  cicd.yml (hanzoai/ci, gated HANZO_CI_ENABLED) + secretless ci.yml (go build/vet).
- kept as data/compat keys: wire capability enums ALLOW_FORMANCE_*, generated SDK type
  client.Formance, domain field FormanceRedirectURL, OTEL_SERVICE_NAME, upstream base image.
- verified: go build ./... (default + -tags ee) + all 20 nested modules green.
2026-07-05 13:08:55 -07:00

119 lines
4.1 KiB
Go

package mappers
import (
"encoding/json"
"fmt"
"strconv"
"github.com/hanzo-fi/payments/ee/plugins/bitstamp/client"
"github.com/hanzo-fi/payments/pkg/domain/models"
)
// PaymentMapResult tells the orchestrator how to handle the row.
// - Payment != nil — emit it.
// - Skip = true — silently skip (orders / conversions / multi-asset
// and similar known non-payment shapes).
// - DerivativesRow = true — log Warn before skipping; spot-only stance.
// - UnknownType = true — log Warn (with tx.id) before emitting as
// PAYMENT_TYPE_OTHER so a new code surfaces.
type PaymentMapResult struct {
Payment *models.PSPPayment
Skip bool
DerivativesRow bool
UnknownType bool
}
// UserTransactionToPSPPayment maps a user_transactions row to a
// PSPPayment. Returns Skip=true for trade/conversion rows (handled by
// orders and conversions respectively) and for any row that does not
// reduce to exactly one non-zero known currency.
//
// Spot-only stance: rows carrying derivatives markers in their Raw
// (margin_mode / leverage_rate) are flagged DerivativesRow=true and
// skipped — the orchestrator logs Warn with the row ID so a
// derivatives-enabled account surfaces the gap loudly.
//
// Transfer rows (types 14 / 33 / 35) are remapped to sign-based
// PAYOUT (negative amount → this connector is the source) /
// PAYIN (positive amount → this connector is the destination) per
// MAPPINGS §3.6. The pair-id metadata (= tx.id stringified) is what
// lets a downstream consumer join the two legs across connectors.
func UserTransactionToPSPPayment(currencies map[string]int, tx client.UserTransaction) (PaymentMapResult, error) {
if tx.HasDerivativesMarker() || IsDerivativesType(tx.Type) {
return PaymentMapResult{Skip: true, DerivativesRow: true}, nil
}
if IsConversionType(tx.Type) {
return PaymentMapResult{Skip: true}, nil
}
asset, amount, hasAmount, err := ResolveSinglePaymentAsset(currencies, tx.CurrencyAmounts)
if err != nil {
return PaymentMapResult{}, fmt.Errorf("resolve amount for tx %d: %w", tx.ID, err)
}
if !hasAmount {
return PaymentMapResult{Skip: true}, nil
}
createdAt, err := ParseBitstampTime(tx.Datetime)
if err != nil {
return PaymentMapResult{}, fmt.Errorf("payment tx %d: %w", tx.ID, err)
}
raw, err := json.Marshal(tx)
if err != nil {
return PaymentMapResult{}, fmt.Errorf("marshal raw for tx %d: %w", tx.ID, err)
}
paymentType, metadata := classifyPaymentType(tx)
return PaymentMapResult{
Payment: &models.PSPPayment{
Reference: strconv.FormatInt(tx.ID, 10),
CreatedAt: createdAt,
Type: paymentType,
Amount: amount,
Asset: asset,
Scheme: models.PAYMENT_SCHEME_OTHER,
// user_transactions returns settled-only history; pending
// withdrawals live on /withdrawal-requests/ which a sibling
// orchestrator polls (MAPPINGS.md §3.3.c).
Status: models.PAYMENT_STATUS_SUCCEEDED,
Metadata: metadata,
Raw: raw,
},
UnknownType: !IsKnownTransactionType(tx.Type),
}, nil
}
// classifyPaymentType maps a user_transactions row to a
// (PaymentType, metadata). Two-legged transfer types (14 / 33 / 35)
// split by amount sign — see MAPPINGS §4.3 sub-section on
// cross-account transfers. Counterparty fields are absent on the
// wire today; the metadata builder omits the keys when empty.
func classifyPaymentType(tx client.UserTransaction) (models.PaymentType, map[string]string) {
base := PaymentMetadata(tx)
if !IsTransferType(tx.Type) {
paymentType, _ := TransactionTypeToPaymentType(tx.Type)
return paymentType, base
}
direction := TransferDirectionIncoming
paymentType := models.PAYMENT_TYPE_PAYIN
if transferAmountIsNegative(tx) {
direction = TransferDirectionOutgoing
paymentType = models.PAYMENT_TYPE_PAYOUT
}
pair := TransferPairMetadata(tx.ID, direction, "", "")
return paymentType, MergeMetadata(base, pair)
}
func transferAmountIsNegative(tx client.UserTransaction) bool {
for _, value := range tx.CurrencyAmounts {
if value == "" || IsZeroAmount(AbsAmount(value)) {
continue
}
return IsNegative(value)
}
return false
}