core_merchant 0.7.0 → 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 +1 -1
- 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 +2 -4
- data/lib/core_merchant/subscription.rb +79 -21
- data/lib/core_merchant/subscription_listener.rb +41 -1
- data/lib/core_merchant/subscription_manager.rb +84 -1
- data/lib/core_merchant/version.rb +1 -1
- data/lib/core_merchant.rb +1 -7
- metadata +4 -2
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
@@ -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,6 +6,8 @@ module CoreMerchant
|
|
6
6
|
extend ActiveSupport::Concern
|
7
7
|
|
8
8
|
included do
|
9
|
+
has_many :subscriptions, class_name: "CoreMerchant::Subscription", as: :customer, dependent: :destroy
|
10
|
+
|
9
11
|
def core_merchant_customer_id
|
10
12
|
id
|
11
13
|
end
|
@@ -17,10 +19,6 @@ module CoreMerchant
|
|
17
19
|
def core_merchant_customer_name
|
18
20
|
name if respond_to?(:name)
|
19
21
|
end
|
20
|
-
|
21
|
-
def subscriptions
|
22
|
-
CoreMerchant::Subscription.where(customer_id: core_merchant_customer_id)
|
23
|
-
end
|
24
22
|
end
|
25
23
|
end
|
26
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,27 +80,79 @@ 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
|
-
|
97
|
-
|
118
|
+
|
119
|
+
notify_subscription_manager(:canceled, reason: reason, immediate: !at_period_end)
|
120
|
+
end
|
121
|
+
|
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
|
138
|
+
|
139
|
+
def in_grace_period?
|
140
|
+
due_for_renewal? && Time.current <= grace_period_end_date
|
141
|
+
end
|
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
|
98
156
|
end
|
99
157
|
|
100
158
|
private
|
@@ -5,10 +5,50 @@ module CoreMerchant
|
|
5
5
|
module SubscriptionListener
|
6
6
|
extend ActiveSupport::Concern
|
7
7
|
|
8
|
-
included do
|
8
|
+
included do # rubocop:disable Metrics/BlockLength
|
9
9
|
def on_test_event_received
|
10
10
|
puts "Test event received by CoreMerchant::SubscriptionListener. Override this method in your application."
|
11
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
|
12
52
|
end
|
13
53
|
end
|
14
54
|
end
|
@@ -1,21 +1,104 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "core_merchant/concerns/subscription_manager_renewals"
|
4
|
+
|
3
5
|
module CoreMerchant
|
4
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.
|
5
10
|
class SubscriptionManager
|
11
|
+
include Concerns::SubscriptionManagerRenewals
|
12
|
+
|
6
13
|
attr_reader :listeners
|
7
14
|
|
8
15
|
def initialize
|
9
16
|
@listeners = []
|
10
17
|
end
|
11
18
|
|
19
|
+
def check_subscriptions
|
20
|
+
check_renewals
|
21
|
+
end
|
22
|
+
|
12
23
|
def add_listener(listener)
|
13
24
|
@listeners << listener
|
14
25
|
end
|
15
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
|
+
|
16
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)
|
17
100
|
@listeners.each do |listener|
|
18
|
-
listener.
|
101
|
+
listener.send(method_name, subscription, **args) if listener.respond_to?(method_name)
|
19
102
|
end
|
20
103
|
end
|
21
104
|
end
|
data/lib/core_merchant.rb
CHANGED
@@ -47,15 +47,9 @@ module CoreMerchant
|
|
47
47
|
end
|
48
48
|
|
49
49
|
# Default customer class in CoreMerchant. Use this class if you don't have a model for customers in your application.
|
50
|
-
class Customer
|
50
|
+
class Customer < ActiveRecord::Base
|
51
51
|
include CustomerBehavior
|
52
52
|
|
53
53
|
attr_accessor :id, :email, :name
|
54
|
-
|
55
|
-
def initialize(id:, email:, name: nil)
|
56
|
-
@id = id
|
57
|
-
@email = email
|
58
|
-
@name = name
|
59
|
-
end
|
60
54
|
end
|
61
55
|
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
|
@@ -144,6 +144,8 @@ files:
|
|
144
144
|
- Rakefile
|
145
145
|
- core_merchant.gemspec
|
146
146
|
- lib/core_merchant.rb
|
147
|
+
- lib/core_merchant/concerns/subscription_manager_renewals.rb
|
148
|
+
- lib/core_merchant/concerns/subscription_notifications.rb
|
147
149
|
- lib/core_merchant/concerns/subscription_state_machine.rb
|
148
150
|
- lib/core_merchant/customer_behavior.rb
|
149
151
|
- lib/core_merchant/subscription.rb
|