Skip to content

Commit d5135cd

Browse files
ralyodioclaude
andcommitted
payments: accept CoinPay crypto_amount as number or string
The live CoinPay API returns crypto_amount as a bare JSON number (e.g. 0.0031), but the struct decoded it as a string, so /payments/create failed with "cannot unmarshal number into ... crypto_amount of type string" and join@ showed "Payment is temporarily unavailable". The unit test had hidden the bug by sending the value quoted. Add a flexStr type that unmarshals from either a JSON number or string and use it for crypto_amount; update the test to send a number and add a direct flexStr decode test for both forms. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 641fa01 commit d5135cd

2 files changed

Lines changed: 48 additions & 8 deletions

File tree

internal/payments/payments.go

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -92,15 +92,39 @@ func Reference(plan, pubkeyFP string) string {
9292
return "abbs-" + plan + "-" + hex.EncodeToString(mac.Sum(nil))[:12]
9393
}
9494

95+
// flexStr decodes a JSON value that may arrive as either a string or a number
96+
// into a string. CoinPay returns crypto_amount as a bare JSON number (e.g.
97+
// 0.0031), but has sent it quoted in the past — accept both so a representation
98+
// change on their side can't break the charge again.
99+
type flexStr string
100+
101+
func (f *flexStr) UnmarshalJSON(b []byte) error {
102+
b = bytes.TrimSpace(b)
103+
if len(b) == 0 || string(b) == "null" {
104+
*f = ""
105+
return nil
106+
}
107+
if b[0] == '"' {
108+
var s string
109+
if err := json.Unmarshal(b, &s); err != nil {
110+
return err
111+
}
112+
*f = flexStr(s)
113+
return nil
114+
}
115+
*f = flexStr(b) // number (or other scalar) — keep its literal text
116+
return nil
117+
}
118+
95119
// coinpayPayment is the (subset of the) CoinPay payment object, returned
96120
// wrapped as {"payment": {…}}.
97121
type coinpayPayment struct {
98-
ID string `json:"id"`
99-
Status string `json:"status"`
100-
Address string `json:"payment_address"`
101-
CryptoAmount string `json:"crypto_amount"`
102-
CryptoCurr string `json:"crypto_currency"`
103-
QR string `json:"qr_code"`
122+
ID string `json:"id"`
123+
Status string `json:"status"`
124+
Address string `json:"payment_address"`
125+
CryptoAmount flexStr `json:"crypto_amount"`
126+
CryptoCurr string `json:"crypto_currency"`
127+
QR string `json:"qr_code"`
104128
}
105129

106130
type paymentEnvelope struct {
@@ -174,7 +198,7 @@ func CreatePremiumCharge(ref string) (Charge, bool, error) {
174198
}
175199
return Charge{
176200
Address: p.Address,
177-
CryptoAmount: p.CryptoAmount,
201+
CryptoAmount: string(p.CryptoAmount),
178202
Currency: p.CryptoCurr,
179203
FiatAmount: PremiumAmount(),
180204
FiatCurrency: PremiumCurrency(),

internal/payments/payments_test.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ func TestCreateAndVerifyPremium(t *testing.T) {
2020
_ = json.Unmarshal(b, &body)
2121
gotBlockchain, _ = body["blockchain"].(string)
2222
gotBusiness, _ = body["business_id"].(string)
23-
_, _ = w.Write([]byte(`{"payment":{"id":"pay_1","payment_address":"0xABC","crypto_amount":"0.0031","crypto_currency":"ETH","status":"pending"}}`))
23+
// crypto_amount comes back as a bare JSON number from the live API.
24+
_, _ = w.Write([]byte(`{"payment":{"id":"pay_1","payment_address":"0xABC","crypto_amount":0.0031,"crypto_currency":"ETH","status":"pending"}}`))
2425
case r.Method == http.MethodGet && r.URL.Path == "/payments/pay_1":
2526
_, _ = w.Write([]byte(`{"payment":{"id":"pay_1","status":"confirmed"}}`))
2627
default:
@@ -76,3 +77,18 @@ func TestNotConfigured(t *testing.T) {
7677
t.Fatal("unconfigured verify must not be checked")
7778
}
7879
}
80+
81+
func TestFlexStrDecodesNumberOrString(t *testing.T) {
82+
for _, raw := range []string{
83+
`{"payment":{"crypto_amount":0.0031}}`, // live API: bare number
84+
`{"payment":{"crypto_amount":"0.0031"}}`, // legacy: quoted string
85+
} {
86+
var env paymentEnvelope
87+
if err := json.Unmarshal([]byte(raw), &env); err != nil {
88+
t.Fatalf("unmarshal %s: %v", raw, err)
89+
}
90+
if got := string(env.Payment.CryptoAmount); got != "0.0031" {
91+
t.Fatalf("crypto_amount from %s = %q, want 0.0031", raw, got)
92+
}
93+
}
94+
}

0 commit comments

Comments
 (0)