killbill-stripe 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,44 @@
1
+ module Killbill::Stripe
2
+ class StripeTransaction < ActiveRecord::Base
3
+ belongs_to :stripe_response
4
+ attr_accessible :amount_in_cents, :currency, :api_call, :kb_payment_id, :stripe_txn_id
5
+
6
+ def self.from_kb_payment_id(kb_payment_id)
7
+ transaction_from_kb_payment_id :charge, kb_payment_id, :single
8
+ end
9
+
10
+ def self.refunds_from_kb_payment_id(kb_payment_id)
11
+ transaction_from_kb_payment_id :refund, kb_payment_id, :multiple
12
+ end
13
+
14
+ def self.find_candidate_transaction_for_refund(kb_payment_id, amount_in_cents)
15
+ # Find one successful charge which amount is at least the amount we are trying to refund
16
+ stripe_transactions = StripeTransaction.where("stripe_transactions.amount_in_cents >= ?", amount_in_cents)
17
+ .find_all_by_api_call_and_kb_payment_id(:charge, kb_payment_id)
18
+ raise "Unable to find Stripe transaction id for payment #{kb_payment_id}" if stripe_transactions.size == 0
19
+
20
+ # We have candidates, but we now need to make sure we didn't refund more than for the specified amount
21
+ amount_refunded_in_cents = Killbill::Stripe::StripeTransaction.where("api_call = ? and kb_payment_id = ?", :refund, kb_payment_id)
22
+ .sum("amount_in_cents")
23
+
24
+ amount_left_to_refund_in_cents = -amount_refunded_in_cents
25
+ stripe_transactions.map { |transaction| amount_left_to_refund_in_cents += transaction.amount_in_cents }
26
+ raise "Amount #{amount_in_cents} too large to refund for payment #{kb_payment_id}" if amount_left_to_refund_in_cents < amount_in_cents
27
+
28
+ stripe_transactions.first
29
+ end
30
+
31
+ private
32
+
33
+ def self.transaction_from_kb_payment_id(api_call, kb_payment_id, how_many)
34
+ stripe_transactions = find_all_by_api_call_and_kb_payment_id(api_call, kb_payment_id)
35
+ raise "Unable to find Stripe transaction id for payment #{kb_payment_id}" if stripe_transactions.empty?
36
+ if how_many == :single
37
+ raise "Killbill payment mapping to multiple Stripe transactions for payment #{kb_payment_id}" if stripe_transactions.size > 1
38
+ stripe_transactions[0]
39
+ else
40
+ stripe_transactions
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,52 @@
1
+ module Killbill::Stripe
2
+ class PrivatePaymentPlugin
3
+ include Singleton
4
+
5
+ def add_payment_method(params)
6
+ stripe_customer_id = StripePaymentMethod.stripe_customer_id_from_kb_account_id(params[:kbAccountId])
7
+
8
+ # This will either update the current customer if present, or create a new one
9
+ stripe_response = gateway.store params[:stripeToken], { :description => params[:kbAccountId], :customer => stripe_customer_id }
10
+ response = save_response stripe_response, :add_payment_method
11
+ raise response.message unless response.success
12
+
13
+ # Create the payment method (not associated to a Kill Bill payment method yet)
14
+ pm = Killbill::Stripe::StripePaymentMethod.create! :kb_account_id => params[:kbAccountId],
15
+ :kb_payment_method_id => nil,
16
+ :stripe_customer_id => stripe_customer_id,
17
+ :stripe_token => params[:stripeToken],
18
+ :cc_first_name => params[:stripeCardName],
19
+ :cc_last_name => nil,
20
+ :cc_type => params[:stripeCardType],
21
+ :cc_exp_month => params[:stripeCardExpMonth],
22
+ :cc_exp_year => params[:stripeCardExpYear],
23
+ :cc_last_4 => params[:stripeCardLast4],
24
+ :address1 => params[:stripeCardAddressLine1],
25
+ :address2 => params[:stripeCardAddressLine2],
26
+ :city => params[:stripeCardAddressCity],
27
+ :state => params[:stripeCardAddressState],
28
+ :zip => params[:stripeCardAddressZip],
29
+ :country => params[:stripeCardAddressCountry] || params[:stripeCardCountry]
30
+ pm
31
+ end
32
+
33
+ def save_response(stripe_response, api_call)
34
+ logger.warn "Unsuccessful #{api_call}: #{stripe_response.message}" unless stripe_response.success?
35
+
36
+ # Save the response to our logs
37
+ response = StripeResponse.from_response(api_call, nil, stripe_response)
38
+ response.save!
39
+ response
40
+ end
41
+
42
+ def gateway
43
+ # The gateway should have been configured when the plugin started
44
+ Killbill::Stripe.gateway
45
+ end
46
+
47
+ def logger
48
+ # The logger should have been configured when the plugin started
49
+ Killbill::Stripe.logger
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,24 @@
1
+ module Killbill::Stripe
2
+ class Gateway
3
+ def self.from_config(config)
4
+ if config[:test]
5
+ ActiveMerchant::Billing::Base.mode = :test
6
+ end
7
+
8
+ if config[:log_file]
9
+ ActiveMerchant::Billing::StripeGateway.wiredump_device = File.open(config[:log_file], 'w')
10
+ ActiveMerchant::Billing::StripeGateway.wiredump_device.sync = true
11
+ end
12
+
13
+ Gateway.new(config[:api_secret_key])
14
+ end
15
+
16
+ def initialize(api_secret_key)
17
+ @gateway = ActiveMerchant::Billing::StripeGateway.new(:login => api_secret_key)
18
+ end
19
+
20
+ def method_missing(m, *args, &block)
21
+ @gateway.send(m, *args, &block)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,27 @@
1
+ module Killbill::Stripe
2
+ # Closest from a streaming API as we can get with ActiveRecord
3
+ class StreamyResultSet
4
+ include Enumerable
5
+
6
+ def initialize(limit, batch_size = 100, &delegate)
7
+ @limit = limit
8
+ @batch = [batch_size, limit].min
9
+ @delegate = delegate
10
+ end
11
+
12
+ def each(&block)
13
+ (0..(@limit - @batch)).step(@batch) do |i|
14
+ result = @delegate.call(i, @batch)
15
+ block.call(result)
16
+ # Optimization: bail out if no more results
17
+ break if result.nil? || result.empty?
18
+ end if @batch > 0
19
+ # Make sure to return DB connections to the Pool
20
+ ActiveRecord::Base.connection.close
21
+ end
22
+
23
+ def to_a
24
+ super.to_a.flatten
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,88 @@
1
+ <!-- See https://stripe.com/docs/stripe.js and https://gist.github.com/briancollins/6365455 -->
2
+ <!DOCTYPE html>
3
+ <html>
4
+ <head>
5
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
6
+ <title>Stripe.js Checkout</title>
7
+ <script src="<%= stripejs_url %>" type="text/javascript"></script>
8
+ <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
9
+ <script type="text/javascript">
10
+ Stripe.setPublishableKey("<%= publishable_key %>");
11
+
12
+ var stripeResponseHandler = function(status, response) {
13
+ var $form = $('#payment-form');
14
+
15
+ if (response.error) {
16
+ // Show the errors on the form
17
+ $form.find('.payment-errors').text(response.error.message);
18
+ $form.find('button').prop('disabled', false);
19
+ } else {
20
+ // Insert the response values into the form so it gets submitted to the server
21
+ $form.append($('<input type="hidden" name="stripeToken" />').val(response.id));
22
+ $form.append($('<input type="hidden" name="stripeCardName" />').val((response.card || {}).name));
23
+ $form.append($('<input type="hidden" name="stripeCardAddressLine1" />').val((response.card || {}).address_line1));
24
+ $form.append($('<input type="hidden" name="stripeCardAddressLine2" />').val((response.card || {}).address_line2));
25
+ $form.append($('<input type="hidden" name="stripeCardAddressCity" />').val((response.card || {}).address_city));
26
+ $form.append($('<input type="hidden" name="stripeCardAddressState" />').val((response.card || {}).address_state));
27
+ $form.append($('<input type="hidden" name="stripeCardAddressZip" />').val((response.card || {}).address_zip));
28
+ $form.append($('<input type="hidden" name="stripeCardAddressCountry" />').val((response.card || {}).address_country));
29
+ $form.append($('<input type="hidden" name="stripeCardCountry" />').val((response.card || {}).country));
30
+ $form.append($('<input type="hidden" name="stripeCardExpMonth" />').val((response.card || {}).exp_month));
31
+ $form.append($('<input type="hidden" name="stripeCardExpYear" />').val((response.card || {}).exp_year));
32
+ $form.append($('<input type="hidden" name="stripeCardLast4" />').val((response.card || {}).last4));
33
+ $form.append($('<input type="hidden" name="stripeCardFingerprint" />').val((response.card || {}).fingerprint));
34
+ $form.append($('<input type="hidden" name="stripeCardObject" />').val((response.card || {}).object));
35
+ $form.append($('<input type="hidden" name="stripeCardType" />').val((response.card || {}).type));
36
+ $form.append($('<input type="hidden" name="stripeCreated" />').val(response.created));
37
+ $form.append($('<input type="hidden" name="stripeLivemode" />').val(response.livemode));
38
+ $form.append($('<input type="hidden" name="stripeType" />').val(response.type));
39
+ $form.append($('<input type="hidden" name="stripeObject" />').val(response.object));
40
+ $form.append($('<input type="hidden" name="stripeUsed" />').val(response.used));
41
+ // and re-submit
42
+ $form.get(0).submit();
43
+ }
44
+ };
45
+
46
+ $(document).ready(function() {
47
+ $('#payment-form').submit(function(e) {
48
+ var $form = $(this);
49
+
50
+ // Disable the submit button to prevent repeated clicks
51
+ $form.find('button').prop('disabled', true);
52
+
53
+ Stripe.createToken($form, stripeResponseHandler);
54
+
55
+ // Prevent the form from submitting with the default action
56
+ return false;
57
+ });
58
+ });
59
+ </script>
60
+ </head>
61
+ <body>
62
+ <form method="post" id="payment-form" action="">
63
+ <span class='payment-errors'></span>
64
+ <input type="hidden" name="kbAccountId" value="<%= kb_account_id %>" />
65
+ <div class="form-row">
66
+ <label>
67
+ <span>Card Number</span>
68
+ <input type="text" size="20" data-stripe="number"/>
69
+ </label>
70
+ </div>
71
+ <div class="form-row">
72
+ <label>
73
+ <span>CVC</span>
74
+ <input type="text" size="4" data-stripe="cvc"/>
75
+ </label>
76
+ </div>
77
+ <div class="form-row">
78
+ <label>
79
+ <span>Expiration (MM/YYYY)</span>
80
+ <input type="text" size="2" data-stripe="exp-month"/>
81
+ </label>
82
+ <span> / </span>
83
+ <input type="text" size="4" data-stripe="exp-year"/>
84
+ </div>
85
+ <button type="submit">Save credit card</button>
86
+ </form>
87
+ </body>
88
+ </html>
data/lib/stripe.rb ADDED
@@ -0,0 +1,30 @@
1
+ require 'active_record'
2
+ require 'activemerchant'
3
+ require 'bigdecimal'
4
+ require 'money'
5
+ require 'pathname'
6
+ require 'set'
7
+ require 'sinatra'
8
+ require 'singleton'
9
+ require 'yaml'
10
+
11
+ require 'killbill'
12
+
13
+ require 'stripe/config/configuration'
14
+ require 'stripe/config/properties'
15
+
16
+ require 'stripe/api'
17
+ require 'stripe/private_api'
18
+
19
+ require 'stripe/models/stripe_payment_method'
20
+ require 'stripe/models/stripe_response'
21
+ require 'stripe/models/stripe_transaction'
22
+
23
+ require 'stripe/stripe_utils'
24
+ require 'stripe/stripe/gateway'
25
+
26
+ class Object
27
+ def blank?
28
+ respond_to?(:empty?) ? empty? : !self
29
+ end
30
+ end
data/pom.xml ADDED
@@ -0,0 +1,44 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!--
3
+ ~ Copyright 2010-2013 Ning, Inc.
4
+ ~
5
+ ~ Ning licenses this file to you under the Apache License, version 2.0
6
+ ~ (the "License"); you may not use this file except in compliance with the
7
+ ~ License. You may obtain a copy of the License at:
8
+ ~
9
+ ~ http://www.apache.org/licenses/LICENSE-2.0
10
+ ~
11
+ ~ Unless required by applicable law or agreed to in writing, software
12
+ ~ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13
+ ~ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14
+ ~ License for the specific language governing permissions and limitations
15
+ ~ under the License.
16
+ -->
17
+
18
+ <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
19
+ <parent>
20
+ <groupId>org.sonatype.oss</groupId>
21
+ <artifactId>oss-parent</artifactId>
22
+ <version>5</version>
23
+ </parent>
24
+ <modelVersion>4.0.0</modelVersion>
25
+ <groupId>org.kill-bill.billing.plugin.ruby</groupId>
26
+ <artifactId>stripe-plugin</artifactId>
27
+ <packaging>pom</packaging>
28
+ <version>0.1.0</version>
29
+ <name>stripe-plugin</name>
30
+ <url>http://github.com/killbill/killbill-stripe-plugin</url>
31
+ <description>Plugin for accessing Stripe as a payment gateway</description>
32
+ <licenses>
33
+ <license>
34
+ <name>Apache License 2.0</name>
35
+ <url>http://www.apache.org/licenses/LICENSE-2.0.html</url>
36
+ <distribution>repo</distribution>
37
+ </license>
38
+ </licenses>
39
+ <scm>
40
+ <connection>scm:git:git://github.com/killbill/killbill-stripe-plugin.git</connection>
41
+ <url>https://github.com/killbill/killbill-stripe-plugin/</url>
42
+ <developerConnection>scm:git:git@github.com:killbill/killbill-stripe-plugin.git</developerConnection>
43
+ </scm>
44
+ </project>
data/release.sh ADDED
@@ -0,0 +1,41 @@
1
+ set -e
2
+
3
+ if [ "GNU" != "$(tar --help | grep GNU | head -1 | awk '{print $1}')" ]; then
4
+ echo "Unable to release: make sure to use GNU tar"
5
+ exit 1
6
+ fi
7
+
8
+ if $(ruby -e'require "java"'); then
9
+ # Good
10
+ echo "Detected JRuby"
11
+ else
12
+ echo "Unable to release: make sure to use JRuby"
13
+ exit 1
14
+ fi
15
+
16
+ VERSION=`grep -E '<version>([0-9]+\.[0-9]+\.[0-9]+)</version>' pom.xml | sed 's/[\t \n]*<version>\(.*\)<\/version>[\t \n]*/\1/'`
17
+ if [ "$VERSION" != "$(cat $PWD/VERSION)" ]; then
18
+ echo "Unable to release: make sure the versions in pom.xml and VERSION match"
19
+ exit 1
20
+ fi
21
+
22
+ echo "Cleaning up"
23
+ rake killbill:clean ; rake build
24
+
25
+ echo "Pushing the gem to Rubygems"
26
+ rake release
27
+
28
+ echo "Building artifact"
29
+ rake killbill:package
30
+
31
+ ARTIFACT="$PWD/pkg/killbill-stripe-$VERSION.tar.gz"
32
+ echo "Pushing $ARTIFACT to Maven Central"
33
+ mvn gpg:sign-and-deploy-file \
34
+ -DgroupId=org.kill-bill.billing.plugin.ruby \
35
+ -DartifactId=stripe-plugin \
36
+ -Dversion=$VERSION \
37
+ -Dpackaging=tar.gz \
38
+ -DrepositoryId=ossrh-releases \
39
+ -Durl=https://oss.sonatype.org/service/local/staging/deploy/maven2/ \
40
+ -Dfile=$ARTIFACT \
41
+ -DpomFile=pom.xml
@@ -0,0 +1,37 @@
1
+ require 'bundler'
2
+ require 'stripe'
3
+
4
+ require 'logger'
5
+
6
+ require 'rspec'
7
+
8
+ RSpec.configure do |config|
9
+ config.color_enabled = true
10
+ config.tty = true
11
+ config.formatter = 'documentation'
12
+ end
13
+
14
+ require 'active_record'
15
+ ActiveRecord::Base.establish_connection(
16
+ :adapter => 'sqlite3',
17
+ :database => 'test.db'
18
+ )
19
+ # For debugging
20
+ #ActiveRecord::Base.logger = Logger.new(STDOUT)
21
+ # Create the schema
22
+ require File.expand_path(File.dirname(__FILE__) + '../../db/schema.rb')
23
+
24
+ begin
25
+ require 'securerandom'
26
+ SecureRandom.uuid
27
+ rescue LoadError, NoMethodError
28
+ # See http://jira.codehaus.org/browse/JRUBY-6176
29
+ module SecureRandom
30
+ def self.uuid
31
+ ary = self.random_bytes(16).unpack("NnnnnN")
32
+ ary[2] = (ary[2] & 0x0fff) | 0x4000
33
+ ary[3] = (ary[3] & 0x3fff) | 0x8000
34
+ "%08x-%04x-%04x-%04x-%04x%08x" % ary
35
+ end unless respond_to?(:uuid)
36
+ end
37
+ end
@@ -0,0 +1,85 @@
1
+ require 'spec_helper'
2
+
3
+ describe Killbill::Stripe::PaymentPlugin do
4
+ before(:each) do
5
+ Dir.mktmpdir do |dir|
6
+ file = File.new(File.join(dir, 'stripe.yml'), "w+")
7
+ file.write(<<-eos)
8
+ :stripe:
9
+ :api_secret_key: 'j2lkb12'
10
+ # As defined by spec_helper.rb
11
+ :database:
12
+ :adapter: 'sqlite3'
13
+ :database: 'test.db'
14
+ eos
15
+ file.close
16
+
17
+ @plugin = Killbill::Stripe::PaymentPlugin.new
18
+ @plugin.logger = Logger.new(STDOUT)
19
+ @plugin.logger.level = Logger::INFO
20
+ @plugin.conf_dir = File.dirname(file)
21
+
22
+ # Start the plugin here - since the config file will be deleted
23
+ @plugin.start_plugin
24
+ end
25
+ end
26
+
27
+ it 'should start and stop correctly' do
28
+ @plugin.stop_plugin
29
+ end
30
+
31
+ it 'should reset payment methods' do
32
+ kb_account_id = '129384'
33
+
34
+ @plugin.get_payment_methods(kb_account_id).size.should == 0
35
+ verify_pms kb_account_id, 0
36
+
37
+ # Create a pm with a kb_payment_method_id
38
+ Killbill::Stripe::StripePaymentMethod.create :kb_account_id => kb_account_id,
39
+ :kb_payment_method_id => 'kb-1',
40
+ :stripe_card_id_or_token => 'stripe-1'
41
+ verify_pms kb_account_id, 1
42
+
43
+ # Add some in KillBill and reset
44
+ payment_methods = []
45
+ # Random order... Shouldn't matter...
46
+ payment_methods << create_pm_info_plugin(kb_account_id, 'kb-3', false, 'stripe-3')
47
+ payment_methods << create_pm_info_plugin(kb_account_id, 'kb-2', false, 'stripe-2')
48
+ payment_methods << create_pm_info_plugin(kb_account_id, 'kb-4', false, 'stripe-4')
49
+ @plugin.reset_payment_methods kb_account_id, payment_methods
50
+ verify_pms kb_account_id, 4
51
+
52
+ # Add a payment method without a kb_payment_method_id
53
+ Killbill::Stripe::StripePaymentMethod.create :kb_account_id => kb_account_id,
54
+ :stripe_card_id_or_token => 'stripe-5'
55
+ @plugin.get_payment_methods(kb_account_id).size.should == 5
56
+
57
+ # Verify we can match it
58
+ payment_methods << create_pm_info_plugin(kb_account_id, 'kb-5', false, 'stripe-5')
59
+ @plugin.reset_payment_methods kb_account_id, payment_methods
60
+ verify_pms kb_account_id, 5
61
+
62
+ @plugin.stop_plugin
63
+ end
64
+
65
+ private
66
+
67
+ def verify_pms(kb_account_id, size)
68
+ pms = @plugin.get_payment_methods(kb_account_id)
69
+ pms.size.should == size
70
+ pms.each do |pm|
71
+ pm.account_id.should == kb_account_id
72
+ pm.is_default.should == false
73
+ pm.external_payment_method_id.should == 'stripe-' + pm.payment_method_id.split('-')[1]
74
+ end
75
+ end
76
+
77
+ def create_pm_info_plugin(kb_account_id, kb_payment_method_id, is_default, external_payment_method_id)
78
+ pm_info_plugin = Killbill::Plugin::Model::PaymentMethodInfoPlugin.new
79
+ pm_info_plugin.account_id = kb_account_id
80
+ pm_info_plugin.payment_method_id = kb_payment_method_id
81
+ pm_info_plugin.is_default = is_default
82
+ pm_info_plugin.external_payment_method_id = external_payment_method_id
83
+ pm_info_plugin
84
+ end
85
+ end