summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlessandro Desantis <desa.alessandro@gmail.com>2020-10-10 10:16:37 +0200
committerGitHub <noreply@github.com>2020-10-10 10:16:37 +0200
commit6b5c54f6998ed9ebe99e9c047e077cffdd7827b6 (patch)
tree018fd3c502ee833e625fd659170fe3d1e0aa35d4
parentceb0b5a3c7d21213afdf75e496db98ae51640098 (diff)
parent5b1795151eb52ec8c5097f3ad58db055d9acb1bb (diff)
Merge pull request #152 from solidusio-contrib/aldesantis/churn-buster
Integrate Churn Buster
-rw-r--r--README.md18
-rw-r--r--app/decorators/models/solidus_subscriptions/spree/wallet_payment_source/report_default_change_to_subscriptions.rb28
-rw-r--r--app/models/solidus_subscriptions/subscription.rb11
-rw-r--r--app/services/solidus_subscriptions/payment_failed_dispatcher.rb6
-rw-r--r--app/services/solidus_subscriptions/success_dispatcher.rb6
-rw-r--r--app/subscribers/solidus_subscriptions/churn_buster_subscriber.rb39
-rw-r--r--config/initializers/subscribers.rb8
-rw-r--r--lib/generators/solidus_subscriptions/install/templates/initializer.rb14
-rw-r--r--lib/solidus_subscriptions.rb16
-rw-r--r--lib/solidus_subscriptions/churn_buster/client.rb48
-rw-r--r--lib/solidus_subscriptions/churn_buster/order_serializer.rb19
-rw-r--r--lib/solidus_subscriptions/churn_buster/serializer.rb23
-rw-r--r--lib/solidus_subscriptions/churn_buster/subscription_customer_serializer.rb19
-rw-r--r--lib/solidus_subscriptions/churn_buster/subscription_payment_method_serializer.rb37
-rw-r--r--lib/solidus_subscriptions/churn_buster/subscription_serializer.rb17
-rw-r--r--lib/solidus_subscriptions/configuration.rb8
-rw-r--r--solidus_subscriptions.gemspec3
-rw-r--r--spec/fixtures/cassettes/churn_buster.yml229
-rw-r--r--spec/lib/solidus_subscriptions/churn_buster/client_spec.rb57
-rw-r--r--spec/lib/solidus_subscriptions_spec.rb28
-rw-r--r--spec/models/solidus_subscriptions/subscription_spec.rb16
-rw-r--r--spec/models/spree/wallet_payment_source_spec.rb18
-rw-r--r--spec/services/solidus_subscriptions/payment_failed_dispatcher_spec.rb15
-rw-r--r--spec/services/solidus_subscriptions/success_dispatcher_spec.rb15
-rw-r--r--spec/subscribers/solidus_subscriptions/churn_buster_subscriber_spec.rb74
-rw-r--r--spec/support/vcr.rb10
26 files changed, 777 insertions, 5 deletions
diff --git a/README.md b/README.md
index 81b4a29..5dbacb6 100644
--- a/README.md
+++ b/README.md
@@ -95,6 +95,24 @@ We suggest using the [Whenever](https://github.com/javan/whenever) gem to schedu
You can find the API documentation [here](https://stoplight.io/p/docs/gh/solidusio-contrib/solidus_subscriptions?group=master).
+### Churn Buster integration
+
+This extension optionally integrates with [Churn Buster](https://churnbuster.io) for failed payment
+recovery. In order to enable the integration, simply add your Churn Buster credentials to your
+configuration:
+
+```ruby
+SolidusSubscriptions.configure do |config|
+ # ...
+
+ config.churn_buster_account_id = 'YOUR_CHURN_BUSTER_ACCOUNT_ID'
+ config.churn_buster_api_key = 'YOUR_CHURN_BUSTER_API_KEY'
+end
+```
+
+The extension will take care of reporting successful/failed payments and payment method changes
+to Churn Buster.
+
## Development
### Testing the extension
diff --git a/app/decorators/models/solidus_subscriptions/spree/wallet_payment_source/report_default_change_to_subscriptions.rb b/app/decorators/models/solidus_subscriptions/spree/wallet_payment_source/report_default_change_to_subscriptions.rb
new file mode 100644
index 0000000..6ee754e
--- /dev/null
+++ b/app/decorators/models/solidus_subscriptions/spree/wallet_payment_source/report_default_change_to_subscriptions.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module SolidusSubscriptions
+ module Spree
+ module WalletPaymentSource
+ module ReportDefaultChangeToSubscriptions
+ def self.prepended(base)
+ base.after_save :report_default_change_to_subscriptions
+ end
+
+ private
+
+ def report_default_change_to_subscriptions
+ return if !previous_changes.key?('default') || !default?
+
+ user.subscriptions.with_default_payment_source.each do |subscription|
+ ::Spree::Event.fire(
+ 'solidus_subscriptions.subscription_payment_method_changed',
+ subscription: subscription,
+ )
+ end
+ end
+ end
+ end
+ end
+end
+
+Spree::WalletPaymentSource.prepend(SolidusSubscriptions::Spree::WalletPaymentSource::ReportDefaultChangeToSubscriptions)
diff --git a/app/models/solidus_subscriptions/subscription.rb b/app/models/solidus_subscriptions/subscription.rb
index d3dd2f5..08de3c9 100644
--- a/app/models/solidus_subscriptions/subscription.rb
+++ b/app/models/solidus_subscriptions/subscription.rb
@@ -74,6 +74,10 @@ module SolidusSubscriptions
joins(:installments).merge(Installment.unfulfilled)
end)
+ scope :with_default_payment_source, (lambda do
+ where(payment_method: nil, payment_source: nil)
+ end)
+
def self.ransackable_scopes(_auth_object = nil)
[:in_processing_state]
end
@@ -325,6 +329,13 @@ module SolidusSubscriptions
subscription: self,
)
end
+
+ if previous_changes.key?('payment_source_id') || previous_changes.key?('payment_source_type') || previous_changes.key?('payment_method_id')
+ ::Spree::Event.fire(
+ 'solidus_subscriptions.subscription_payment_method_changed',
+ subscription: self,
+ )
+ end
end
end
end
diff --git a/app/services/solidus_subscriptions/payment_failed_dispatcher.rb b/app/services/solidus_subscriptions/payment_failed_dispatcher.rb
index 562992b..29eb291 100644
--- a/app/services/solidus_subscriptions/payment_failed_dispatcher.rb
+++ b/app/services/solidus_subscriptions/payment_failed_dispatcher.rb
@@ -9,6 +9,12 @@ module SolidusSubscriptions
installments.each do |installment|
installment.payment_failed!(order)
end
+
+ ::Spree::Event.fire(
+ 'solidus_subscriptions.installments_failed_payment',
+ installments: installments,
+ order: order,
+ )
end
end
end
diff --git a/app/services/solidus_subscriptions/success_dispatcher.rb b/app/services/solidus_subscriptions/success_dispatcher.rb
index 4ac5e94..ce55266 100644
--- a/app/services/solidus_subscriptions/success_dispatcher.rb
+++ b/app/services/solidus_subscriptions/success_dispatcher.rb
@@ -7,6 +7,12 @@ module SolidusSubscriptions
installments.each do |installment|
installment.success!(order)
end
+
+ ::Spree::Event.fire(
+ 'solidus_subscriptions.installments_succeeded',
+ installments: installments,
+ order: order,
+ )
end
end
end
diff --git a/app/subscribers/solidus_subscriptions/churn_buster_subscriber.rb b/app/subscribers/solidus_subscriptions/churn_buster_subscriber.rb
new file mode 100644
index 0000000..aa9dfa4
--- /dev/null
+++ b/app/subscribers/solidus_subscriptions/churn_buster_subscriber.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module SolidusSubscriptions
+ module ChurnBusterSubscriber
+ include ::Spree::Event::Subscriber
+
+ event_action :report_subscription_cancellation, event_name: 'solidus_subscriptions.subscription_canceled'
+ event_action :report_subscription_ending, event_name: 'solidus_subscriptions.subscription_ended'
+ event_action :report_payment_success, event_name: 'solidus_subscriptions.installments_succeeded'
+ event_action :report_payment_failure, event_name: 'solidus_subscriptions.installments_failed_payment'
+ event_action :report_payment_method_change, event_name: 'solidus_subscriptions.subscription_payment_method_changed'
+
+ def report_subscription_cancellation(event)
+ churn_buster&.report_subscription_cancellation(event.payload.fetch(:subscription))
+ end
+
+ def report_subscription_ending(event)
+ churn_buster&.report_subscription_cancellation(event.payload.fetch(:subscription))
+ end
+
+ def report_payment_success(event)
+ churn_buster&.report_successful_payment(event.payload.fetch(:order))
+ end
+
+ def report_payment_failure(event)
+ churn_buster&.report_failed_payment(event.payload.fetch(:order))
+ end
+
+ def report_payment_method_change(event)
+ churn_buster&.report_payment_method_change(event.payload.fetch(:subscription))
+ end
+
+ private
+
+ def churn_buster
+ SolidusSubscriptions.churn_buster
+ end
+ end
+end
diff --git a/config/initializers/subscribers.rb b/config/initializers/subscribers.rb
index fe4c88b..486fe72 100644
--- a/config/initializers/subscribers.rb
+++ b/config/initializers/subscribers.rb
@@ -1,5 +1,9 @@
# frozen_string_literal: true
-Spree.config do |config|
- config.events.subscribers << 'SolidusSubscriptions::EventStorageSubscriber'
+if Spree.solidus_gem_version < Gem::Version.new('2.11.0')
+ require SolidusSubscriptions::Engine.root.join('app/subscribers/solidus_subscriptions/event_storage_subscriber')
+ require SolidusSubscriptions::Engine.root.join('app/subscribers/solidus_subscriptions/churn_buster_subscriber')
+
+ SolidusSubscriptions::ChurnBusterSubscriber.subscribe!
+ SolidusSubscriptions::EventStorageSubscriber.subscribe!
end
diff --git a/lib/generators/solidus_subscriptions/install/templates/initializer.rb b/lib/generators/solidus_subscriptions/install/templates/initializer.rb
index 17a6f51..e1e9731 100644
--- a/lib/generators/solidus_subscriptions/install/templates/initializer.rb
+++ b/lib/generators/solidus_subscriptions/install/templates/initializer.rb
@@ -64,4 +64,18 @@ SolidusSubscriptions.configure do |config|
# :interval_units,
# :end_date,
# ]
+
+ # ========================================= Churn Buster =========================================
+ #
+ # This extension can integrate with Churn Buster for churn mitigation and failed payment recovery.
+ # If you want to integrate with Churn Buster, simply configure your credentials below.
+ #
+ # NOTE: If you integrate with Churn Buster and override any of the handlers, make sure to call
+ # `super` or copy-paste the original integration code or things won't work!
+
+ # Your Churn Buster account ID.
+ # config.churn_buster_account_id = 'YOUR_CHURN_BUSTER_ACCOUNT_ID'
+
+ # Your Churn Buster API key.
+ # config.churn_buster_api_key = 'YOUR_CHURN_BUSTER_API_KEY'
end
diff --git a/lib/solidus_subscriptions.rb b/lib/solidus_subscriptions.rb
index 039b172..e9dae93 100644
--- a/lib/solidus_subscriptions.rb
+++ b/lib/solidus_subscriptions.rb
@@ -4,6 +4,7 @@ require 'solidus_core'
require 'solidus_support'
require 'deface'
+require 'httparty'
require 'state_machines'
require 'solidus_subscriptions/configuration'
@@ -11,6 +12,12 @@ require 'solidus_subscriptions/permission_sets/default_customer'
require 'solidus_subscriptions/permission_sets/subscription_management'
require 'solidus_subscriptions/version'
require 'solidus_subscriptions/engine'
+require 'solidus_subscriptions/churn_buster/client'
+require 'solidus_subscriptions/churn_buster/serializer'
+require 'solidus_subscriptions/churn_buster/subscription_customer_serializer'
+require 'solidus_subscriptions/churn_buster/subscription_payment_method_serializer'
+require 'solidus_subscriptions/churn_buster/subscription_serializer'
+require 'solidus_subscriptions/churn_buster/order_serializer'
module SolidusSubscriptions
class << self
@@ -21,5 +28,14 @@ module SolidusSubscriptions
def configuration
@configuration ||= Configuration.new
end
+
+ def churn_buster
+ return unless configuration.churn_buster?
+
+ @churn_buster ||= ChurnBuster::Client.new(
+ account_id: SolidusSubscriptions.configuration.churn_buster_account_id,
+ api_key: SolidusSubscriptions.configuration.churn_buster_api_key,
+ )
+ end
end
end
diff --git a/lib/solidus_subscriptions/churn_buster/client.rb b/lib/solidus_subscriptions/churn_buster/client.rb
new file mode 100644
index 0000000..915bb40
--- /dev/null
+++ b/lib/solidus_subscriptions/churn_buster/client.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module SolidusSubscriptions
+ module ChurnBuster
+ class Client
+ BASE_API_URL = 'https://api.churnbuster.io/v1'
+
+ attr_reader :account_id, :api_key
+
+ def initialize(account_id:, api_key:)
+ @account_id = account_id
+ @api_key = api_key
+ end
+
+ def report_failed_payment(order)
+ post('/failed_payments', OrderSerializer.serialize(order))
+ end
+
+ def report_successful_payment(order)
+ post('/successful_payments', OrderSerializer.serialize(order))
+ end
+
+ def report_subscription_cancellation(subscription)
+ post('/cancellations', SubscriptionSerializer.serialize(subscription))
+ end
+
+ def report_payment_method_change(subscription)
+ post('/payment_methods', SubscriptionPaymentMethodSerializer.serialize(subscription))
+ end
+
+ private
+
+ def post(path, body)
+ HTTParty.post(
+ "#{BASE_API_URL}#{path}",
+ body: body.to_json,
+ headers: {
+ 'Content-Type' => 'application/json',
+ },
+ basic_auth: {
+ username: account_id,
+ password: api_key,
+ },
+ )
+ end
+ end
+ end
+end
diff --git a/lib/solidus_subscriptions/churn_buster/order_serializer.rb b/lib/solidus_subscriptions/churn_buster/order_serializer.rb
new file mode 100644
index 0000000..5bc5dee
--- /dev/null
+++ b/lib/solidus_subscriptions/churn_buster/order_serializer.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module SolidusSubscriptions
+ module ChurnBuster
+ class OrderSerializer < Serializer
+ def to_h
+ {
+ payment: {
+ source: 'in_house',
+ source_id: object.number,
+ amount_in_cents: object.display_total.cents,
+ currency: object.currency,
+ },
+ customer: SubscriptionCustomerSerializer.serialize(object.subscription),
+ }
+ end
+ end
+ end
+end
diff --git a/lib/solidus_subscriptions/churn_buster/serializer.rb b/lib/solidus_subscriptions/churn_buster/serializer.rb
new file mode 100644
index 0000000..d8b83bb
--- /dev/null
+++ b/lib/solidus_subscriptions/churn_buster/serializer.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module SolidusSubscriptions
+ module ChurnBuster
+ class Serializer
+ attr_reader :object
+
+ class << self
+ def serialize(object)
+ new(object).to_h
+ end
+ end
+
+ def initialize(object)
+ @object = object
+ end
+
+ def to_h
+ raise NotImplementedError
+ end
+ end
+ end
+end
diff --git a/lib/solidus_subscriptions/churn_buster/subscription_customer_serializer.rb b/lib/solidus_subscriptions/churn_buster/subscription_customer_serializer.rb
new file mode 100644
index 0000000..700c1c6
--- /dev/null
+++ b/lib/solidus_subscriptions/churn_buster/subscription_customer_serializer.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module SolidusSubscriptions
+ module ChurnBuster
+ class SubscriptionCustomerSerializer < Serializer
+ def to_h
+ {
+ source: 'in_house',
+ source_id: object.id,
+ email: object.user.email,
+ properties: {
+ first_name: object.shipping_address_to_use.firstname,
+ last_name: object.shipping_address_to_use.lastname,
+ },
+ }
+ end
+ end
+ end
+end
diff --git a/lib/solidus_subscriptions/churn_buster/subscription_payment_method_serializer.rb b/lib/solidus_subscriptions/churn_buster/subscription_payment_method_serializer.rb
new file mode 100644
index 0000000..984059d
--- /dev/null
+++ b/lib/solidus_subscriptions/churn_buster/subscription_payment_method_serializer.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module SolidusSubscriptions
+ module ChurnBuster
+ class SubscriptionPaymentMethodSerializer < Serializer
+ def to_h
+ {
+ payment_method: {
+ source: 'in_house',
+ source_id: [
+ object.payment_method_to_use&.id,
+ object.payment_source_to_use&.id
+ ].compact.join('-'),
+ type: 'card',
+ properties: payment_source_properties,
+ },
+ customer: SubscriptionCustomerSerializer.serialize(object),
+ }
+ end
+
+ private
+
+ def payment_source_properties
+ if object.payment_source.is_a?(::Spree::CreditCard)
+ {
+ brand: object.payment_source.cc_type,
+ last4: object.payment_source.last_digits,
+ exp_month: object.payment_source.month,
+ exp_year: object.payment_source.year,
+ }
+ else
+ {}
+ end
+ end
+ end
+ end
+end
diff --git a/lib/solidus_subscriptions/churn_buster/subscription_serializer.rb b/lib/solidus_subscriptions/churn_buster/subscription_serializer.rb
new file mode 100644
index 0000000..06c08d1
--- /dev/null
+++ b/lib/solidus_subscriptions/churn_buster/subscription_serializer.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module SolidusSubscriptions
+ module ChurnBuster
+ class SubscriptionSerializer < Serializer
+ def to_h
+ {
+ subscription: {
+ source: 'in_house',
+ source_id: object.id
+ },
+ customer: SubscriptionCustomerSerializer.serialize(object),
+ }
+ end
+ end
+ end
+end
diff --git a/lib/solidus_subscriptions/configuration.rb b/lib/solidus_subscriptions/configuration.rb
index d5dc80f..22b7d84 100644
--- a/lib/solidus_subscriptions/configuration.rb
+++ b/lib/solidus_subscriptions/configuration.rb
@@ -2,7 +2,9 @@
module SolidusSubscriptions
class Configuration
- attr_accessor :maximum_total_skips
+ attr_accessor(
+ :maximum_total_skips, :churn_buster_account_id, :churn_buster_api_key,
+ )
attr_writer(
:success_dispatcher_class, :failure_dispatcher_class, :payment_failed_dispatcher_class,
@@ -73,5 +75,9 @@ module SolidusSubscriptions
@subscribable_class ||= 'Spree::Variant'
@subscribable_class.constantize
end
+
+ def churn_buster?
+ churn_buster_account_id.present? && churn_buster_api_key.present?
+ end
end
end
diff --git a/solidus_subscriptions.gemspec b/solidus_subscriptions.gemspec
index a084471..46fec6e 100644
--- a/solidus_subscriptions.gemspec
+++ b/solidus_subscriptions.gemspec
@@ -30,6 +30,7 @@ Gem::Specification.new do |spec|
spec.require_paths = ["lib"]
spec.add_dependency 'deface'
+ spec.add_dependency 'httparty', '~> 0.18'
spec.add_dependency 'i18n'
spec.add_dependency 'solidus_core', ['>= 2.0.0', '< 3']
spec.add_dependency 'solidus_support', '~> 0.5'
@@ -39,6 +40,8 @@ Gem::Specification.new do |spec|
spec.add_development_dependency 'shoulda-matchers', '~> 4.4'
spec.add_development_dependency 'solidus_dev_support', '~> 2.0'
spec.add_development_dependency 'timecop'
+ spec.add_development_dependency 'vcr'
spec.add_development_dependency 'versioncake'
+ spec.add_development_dependency 'webmock'
spec.add_development_dependency 'yard'
end
diff --git a/spec/fixtures/cassettes/churn_buster.yml b/spec/fixtures/cassettes/churn_buster.yml
new file mode 100644
index 0000000..0d3803a
--- /dev/null
+++ b/spec/fixtures/cassettes/churn_buster.yml
@@ -0,0 +1,229 @@
+---
+http_interactions:
+- request:
+ method: post
+ uri: https://api.churnbuster.io/v1/successful_payments
+ body:
+ encoding: UTF-8
+ string: '{"payment":{"source":"in_house","source_id":"R863897282","amount_in_cents":0,"currency":"USD"},"customer":{"source":"in_house","source_id":1,"email":"email2@example.com","properties":{"first_name":"John","last_name":"John"}}}'
+ headers:
+ Content-Type:
+ - application/json
+ Accept-Encoding:
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
+ Accept:
+ - "*/*"
+ User-Agent:
+ - Ruby
+ response:
+ status:
+ code: 200
+ message: OK
+ headers:
+ Server:
+ - Cowboy
+ Date:
+ - Thu, 01 Oct 2020 13:46:09 GMT
+ Connection:
+ - keep-alive
+ X-Frame-Options:
+ - SAMEORIGIN
+ X-Xss-Protection:
+ - 1; mode=block
+ X-Content-Type-Options:
+ - nosniff
+ X-Download-Options:
+ - noopen
+ X-Permitted-Cross-Domain-Policies:
+ - none
+ Referrer-Policy:
+ - strict-origin-when-cross-origin
+ Content-Type:
+ - application/json
+ Cache-Control:
+ - no-cache
+ X-Request-Id:
+ - a664eaa0-2735-4a88-990d-fb9c29766e00
+ X-Runtime:
+ - '0.070480'
+ Strict-Transport-Security:
+ - max-age=31536000; includeSubDomains
+ Transfer-Encoding:
+ - chunked
+ Via:
+ - 1.1 vegur
+ body:
+ encoding: UTF-8
+ string: ''
+ recorded_at: Thu, 01 Oct 2020 13:46:09 GMT
+- request:
+ method: post
+ uri: https://api.churnbuster.io/v1/failed_payments
+ body:
+ encoding: UTF-8
+ string: '{"payment":{"source":"in_house","source_id":"R276044153","amount_in_cents":0,"currency":"USD"},"customer":{"source":"in_house","source_id":1,"email":"email2@example.com","properties":{"first_name":"John","last_name":"John"}}}'
+ headers:
+ Content-Type:
+ - application/json
+ Accept-Encoding:
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
+ Accept:
+ - "*/*"
+ User-Agent:
+ - Ruby
+ response:
+ status:
+ code: 200
+ message: OK
+ headers:
+ Server:
+ - Cowboy
+ Date:
+ - Thu, 01 Oct 2020 14:24:32 GMT
+ Connection:
+ - keep-alive
+ X-Frame-Options:
+ - SAMEORIGIN
+ X-Xss-Protection:
+ - 1; mode=block
+ X-Content-Type-Options:
+ - nosniff
+ X-Download-Options:
+ - noopen
+ X-Permitted-Cross-Domain-Policies:
+ - none
+ Referrer-Policy:
+ - strict-origin-when-cross-origin
+ Content-Type:
+ - application/json
+ Cache-Control:
+ - no-cache
+ X-Request-Id:
+ - bfd0c00b-dafa-4122-95e3-6fae6676bf05
+ X-Runtime:
+ - '0.052157'
+ Strict-Transport-Security:
+ - max-age=31536000; includeSubDomains
+ Transfer-Encoding:
+ - chunked
+ Via:
+ - 1.1 vegur
+ body:
+ encoding: UTF-8
+ string: ''
+ recorded_at: Thu, 01 Oct 2020 14:24:32 GMT
+- request:
+ method: post
+ uri: https://api.churnbuster.io/v1/cancellations
+ body:
+ encoding: UTF-8
+ string: '{"subscription":{"source":"in_house","source_id":1},"customer":{"source":"in_house","source_id":1,"email":"email5@example.com","properties":{"first_name":"John","last_name":"Von
+ Doe"}}}'
+ headers:
+ Content-Type:
+ - application/json
+ Accept-Encoding:
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
+ Accept:
+ - "*/*"
+ User-Agent:
+ - Ruby
+ response:
+ status:
+ code: 200
+ message: OK
+ headers:
+ Server:
+ - Cowboy
+ Date:
+ - Thu, 01 Oct 2020 14:24:33 GMT
+ Connection:
+ - keep-alive
+ X-Frame-Options:
+ - SAMEORIGIN
+ X-Xss-Protection:
+ - 1; mode=block
+ X-Content-Type-Options:
+ - nosniff
+ X-Download-Options:
+ - noopen
+ X-Permitted-Cross-Domain-Policies:
+ - none
+ Referrer-Policy:
+ - strict-origin-when-cross-origin
+ Content-Type:
+ - application/json
+ Cache-Control:
+ - no-cache
+ X-Request-Id:
+ - aa57bbfb-aa49-4f4c-bad9-a3e9a87335bd
+ X-Runtime:
+ - '0.070606'
+ Strict-Transport-Security:
+ - max-age=31536000; includeSubDomains
+ Transfer-Encoding:
+ - chunked
+ Via:
+ - 1.1 vegur
+ body:
+ encoding: UTF-8
+ string: ''
+ recorded_at: Thu, 01 Oct 2020 14:24:33 GMT
+- request:
+ method: post
+ uri: https://api.churnbuster.io/v1/payment_methods
+ body:
+ encoding: UTF-8
+ string: '{"payment_method":{"source":"in_house","source_id":"1-1","type":"card","properties":{}},"customer":{"source":"in_house","source_id":1,"email":"email6@example.com","properties":{"first_name":"John","last_name":"Von
+ Doe"}}}'
+ headers:
+ Content-Type:
+ - application/json
+ Accept-Encoding:
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
+ Accept:
+ - "*/*"
+ User-Agent:
+ - Ruby
+ response:
+ status:
+ code: 200
+ message: OK
+ headers:
+ Server:
+ - Cowboy
+ Date:
+ - Mon, 05 Oct 2020 10:55:30 GMT
+ Connection:
+ - keep-alive
+ X-Frame-Options:
+ - SAMEORIGIN
+ X-Xss-Protection:
+ - 1; mode=block
+ X-Content-Type-Options:
+ - nosniff
+ X-Download-Options:
+ - noopen
+ X-Permitted-Cross-Domain-Policies:
+ - none
+ Referrer-Policy:
+ - strict-origin-when-cross-origin
+ Content-Type:
+ - application/json
+ Cache-Control:
+ - no-cache
+ X-Request-Id:
+ - 585f1c6a-fb36-4cb4-9127-2f80636d33d9
+ X-Runtime:
+ - '1.184629'
+ Strict-Transport-Security:
+ - max-age=31536000; includeSubDomains
+ Transfer-Encoding:
+ - chunked
+ Via:
+ - 1.1 vegur
+ body:
+ encoding: UTF-8
+ string: ''
+ recorded_at: Mon, 05 Oct 2020 10:55:31 GMT
+recorded_with: VCR 6.0.0
diff --git a/spec/lib/solidus_subscriptions/churn_buster/client_spec.rb b/spec/lib/solidus_subscriptions/churn_buster/client_spec.rb
new file mode 100644
index 0000000..abb2b95
--- /dev/null
+++ b/spec/lib/solidus_subscriptions/churn_buster/client_spec.rb
@@ -0,0 +1,57 @@
+RSpec.describe SolidusSubscriptions::ChurnBuster::Client, vcr: { cassette_name: 'churn_buster', record: :new_episodes } do
+ describe '#report_failed_payment' do
+ it 'reports the failed payment to Churn Buster' do
+ client = described_class.new(
+ account_id: 'test_account_id',
+ api_key: 'test_api_key',
+ )
+
+ order = create(:order, subscription: create(:subscription))
+ response = client.report_failed_payment(order)
+
+ expect(response).to be_success
+ end
+ end
+
+ describe '#report_successful_payment' do
+ it 'reports the successful payment to Churn Buster' do
+ client = described_class.new(
+ account_id: 'test_account_id',
+ api_key: 'test_api_key',
+ )
+
+ order = create(:order, subscription: create(:subscription))
+ response = client.report_successful_payment(order)
+
+ expect(response).to be_success
+ end
+ end
+
+ describe '#report_subscription_cancellation' do
+ it 'reports the failed payment to Churn Buster' do
+ client = described_class.new(
+ account_id: 'test_account_id',
+ api_key: 'test_api_key',
+ )
+
+ subscription = create(:subscription)
+ response = client.report_subscription_cancellation(subscription)
+
+ expect(response).to be_success
+ end
+ end
+
+ describe '#report_payment_method_change' do
+ it 'reports the payment method change to Churn Buster' do
+ client = described_class.new(
+ account_id: 'test_account_id',
+ api_key: 'test_api_key',
+ )
+
+ subscription = create(:subscription)
+ response = client.report_payment_method_change(subscription)
+
+ expect(response).to be_success
+ end
+ end
+end
diff --git a/spec/lib/solidus_subscriptions_spec.rb b/spec/lib/solidus_subscriptions_spec.rb
new file mode 100644
index 0000000..201c576
--- /dev/null
+++ b/spec/lib/solidus_subscriptions_spec.rb
@@ -0,0 +1,28 @@
+RSpec.describe SolidusSubscriptions do
+ describe '.churn_buster' do
+ context 'when Churn Buster was configured' do
+ it 'returns a Churn Buster client instance' do
+ allow(described_class.configuration).to receive_messages(
+ churn_buster?: true,
+ churn_buster_account_id: 'account_id',
+ churn_buster_api_key: 'api_key',
+ )
+ churn_buster = instance_double(SolidusSubscriptions::ChurnBuster::Client)
+ allow(SolidusSubscriptions::ChurnBuster::Client).to receive(:new).with(
+ account_id: 'account_id',
+ api_key: 'api_key',
+ ).and_return(churn_buster)
+
+ expect(described_class.churn_buster).to eq(churn_buster)
+ end
+ end
+
+ context 'when Churn Buster was not configured' do
+ it 'returns nil' do
+ allow(described_class.configuration).to receive_messages(churn_buster?: false)
+
+ expect(described_class.churn_buster).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/models/solidus_subscriptions/subscription_spec.rb b/spec/models/solidus_subscriptions/subscription_spec.rb
index 60a37e6..21e1015 100644
--- a/spec/models/solidus_subscriptions/subscription_spec.rb
+++ b/spec/models/solidus_subscriptions/subscription_spec.rb
@@ -64,6 +64,18 @@ RSpec.describe SolidusSubscriptions::Subscription, type: :model do
subscription: subscription,
)
end
+
+ it 'tracks payment method changes' do
+ stub_const('Spree::Event', class_spy(Spree::Event))
+
+ subscription = create(:subscription)
+ subscription.update!(payment_source: create(:credit_card))
+
+ expect(Spree::Event).to have_received(:fire).with(
+ 'solidus_subscriptions.subscription_payment_method_changed',
+ subscription: subscription,
+ )
+ end
end
describe '#cancel' do
@@ -445,7 +457,7 @@ RSpec.describe SolidusSubscriptions::Subscription, type: :model do
end
context 'when the subscription has no payment method' do
- it "returns the default source from the user's wallet" do
+ it "returns the default source from the user's wallet_payment_source" do
user = create(:user)
payment_source = create(:credit_card, gateway_customer_profile_id: 'BGS-123', user: user)
wallet_payment_source = user.wallet.add(payment_source)
@@ -487,7 +499,7 @@ RSpec.describe SolidusSubscriptions::Subscription, type: :model do
end
context 'when the subscription has no payment method' do
- it "returns the method from the default source in the user's wallet" do
+ it "returns the method from the default source in the user's wallet_payment_source" do
user = create(:user)
payment_source = create(:credit_card, gateway_customer_profile_id: 'BGS-123', user: user)
wallet_payment_source = user.wallet.add(payment_source)
diff --git a/spec/models/spree/wallet_payment_source_spec.rb b/spec/models/spree/wallet_payment_source_spec.rb
new file mode 100644
index 0000000..374e235
--- /dev/null
+++ b/spec/models/spree/wallet_payment_source_spec.rb
@@ -0,0 +1,18 @@
+RSpec.describe Spree::WalletPaymentSource do
+ describe 'setting it as the default' do
+ it 'reports a payment method changed event for subscriptions that use the default payment source' do
+ stub_const('Spree::Event', class_spy(Spree::Event))
+ user = create(:user)
+ subscription = create(:subscription, user: user)
+ payment_source = create(:credit_card, user: user)
+ wallet_payment_source = user.wallet.add(payment_source)
+
+ user.wallet.default_wallet_payment_source = wallet_payment_source
+
+ expect(Spree::Event).to have_received(:fire).with(
+ 'solidus_subscriptions.subscription_payment_method_changed',
+ subscription: subscription,
+ )
+ end
+ end
+end
diff --git a/spec/services/solidus_subscriptions/payment_failed_dispatcher_spec.rb b/spec/services/solidus_subscriptions/payment_failed_dispatcher_spec.rb
index 18a018d..a6a8a0c 100644
--- a/spec/services/solidus_subscriptions/payment_failed_dispatcher_spec.rb
+++ b/spec/services/solidus_subscriptions/payment_failed_dispatcher_spec.rb
@@ -23,5 +23,20 @@ RSpec.describe SolidusSubscriptions::PaymentFailedDispatcher do
expect(order.state).to eq('canceled')
end
+
+ it 'fires an installments_failed_payment event' do
+ stub_const('Spree::Event', class_spy(Spree::Event))
+ installments = Array.new(2) { instance_spy(SolidusSubscriptions::Installment) }
+ order = create(:order_with_line_items)
+
+ dispatcher = described_class.new(installments, order)
+ dispatcher.dispatch
+
+ expect(Spree::Event).to have_received(:fire).with(
+ 'solidus_subscriptions.installments_failed_payment',
+ installments: installments,
+ order: order,
+ )
+ end
end
end
diff --git a/spec/services/solidus_subscriptions/success_dispatcher_spec.rb b/spec/services/solidus_subscriptions/success_dispatcher_spec.rb
index e8a6c3d..ce41638 100644
--- a/spec/services/solidus_subscriptions/success_dispatcher_spec.rb
+++ b/spec/services/solidus_subscriptions/success_dispatcher_spec.rb
@@ -9,5 +9,20 @@ RSpec.describe SolidusSubscriptions::SuccessDispatcher do
expect(installments).to all(have_received(:success!).with(order).once)
end
+
+ it 'fires an installments_succeeded event' do
+ stub_const('Spree::Event', class_spy(Spree::Event))
+ installments = Array.new(2) { instance_spy(SolidusSubscriptions::Installment) }
+ order = create(:order_with_line_items)
+
+ dispatcher = described_class.new(installments, order)
+ dispatcher.dispatch
+
+ expect(Spree::Event).to have_received(:fire).with(
+ 'solidus_subscriptions.installments_succeeded',
+ installments: installments,
+ order: order,
+ )
+ end
end
end
diff --git a/spec/subscribers/solidus_subscriptions/churn_buster_subscriber_spec.rb b/spec/subscribers/solidus_subscriptions/churn_buster_subscriber_spec.rb
new file mode 100644
index 0000000..572913b
--- /dev/null
+++ b/spec/subscribers/solidus_subscriptions/churn_buster_subscriber_spec.rb
@@ -0,0 +1,74 @@
+RSpec.describe SolidusSubscriptions::ChurnBusterSubscriber do
+ describe '#report_subscription_cancellation' do
+ it 'reports the cancellation to Churn Buster' do
+ churn_buster = instance_spy(SolidusSubscriptions::ChurnBuster::Client)
+ allow(SolidusSubscriptions).to receive(:churn_buster).and_return(churn_buster)
+
+ subscription = create(:subscription)
+ Spree::Event.fire('solidus_subscriptions.subscription_canceled', subscription: subscription)
+
+ expect(churn_buster).to have_received(:report_subscription_cancellation).with(subscription)
+ end
+ end
+
+ describe '#report_subscription_ending' do
+ it 'reports the cancellation to Churn Buster' do
+ churn_buster = instance_spy(SolidusSubscriptions::ChurnBuster::Client)
+ allow(SolidusSubscriptions).to receive(:churn_buster).and_return(churn_buster)
+
+ subscription = create(:subscription)
+ Spree::Event.fire('solidus_subscriptions.subscription_ended', subscription: subscription)
+
+ expect(churn_buster).to have_received(:report_subscription_cancellation).with(subscription)
+ end
+ end
+
+ describe '#report_payment_success' do
+ it 'reports the success to Churn Buster' do
+ churn_buster = instance_spy(SolidusSubscriptions::ChurnBuster::Client)
+ allow(SolidusSubscriptions).to receive(:churn_buster).and_return(churn_buster)
+
+ order = build_stubbed(:order)
+ installments = build_list(:installment, 2)
+ Spree::Event.fire(
+ 'solidus_subscriptions.installments_succeeded',
+ installments: installments,
+ order: order,
+ )
+
+ expect(churn_buster).to have_received(:report_successful_payment).with(order)
+ end
+ end
+
+ describe '#report_payment_failure' do
+ it 'reports the failure to Churn Buster' do
+ churn_buster = instance_spy(SolidusSubscriptions::ChurnBuster::Client)
+ allow(SolidusSubscriptions).to receive(:churn_buster).and_return(churn_buster)
+
+ order = build_stubbed(:order)
+ installments = build_list(:installment, 2)
+ Spree::Event.fire(
+ 'solidus_subscriptions.installments_failed_payment',
+ installments: installments,
+ order: order,
+ )
+
+ expect(churn_buster).to have_received(:report_failed_payment).with(order)
+ end
+ end
+
+ describe '#report_payment_method_change' do
+ it 'reports the payment method change to Churn Buster' do
+ churn_buster = instance_spy(SolidusSubscriptions::ChurnBuster::Client)
+ allow(SolidusSubscriptions).to receive(:churn_buster).and_return(churn_buster)
+
+ subscription = create(:subscription)
+ Spree::Event.fire(
+ 'solidus_subscriptions.subscription_payment_method_changed',
+ subscription: subscription,
+ )
+
+ expect(churn_buster).to have_received(:report_payment_method_change).with(subscription)
+ end
+ end
+end
diff --git a/spec/support/vcr.rb b/spec/support/vcr.rb
new file mode 100644
index 0000000..ca8a14a
--- /dev/null
+++ b/spec/support/vcr.rb
@@ -0,0 +1,10 @@
+require 'webmock/rspec'
+require 'vcr'
+
+WebMock.disable_net_connect!
+
+VCR.configure do |config|
+ config.cassette_library_dir = "spec/fixtures/cassettes"
+ config.hook_into :webmock
+ config.configure_rspec_metadata!
+end