package sendico import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "git.sendico.io/tech/sxchnge/product/payments" "github.com/stretchr/testify/require" ) func TestPaymentPublicWireAndExactMoney(t *testing.T) { ctx := context.Background() ledger := "507f1f77bcf86cd799439015" method := "507f1f77bcf86cd799439016" calls := 0 acknowledge := true server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.NotEmpty(t, r.Header.Get("Authorization")) var result any switch r.URL.Path { case "/api/v1/payments/fx/currencies/" + org: result = map[string]any{"currencies": []any{map[string]any{"code": "EUR", "accountingScale": 2}}} case "/api/v1/ledger_accounts/" + org: result = map[string]any{"accounts": []any{ map[string]any{"ledgerAccountRef": source, "organizationRef": org, "ownerRef": source, "currency": "EUR", "accountType": "ACCOUNT_TYPE_ASSET", "role": "ACCOUNT_ROLE_OPERATING", "status": "ACCOUNT_STATUS_ACTIVE"}, map[string]any{"ledgerAccountRef": ledger, "organizationRef": org, "currency": "EUR", "accountType": "ACCOUNT_TYPE_ASSET", "role": "ACCOUNT_ROLE_OPERATING", "status": "ACCOUNT_STATUS_ACTIVE", "metadata": map[string]string{"sxchnge_subject_ref": subject, "sxchnge_payout_method_ref": method, "sxchnge_payout_label": "Saved bank ending 1234"}}, }} case "/api/v1/ledger_accounts/" + org + "/" + ledger + "/balance": result = map[string]any{"balance": map[string]any{"ledgerAccountRef": ledger, "balance": moneyDTO{"75.00", "EUR"}}} case "/api/v1/payments/quote/" + org: var request quoteRequestDTO require.NoError(t, json.NewDecoder(r.Body).Decode(&request)) require.Equal(t, subject, request.Intent.Subject) require.Equal(t, "20.01", request.Intent.Amount.Amount) require.Equal(t, ledger, request.Intent.Source.Data["account_ref"]) require.Equal(t, method, request.Intent.Destination.Data["ref"]) confirmedSubject := subject if !acknowledge { confirmedSubject = "" } result = map[string]any{"externalSubjectRef": confirmedSubject, "quote": paymentQuoteDTO{ID: "quote:1", State: "executable", Executable: true, Expires: time.Now().Add(time.Minute), Amounts: quoteAmountsDTO{Principal: moneyDTO{"20.01", "EUR"}, Debit: moneyDTO{"21.01", "EUR"}, Credit: moneyDTO{"20.01", "EUR"}}}} case "/api/v1/payments/by-quote/" + org: calls++ var payload map[string]string require.NoError(t, json.NewDecoder(r.Body).Decode(&payload)) require.Equal(t, "stable-execution", payload["idempotencyKey"]) require.Equal(t, "quote:1", payload["quoteRef"]) result = map[string]any{"payment": paymentDTO{ID: "payment:1", State: "orchestration_state_needs_attention", Quote: paymentQuoteDTO{ID: "quote:1"}, Created: time.Now()}} case "/api/v1/payments/by-ref/payment:1": result = map[string]any{"payment": paymentDTO{ID: "payment:1", State: "orchestration_state_settled", Quote: paymentQuoteDTO{ID: "quote:1"}, Created: time.Now()}} default: t.Errorf("unexpected path: %s", r.URL.Path) w.WriteHeader(404) return } require.NoError(t, json.NewEncoder(w).Encode(map[string]any{"status": "success", "data": result})) })) defer server.Close() client, err := Open(config(t, server.URL)) require.NoError(t, err) accounts, err := client.Accounts(ctx, subject) require.NoError(t, err) require.Len(t, accounts, 1) require.EqualValues(t, 7500, accounts[0].Available) q, err := client.Quote(ctx, subject, payments.Request{Kind: "payout", Source: ledger, Amount: 2001, Recipient: method}, accounts) require.NoError(t, err) require.EqualValues(t, 100, q.Fee) require.EqualValues(t, 2101, q.Debit) require.Equal(t, "Saved bank ending 1234", q.Request.Recipient) _, err = client.Quote(ctx, subject, payments.Request{Kind: "payout", Source: source, Amount: 2001, Recipient: method}, accounts) require.ErrorIs(t, err, payments.NotFound) _, err = client.Quote(ctx, subject, payments.Request{Kind: "payout", Source: ledger, Amount: 2001, Recipient: "foreign-method"}, accounts) require.ErrorIs(t, err, payments.InvalidRecipient) acknowledge = false _, err = client.Quote(ctx, subject, payments.Request{Kind: "payout", Source: ledger, Amount: 2001, Recipient: method}, accounts) require.ErrorIs(t, err, payments.Unavailable) acknowledge = true op, err := client.Execute(ctx, q.ID, "stable-execution") require.NoError(t, err) require.Equal(t, "pending", op.Status) require.Equal(t, 1, calls) op, err = client.Payment(ctx, op.ID) require.NoError(t, err) require.Equal(t, "completed", op.Status) } func TestPaymentMappingRejectsUnsafeOrUnexecutableValues(t *testing.T) { for _, amount := range []string{"1e3", "0.001", "-2", "NaN", "90071992547409.92", ""} { _, err := minorMoney(moneyDTO{amount, "EUR"}, "EUR", 2) require.Error(t, err, amount) } _, err := minorMoney(moneyDTO{"10.00", "USD"}, "EUR", 2) require.Error(t, err) _, err = mapPayment(paymentDTO{ID: "p", State: "new_unknown_state", Quote: paymentQuoteDTO{ID: "q"}, Created: time.Now()}) require.Error(t, err) _, err = mapPaymentQuote(paymentQuoteDTO{State: "blocked", BlockReason: "limit_blocked"}, payments.Request{}, payments.Account{}, payments.Account{}) require.ErrorIs(t, err, payments.LimitExceeded) _, err = mapPaymentQuote(paymentQuoteDTO{State: "executable", Executable: false}, payments.Request{}, payments.Account{}, payments.Account{}) require.Error(t, err) }