diff --git a/internal/payments/payments.go b/internal/payments/payments.go index 7003d7d..f05bae5 100644 --- a/internal/payments/payments.go +++ b/internal/payments/payments.go @@ -92,15 +92,39 @@ func Reference(plan, pubkeyFP string) string { return "abbs-" + plan + "-" + hex.EncodeToString(mac.Sum(nil))[:12] } +// flexStr decodes a JSON value that may arrive as either a string or a number +// into a string. CoinPay returns crypto_amount as a bare JSON number (e.g. +// 0.0031), but has sent it quoted in the past — accept both so a representation +// change on their side can't break the charge again. +type flexStr string + +func (f *flexStr) UnmarshalJSON(b []byte) error { + b = bytes.TrimSpace(b) + if len(b) == 0 || string(b) == "null" { + *f = "" + return nil + } + if b[0] == '"' { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + *f = flexStr(s) + return nil + } + *f = flexStr(b) // number (or other scalar) — keep its literal text + return nil +} + // coinpayPayment is the (subset of the) CoinPay payment object, returned // wrapped as {"payment": {…}}. type coinpayPayment struct { - ID string `json:"id"` - Status string `json:"status"` - Address string `json:"payment_address"` - CryptoAmount string `json:"crypto_amount"` - CryptoCurr string `json:"crypto_currency"` - QR string `json:"qr_code"` + ID string `json:"id"` + Status string `json:"status"` + Address string `json:"payment_address"` + CryptoAmount flexStr `json:"crypto_amount"` + CryptoCurr string `json:"crypto_currency"` + QR string `json:"qr_code"` } type paymentEnvelope struct { @@ -174,7 +198,7 @@ func CreatePremiumCharge(ref string) (Charge, bool, error) { } return Charge{ Address: p.Address, - CryptoAmount: p.CryptoAmount, + CryptoAmount: string(p.CryptoAmount), Currency: p.CryptoCurr, FiatAmount: PremiumAmount(), FiatCurrency: PremiumCurrency(), diff --git a/internal/payments/payments_test.go b/internal/payments/payments_test.go index e2dd052..d377d61 100644 --- a/internal/payments/payments_test.go +++ b/internal/payments/payments_test.go @@ -20,7 +20,8 @@ func TestCreateAndVerifyPremium(t *testing.T) { _ = json.Unmarshal(b, &body) gotBlockchain, _ = body["blockchain"].(string) gotBusiness, _ = body["business_id"].(string) - _, _ = w.Write([]byte(`{"payment":{"id":"pay_1","payment_address":"0xABC","crypto_amount":"0.0031","crypto_currency":"ETH","status":"pending"}}`)) + // crypto_amount comes back as a bare JSON number from the live API. + _, _ = w.Write([]byte(`{"payment":{"id":"pay_1","payment_address":"0xABC","crypto_amount":0.0031,"crypto_currency":"ETH","status":"pending"}}`)) case r.Method == http.MethodGet && r.URL.Path == "/payments/pay_1": _, _ = w.Write([]byte(`{"payment":{"id":"pay_1","status":"confirmed"}}`)) default: @@ -76,3 +77,18 @@ func TestNotConfigured(t *testing.T) { t.Fatal("unconfigured verify must not be checked") } } + +func TestFlexStrDecodesNumberOrString(t *testing.T) { + for _, raw := range []string{ + `{"payment":{"crypto_amount":0.0031}}`, // live API: bare number + `{"payment":{"crypto_amount":"0.0031"}}`, // legacy: quoted string + } { + var env paymentEnvelope + if err := json.Unmarshal([]byte(raw), &env); err != nil { + t.Fatalf("unmarshal %s: %v", raw, err) + } + if got := string(env.Payment.CryptoAmount); got != "0.0031" { + t.Fatalf("crypto_amount from %s = %q, want 0.0031", raw, got) + } + } +}