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

194 lines
5.8 KiB
Go

package mappers
import (
"encoding/json"
"math/big"
"testing"
"github.com/hanzo-fi/payments/ee/plugins/bitstamp/client"
"github.com/hanzo-fi/payments/pkg/domain/models"
)
// TestUserTransactionToPSPConversion_Quentin679Fixture locks the exact
// regression case raised on the original Bitstamp PR by Quentin:
// {eur: -5.00, usdc: 5.810770, usdc_eur: 0.86047, type: 36}
// must produce EUR → USDC with the right minor units AND surface the
// rate in metadata.
func TestUserTransactionToPSPConversion_Quentin679Fixture(t *testing.T) {
t.Parallel()
tx := newTx(`{
"id": 458254264,
"datetime": "2025-09-25 14:42:59.894846",
"type": "36",
"fee": "0.000000",
"eur": "-5.00",
"usdc": "5.810770",
"usdc_eur": 0.86047,
"usd": "0.00",
"btc": "0.00000000"
}`)
res, err := UserTransactionToPSPConversion(testCurrencies, tx)
if err != nil {
t.Fatalf("err: %v", err)
}
if res.Skip || res.Conversion == nil {
t.Fatalf("expected conversion, got %#v", res)
}
c := res.Conversion
if c.Reference != "458254264" {
t.Errorf("reference=%q", c.Reference)
}
if c.SourceAsset != "EUR/2" || c.DestinationAsset != "USDC/6" {
t.Errorf("assets: got (%s, %s), want (EUR/2, USDC/6)", c.SourceAsset, c.DestinationAsset)
}
if c.SourceAmount.Cmp(big.NewInt(500)) != 0 {
t.Errorf("sourceAmount=%s, want 500", c.SourceAmount)
}
if c.DestinationAmount.Cmp(big.NewInt(5810770)) != 0 {
t.Errorf("destinationAmount=%s, want 5810770", c.DestinationAmount)
}
if c.Status != models.CONVERSION_STATUS_COMPLETED {
t.Errorf("status=%v, want COMPLETED", c.Status)
}
if c.Fee != nil {
t.Errorf("zero fee should map to nil, got %v", c.Fee)
}
if c.Metadata[MetadataKeyRate] != "0.86047" {
t.Errorf("rate metadata=%q, want 0.86047", c.Metadata[MetadataKeyRate])
}
if c.Metadata[MetadataKeyCurrencyPair] != "usdc_eur" && c.Metadata[MetadataKeyCurrencyPair] != "eur_usdc" {
t.Errorf("currency_pair metadata=%q, want eur_usdc or usdc_eur", c.Metadata[MetadataKeyCurrencyPair])
}
if src, dst := c.SourceAccountReference, c.DestinationAccountReference; src == nil || dst == nil || *src != "EUR" || *dst != "USDC" {
t.Errorf("account refs: got (%v, %v), want (EUR, USDC)", src, dst)
}
// Raw must round-trip back to a UserTransaction.
var roundTrip client.UserTransaction
if err := json.Unmarshal(c.Raw, &roundTrip); err != nil {
t.Fatalf("raw round-trip: %v", err)
}
if roundTrip.ID != 458254264 {
t.Errorf("raw id=%d, want 458254264", roundTrip.ID)
}
}
func TestUserTransactionToPSPConversionWithFee(t *testing.T) {
t.Parallel()
tx := newTx(`{
"id": 600,
"datetime": "2025-09-25 14:42:59.000000",
"type": "36",
"fee": "0.05",
"eur": "-100.00",
"usdc": "116.21",
"usdc_eur": "0.86047"
}`)
res, err := UserTransactionToPSPConversion(testCurrencies, tx)
if err != nil {
t.Fatalf("err: %v", err)
}
if res.Conversion == nil {
t.Fatal("expected conversion")
}
// pair key is "usdc_eur" → quote = EUR = source; fee 0.05 at EUR/2 = 5 minor units.
if res.Conversion.Fee == nil || res.Conversion.Fee.Cmp(big.NewInt(5)) != 0 {
t.Errorf("fee=%v, want 5 minor units at EUR/2", res.Conversion.Fee)
}
if res.Conversion.FeeAsset == nil || *res.Conversion.FeeAsset != "EUR/2" {
t.Errorf("feeAsset=%v, want EUR/2", res.Conversion.FeeAsset)
}
}
func TestUserTransactionToPSPConversionSkipsNonConversionTypes(t *testing.T) {
t.Parallel()
for _, txType := range []string{TxTypeDeposit, TxTypeWithdrawal, TxTypeStakingReward} {
tx := newTx(`{
"id": 700,
"datetime": "2025-09-25 14:42:59.000000",
"type": "` + txType + `",
"fee": "0",
"eur": "-5.00",
"usdc": "5.81"
}`)
res, err := UserTransactionToPSPConversion(testCurrencies, tx)
if err != nil {
t.Fatalf("type %s err: %v", txType, err)
}
if !res.Skip || res.Conversion != nil {
t.Errorf("type %s must be skipped, got %#v", txType, res)
}
}
}
func TestUserTransactionToPSPConversionSkipsDerivatives(t *testing.T) {
t.Parallel()
tx := newTx(`{
"id": 800,
"datetime": "2025-09-25 14:42:59.000000",
"type": "36",
"fee": "0",
"eur": "-5.00",
"usdc": "5.81",
"margin_mode": "FLEXIBLE"
}`)
res, err := UserTransactionToPSPConversion(testCurrencies, tx)
if err != nil {
t.Fatalf("err: %v", err)
}
if !res.Skip || !res.DerivativesRow {
t.Errorf("expected DerivativesRow skip, got %#v", res)
}
}
func TestUserTransactionToPSPConversionSkipsSingleAsset(t *testing.T) {
t.Parallel()
tx := newTx(`{
"id": 900,
"datetime": "2025-09-25 14:42:59.000000",
"type": "36",
"fee": "0",
"eur": "-5.00",
"usdc": "0",
"btc": "0"
}`)
res, err := UserTransactionToPSPConversion(testCurrencies, tx)
if err != nil {
t.Fatalf("err: %v", err)
}
if !res.Skip || res.Conversion != nil {
t.Errorf("single-asset type-36 row must skip, got %#v", res)
}
}
func TestUserTransactionToPSPConversionSmallBalanceTypes(t *testing.T) {
t.Parallel()
// Types 53 and 55 (small balance conversion) follow the same two-asset
// shape as type 36 and must be emitted as PSPConversions.
for _, txType := range []string{TxTypeSmallBalanceConversionSrc, TxTypeSmallBalanceConversionDst} {
txType := txType
t.Run("type_"+txType, func(t *testing.T) {
t.Parallel()
tx := newTx(`{
"id": 1000,
"datetime": "2025-09-25 14:42:59.000000",
"type": "` + txType + `",
"fee": "0",
"eur": "-100.00",
"usdc": "116.21",
"usdc_eur": "0.86047"
}`)
res, err := UserTransactionToPSPConversion(testCurrencies, tx)
if err != nil {
t.Fatalf("type %s: err: %v", txType, err)
}
if res.Skip || res.Conversion == nil {
t.Fatalf("type %s: expected conversion, got %#v", txType, res)
}
if res.Conversion.SourceAsset != "EUR/2" || res.Conversion.DestinationAsset != "USDC/6" {
t.Errorf("type %s: assets: got (%s, %s), want (EUR/2, USDC/6)",
txType, res.Conversion.SourceAsset, res.Conversion.DestinationAsset)
}
})
}
}