dmjio / stripe

:moneybag: Stripe API
http://hackage.haskell.org/package/stripe-haskell
177 stars 74 forks source link

Customer incorrectly parses as deleted. #81

Closed jkarni closed 5 years ago

jkarni commented 6 years ago

With "Stripe-Version: 2014-10-07", a GET to v1/customers/cus_BoNe5SkscQSGWA with some test data gives me:

{
  "id": "cus_BoNe5SkscQSGWA",
  "object": "customer",
  "account_balance": 5000000,
  "created": 1511305860,
  "currency": "usd",
  "default_source": "card_1BSt2UH9igYuDCMjq4JMoIkY",
  "delinquent": false,
  "description": "Julian Test",
  "discount": null,
  "email": "julian.arni@tweag.io",
  "livemode": false,
  "metadata": {},
  "shipping": null,
  "sources": {
    "object": "list",
    "data": [
      {
        "id": "card_1BSt2UH9igYuDCMjq4JMoIkY",
        "object": "card",
        "address_city": null,
        "address_country": null,
        "address_line1": null,
        "address_line1_check": null,
        "address_line2": null,
        "address_state": null,
        "address_zip": null,
        "address_zip_check": null,
        "brand": "Visa",
        "country": "US",
        "customer": "cus_BoNe5SkscQSGWA",
        "cvc_check": "pass",
        "dynamic_last4": null,
        "exp_month": 12,
        "exp_year": 2020,
        "funding": "credit",
        "last4": "4242",
        "metadata": {},
        "name": null,
        "tokenization_method": null
      }
    ],
    "has_more": false,
    "total_count": 1,
    "url": "/v1/customers/cus_BoNe5SkscQSGWA/sources"
  },
  "subscriptions": {
    "object": "list",
    "data": [
      {
        "id": "sub_BqaD7jXxACOYYV",
        "object": "subscription",
        "application_fee_percent": null,
        "billing": "charge_automatically",
        "cancel_at_period_end": false,
        "canceled_at": null,
        "created": 1511813853,
        "current_period_end": 1514405853,
        "current_period_start": 1511813853,
        "customer": "cus_BoNe5SkscQSGWA",
        "discount": null,
        "ended_at": null,
        "items": {
          "object": "list",
          "data": [
            {
              "id": "si_BqaD35Qgz9A3S8",
              "object": "subscription_item",
              "created": 1511813854,
              "metadata": {},
              "plan": {
                "id": "1",
                "object": "plan",
                "amount": 999,
                "created": 1511813715,
                "currency": "usd",
                "interval": "month",
                "interval_count": 1,
                "livemode": false,
                "metadata": {},
                "name": "Test Plan",
                "statement_descriptor": null,
                "trial_period_days": null,
                "statement_description": null
              },
              "quantity": 1
            }
          ],
          "has_more": false,
          "total_count": 1,
          "url": "/v1/subscription_items?subscription=sub_BqaD7jXxACOYYV"
        },
        "livemode": false,
        "metadata": {},
        "plan": {
          "id": "1",
          "object": "plan",
          "amount": 999,
          "created": 1511813715,
          "currency": "usd",
          "interval": "month",
          "interval_count": 1,
          "livemode": false,
          "metadata": {},
          "name": "Test Plan",
          "statement_descriptor": null,
          "trial_period_days": null,
          "statement_description": null
        },
        "quantity": 1,
        "start": 1511813853,
        "status": "active",
        "tax_percent": null,
        "trial_end": null,
        "trial_start": null
      }
    ],
    "has_more": false,
    "total_count": 1,
    "url": "/v1/customers/cus_BoNe5SkscQSGWA/subscriptions"
  },
  "cards": {
    "object": "list",
    "data": [
      {
        "id": "card_1BSt2UH9igYuDCMjq4JMoIkY",
        "object": "card",
        "address_city": null,
        "address_country": null,
        "address_line1": null,
        "address_line1_check": null,
        "address_line2": null,
        "address_state": null,
        "address_zip": null,
        "address_zip_check": null,
        "brand": "Visa",
        "country": "US",
        "customer": "cus_BoNe5SkscQSGWA",
        "cvc_check": "pass",
        "dynamic_last4": null,
        "exp_month": 12,
        "exp_year": 2020,
        "funding": "credit",
        "last4": "4242",
        "metadata": {},
        "name": null,
        "tokenization_method": null
      }
    ],
    "has_more": false,
    "total_count": 1,
    "url": "/v1/customers/cus_BoNe5SkscQSGWA/cards"
  },
  "default_card": "card_1BSt2UH9igYuDCMjq4JMoIkY"
}

Parsing this, however, gives me a DeletedCustomer. Prior to adding a card and subscription, the customer parsed correctly, so I imagine this is an issue parsing one of those nested types (or Plan).

More generally, the FromJSON instance for Customer seems dangerous - failures will silently be treated as DeletedCustomers.

(@dmjio hi! Thanks so much for this library! It's been really useful)

dmjio commented 6 years ago

@jkarni hey! πŸ‘‹

Ah, yea, that definitely seems like a bug. The DeletedCustomer default parsing rules might need some changing, its something I can look into when I get free (weekends are best). I'm glad the library is useful, and good to hear from you :)

dmjio commented 6 years ago

@jkarni Also, if you get a chance to fix this on your own, a PR is appreciated!

jkarni commented 6 years ago

On it!

dmjio commented 6 years ago

@jkarni any updates on this per chance?

danstn commented 5 years ago

Any movement on this? Kind of a big deal this one :)

dmjio commented 5 years ago

@danstn https://github.com/dmjio/stripe/pull/118 ... some movement :)

Right (Customer {customerObject = "customer", customerCreated = 2017-11-21 23:11:00 UTC, customerId = CustomerId "cus_BoNe5SkscQSGWA", customerLiveMode = False, customerDescription = Just (D\
escription "Julian Test"), customerEmail = Just (Email "julian.arni@tweag.io"), customerDelinquent = False, customerSubscriptions = StripeList {list = [Subscription {subscriptionId = Subscri\
ptionId {getSubscriptionId = "sub_BqaD7jXxACOYYV"}, subscriptionPlan = Plan {planInterval = month, planName = "Test Plan", planCreated = 2017-11-27 20:15:15 UTC, planAmount = 999, planCurren\
cy = USD, planId = PlanId "1", planObject = "plan", planLiveMode = False, planIntervalCount = Just 1, planTrialPeriodDays = Nothing, planMetaData = MetaData [], planDescription = Nothing}, s\
ubscriptionObject = "subscription", subscriptionStart = 2017-11-27 20:17:33 UTC, subscriptionStatus = Active, subscriptionCustomerId = Id (CustomerId "cus_BoNe5SkscQSGWA"), subscriptionCance\
lAtPeriodEnd = False, subscriptionCurrentPeriodStart = 2017-11-27 20:17:33 UTC, subscriptionCurrentPeriodEnd = 2017-12-27 20:17:33 UTC, subscriptionEndedAt = Nothing, subscriptionTrialStart \
= Nothing, subscriptionTrialEnd = Nothing, subscriptionCanceledAt = Nothing, subscriptionQuantity = Quantity 1, subscriptionApplicationFeePercent = Nothing, subscriptionDiscount = Nothing, s\
ubscriptionMetaData = MetaData [], subscriptionTaxPercent = Nothing}], stripeUrl = "/v1/customers/cus_BoNe5SkscQSGWA/subscriptions", object = "list", totalCount = Just 1, hasMore = False}, c\
ustomerDiscount = Nothing, customerAccountBalance = 5000000, customerCards = StripeList {list = [Card {cardId = CardId "card_1BSt2UH9igYuDCMjq4JMoIkY", cardObject = "card", cardLastFour = "4\
242", cardBrand = Visa, cardFunding = "credit", cardExpMonth = ExpMonth 12, cardExpYear = ExpYear 2020, cardFingerprint = Nothing, cardCountry = Just "US", cardName = Nothing, cardAddressLin\
e1 = Nothing, cardAddressLine2 = Nothing, cardAddressCity = Nothing, cardAddressState = Nothing, cardAddressZip = Nothing, cardAddressCountry = Nothing, cardCVCCheck = Just "pass", cardAddre\
ssLine1Check = Nothing, cardAddressZipCheck = Nothing, cardCustomerId = Just (Id (CustomerId "cus_BoNe5SkscQSGWA")), cardMetaData = MetaData []}], stripeUrl = "/v1/customers/cus_BoNe5SkscQSG\
WA/cards", object = "list", totalCount = Just 1, hasMore = False}, customerCurrency = Just USD, customerDefaultCard = Just (Id (CardId "card_1BSt2UH9igYuDCMjq4JMoIkY")), customerMetaData = M\
etaData []}) 
danstn commented 5 years ago

@dmjio EPIC! So so so need this :)

Btw, noticed that it happens for updateCustomer as well:

updateCustomer customerId -&- MetaData [("stuff", T.pack stuff)]

Also, works just fine when using a Standard API Key. Seeing the bug when using Restricted API Key only... Not sure how this is related.

dmjio commented 5 years ago

@danstn can you paste your Customer JSON here?

dmjio commented 5 years ago

Also, does the restricted API key return the same data as the standard API key ?

danstn commented 5 years ago

Sure @dmjio !

Restricted API:

<= Request:

  let config                = StripeConfig (StripeKey "rk_test_<key_here>")
      customerId            = CustomerId "cus_FZ**********D0"
      updateCustomerRequest = updateCustomer customerId -&- MetaData [("stuff", T.pack stuff)]
  res <- stripe config updateCustomerRequest
  print res

=> Response (restricted):

