killbill-stripe 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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