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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 154e94061339edca03a2e9c88630c4e49ce37272252e5d6436810579ea2bd529
4
- data.tar.gz: 5fe4eb2a4c24b26945a505f35d9a7694de69e7630c03776ac78b7dcd6671616b
3
+ metadata.gz: 2bd7b45750d9491ddf78c12b41e73a1d79d8bc6fa5cdff69df22f536b1765e42
4
+ data.tar.gz: 056e3021e60c7c5edb4a301dae4a29b6184ad3889b55c81e226dccfcfcb00020
5
5
  SHA512:
6
- metadata.gz: 9debecdb722370c173fc97648d5f38e130e95475edb673d5f420b7074db10dfad908221cb2b2a1dec966be064fa07040a4ad287ab10380741cd9d9df27f8d618
7
- data.tar.gz: 6d5675f20dfa56f708026a28f64cb48dcc44792f6e2663a13e1c466867defb2120f5947f84db8314d983246ef511bac2ce434aa3956fc621bdaf9cdd12b4a04c
6
+ metadata.gz: f916fe4fb53d28a9fbb61c8d7306825cb96d82ceeb559a6aeea1373c83e5401c785c0400dd3b41f0d5f9a4c321f0cc91c85eddead5808d546c7fb09d6b671aa7
7
+ data.tar.gz: 218935fdbb73a113262735e5b4a0290d7942121b167932ad00a5ead4c1a75ef69d8f18dfffca5fea14aafbbb8bf79f6cb940042f15f0989a59ce1e6c14a58208
data/.rubocop.yml CHANGED
@@ -12,6 +12,9 @@ Style/StringLiteralsInInterpolation:
12
12
  Layout/LineLength:
13
13
  Max: 120
14
14
 
15
+ Metrics/MethodLength:
16
+ Max: 16
17
+
15
18
  # Disable frozen string literals for lib/generators/**
16
19
  FrozenStringLiteralComment:
17
20
  Exclude:
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- core_merchant (0.6.2)
4
+ core_merchant (0.8.0)
5
5
  activesupport (~> 7.0)
6
6
  rails (~> 7.0)
7
7
 
@@ -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`, `canceled`
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
- # - `canceled` -> `pending`, `active`
17
- # - `expired` -> `pending`, `active`
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: %i[pending],
25
- active: %i[pending trial canceled expired],
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
- canceled: %i[active pending_cancellation trial],
28
- expired: %i[active pending_cancellation canceled trial],
29
- past_due: [],
30
- paused: [],
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, not yet implemented
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: 3, # Logic not yet implemented
65
- pending_cancellation: 4,
66
- canceled: 5,
67
- expired: 6,
68
- paused: 7, # Logic not yet implemented
69
- pending_change: 8 # Logic not yet implemented
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
- transition_to_active!
82
- update!(
83
- current_period_start: new_period_start,
84
- current_period_end: new_period_end
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
- def cancel(reason:, at_period_end:)
89
- if at_period_end
90
- transition_to_pending_cancellation!
91
- else
92
- transition_to_canceled!
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
- update!(
95
- canceled_at: at_period_end ? current_period_end : Time.current,
96
- cancellation_reason: reason
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.on_test_event_received if listener.respond_to?(:on_test_event_received)
101
+ listener.send(method_name, subscription, **args) if listener.respond_to?(method_name)
19
102
  end
20
103
  end
21
104
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CoreMerchant
4
- VERSION = "0.7.0"
4
+ VERSION = "0.8.0"
5
5
  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.7.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-22 00:00:00.000000000 Z
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