mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-08-13 22:37:28 +00:00
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>
This commit is contained in:
parent
641fa01d9a
commit
d5135cd1fe
2 changed files with 48 additions and 8 deletions
|
|
@ -92,15 +92,39 @@ func Reference(plan, pubkeyFP string) string {
|
||||||
return "abbs-" + plan + "-" + hex.EncodeToString(mac.Sum(nil))[:12]
|
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
|
// coinpayPayment is the (subset of the) CoinPay payment object, returned
|
||||||
// wrapped as {"payment": {…}}.
|
// wrapped as {"payment": {…}}.
|
||||||
type coinpayPayment struct {
|
type coinpayPayment struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Address string `json:"payment_address"`
|
Address string `json:"payment_address"`
|
||||||
CryptoAmount string `json:"crypto_amount"`
|
CryptoAmount flexStr `json:"crypto_amount"`
|
||||||
CryptoCurr string `json:"crypto_currency"`
|
CryptoCurr string `json:"crypto_currency"`
|
||||||
QR string `json:"qr_code"`
|
QR string `json:"qr_code"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type paymentEnvelope struct {
|
type paymentEnvelope struct {
|
||||||
|
|
@ -174,7 +198,7 @@ func CreatePremiumCharge(ref string) (Charge, bool, error) {
|
||||||
}
|
}
|
||||||
return Charge{
|
return Charge{
|
||||||
Address: p.Address,
|
Address: p.Address,
|
||||||
CryptoAmount: p.CryptoAmount,
|
CryptoAmount: string(p.CryptoAmount),
|
||||||
Currency: p.CryptoCurr,
|
Currency: p.CryptoCurr,
|
||||||
FiatAmount: PremiumAmount(),
|
FiatAmount: PremiumAmount(),
|
||||||
FiatCurrency: PremiumCurrency(),
|
FiatCurrency: PremiumCurrency(),
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,8 @@ func TestCreateAndVerifyPremium(t *testing.T) {
|
||||||
_ = json.Unmarshal(b, &body)
|
_ = json.Unmarshal(b, &body)
|
||||||
gotBlockchain, _ = body["blockchain"].(string)
|
gotBlockchain, _ = body["blockchain"].(string)
|
||||||
gotBusiness, _ = body["business_id"].(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":
|
case r.Method == http.MethodGet && r.URL.Path == "/payments/pay_1":
|
||||||
_, _ = w.Write([]byte(`{"payment":{"id":"pay_1","status":"confirmed"}}`))
|
_, _ = w.Write([]byte(`{"payment":{"id":"pay_1","status":"confirmed"}}`))
|
||||||
default:
|
default:
|
||||||
|
|
@ -76,3 +77,18 @@ func TestNotConfigured(t *testing.T) {
|
||||||
t.Fatal("unconfigured verify must not be checked")
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue