core_merchant 0.6.2 → 0.8.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/Gemfile.lock +4 -1
- data/README.md +2 -2
- data/core_merchant.gemspec +1 -0
- data/lib/core_merchant/concerns/subscription_manager_renewals.rb +53 -0
- data/lib/core_merchant/concerns/subscription_notifications.rb +23 -0
- data/lib/core_merchant/concerns/subscription_state_machine.rb +15 -12
- data/lib/core_merchant/customer_behavior.rb +10 -9
- data/lib/core_merchant/subscription.rb +78 -26
- data/lib/core_merchant/subscription_listener.rb +54 -0
- data/lib/core_merchant/subscription_manager.rb +105 -0
- data/lib/core_merchant/version.rb +1 -1
- data/lib/core_merchant.rb +13 -8
- data/lib/generators/core_merchant/install_generator.rb +9 -11
- data/lib/generators/core_merchant/templates/core_merchant.rb +26 -0
- metadata +21 -3
- data/lib/generators/core_merchant/templates/core_merchant.erb +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2bd7b45750d9491ddf78c12b41e73a1d79d8bc6fa5cdff69df22f536b1765e42
|
4
|
+
data.tar.gz: 056e3021e60c7c5edb4a301dae4a29b6184ad3889b55c81e226dccfcfcb00020
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f916fe4fb53d28a9fbb61c8d7306825cb96d82ceeb559a6aeea1373c83e5401c785c0400dd3b41f0d5f9a4c321f0cc91c85eddead5808d546c7fb09d6b671aa7
|
7
|
+
data.tar.gz: 218935fdbb73a113262735e5b4a0290d7942121b167932ad00a5ead4c1a75ef69d8f18dfffca5fea14aafbbb8bf79f6cb940042f15f0989a59ce1e6c14a58208
|
data/.rubocop.yml
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
core_merchant (0.
|
4
|
+
core_merchant (0.8.0)
|
5
5
|
activesupport (~> 7.0)
|
6
6
|
rails (~> 7.0)
|
7
7
|
|
@@ -99,6 +99,8 @@ GEM
|
|
99
99
|
diff-lcs (1.5.1)
|
100
100
|
drb (2.2.1)
|
101
101
|
erubi (1.13.0)
|
102
|
+
factory_bot (6.4.6)
|
103
|
+
activesupport (>= 5.0.0)
|
102
104
|
generator_spec (0.9.5)
|
103
105
|
activesupport (>= 3.0.0)
|
104
106
|
railties (>= 3.0.0)
|
@@ -251,6 +253,7 @@ PLATFORMS
|
|
251
253
|
DEPENDENCIES
|
252
254
|
core_merchant!
|
253
255
|
database_cleaner
|
256
|
+
factory_bot
|
254
257
|
generator_spec (~> 0.9.4)
|
255
258
|
railties (~> 7.0)
|
256
259
|
rake (~> 13.0)
|
data/README.md
CHANGED
@@ -9,8 +9,8 @@ CoreMerchant is a library for customer, product, and subscription management in
|
|
9
9
|
- [X] Add customer behavior
|
10
10
|
- [X] Add initializer generator
|
11
11
|
- [X] Add SubscriptionPlan model
|
12
|
-
- [
|
13
|
-
- [ ]
|
12
|
+
- [X] Add Subscription model
|
13
|
+
- [ ] Implement subscription manager and callbacks
|
14
14
|
- [ ] Add sidekiq jobs for subscription management
|
15
15
|
- [ ] Add Invoice model
|
16
16
|
- [ ] Add billing and invoicing service
|
data/core_merchant.gemspec
CHANGED
@@ -37,6 +37,7 @@ Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength
|
|
37
37
|
spec.add_dependency "rails", "~> 7.0"
|
38
38
|
|
39
39
|
spec.add_development_dependency "database_cleaner"
|
40
|
+
spec.add_development_dependency "factory_bot"
|
40
41
|
spec.add_development_dependency "generator_spec", "~> 0.9.4"
|
41
42
|
spec.add_development_dependency "railties", "~> 7.0"
|
42
43
|
spec.add_development_dependency "rspec-rails", "~> 5.0"
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CoreMerchant
|
4
|
+
module Concerns
|
5
|
+
# Includes logic for renewal processing, grace period handling, and expiration checking.
|
6
|
+
module SubscriptionManagerRenewals
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do # rubocop:disable Metrics/BlockLength
|
10
|
+
def check_renewals
|
11
|
+
Subscription.find_each do |subscription|
|
12
|
+
process_for_renewal(subscription) if subscription.due_for_renewal?
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def process_for_renewal(subscription)
|
17
|
+
return unless subscription.transition_to_processing_renewal
|
18
|
+
|
19
|
+
notify(subscription, :due_for_renewal)
|
20
|
+
end
|
21
|
+
|
22
|
+
def no_payment_needed_for_renewal(subscription)
|
23
|
+
return unless subscription.transition_to_active
|
24
|
+
|
25
|
+
notify(subscription, :renewed)
|
26
|
+
end
|
27
|
+
|
28
|
+
def processing_payment_for_renewal(subscription)
|
29
|
+
return unless subscription.transition_to_processing_payment
|
30
|
+
|
31
|
+
notify(subscription, :renewal_payment_processing)
|
32
|
+
end
|
33
|
+
|
34
|
+
def payment_successful_for_renewal(subscription)
|
35
|
+
return unless subscription.transition_to_active
|
36
|
+
|
37
|
+
notify(subscription, :renewed)
|
38
|
+
end
|
39
|
+
|
40
|
+
def payment_failed_for_renewal(subscription)
|
41
|
+
is_in_grace_period = subscription.in_grace_period?
|
42
|
+
if is_in_grace_period
|
43
|
+
subscription.transition_to_past_due
|
44
|
+
notify(subscription, :grace_period_started, days_remaining: subscription.days_remaining_in_grace_period)
|
45
|
+
else
|
46
|
+
subscription.transition_to_expired
|
47
|
+
notify(subscription, :expired)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
|
5
|
+
module CoreMerchant
|
6
|
+
module Concerns
|
7
|
+
# Includes logic for notifying the SubscriptionManager when a subscription is created or destroyed,
|
8
|
+
# as well as providing a hook for custom notification logic.
|
9
|
+
module SubscriptionNotifications
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
included do
|
13
|
+
# Notify SubscriptionManager on creation and destruction.
|
14
|
+
after_create { notify_subscription_manager(:created) }
|
15
|
+
after_destroy { notify_subscription_manager(:destroyed) }
|
16
|
+
|
17
|
+
def notify_subscription_manager(event, **options)
|
18
|
+
CoreMerchant.subscription_manager.notify(self, event, **options)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -9,26 +9,29 @@ module CoreMerchant
|
|
9
9
|
# Adds state machine logic to a subscription.
|
10
10
|
# This module defines the possible states and transitions for a subscription.
|
11
11
|
# Possible transitions:
|
12
|
-
# - `pending` -> `active`, `trial`
|
13
|
-
# - `trial` -> `active`, `pending_cancellation`, `
|
14
|
-
# - `active` -> `pending_cancellation`, `canceled`, `expired`
|
12
|
+
# - `pending` -> `active`, `trial`, `processing_renewal`
|
13
|
+
# - `trial` -> `processing_renewal`, `active`, `canceled`, `pending_cancellation`, `expired`
|
14
|
+
# - `active` -> `pending_cancellation`, `canceled`, `expired`, `processing_renewal`
|
15
|
+
# - `past_due` -> `processing_renewal`
|
15
16
|
# - `pending_cancellation` -> `canceled`, `expired`
|
16
|
-
# - `
|
17
|
-
# - `
|
17
|
+
# - `processing_renewal` -> `processing_payment`, `active`, `expired`, `past_due`
|
18
|
+
# - `processing_payment` -> `active`, `expired`, `canceled`, `past_due`
|
19
|
+
# - `canceled` -> `pending`, `processing_renewal`
|
20
|
+
# - `expired` -> `pending`, `processing_renewal`
|
18
21
|
module SubscriptionStateMachine
|
19
22
|
extend ActiveSupport::Concern
|
20
23
|
|
21
24
|
# List of possible transitions in the form of { to_state: [from_states] }
|
22
25
|
POSSIBLE_TRANSITIONS = {
|
23
26
|
pending: %i[canceled expired],
|
24
|
-
trial:
|
25
|
-
active: %i[pending trial
|
27
|
+
trial: [:pending],
|
28
|
+
active: %i[pending trial processing_renewal processing_payment],
|
29
|
+
past_due: %i[active processing_renewal processing_payment],
|
26
30
|
pending_cancellation: %i[active trial],
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
pending_change: []
|
31
|
+
processing_renewal: %i[pending trial active past_due canceled expired],
|
32
|
+
processing_payment: [:processing_renewal],
|
33
|
+
canceled: %i[trial active pending_cancellation processing_payment],
|
34
|
+
expired: %i[trial active pending_cancellation processing_renewal processing_payment]
|
32
35
|
}.freeze
|
33
36
|
|
34
37
|
included do
|
@@ -6,18 +6,19 @@ module CoreMerchant
|
|
6
6
|
extend ActiveSupport::Concern
|
7
7
|
|
8
8
|
included do
|
9
|
-
|
9
|
+
has_many :subscriptions, class_name: "CoreMerchant::Subscription", as: :customer, dependent: :destroy
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
def core_merchant_customer_id
|
12
|
+
id
|
13
|
+
end
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
def core_merchant_customer_email
|
16
|
+
email
|
17
|
+
end
|
18
18
|
|
19
|
-
|
20
|
-
|
19
|
+
def core_merchant_customer_name
|
20
|
+
name if respond_to?(:name)
|
21
|
+
end
|
21
22
|
end
|
22
23
|
end
|
23
24
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "core_merchant/concerns/subscription_state_machine"
|
4
|
+
require "core_merchant/concerns/subscription_notifications"
|
4
5
|
|
5
6
|
module CoreMerchant
|
6
7
|
# Represents a subscription in CoreMerchant.
|
@@ -10,8 +11,10 @@ module CoreMerchant
|
|
10
11
|
# - `pending`: Subscription created but not yet started
|
11
12
|
# - `trial`: In a trial period
|
12
13
|
# - `active`: Currently active and paid
|
13
|
-
# - `past_due`: Payment failed but in grace period
|
14
|
+
# - `past_due`: Payment failed but in grace period
|
14
15
|
# - `pending_cancellation`: Will be canceled at period end
|
16
|
+
# - `processing_renewal`: Renewal in progress
|
17
|
+
# - `processing_payment`: Payment processing
|
15
18
|
# - `canceled`: Canceled by user or due to payment failure
|
16
19
|
# - `expired`: Subscription period ended
|
17
20
|
# - `paused`: Temporarily halted, not yet implemented
|
@@ -51,6 +54,7 @@ module CoreMerchant
|
|
51
54
|
# ```
|
52
55
|
class Subscription < ActiveRecord::Base
|
53
56
|
include CoreMerchant::Concerns::SubscriptionStateMachine
|
57
|
+
include CoreMerchant::Concerns::SubscriptionNotifications
|
54
58
|
|
55
59
|
self.table_name = "core_merchant_subscriptions"
|
56
60
|
|
@@ -61,12 +65,14 @@ module CoreMerchant
|
|
61
65
|
pending: 0,
|
62
66
|
trial: 1,
|
63
67
|
active: 2,
|
64
|
-
past_due:
|
65
|
-
pending_cancellation:
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
68
|
+
past_due: 10,
|
69
|
+
pending_cancellation: 11,
|
70
|
+
processing_renewal: 20,
|
71
|
+
processing_payment: 21,
|
72
|
+
canceled: 30,
|
73
|
+
expired: 31,
|
74
|
+
paused: 40, # Logic not yet implemented
|
75
|
+
pending_change: 50 # Logic not yet implemented
|
70
76
|
}
|
71
77
|
|
72
78
|
validates :customer, :subscription_plan, :status, :start_date, presence: true
|
@@ -74,37 +80,83 @@ module CoreMerchant
|
|
74
80
|
validate :end_date_after_start_date, if: :end_date
|
75
81
|
validate :canceled_at_with_reason, if: :canceled_at
|
76
82
|
|
83
|
+
# Starts the subscription.
|
84
|
+
# Sets the current period start and end dates based on the plan's duration.
|
77
85
|
def start
|
78
86
|
new_period_start = start_date
|
79
87
|
new_period_end = new_period_start + subscription_plan.duration_in_date
|
80
88
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
89
|
+
transaction do
|
90
|
+
transition_to_active!
|
91
|
+
update!(
|
92
|
+
current_period_start: new_period_start,
|
93
|
+
current_period_end: new_period_end
|
94
|
+
)
|
95
|
+
end
|
96
|
+
|
97
|
+
notify_subscription_manager(:started)
|
86
98
|
end
|
87
99
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
100
|
+
# Cancels the subscription.
|
101
|
+
# Parameters:
|
102
|
+
# - `reason`: Reason for cancellation
|
103
|
+
# - `at_period_end`: If true, the subscription will be canceled at the end of the current period.
|
104
|
+
# Otherwise, the subscription will be canceled immediately.
|
105
|
+
# Default is `true`.
|
106
|
+
def cancel(reason:, at_period_end: true)
|
107
|
+
transaction do
|
108
|
+
if at_period_end
|
109
|
+
transition_to_pending_cancellation!
|
110
|
+
else
|
111
|
+
transition_to_canceled!
|
112
|
+
end
|
113
|
+
update!(
|
114
|
+
canceled_at: at_period_end ? current_period_end : Time.current,
|
115
|
+
cancellation_reason: reason
|
116
|
+
)
|
93
117
|
end
|
94
|
-
|
95
|
-
|
96
|
-
cancellation_reason: reason
|
97
|
-
)
|
118
|
+
|
119
|
+
notify_subscription_manager(:canceled, reason: reason, immediate: !at_period_end)
|
98
120
|
end
|
99
121
|
|
100
|
-
|
122
|
+
# Returns the days remaining in the current period.
|
123
|
+
# Use to show the user how many days are left before the next renewal or
|
124
|
+
# to refund pro-rated amounts for early cancellations.
|
125
|
+
def days_remaining_in_current_period
|
126
|
+
(current_period_end.to_date - Time.current.to_date).to_i
|
127
|
+
end
|
128
|
+
|
129
|
+
# Returns the number of days as a grace period for past-due subscriptions.
|
130
|
+
# By default, this is 3 days.
|
131
|
+
def grace_period
|
132
|
+
3.days
|
133
|
+
end
|
134
|
+
|
135
|
+
def grace_period_end_date
|
136
|
+
current_period_end + grace_period
|
137
|
+
end
|
101
138
|
|
102
|
-
def
|
103
|
-
|
104
|
-
self.current_period_start ||= start_date
|
105
|
-
self.current_period_end ||= start_date + subscription_plan.duration_in_date
|
139
|
+
def in_grace_period?
|
140
|
+
due_for_renewal? && Time.current <= grace_period_end_date
|
106
141
|
end
|
107
142
|
|
143
|
+
def days_remaining_in_grace_period
|
144
|
+
return 0 unless due_for_renewal?
|
145
|
+
|
146
|
+
(grace_period_end_date.to_date - Time.current.to_date).to_i
|
147
|
+
end
|
148
|
+
|
149
|
+
def grace_period_exceeded?
|
150
|
+
due_for_renewal? && Time.current > grace_period_end_date
|
151
|
+
end
|
152
|
+
|
153
|
+
def due_for_renewal?
|
154
|
+
(active? || trial? || past_due? || processing_renewal? || processing_payment?) &&
|
155
|
+
current_period_end <= Time.current
|
156
|
+
end
|
157
|
+
|
158
|
+
private
|
159
|
+
|
108
160
|
def end_date_after_start_date
|
109
161
|
errors.add(:end_date, "must be after the `start date") if end_date <= start_date
|
110
162
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CoreMerchant
|
4
|
+
# Include this module in your application to listen for subscription events.
|
5
|
+
module SubscriptionListener
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do # rubocop:disable Metrics/BlockLength
|
9
|
+
def on_test_event_received
|
10
|
+
puts "Test event received by CoreMerchant::SubscriptionListener. Override this method in your application."
|
11
|
+
end
|
12
|
+
|
13
|
+
# rubocop:disable Metrics/LineLength
|
14
|
+
|
15
|
+
def on_subscription_created(subscription)
|
16
|
+
puts "Subscription (#{subscription.id}) created. Override #{__method__} in your application to handle this event."
|
17
|
+
end
|
18
|
+
|
19
|
+
def on_subscription_destroyed(subscription)
|
20
|
+
puts "Subscription (#{subscription.id}) destroyed. Override #{__method__} in your application to handle this event."
|
21
|
+
end
|
22
|
+
|
23
|
+
def on_subscription_started(subscription)
|
24
|
+
puts "Subscription (#{subscription.id}) started. Override #{__method__} in your application to handle this event."
|
25
|
+
end
|
26
|
+
|
27
|
+
def on_subscription_canceled(subscription, reason:, immediate:)
|
28
|
+
puts "Subscription (#{subscription.id}) canceled with reason '#{reason}' and immediate=#{immediate}. Override #{__method__} in your application to handle this event."
|
29
|
+
end
|
30
|
+
|
31
|
+
def on_subscription_due_for_renewal(subscription)
|
32
|
+
puts "Subscription (#{subscription.id}) is due for renewal. Override #{__method__} in your application to handle this event."
|
33
|
+
end
|
34
|
+
|
35
|
+
def on_subscription_grace_period_(subscription, days_remaining)
|
36
|
+
puts "Subscription (#{subscription.id}) has entered a grace period with #{days_remaining} days remaining. Override #{__method__} in your application to handle this event."
|
37
|
+
end
|
38
|
+
|
39
|
+
def on_subscription_renewed(subscription)
|
40
|
+
puts "Subscription (#{subscription.id}) renewed. Override #{__method__} in your application to handle this event."
|
41
|
+
end
|
42
|
+
|
43
|
+
def on_subscription_renewal_payment_processing(subscription)
|
44
|
+
puts "Subscription (#{subscription.id}) renewal payment processing. Override #{__method__} in your application to handle this event."
|
45
|
+
end
|
46
|
+
|
47
|
+
def on_subscription_expired(subscription)
|
48
|
+
puts "Subscription (#{subscription.id}) expired. Override #{__method__} in your application to handle this event."
|
49
|
+
end
|
50
|
+
|
51
|
+
# rubocop:enable Metrics/LineLength
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "core_merchant/concerns/subscription_manager_renewals"
|
4
|
+
|
5
|
+
module CoreMerchant
|
6
|
+
# Manages subscriptions in CoreMerchant.
|
7
|
+
# This class is responsible for notifying listeners when subscription events occur.
|
8
|
+
# Attributes:
|
9
|
+
# - `listeners` - An array of listeners that will be notified when subscription events occur.
|
10
|
+
class SubscriptionManager
|
11
|
+
include Concerns::SubscriptionManagerRenewals
|
12
|
+
|
13
|
+
attr_reader :listeners
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@listeners = []
|
17
|
+
end
|
18
|
+
|
19
|
+
def check_subscriptions
|
20
|
+
check_renewals
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_listener(listener)
|
24
|
+
@listeners << listener
|
25
|
+
end
|
26
|
+
|
27
|
+
def notify(subscription, event, **options) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength
|
28
|
+
case event
|
29
|
+
when :created
|
30
|
+
notify_subscription_created(subscription)
|
31
|
+
when :destroyed
|
32
|
+
notify_subscription_destroyed(subscription)
|
33
|
+
when :started
|
34
|
+
notify_subscription_started(subscription)
|
35
|
+
when :canceled
|
36
|
+
notify_subscription_canceled(subscription, options[:reason], options[:immediate])
|
37
|
+
when :due_for_renewal
|
38
|
+
notify_subscription_due_for_renewal(subscription)
|
39
|
+
when :renewed
|
40
|
+
notify_subscription_renewed(subscription)
|
41
|
+
when :renewal_payment_processing
|
42
|
+
notify_subscription_renewal_payment_processing(subscription)
|
43
|
+
when :grace_period_started
|
44
|
+
notify_subscription_grace_period_started(subscription, options[:days_remaining])
|
45
|
+
when :expired
|
46
|
+
notify_subscription_expired(subscription)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def notify_test_event
|
51
|
+
send_notification_to_listeners(nil, :on_test_event_received)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def notify_subscription_created(subscription)
|
57
|
+
send_notification_to_listeners(subscription, :on_subscription_created)
|
58
|
+
end
|
59
|
+
|
60
|
+
def notify_subscription_destroyed(subscription)
|
61
|
+
send_notification_to_listeners(subscription, :on_subscription_destroyed)
|
62
|
+
end
|
63
|
+
|
64
|
+
def notify_subscription_started(subscription)
|
65
|
+
send_notification_to_listeners(subscription, :on_subscription_started)
|
66
|
+
end
|
67
|
+
|
68
|
+
def notify_subscription_canceled(subscription, reason, immediate)
|
69
|
+
send_notification_to_listeners(subscription, :on_subscription_canceled, reason: reason, immediate: immediate)
|
70
|
+
end
|
71
|
+
|
72
|
+
def notify_subscription_due_for_renewal(subscription)
|
73
|
+
send_notification_to_listeners(subscription, :on_subscription_due_for_renewal)
|
74
|
+
end
|
75
|
+
|
76
|
+
def notify_subscription_renewed(subscription)
|
77
|
+
send_notification_to_listeners(subscription, :on_subscription_renewed)
|
78
|
+
end
|
79
|
+
|
80
|
+
def notify_subscription_renewal_payment_processing(subscription)
|
81
|
+
send_notification_to_listeners(subscription, :on_subscription_renewal_payment_processing)
|
82
|
+
end
|
83
|
+
|
84
|
+
def notify_subscription_expired(subscription)
|
85
|
+
send_notification_to_listeners(subscription, :on_subscription_expired)
|
86
|
+
end
|
87
|
+
|
88
|
+
def notify_subscription_grace_period_started(subscription, days_remaining)
|
89
|
+
send_notification_to_listeners(
|
90
|
+
subscription, :on_subscription_grace_period_started,
|
91
|
+
days_remaining: days_remaining
|
92
|
+
)
|
93
|
+
end
|
94
|
+
|
95
|
+
def notify_subscription_grace_period_exceeded(subscription)
|
96
|
+
send_notification_to_listeners(subscription, :on_subscription_grace_period_exceeded)
|
97
|
+
end
|
98
|
+
|
99
|
+
def send_notification_to_listeners(subscription, method_name, **args)
|
100
|
+
@listeners.each do |listener|
|
101
|
+
listener.send(method_name, subscription, **args) if listener.respond_to?(method_name)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
data/lib/core_merchant.rb
CHANGED
@@ -5,6 +5,8 @@ require "active_support/concern"
|
|
5
5
|
require_relative "core_merchant/version"
|
6
6
|
require_relative "core_merchant/customer_behavior"
|
7
7
|
require_relative "core_merchant/subscription_plan"
|
8
|
+
require_relative "core_merchant/subscription_manager"
|
9
|
+
require_relative "core_merchant/subscription_listener"
|
8
10
|
|
9
11
|
# CoreMerchant module
|
10
12
|
module CoreMerchant
|
@@ -19,16 +21,25 @@ module CoreMerchant
|
|
19
21
|
|
20
22
|
def configure
|
21
23
|
yield(configuration)
|
24
|
+
|
25
|
+
return unless configuration.subscription_listener_class
|
26
|
+
|
27
|
+
listener = configuration.subscription_listener_class.constantize.new
|
28
|
+
subscription_manager.add_listener(listener)
|
22
29
|
end
|
23
30
|
|
24
31
|
def customer_class
|
25
32
|
configuration.customer_class.constantize
|
26
33
|
end
|
34
|
+
|
35
|
+
def subscription_manager
|
36
|
+
@subscription_manager ||= CoreMerchant::SubscriptionManager.new
|
37
|
+
end
|
27
38
|
end
|
28
39
|
|
29
40
|
# Used to configure CoreMerchant.
|
30
41
|
class Configuration
|
31
|
-
attr_accessor :customer_class
|
42
|
+
attr_accessor :customer_class, :subscription_listener_class
|
32
43
|
|
33
44
|
def initialize
|
34
45
|
@customer_class = "CoreMerchant::Customer"
|
@@ -36,15 +47,9 @@ module CoreMerchant
|
|
36
47
|
end
|
37
48
|
|
38
49
|
# Default customer class in CoreMerchant. Use this class if you don't have a model for customers in your application.
|
39
|
-
class Customer
|
50
|
+
class Customer < ActiveRecord::Base
|
40
51
|
include CustomerBehavior
|
41
52
|
|
42
53
|
attr_accessor :id, :email, :name
|
43
|
-
|
44
|
-
def initialize(id:, email:, name: nil)
|
45
|
-
@id = id
|
46
|
-
@email = email
|
47
|
-
@name = name
|
48
|
-
end
|
49
54
|
end
|
50
55
|
end
|
@@ -10,12 +10,8 @@ module CoreMerchant
|
|
10
10
|
|
11
11
|
source_root File.expand_path("templates", __dir__)
|
12
12
|
|
13
|
-
class_option :customer_class, type: :string, required: true,
|
14
|
-
desc: "Name of your existing customer class, e.g. User"
|
15
|
-
|
16
13
|
def copy_initializer
|
17
|
-
|
18
|
-
template "core_merchant.erb", "config/initializers/core_merchant.rb"
|
14
|
+
template "core_merchant.rb", "config/initializers/core_merchant.rb"
|
19
15
|
end
|
20
16
|
|
21
17
|
def copy_locales
|
@@ -36,20 +32,22 @@ module CoreMerchant
|
|
36
32
|
|
37
33
|
def show_post_install
|
38
34
|
say "CoreMerchant has been successfully installed.", :green
|
39
|
-
|
40
|
-
|
35
|
+
next_steps = <<~MESSAGE
|
36
|
+
Next steps:
|
37
|
+
1. Set the customer class in the initializer file (config/initializers/core_merchant.rb) to the class you want to use for customers.
|
38
|
+
2. Create a subscription listener class (should include CoreMerchant::SubscriptionListener) in your app and set this class in the initializer file (config/initializers/core_merchant.rb) to the class you want to use for subscription listeners.
|
39
|
+
3. Run `rails db:migrate` to create the subscription and subscription plan tables.
|
41
40
|
MESSAGE
|
42
|
-
say
|
41
|
+
say next_steps, :yellow
|
43
42
|
end
|
44
43
|
|
45
44
|
def self.banner
|
46
|
-
"rails generate core_merchant:install
|
45
|
+
"rails generate core_merchant:install"
|
47
46
|
end
|
48
47
|
|
49
48
|
def self.description
|
50
49
|
<<~DESC
|
51
|
-
Installs CoreMerchant into your application
|
52
|
-
This could be User, Customer, or any other existing model in your application that represents a customer."
|
50
|
+
Installs CoreMerchant into your application. This generator will create an initializer file, migration files for the subscription and subscription plan tables, and a locale file."
|
53
51
|
DESC
|
54
52
|
end
|
55
53
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# Created by: rails generate core_merchant:install
|
2
|
+
|
3
|
+
CoreMerchant.configure do |config|
|
4
|
+
# Set the class that represents a customer in your application.
|
5
|
+
# This class must include the CoreMerchant::CustomerBehavior module as such:
|
6
|
+
# class User < ApplicationRecord
|
7
|
+
# include CoreMerchant::CustomerBehavior
|
8
|
+
# ...
|
9
|
+
# end
|
10
|
+
config.customer_class = "User"
|
11
|
+
|
12
|
+
# Set the class that will receive subscription notifications/
|
13
|
+
# This class must include the CoreMerchant::SubscriptionListener module.
|
14
|
+
# class SubscriptionListener
|
15
|
+
# include CoreMerchant::SubscriptionListener
|
16
|
+
# def on_subscription_created(subscription)
|
17
|
+
# ...
|
18
|
+
# end
|
19
|
+
# ...
|
20
|
+
# end
|
21
|
+
# This class will receive notifications when a subscription is created, updated, canceled etc.
|
22
|
+
#
|
23
|
+
# To test notifications to this class, you can override `on_test_event_received` method
|
24
|
+
# and call `CoreMerchant.subscription_manager.notify_test_event`.
|
25
|
+
config.subscription_listener_class = "SubscriptionListener"
|
26
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: core_merchant
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Seyithan Teymur
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-07-
|
11
|
+
date: 2024-07-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: factory_bot
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
70
|
name: generator_spec
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -130,14 +144,18 @@ files:
|
|
130
144
|
- Rakefile
|
131
145
|
- core_merchant.gemspec
|
132
146
|
- lib/core_merchant.rb
|
147
|
+
- lib/core_merchant/concerns/subscription_manager_renewals.rb
|
148
|
+
- lib/core_merchant/concerns/subscription_notifications.rb
|
133
149
|
- lib/core_merchant/concerns/subscription_state_machine.rb
|
134
150
|
- lib/core_merchant/customer_behavior.rb
|
135
151
|
- lib/core_merchant/subscription.rb
|
152
|
+
- lib/core_merchant/subscription_listener.rb
|
153
|
+
- lib/core_merchant/subscription_manager.rb
|
136
154
|
- lib/core_merchant/subscription_plan.rb
|
137
155
|
- lib/core_merchant/version.rb
|
138
156
|
- lib/generators/core_merchant/install_generator.rb
|
139
157
|
- lib/generators/core_merchant/templates/core_merchant.en.yml
|
140
|
-
- lib/generators/core_merchant/templates/core_merchant.
|
158
|
+
- lib/generators/core_merchant/templates/core_merchant.rb
|
141
159
|
- lib/generators/core_merchant/templates/migrate/create_core_merchant_subscription_plans.erb
|
142
160
|
- lib/generators/core_merchant/templates/migrate/create_core_merchant_subscriptions.erb
|
143
161
|
- sig/core_merchant.rbs
|