pay_me 0.5.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.
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