Right (DeletedCustomer {deletedCustomer = Nothing, deletedCustomerId = CustomerId "cus_FZ**********D0"}) 

=> Response (standard):

Right (Customer {customerObject = "customer", customerCreated = 2019-08-07 00:31:02 UTC, customerId = CustomerId "cus_FZ**********D0", customerLiveMode = False, customerDescription = Just (Description "<email>"), customerEmail = Just (Email "<email>"), customerDelinquent = False, customerSubscriptions = StripeList {list = [], stripeUrl = "/v1/customers/cus_FZ**********D0/subscriptions", object = "list", totalCount = Just 0, hasMore = False}, customerDiscount = Nothing, customerAccountBalance = 0, customerCards = StripeList {list = [Card {cardId = CardId "<cardid>", cardObject = "card", cardLastFour = "xxxx", cardBrand = Visa, cardFunding = "credit", cardExpMonth = ExpMonth 12, cardExpYear = ExpYear 2021, cardFingerprint = "<fignerprint>", cardCountry = Just "CA", cardName = Nothing, cardAddressLine1 = Nothing, cardAddressLine2 = Nothing, cardAddressCity = Nothing, cardAddressState = Nothing, cardAddressZip = Nothing, cardAddressCountry = Nothing, cardCVCCheck = Just "pass", cardAddressLine1Check = Nothing, cardAddressZipCheck = Nothing, cardCustomerId = Just (Id (CustomerId "cus_FZ**********D0")), cardMetaData = MetaData []}], stripeUrl = "/v1/customers/cus_FZ**********D0/cards", object = "list", totalCount = Just 1, hasMore = False}, customerCurrency = Nothing, customerDefaultCard = Just (Id (CardId "<cardid>")), customerMetaData = MetaData [("stuff","stuff")]})
danstn commented 5 years ago

The data is updated correctly in Stripe though, so it's just the decoder it seems.

dmjio commented 5 years ago

@danstn can you try again, but use the branch from PR #118 ?

danstn commented 5 years ago

Hmmm...I think I'm doing something wrong.

StripeConfig is now expecting an endpoint for some reason. So, I had to StripeConfig <key> Nothing to make it happy (assuming it'll use the default).

Anyway - here's a response when getting a customer:

StripeError {errorType = ParseFailure, errorMsg = "key \"subscriptions\" not present", errorCode = Nothing, errorParam = Nothing, errorHTTP = Nothing}

Not sure if I'm doing git shenanigans right (see stack.yaml snippet below) ?

extra-deps:
  #- stripe-haskell-2.4.1
  #- stripe-core-2.4.1
  #- stripe-http-client-2.4.1
  - git: https://github.com/dmjio/stripe.git
    commit: 7410fba5e6483a09bc64280681091e547ba0f8be
    subdirs:
      - stripe-haskell
      - stripe-core
      - stripe-http-client

@dmjio thanks for your help, by the way :) Really appreciate it.

dmjio commented 5 years ago

Thanks for helping me debug tips hat. Making the endpoint configurable was a new feature added so we can take advantage of stripe-mock. Can you try using the stripeRaw function ? This will allow us to dump the raw JSON response. Can you print that bytestring and paste it here? Then I can test the parser against it. subscriptions might be an optional field.

danstn commented 5 years ago

Making the endpoint configurable was a new feature added so we can take advantage of stripe-mock

Makes sense.

Can you try using the stripeRaw function ?

Doesn't seem like stripeRaw is exported by anything. What am I missing? :)

dmjio commented 5 years ago

Must have gotten hidden in a refactor, will expose it when I get home

Sent from my iPhone

On Aug 8, 2019, at 9:26 PM, Daniel Stankevich notifications@github.com wrote:

Making the endpoint configurable was a new feature added so we can take advantage of stripe-mock

Makes sense.

Can you try using the stripeRaw function ?

Doesn't seem like stripeRaw is exported by anything.

β€” You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

dmjio commented 5 years ago

@danstn It seems stripeRaw died in the refactor to stripe-http-client, I've made subscriptions optional on Card, care to try again with 753967b ?

danstn commented 5 years ago

Just did! All working as expected now :)

You're a LEGEND, thanks so much for such a quick turnaround.

Any ETA on when it can be merged?

Also, on an unrelated note - any tools you're aware of for Stripe webhook signing?

dmjio commented 5 years ago
dmjio commented 5 years ago

@danstn I'm unaware of any tools for Stripe webhook signing, am unsure what this tbh. I assume the webhooks sent are over https and you can add headers via the stripe UI.

danstn commented 5 years ago

Yep, it's header-based (just need to HMAC it and sha it) https://stripe.com/docs/webhooks/signatures

Thanks for the release!!! 2.5.0 tested E2E.

I owe you a beer next time you're in Melbourne...or a miso ;)

dmjio commented 5 years ago

@danstn never been to Australia, would love to visit. If I do, I'll take you up on the offer for sure. On both a 🍺 and a 🍜 that is ;)