Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I test unpaid subscriptions in Stripe with Ruby on Rails?

I'm trying to test the scenario in my Rails app where a customer has allowed a subscription to go to 'unpaid' (usually because the card expired and was not updated during the two weeks while Stripe retried the charge) and is finally getting around to updating the card and reactivating the account. I believe I have the logic correct (update the card and pay each unpaid invoice) but I'd like to be able to test it, or better yet, write some RSpec tests (a feature test and possibly a controller test). The problem is, I can't figure out how to create a mock situation in which a subscription is 'unpaid'. (I suppose I could create a bunch of accounts with expired cards and wait two weeks to test, but that's not an acceptable solution. I can't even change the subscription retry settings for only the 'test' context to speed up the process.) I found stripe-ruby-mock but I can't manually set the status of a subscription.

Here's what I've tried:

plan = Stripe::Plan.create(id: 'test')
  customer = Stripe::Customer.create(id: 'test_customer', card: 'tk', plan: 'test')

  sub = customer.subscriptions.retrieve(customer.subscriptions.data.first.id)
  sub.status = 'unpaid'
  sub.save
  sub = customer.subscriptions.retrieve(customer.subscriptions.data.first.id)
  expect(sub.status).to eq 'unpaid'

This was the result with stripe-ruby-mock:

Failure/Error: expect(sub.status).to eq 'unpaid'

   expected: "unpaid"
        got: "active"
like image 576
Erik Sandberg Avatar asked Jul 18 '14 00:07

Erik Sandberg


2 Answers

Stripe's recommended procedure is:

  1. Setup the customer with the card 4000000000000341 ("Attaching this card to a Customer object will succeed, but attempts to charge the customer will fail.")

  2. Give the customer a subscription but with a trial date ending today/tomorrow.

  3. Wait

Step three is annoying, but it'll get the job done.

like image 90
VoteyDisciple Avatar answered Nov 09 '22 11:11

VoteyDisciple


Taking @VoteyDisciple's suggestion, I've worked through something to have this reasonably automated in RSpec ('reasonably', given the circumstances). I'm using VCR to capture the API calls (against the test Stripe environment), which is essential, as it means the sleep call only happens when recording the test the first time.

Using comments to indicate behaviour done via Stripe's API, as my implementation is rather caught up in my project, and that's not so useful.

VCR.use_cassette('retry') do |cassette|
  # Clear out existing Stripe data: customers, coupons, plans.
  # This is so the test is reliably repeatable.
  Stripe::Customer.all.each &:delete
  Stripe::Coupon.all.each &:delete
  Stripe::Plan.all.each &:delete

  # Create a plan, in my case it has the id 'test'.
  Stripe::Plan.create(
    id:             'test',
    amount:         100_00,
    currency:       'AUD',
    interval:       'month',
    interval_count: 1,
    name:           'RSpec Test'
  )

  # Create a customer
  customer = Stripe::Customer.create email: '[email protected]'
  token    = card_token cassette, '4000000000000341'
  # Add the card 4000000000000341 to the customer
  customer.sources.create token: 'TOKEN for 0341'
  # Create a subscription with a trial ending in two seconds.
  subscription = customer.subscriptions.create(
    plan:      'test',
    trial_end: 2.seconds.from_now.to_i
  )

  # Wait for Stripe to create a proper invoice. I wish this
  # was unavoidable, but I don't think it is.
  sleep 180 if cassette.recording?

  # Grab the invoice that actually has a dollar value.
  # There's an invoice for the trial, and we don't care about that.
  invoice = customer.invoices.detect { |invoice| invoice.total > 0 }
  # Force Stripe to attempt payment for the first time (instead
  # of waiting for hours).
  begin
    invoice.pay
  rescue Stripe::CardError
    # Expecting this to fail.
  end

  invoice.refresh
  expect(invoice.paid).to eq(false)
  expect(invoice.attempted).to eq(true)

  # Add a new (valid) card to the customer.
  token = card_token cassette, '4242424242424242'
  card  = customer.sources.create token: token
  # and set it as the default
  customer.default_source = card.id
  customer.save

  # Run the code in your app that retries the payment, which
  # essentially invokes invoice.pay again.
  # THIS IS FOR YOU TO IMPLEMENT

  # And now we can check that the invoice wass successfully paid
  invoice.refresh
  expect(invoice.paid).to eq(true)
end

Getting card tokens in an automated fashion is a whole new area of complexity. What I've got may not work for others, but here's the ruby method, calling out to phantomjs (you'll need to send the VCR cassette object through):

def card_token(cassette, card = '4242424242424242')
  return 'tok_my_default_test_token' unless cassette.recording?

  token = `phantomjs --ignore-ssl-errors=true --ssl-protocol=any ./spec/fixtures/stripe_tokens.js #{ENV['STRIPE_PUBLISH_KEY']} #{card}`.strip
  raise "Unexpected token: #{token}" unless token[/^tok_/]

  token
end

And the javascript file that phantomjs is running (stripe_tokens.js) contains:

var page   = require('webpage').create(),
    system = require('system');

var key  = system.args[1],
    card = system.args[2];

page.onCallback = function(data) {
  console.log(data);
  phantom.exit();
};

page.open('spec/fixtures/stripe_tokens.html', function(status) {
  if (status == 'success') {
    page.evaluate(function(key, card) {
      Stripe.setPublishableKey(key);
      Stripe.card.createToken(
        {number: card, cvc: "123", exp_month: "12", exp_year: "2019"},
        function(status, response) { window.callPhantom(response.id) }
      );
    }, key, card);
  }
});

Finally, the HTML file involved (stripe_tokens.html) is pretty simple:

<html>
  <head>
    <script type="text/javascript" src="https://js.stripe.com/v2/"></script>
  </head>
  <body></body>
</html>

Put all of that together, and, well, it might work! It does for our app :)

like image 36
pat Avatar answered Nov 09 '22 11:11

pat