pay_me 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (113) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +111 -0
  3. data/Rakefile +33 -0
  4. data/app/controllers/pay_me/api/v1/invoices_controller.rb +30 -0
  5. data/app/controllers/pay_me/api/v1/stripe_plans_controller.rb +35 -0
  6. data/app/controllers/pay_me/application_controller.rb +10 -0
  7. data/app/helpers/pay_me/application_helper.rb +4 -0
  8. data/app/subscribers/pay_me/customer_subscriber.rb +42 -0
  9. data/app/subscribers/pay_me/plan_subscriber.rb +38 -0
  10. data/app/subscribers/pay_me/subscription_subscriber.rb +50 -0
  11. data/app/subscribers/pay_me/webhook_event_subscriber.rb +11 -0
  12. data/config/routes.rb +11 -0
  13. data/db/migrate/20160725204454_create_pay_me_subscriptions.rb +16 -0
  14. data/db/migrate/20161011170659_create_pay_me_plans.rb +8 -0
  15. data/db/migrate/20171030174731_add_quantity_to_pay_me_subscription.rb +6 -0
  16. data/db/migrate/20171130192525_create_pay_me_customers.rb +11 -0
  17. data/db/migrate/20180116193943_remove_customerable_from_subscriptions.rb +11 -0
  18. data/db/migrate/20180215235017_add_past_due_to_subscriptions.rb +5 -0
  19. data/lib/generators/pay_me/controllers/controllers_generator.rb +11 -0
  20. data/lib/generators/pay_me/controllers/templates/subscriptions_controller.rb +12 -0
  21. data/lib/generators/pay_me/install_generator.rb +39 -0
  22. data/lib/generators/pay_me/migrate_to_customer_model/USAGE +8 -0
  23. data/lib/generators/pay_me/migrate_to_customer_model/migrate_to_customer_model_generator.rb +9 -0
  24. data/lib/generators/pay_me/migrate_to_customer_model/templates/migration.rb.erb +64 -0
  25. data/lib/generators/pay_me/models/models_generator.rb +13 -0
  26. data/lib/generators/pay_me/models/templates/customer.rb +14 -0
  27. data/lib/generators/pay_me/models/templates/plan.rb +14 -0
  28. data/lib/generators/pay_me/models/templates/subscription.rb +14 -0
  29. data/lib/generators/pay_me/policies/policies_generator.rb +11 -0
  30. data/lib/generators/pay_me/policies/templates/subscription_policy.rb +34 -0
  31. data/lib/generators/pay_me/templates/pay_me_initializer.rb +17 -0
  32. data/lib/pay_me.rb +28 -0
  33. data/lib/pay_me/active_record.rb +6 -0
  34. data/lib/pay_me/concerns/controllers/customerable.rb +116 -0
  35. data/lib/pay_me/concerns/models/customerable.rb +54 -0
  36. data/lib/pay_me/concerns/models/stripe_customerable.rb +24 -0
  37. data/lib/pay_me/concerns/models/stripe_plannable.rb +31 -0
  38. data/lib/pay_me/concerns/models/stripe_subscribable.rb +43 -0
  39. data/lib/pay_me/configuration.rb +27 -0
  40. data/lib/pay_me/engine.rb +8 -0
  41. data/lib/pay_me/models.rb +4 -0
  42. data/lib/pay_me/railtie.rb +2 -0
  43. data/lib/pay_me/route_helpers.rb +21 -0
  44. data/lib/pay_me/services/customer.rb +141 -0
  45. data/lib/pay_me/version.rb +3 -0
  46. data/lib/pay_me/view_models/charge.rb +48 -0
  47. data/lib/tasks/pay_me_tasks.rake +4 -0
  48. data/lib/tasks/stripe.rake +22 -0
  49. data/test/controllers/pay_me/api/v1/invoices_controller_test.rb +82 -0
  50. data/test/controllers/pay_me/api/v1/plans_controller_test.rb +62 -0
  51. data/test/dummy/README.rdoc +28 -0
  52. data/test/dummy/Rakefile +6 -0
  53. data/test/dummy/app/assets/javascripts/application.js +13 -0
  54. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  55. data/test/dummy/app/controllers/api/v1/users_controller.rb +10 -0
  56. data/test/dummy/app/controllers/application_controller.rb +5 -0
  57. data/test/dummy/app/controllers/pay_me/api/v1/subscriptions_controller.rb +12 -0
  58. data/test/dummy/app/helpers/application_helper.rb +2 -0
  59. data/test/dummy/app/models/pay_me/customer.rb +5 -0
  60. data/test/dummy/app/models/pay_me/plan.rb +5 -0
  61. data/test/dummy/app/models/pay_me/subscription.rb +5 -0
  62. data/test/dummy/app/models/user.rb +8 -0
  63. data/test/dummy/app/policies/pay_me/subscription_policy.rb +34 -0
  64. data/test/dummy/app/serializers/pay_me/subscription_serializer.rb +3 -0
  65. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  66. data/test/dummy/bin/bundle +3 -0
  67. data/test/dummy/bin/rails +4 -0
  68. data/test/dummy/bin/rake +4 -0
  69. data/test/dummy/bin/setup +29 -0
  70. data/test/dummy/config.ru +4 -0
  71. data/test/dummy/config/application.rb +25 -0
  72. data/test/dummy/config/boot.rb +5 -0
  73. data/test/dummy/config/database.yml +25 -0
  74. data/test/dummy/config/environment.rb +5 -0
  75. data/test/dummy/config/environments/development.rb +41 -0
  76. data/test/dummy/config/environments/production.rb +79 -0
  77. data/test/dummy/config/environments/test.rb +42 -0
  78. data/test/dummy/config/initializers/assets.rb +11 -0
  79. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  80. data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  81. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  82. data/test/dummy/config/initializers/inflections.rb +16 -0
  83. data/test/dummy/config/initializers/mime_types.rb +4 -0
  84. data/test/dummy/config/initializers/pay_me.rb +17 -0
  85. data/test/dummy/config/initializers/session_store.rb +3 -0
  86. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  87. data/test/dummy/config/locales/en.yml +23 -0
  88. data/test/dummy/config/routes.rb +10 -0
  89. data/test/dummy/config/secrets.yml +22 -0
  90. data/test/dummy/db/migrate/20160630181300_create_pay_me_users.rb +12 -0
  91. data/test/dummy/db/migrate/20180118001940_api_me_migrate_to_customer_model.rb +9 -0
  92. data/test/dummy/db/schema.rb +53 -0
  93. data/test/dummy/public/404.html +67 -0
  94. data/test/dummy/public/422.html +67 -0
  95. data/test/dummy/public/500.html +66 -0
  96. data/test/dummy/public/favicon.ico +0 -0
  97. data/test/integration/customer_test.rb +395 -0
  98. data/test/lib/generators/pay_me/pay_me/migrate_to_customer_model_generator_test.rb +16 -0
  99. data/test/models/pay_me/customer_test.rb +9 -0
  100. data/test/models/pay_me/subscription_test.rb +9 -0
  101. data/test/models/pay_me/user_test.rb +6 -0
  102. data/test/pay_me_test.rb +7 -0
  103. data/test/support/concerns/api_test_helper.rb +15 -0
  104. data/test/support/concerns/stripe_helpers.rb +25 -0
  105. data/test/test_helper.rb +52 -0
  106. data/test/unit/concerns/customerable.rb +21 -0
  107. data/test/unit/models/customerable_test.rb +18 -0
  108. data/test/unit/services/customer_test.rb +75 -0
  109. data/test/unit/stubs/customerable.rb +28 -0
  110. data/test/unit/stubs/customerable_test.rb +16 -0
  111. data/test/webhooks/customer_webhook_test.rb +74 -0
  112. data/test/webhooks/plan_webhook_test.rb +74 -0
  113. metadata +288 -0
@@ -0,0 +1,54 @@
1
+ require 'stripe'
2
+ require 'pay_me/services/customer'
3
+
4
+ module PayMe
5
+ module Concerns
6
+ module Models
7
+ module Customerable
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ belongs_to :customer, class_name: 'PayMe::Customer'
12
+ has_many :subscriptions, class_name: 'PayMe::Subscription', through: :customer
13
+ has_many :plans, class_name: 'PayMe::Plan', through: :subscriptions
14
+
15
+ def customer_service
16
+ PayMe::Services::Customer.new(customerable: self, customer_class: PayMe.configuration.stripe_customer_class)
17
+ end
18
+
19
+ def stripe_customer
20
+ customer_service.customer
21
+ end
22
+
23
+ def new_customer_params
24
+ {}
25
+ end
26
+
27
+ def add_default_payment_source(source_id)
28
+ customer_service.add_default_payment_source(source_id)
29
+ end
30
+
31
+ def add_payment_source(source_id)
32
+ customer_service.add_payment_source(source_id)
33
+ end
34
+
35
+ def subscribe(plan_id:, quantity: 1, coupon_id: nil, source: nil)
36
+ customer_service.subscribe!(plan_id: plan_id, quantity: quantity, coupon_id: coupon_id, source: source)
37
+ end
38
+
39
+ def update_subscription(quantity:, coupon_id: nil)
40
+ customer_service.update_subscription!(quantity: quantity, coupon_id: coupon_id)
41
+ end
42
+
43
+ def cancel_subscription(cancel_immediately: false)
44
+ customer_service.cancel_subscription!(cancel_immediately)
45
+ end
46
+
47
+ def stripe_customer_id
48
+ customer_service.customer_id
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,24 @@
1
+ module PayMe
2
+ module Concerns
3
+ module Models
4
+ module StripeCustomerable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ has_many :subscriptions, class_name: 'PayMe::Subscription'
9
+ has_many :plans, through: :subscriptions, class_name: 'PayMe::Plan'
10
+ end
11
+ class_methods do
12
+ def created_webhook!(stripe_customer)
13
+ end
14
+
15
+ def updated_webhook!(stripe_customer)
16
+ end
17
+
18
+ def deleted_webhook!(stripe_customer)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,31 @@
1
+ module PayMe
2
+ module Concerns
3
+ module Models
4
+ module StripePlannable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ has_many :subscriptions, class_name: 'PayMe::Subscription', foreign_key: :pay_me_plan_id
9
+
10
+ validates :plan_id, presence: true
11
+
12
+ def as_json(options = {})
13
+ { subscription: super({ }.merge(options)) }
14
+ end
15
+ end
16
+ class_methods do
17
+ def created_webhook!(stripe_plan)
18
+ end
19
+
20
+ def updated_webhook!(stripe_plan)
21
+ end
22
+
23
+ def deleted_webhook!(stripe_plan)
24
+ # Suggested implementation:
25
+ # Handle customers who's plan was removed?
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,43 @@
1
+ module PayMe
2
+ module Concerns
3
+ module Models
4
+ module StripeSubscribable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ belongs_to :customer, class_name: 'PayMe::Customer'
9
+ belongs_to :plan, foreign_key: :pay_me_plan_id, class_name: 'PayMe::Plan'
10
+
11
+ validates :customer, presence: true
12
+ validates :is_active, inclusion: { in: [ true, false ] }
13
+ validates :subscription_id, presence: true
14
+ validates :plan, presence: true
15
+
16
+ scope :most_current, -> { order(updated_at: :desc).limit(1) }
17
+
18
+ def stripe_plan_id=(stripe_plan_id)
19
+ self.plan = Plan.find_by(plan_id: stripe_plan_id)
20
+ end
21
+
22
+ def stripe_plan_id
23
+ self.plan.blank? ? nil : self.plan.plan_id
24
+ end
25
+
26
+ def as_json(options = {})
27
+ { subscription: super({ }.merge(options)) }
28
+ end
29
+ end
30
+ class_methods do
31
+ def created_webhook!(stripe_subscription)
32
+ end
33
+
34
+ def updated_webhook!(stripe_subscription)
35
+ end
36
+
37
+ def deleted_webhook!(stripe_subscription)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,27 @@
1
+ require 'singleton'
2
+
3
+ module PayMe
4
+ class Configuration
5
+ include Singleton
6
+
7
+ attr_accessor :mount_path
8
+ attr_accessor :live_mode
9
+
10
+ def initialize
11
+ self.mount_path = 'pay_me'
12
+ self.live_mode = true
13
+ end
14
+
15
+ def stripe_customer_class
16
+ Stripe::Customer
17
+ end
18
+
19
+ def stripe_invoice_class
20
+ Stripe::Invoice
21
+ end
22
+
23
+ def stripe_plan_class
24
+ Stripe::Plan
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,8 @@
1
+ module PayMe
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace PayMe
4
+ initializer "pay_me.setup" do
5
+ require "pay_me/route_helpers"
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,4 @@
1
+ module PayMe
2
+ module Models
3
+ end
4
+ end
@@ -0,0 +1,2 @@
1
+ module PayMe
2
+ end
@@ -0,0 +1,21 @@
1
+ module ActionDispatch
2
+ module Routing
3
+ class Mapper
4
+ def pay_me_customerable_for(*resources)
5
+ resources.each do |r|
6
+ resources r, only: [] do
7
+ member do
8
+ post 'subscribe'
9
+ put 'update_subscription'
10
+ delete 'cancel_subscription'
11
+ post 'charge'
12
+ get 'invoices'
13
+ put 'update_default_source'
14
+ get 'default_source'
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,141 @@
1
+ module PayMe
2
+ module Services
3
+ class Customer
4
+ attr_reader :customerable, :customer_class
5
+
6
+ def initialize(customerable:, customer_class: PayMe.configuration.stripe_customer_class, subcription_class: Stripe::Subscription)
7
+ @customerable = customerable
8
+ @customer_class = customer_class
9
+ end
10
+
11
+ def destroy!
12
+ customer.delete
13
+ end
14
+
15
+ def add_default_payment_source(source_id)
16
+ customer.default_source = customer.sources.create(source: source_id).id
17
+ customer.save
18
+ end
19
+
20
+ def add_payment_source(source_id)
21
+ customer.sources.create(source: source_id)
22
+ end
23
+
24
+ def customer_id
25
+ customer.id
26
+ end
27
+
28
+
29
+ ### Notes
30
+ # Assumes only one active subcription at a time.
31
+ # Updates current subscription.
32
+ # Source must be a token
33
+ ###
34
+ def subscribe!(plan_id:, quantity: 1, coupon_id: nil, source: nil)
35
+ new_subscription = nil
36
+ ActiveRecord::Base.transaction do
37
+ stripe_hash = {
38
+ customer: customer_id,
39
+ plan: plan_id,
40
+ quantity: quantity,
41
+ coupon: coupon_id,
42
+ source: source
43
+ }
44
+ stripe_subscription = Stripe::Subscription.create(stripe_hash)
45
+ subscription_status = stripe_subscription.status
46
+ is_not_active = subscription_status == 'canceled' || subscription_status == 'unpaid'
47
+
48
+ new_subscription = PayMe::Subscription.create!(
49
+ subscription_id: stripe_subscription.id,
50
+ customer: customerable.customer,
51
+ is_active: !is_not_active,
52
+ stripe_plan_id: plan_id,
53
+ quantity: quantity,
54
+ coupon_id: coupon_id
55
+ )
56
+ end
57
+ return new_subscription
58
+ end
59
+
60
+ ### Notes
61
+ # Assumes only one active subcription at a time.
62
+ # Updates current subscription quantity and coupon
63
+ ###
64
+ def update_subscription!(quantity:, coupon_id: nil)
65
+ subscription = nil
66
+ ActiveRecord::Base.transaction do
67
+ subscription = current_subscription
68
+ unless subscription.blank?
69
+ stripe_subscription = Stripe::Subscription.retrieve(subscription.subscription_id)
70
+ stripe_subscription.quantity = quantity
71
+ stripe_subscription.coupon = coupon_id
72
+ subscription.update!(
73
+ quantity: quantity,
74
+ coupon_id: coupon_id
75
+ )
76
+ stripe_subscription.save
77
+ end
78
+ end
79
+ return subscription
80
+ end
81
+
82
+ ### Notes
83
+ # Assumes only one active subcription at a time.
84
+ # Cancels current subscription.
85
+ ###
86
+ def cancel_subscription!(cancel_immediately)
87
+ subscription = nil
88
+ ActiveRecord::Base.transaction do
89
+ subscription = current_subscription
90
+ unless subscription.blank?
91
+ stripe_subscription = Stripe::Subscription.retrieve(subscription.subscription_id)
92
+ subscription.update!(is_active: !cancel_immediately)
93
+ stripe_subscription.delete(at_period_end: !cancel_immediately)
94
+ end
95
+ end
96
+ return subscription
97
+ end
98
+
99
+ def customer
100
+ if @customerable.customer && @customerable.customer.customer_id
101
+ @customer ||= customer_class.retrieve(customerable.customer.customer_id)
102
+ else
103
+ @customer = create!
104
+ end
105
+
106
+ @customer
107
+ end
108
+
109
+ def current_subscription
110
+ @customerable.subscriptions.most_current.limit(1).first
111
+ end
112
+
113
+ protected
114
+
115
+ def create!(_params = {})
116
+ ActiveRecord::Base.transaction do
117
+ @customer = customer_class.create(
118
+ customerable.new_customer_params.merge(
119
+ metadata: { customerable_id: customerable.id }
120
+ )
121
+ )
122
+
123
+ begin
124
+ if @customerable.customer
125
+ @customerable.customer.update!(customer_id: @customer.id)
126
+ else
127
+ @customerable.update!(
128
+ customer: PayMe::Customer.create!(customer_id: @customer.id)
129
+ )
130
+ end
131
+ rescue
132
+ @customer.delete
133
+ raise
134
+ end
135
+ end
136
+
137
+ @customer
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,3 @@
1
+ module PayMe
2
+ VERSION = '0.5.0'.freeze
3
+ end
@@ -0,0 +1,48 @@
1
+ require 'virtus'
2
+
3
+ module PayMe
4
+ module ViewModels
5
+ class Charge
6
+ include Virtus.model
7
+ include ActiveModel::Validations
8
+
9
+ attribute :amount, Integer
10
+ attribute :currency, String
11
+ attribute :customerable
12
+ attribute :description, String
13
+ attribute :source, String
14
+
15
+ validates :amount,
16
+ presence: true,
17
+ numericality: { greater_than_or_equal_to: 0, only_integer: true }
18
+ validates :currency, presence: true, format: { with: /\A[a-z]{3}\z/i }
19
+ validates :customerable, presence: true
20
+ validate :customer_is_valid, unless: proc { |c| c.customerable.blank? }
21
+ validates :description, presence: true
22
+ validates :source, presence: true, unless: proc { |c| c.source.blank? }
23
+
24
+ def charge
25
+ valid!
26
+ stripe_payload_hash = {
27
+ amount: amount,
28
+ currency: currency,
29
+ customer: customerable.stripe_customer_id,
30
+ receipt_email: customerable.receipt_email,
31
+ description: description
32
+ }
33
+ stripe_payload_hash[:source] = source unless source.blank?
34
+ Stripe::Charge.create(stripe_payload_hash)
35
+ end
36
+
37
+ private
38
+
39
+ def customer_is_valid
40
+ errors.add(:base, 'customer invalid') unless customerable.stripe_customer_id.present?
41
+ end
42
+
43
+ def valid!
44
+ raise ActiveRecord::RecordInvalid, self unless valid?
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :pay_me do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,22 @@
1
+
2
+ namespace :pay_me do
3
+ namespace :stripe do
4
+ desc 'Additively caches stripe plans locally.'
5
+ task add_stripe_plans: :environment do |_t, args|
6
+ Stripe::Plan.list.each do |plan|
7
+ PayMe::Plan.create!(plan_id: plan.id) unless PayMe::Plan.exists?(plan_id: plan.id)
8
+ end
9
+ end
10
+
11
+ desc 'Destructively caches stripe plans locally to match stripe.'
12
+ task sync_stripe_plans: :environment do |_t, args|
13
+ @stripe_plans_ids = Stripe::Plan.list.map(&:id)
14
+ PayMe::Plan.find_each do |pay_me_plan|
15
+ pay_me_plan.destroy unless @stripe_plans_ids.delete(pay_me_plan.plan_id) != nil
16
+ end
17
+ Stripe::Plan.list.each do |plan|
18
+ PayMe::Plan.create!(plan_id: plan.id) unless PayMe::Plan.exists?(plan_id: plan.id)
19
+ end
20
+ end
21
+ end
22
+ end