core_merchant 0.8.0 → 0.9.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/README.md +204 -6
- data/lib/core_merchant/concerns/subscription_manager_notifications.rb +80 -0
- data/lib/core_merchant/subscription.rb +39 -9
- data/lib/core_merchant/subscription_manager.rb +65 -54
- data/lib/core_merchant/subscription_plan.rb +1 -1
- data/lib/core_merchant/version.rb +1 -1
- metadata +3 -3
- data/lib/core_merchant/concerns/subscription_manager_renewals.rb +0 -53
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ff6bdd381d58ecdb8153a14ceff12482d22b55b70a738de1e46df9480f3ab472
|
|
4
|
+
data.tar.gz: a87c17013865cdbe94f70daf969624f494135955568c0662e86cfe41463a6d88
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 044f0541c54678616eff01372b20b54e4a2bfd3f3d087ba276dc749ce06bc550f6bb047ba90bad084d1edb04f1600774cf5dd681fb56b4c91ef0c8530dbcfa38
|
|
7
|
+
data.tar.gz: f471e4c215f2d9f36944621e9bb067f9fcde0a6f67d72ee9b6e1cc6a3a2aeb507dd93cdc110bdaf9058be19c7cb700062932609ce9f10437bb5c813c75f0ed82
|
data/README.md
CHANGED
|
@@ -10,11 +10,33 @@ CoreMerchant is a library for customer, product, and subscription management in
|
|
|
10
10
|
- [X] Add initializer generator
|
|
11
11
|
- [X] Add SubscriptionPlan model
|
|
12
12
|
- [X] Add Subscription model
|
|
13
|
-
- [
|
|
13
|
+
- [X] 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
|
|
17
17
|
|
|
18
|
+
## Table of contents
|
|
19
|
+
- [CoreMerchant](#coremerchant)
|
|
20
|
+
- [To-dos until 1.0.0 release](#to-dos-until-100-release)
|
|
21
|
+
- [Table of contents](#table-of-contents)
|
|
22
|
+
- [Installation](#installation)
|
|
23
|
+
- [Usage](#usage)
|
|
24
|
+
- [Initialization](#initialization)
|
|
25
|
+
- [Configuration](#configuration)
|
|
26
|
+
- [1. Customer class](#1-customer-class)
|
|
27
|
+
- [2. Subscription listener class](#2-subscription-listener-class)
|
|
28
|
+
- [Subscription management](#subscription-management)
|
|
29
|
+
- [Creating a subscription plan](#creating-a-subscription-plan)
|
|
30
|
+
- [Creating a subscription](#creating-a-subscription)
|
|
31
|
+
- [Cancelling a subscription](#cancelling-a-subscription)
|
|
32
|
+
- [Handling subscription events](#handling-subscription-events)
|
|
33
|
+
- [Models](#models)
|
|
34
|
+
- [SubscriptionManager](#subscriptionmanager)
|
|
35
|
+
- [SubscriptionPlan](#subscriptionplan)
|
|
36
|
+
- [Subscription](#subscription)
|
|
37
|
+
- [Contributing](#contributing)
|
|
38
|
+
|
|
39
|
+
|
|
18
40
|
## Installation
|
|
19
41
|
Add this line to your application's Gemfile:
|
|
20
42
|
```
|
|
@@ -31,14 +53,14 @@ $ gem install core_merchant
|
|
|
31
53
|
### Initialization
|
|
32
54
|
Run the generator to create the initializer file and the migrations:
|
|
33
55
|
```
|
|
34
|
-
$ rails generate core_merchant:install
|
|
56
|
+
$ rails generate core_merchant:install
|
|
35
57
|
```
|
|
36
|
-
`--customer_class` option is required and should be the name of the model that represents the customer in your application. For example, if you already have a `User` model that represents the users of your application, you can use it as the customer class in CoreMerchant.
|
|
37
58
|
|
|
38
59
|
This will create the following files:
|
|
39
60
|
- `config/initializers/core_merchant.rb` - Configuration file
|
|
61
|
+
- `config/locales/core_merchant.en.yml` - English translations for core merchant models
|
|
40
62
|
- `db/migrate/xxxxxx_create_core_merchant_subscription_plans.rb` - Migration for subscription plans
|
|
41
|
-
|
|
63
|
+
- `db/migrate/xxxxxx_create_core_merchant_subscriptions.rb` - Migration for subscriptions
|
|
42
64
|
You can then run the migrations:
|
|
43
65
|
```
|
|
44
66
|
$ rails db:migrate
|
|
@@ -46,11 +68,13 @@ $ rails db:migrate
|
|
|
46
68
|
|
|
47
69
|
### Configuration
|
|
48
70
|
The initializer file `config/initializers/core_merchant.rb` contains the following configuration options:
|
|
71
|
+
|
|
72
|
+
#### 1. Customer class
|
|
49
73
|
```ruby
|
|
50
74
|
config.customer_class = 'User'
|
|
51
75
|
```
|
|
52
76
|
|
|
53
|
-
|
|
77
|
+
This is the class that includes the `CoreMerchant::Customer` module. It should be the model class that represents your customers. For example, if you have a `User` model that represents your customers, you can include the `CoreMerchant::Customer` module in the `User` class:
|
|
54
78
|
```ruby
|
|
55
79
|
# app/models/user.rb
|
|
56
80
|
class User < ApplicationRecord
|
|
@@ -60,9 +84,127 @@ class User < ApplicationRecord
|
|
|
60
84
|
end
|
|
61
85
|
```
|
|
62
86
|
|
|
87
|
+
#### 2. Subscription listener class
|
|
88
|
+
```ruby
|
|
89
|
+
config.subscription_listener_class = 'MySubscriptionListener'
|
|
90
|
+
```
|
|
91
|
+
This is the class that will receive subscription events. It should include the `CoreMerchant::SubscriptionListener` module and implement event handlers for the subscription events. Some examples:
|
|
92
|
+
```ruby
|
|
93
|
+
class MySubscriptionListener
|
|
94
|
+
include CoreMerchant::SubscriptionListener
|
|
95
|
+
|
|
96
|
+
def on_test_event_received
|
|
97
|
+
puts 'Test event received, hooray!'
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
More about subscription events in the [Handling subscription events](#handling-subscription-events) section.
|
|
103
|
+
|
|
104
|
+
### Subscription management
|
|
105
|
+
|
|
106
|
+
#### Creating a subscription plan
|
|
107
|
+
You can create a subscription plan using the `SubscriptionPlan` model:
|
|
108
|
+
```ruby
|
|
109
|
+
CoreMerchant::SubscriptionPlan.create(name_key: 'basic', price_cents: 10_00, duration: '1m')
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
#### Creating a subscription
|
|
113
|
+
You can create a subscription for a customer using the `Subscription` model:
|
|
114
|
+
```ruby
|
|
115
|
+
customer = User.find(1)
|
|
116
|
+
plan = CoreMerchant::SubscriptionPlan.find_by(name_key: 'basic')
|
|
117
|
+
subscription = CoreMerchant::Subscription.create(customer: customer, plan: plan)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Note that the subscription will not be active until you start it:
|
|
121
|
+
```ruby
|
|
122
|
+
subscription.start
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### Cancelling a subscription
|
|
126
|
+
You can cancel a subscription by calling the `cancel` method. You can also specify a reason for the cancellation and whether the cancellation should take effect immediately or at the end of the current billing period:
|
|
127
|
+
```ruby
|
|
128
|
+
subscription.cancel(reason: 'Customer request', at_period_end: false)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
#### Handling subscription events
|
|
132
|
+
You can handle subscription events by implementing event handlers in the subscription listener class. For example, you can send an email to the customer when a subscription is created:
|
|
133
|
+
```ruby
|
|
134
|
+
class MySubscriptionListener
|
|
135
|
+
include CoreMerchant::SubscriptionListener
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def on_subscription_created(subscription)
|
|
139
|
+
FakeEmailService.send_email(subscription.customer.email, "We're happy to have you on board!")
|
|
140
|
+
|
|
141
|
+
# You can also start the subscription automatically
|
|
142
|
+
subscription.start
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def on_subscription_started(subscription)
|
|
146
|
+
FakeEmailService.send_email(subscription.customer.email, "Your subscription has started!")
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def on_subscription_due_for_renewal(subscription)
|
|
150
|
+
success = FakePaymentService.charge(subscription.customer, subscription.plan.price_cents)
|
|
151
|
+
|
|
152
|
+
if success
|
|
153
|
+
CoreMerchant.subscription_manager.payment_successful_for_renewal(subscription)
|
|
154
|
+
else
|
|
155
|
+
FakeEmailService.send_email(subscription.customer.email, "Payment failed, please update your payment method.")
|
|
156
|
+
CoreMerchant.subscription_manager.payment_failed_for_renewal(subscription)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def on_subscription_renewed(subscription)
|
|
161
|
+
FakeEmailService.send_email(subscription.customer.email, "Your subscription has been renewed until #{subscription.current_period_end_date}")
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Available subscription events:
|
|
167
|
+
- `on_subscription_created(subscription)`
|
|
168
|
+
- `on_subscription_destroyed(subscription)`
|
|
169
|
+
- `on_subscription_started(subscription)`
|
|
170
|
+
- `on_subscription_canceled(subscription, reason:, at_period_end:)`
|
|
171
|
+
- `on_subscription_due_for_renewal(subscription)`
|
|
172
|
+
- `on_subscription_renewed(subscription)`
|
|
173
|
+
- `on_subscription_renewal_payment_processing(subscription)`
|
|
174
|
+
- `on_subscription_grace_period_started(subscription, days_remaining:)`
|
|
175
|
+
|
|
63
176
|
## Models
|
|
177
|
+
### SubscriptionManager
|
|
178
|
+
Access from `CoreMerchant.subscription_manager`. The `SubscriptionManager` class is responsible for managing subscriptions. It is responsible for notifying listeners when subscription events occur and checking for and handling renewals.
|
|
179
|
+
|
|
180
|
+
**Attributes**:
|
|
181
|
+
- `listeners` - An array of listeners that will be notified when subscription events occur.
|
|
182
|
+
|
|
183
|
+
**Methods**:
|
|
184
|
+
- `check_subscriptions` - Checks all subscriptions for renewals
|
|
185
|
+
- `add_listener(listener)` - Adds a listener to the list of listeners
|
|
186
|
+
- `no_payment_needed_for_renewal(subscription)` - Handles the case where no payment is needed for a renewal.
|
|
187
|
+
Call when a subscription is renewed without payment.
|
|
188
|
+
- `processing_payment_for_renewal(subscription)` - Handles the case where payment is being processed for a renewal.
|
|
189
|
+
Call when payment is being processed for a renewal.
|
|
190
|
+
- `payment_successful_for_renewal(subscription)` - Handles the case where payment was successful for a renewal.
|
|
191
|
+
Call when payment was successful for a renewal.
|
|
192
|
+
- `payment_failed_for_renewal(subscription)` - Handles the case where payment failed for a renewal.
|
|
193
|
+
Call when payment failed for a renewal.
|
|
194
|
+
|
|
195
|
+
**Usage**:
|
|
196
|
+
```ruby
|
|
197
|
+
manager = CoreMerchant.subscription_manager
|
|
198
|
+
manager.check_subscriptions
|
|
199
|
+
# ... somewhere else in the code ...
|
|
200
|
+
manager.payment_successful_for_renewal(subscription1)
|
|
201
|
+
manager.payment_failed_for_renewal(subscription2)
|
|
202
|
+
```
|
|
203
|
+
|
|
64
204
|
### SubscriptionPlan
|
|
65
|
-
The `SubscriptionPlan` model represents a subscription plan in your application.
|
|
205
|
+
The `SubscriptionPlan` model represents a subscription plan in your application. Subscription plans are used to define the pricing and features of a subscription. All prices are in cents.
|
|
206
|
+
|
|
207
|
+
**Attributes**:
|
|
66
208
|
- `name_key`: A unique key for the subscription plan.
|
|
67
209
|
This key is used to identify the plan in the application,
|
|
68
210
|
as well as the translation key for the plan name through `core_merchant.subscription_plans`.
|
|
@@ -73,6 +215,62 @@ The `SubscriptionPlan` model represents a subscription plan in your application.
|
|
|
73
215
|
- `introductory_price_cents`: The introductory price of the subscription plan in cents.
|
|
74
216
|
- `introductory_duration`: The duration of the introductory price of the subscription plan.
|
|
75
217
|
|
|
218
|
+
**Usage**:
|
|
219
|
+
``` ruby
|
|
220
|
+
plan = CoreMerchant::SubscriptionPlan.new(name_key: "basic_monthly", price_cents: 7_99)
|
|
221
|
+
plan.save
|
|
222
|
+
```
|
|
223
|
+
### Subscription
|
|
224
|
+
Represents a subscription in CoreMerchant.
|
|
225
|
+
This class manages the lifecycle of a customer's subscription to a specific plan.
|
|
226
|
+
|
|
227
|
+
**Subscriptions can transition through various statuses**:
|
|
228
|
+
- `pending`: Subscription created but not yet started
|
|
229
|
+
- `trial`: In a trial period
|
|
230
|
+
- `active`: Currently active and paid
|
|
231
|
+
- `past_due`: Payment failed but in grace period
|
|
232
|
+
- `pending_cancellation`: Will be canceled at period end
|
|
233
|
+
- `processing_renewal`: Renewal in progress
|
|
234
|
+
- `processing_payment`: Payment processing
|
|
235
|
+
- `canceled`: Canceled by user or due to payment failure
|
|
236
|
+
- `expired`: Subscription period ended
|
|
237
|
+
- `paused`: Temporarily halted, not yet implemented
|
|
238
|
+
- `pending_change`: Plan change scheduled for next renewal, not yet implemented
|
|
239
|
+
|
|
240
|
+
**Key features**:
|
|
241
|
+
- Supports immediate and end-of-period cancellations
|
|
242
|
+
- Allows plan changes, effective immediately or at next renewal
|
|
243
|
+
- Handles subscription pausing and resuming
|
|
244
|
+
- Manages trial periods
|
|
245
|
+
- Supports variable pricing for renewals
|
|
246
|
+
|
|
247
|
+
**Attributes**:
|
|
248
|
+
- `customer`: Polymorphic association to the customer
|
|
249
|
+
- `subscription_plan`: The current plan for this subscription
|
|
250
|
+
- `status`: Current status of the subscription (see enum definition)
|
|
251
|
+
- `start_date`: When the subscription started
|
|
252
|
+
- `end_date`: When the subscription ended (or will end)
|
|
253
|
+
- `trial_end_date`: End date of the trial period (if applicable)
|
|
254
|
+
- `canceled_at`: When the subscription was canceled
|
|
255
|
+
- `current_period_start`: Start of the current billing period
|
|
256
|
+
- `current_period_end`: End of the current billing period
|
|
257
|
+
- `pause_start_date`: When the subscription was paused
|
|
258
|
+
- `pause_end_date`: When the paused subscription will resume
|
|
259
|
+
- `current_period_price_cents`: Price for the current period
|
|
260
|
+
- `next_renewal_price_cents`: Price for the next renewal (if different from plan)
|
|
261
|
+
- `cancellation_reason`: Reason for cancellation (if applicable)
|
|
262
|
+
|
|
263
|
+
**Methods**:
|
|
264
|
+
- `start` - Starts the subscription
|
|
265
|
+
- `cancel(reason:, at_period_end:)` - Cancels the subscription, optionally at the end of the current period
|
|
266
|
+
|
|
267
|
+
**Usage**:
|
|
268
|
+
```ruby
|
|
269
|
+
subscription = CoreMerchant::Subscription.create(customer: user, subscription_plan: plan, status: :active)
|
|
270
|
+
subscription.start
|
|
271
|
+
subscription.cancel(reason: "Too expensive", at_period_end: true)
|
|
272
|
+
```
|
|
273
|
+
|
|
76
274
|
> [!NOTE]
|
|
77
275
|
> Other models and features are being developed and will be added in future releases.
|
|
78
276
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CoreMerchant
|
|
4
|
+
module Concerns
|
|
5
|
+
# Includes logic for notifying listeners of subscription events.
|
|
6
|
+
module SubscriptionManagerNotifications
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
included do # rubocop:disable Metrics/BlockLength
|
|
10
|
+
def notify(subscription, event, **options) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength
|
|
11
|
+
case event
|
|
12
|
+
when :created
|
|
13
|
+
notify_subscription_created(subscription)
|
|
14
|
+
when :destroyed
|
|
15
|
+
notify_subscription_destroyed(subscription)
|
|
16
|
+
when :started
|
|
17
|
+
notify_subscription_started(subscription)
|
|
18
|
+
when :canceled
|
|
19
|
+
notify_subscription_canceled(subscription, options[:reason], options[:immediate])
|
|
20
|
+
when :due_for_renewal
|
|
21
|
+
notify_subscription_due_for_renewal(subscription)
|
|
22
|
+
when :renewed
|
|
23
|
+
notify_subscription_renewed(subscription)
|
|
24
|
+
when :renewal_payment_processing
|
|
25
|
+
notify_subscription_renewal_payment_processing(subscription)
|
|
26
|
+
when :grace_period_started
|
|
27
|
+
notify_subscription_grace_period_started(subscription, options[:days_remaining])
|
|
28
|
+
when :expired
|
|
29
|
+
notify_subscription_expired(subscription)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def notify_test_event
|
|
34
|
+
send_notification_to_listeners(nil, :on_test_event_received)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def notify_subscription_created(subscription)
|
|
40
|
+
send_notification_to_listeners(subscription, :on_subscription_created)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def notify_subscription_destroyed(subscription)
|
|
44
|
+
send_notification_to_listeners(subscription, :on_subscription_destroyed)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def notify_subscription_started(subscription)
|
|
48
|
+
send_notification_to_listeners(subscription, :on_subscription_started)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def notify_subscription_canceled(subscription, reason, immediate)
|
|
52
|
+
send_notification_to_listeners(subscription, :on_subscription_canceled, reason: reason, immediate: immediate)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def notify_subscription_due_for_renewal(subscription)
|
|
56
|
+
send_notification_to_listeners(subscription, :on_subscription_due_for_renewal)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def notify_subscription_renewed(subscription)
|
|
60
|
+
send_notification_to_listeners(subscription, :on_subscription_renewed)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def notify_subscription_renewal_payment_processing(subscription)
|
|
64
|
+
send_notification_to_listeners(subscription, :on_subscription_renewal_payment_processing)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def notify_subscription_expired(subscription)
|
|
68
|
+
send_notification_to_listeners(subscription, :on_subscription_expired)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def notify_subscription_grace_period_started(subscription, days_remaining)
|
|
72
|
+
send_notification_to_listeners(
|
|
73
|
+
subscription, :on_subscription_grace_period_started,
|
|
74
|
+
days_remaining: days_remaining
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -46,13 +46,10 @@ module CoreMerchant
|
|
|
46
46
|
# **Usage**:
|
|
47
47
|
# ```ruby
|
|
48
48
|
# subscription = CoreMerchant::Subscription.create(customer: user, subscription_plan: plan, status: :active)
|
|
49
|
+
# subscription.start
|
|
49
50
|
# subscription.cancel(reason: "Too expensive", at_period_end: true)
|
|
50
|
-
# subscription.change_plan(new_plan, at_period_end: false)
|
|
51
|
-
# subscription.pause(until_date: 1.month.from_now)
|
|
52
|
-
# subscription.resume
|
|
53
|
-
# subscription.renew(price_cents: 1999)
|
|
54
51
|
# ```
|
|
55
|
-
class Subscription < ActiveRecord::Base
|
|
52
|
+
class Subscription < ActiveRecord::Base # rubocop:disable Metrics/ClassLength
|
|
56
53
|
include CoreMerchant::Concerns::SubscriptionStateMachine
|
|
57
54
|
include CoreMerchant::Concerns::SubscriptionNotifications
|
|
58
55
|
|
|
@@ -80,6 +77,12 @@ module CoreMerchant
|
|
|
80
77
|
validate :end_date_after_start_date, if: :end_date
|
|
81
78
|
validate :canceled_at_with_reason, if: :canceled_at
|
|
82
79
|
|
|
80
|
+
scope :due_for_renewal,
|
|
81
|
+
lambda {
|
|
82
|
+
where(status: %i[active trial past_due processing_renewal processing_payment])
|
|
83
|
+
.where("current_period_end <= ?", Time.current)
|
|
84
|
+
}
|
|
85
|
+
|
|
83
86
|
# Starts the subscription.
|
|
84
87
|
# Sets the current period start and end dates based on the plan's duration.
|
|
85
88
|
def start
|
|
@@ -89,8 +92,8 @@ module CoreMerchant
|
|
|
89
92
|
transaction do
|
|
90
93
|
transition_to_active!
|
|
91
94
|
update!(
|
|
92
|
-
current_period_start: new_period_start,
|
|
93
|
-
current_period_end: new_period_end
|
|
95
|
+
current_period_start: new_period_start.to_date,
|
|
96
|
+
current_period_end: new_period_end.to_date
|
|
94
97
|
)
|
|
95
98
|
end
|
|
96
99
|
|
|
@@ -119,6 +122,18 @@ module CoreMerchant
|
|
|
119
122
|
notify_subscription_manager(:canceled, reason: reason, immediate: !at_period_end)
|
|
120
123
|
end
|
|
121
124
|
|
|
125
|
+
# Starts a new period for the subscription.
|
|
126
|
+
# This is called by SubscriptionManager when a subscription renewal is successful.
|
|
127
|
+
def start_new_period
|
|
128
|
+
new_period_start = current_period_end
|
|
129
|
+
new_period_end = new_period_start + subscription_plan.duration_in_date
|
|
130
|
+
|
|
131
|
+
update!(
|
|
132
|
+
current_period_start: new_period_start.to_date,
|
|
133
|
+
current_period_end: new_period_end.to_date
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
|
|
122
137
|
# Returns the days remaining in the current period.
|
|
123
138
|
# Use to show the user how many days are left before the next renewal or
|
|
124
139
|
# to refund pro-rated amounts for early cancellations.
|
|
@@ -151,8 +166,23 @@ module CoreMerchant
|
|
|
151
166
|
end
|
|
152
167
|
|
|
153
168
|
def due_for_renewal?
|
|
154
|
-
|
|
155
|
-
|
|
169
|
+
renewable? && current_period_end <= Time.current
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def expired_or_canceled?
|
|
173
|
+
expired? || canceled?
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def processing?
|
|
177
|
+
processing_renewal? || processing_payment?
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def ongoing?
|
|
181
|
+
active? || trial? || past_due? || processing_renewal? || processing_payment? || pending_cancellation?
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def renewable?
|
|
185
|
+
active? || trial? || past_due? || processing?
|
|
156
186
|
end
|
|
157
187
|
|
|
158
188
|
private
|
|
@@ -1,14 +1,38 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "core_merchant/concerns/
|
|
3
|
+
require "core_merchant/concerns/subscription_manager_notifications"
|
|
4
4
|
|
|
5
5
|
module CoreMerchant
|
|
6
6
|
# Manages subscriptions in CoreMerchant.
|
|
7
|
-
# This class is responsible for notifying listeners when subscription events
|
|
8
|
-
#
|
|
7
|
+
# This class is responsible for notifying listeners when subscription events
|
|
8
|
+
# occur and checking for and handling renewals.
|
|
9
|
+
# **Attributes**:
|
|
9
10
|
# - `listeners` - An array of listeners that will be notified when subscription events occur.
|
|
11
|
+
#
|
|
12
|
+
# **Methods**:
|
|
13
|
+
# - `check_subscriptions` - Checks all subscriptions for renewals
|
|
14
|
+
# - `add_listener(listener)` - Adds a listener to the list of listeners
|
|
15
|
+
# - `no_payment_needed_for_renewal(subscription)` - Handles the case where no payment is needed for a renewal.
|
|
16
|
+
# Call when a subscription is renewed without payment.
|
|
17
|
+
# - `processing_payment_for_renewal(subscription)` - Handles the case where payment is being processed for a renewal.
|
|
18
|
+
# Call when payment is being processed for a renewal.
|
|
19
|
+
# - `payment_successful_for_renewal(subscription)` - Handles the case where payment was successful for a renewal.
|
|
20
|
+
# Call when payment was successful for a renewal.
|
|
21
|
+
# - `payment_failed_for_renewal(subscription)` - Handles the case where payment failed for a renewal.
|
|
22
|
+
# Call when payment failed for a renewal.
|
|
23
|
+
#
|
|
24
|
+
# **Usage**:
|
|
25
|
+
# ```ruby
|
|
26
|
+
# manager = CoreMerchant.subscription_manager
|
|
27
|
+
# manager.check_subscriptions
|
|
28
|
+
#
|
|
29
|
+
# # ... somewhere else in the code ...
|
|
30
|
+
# manager.payment_successful_for_renewal(subscription1)
|
|
31
|
+
# manager.payment_failed_for_renewal(subscription2)
|
|
32
|
+
# ```
|
|
33
|
+
#
|
|
10
34
|
class SubscriptionManager
|
|
11
|
-
include Concerns::
|
|
35
|
+
include Concerns::SubscriptionManagerNotifications
|
|
12
36
|
|
|
13
37
|
attr_reader :listeners
|
|
14
38
|
|
|
@@ -18,82 +42,69 @@ module CoreMerchant
|
|
|
18
42
|
|
|
19
43
|
def check_subscriptions
|
|
20
44
|
check_renewals
|
|
45
|
+
check_cancellations
|
|
21
46
|
end
|
|
22
47
|
|
|
23
48
|
def add_listener(listener)
|
|
24
49
|
@listeners << listener
|
|
25
50
|
end
|
|
26
51
|
|
|
27
|
-
def
|
|
28
|
-
|
|
29
|
-
|
|
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)
|
|
52
|
+
def check_renewals
|
|
53
|
+
Subscription.find_each do |subscription|
|
|
54
|
+
process_for_renewal(subscription) if subscription.due_for_renewal?
|
|
47
55
|
end
|
|
48
56
|
end
|
|
49
57
|
|
|
50
|
-
def
|
|
51
|
-
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
private
|
|
58
|
+
def process_for_renewal(subscription)
|
|
59
|
+
return unless subscription.transition_to_processing_renewal
|
|
55
60
|
|
|
56
|
-
|
|
57
|
-
send_notification_to_listeners(subscription, :on_subscription_created)
|
|
61
|
+
notify(subscription, :due_for_renewal)
|
|
58
62
|
end
|
|
59
63
|
|
|
60
|
-
def
|
|
61
|
-
|
|
64
|
+
def no_payment_needed_for_renewal(subscription)
|
|
65
|
+
renew_subscription(subscription)
|
|
62
66
|
end
|
|
63
67
|
|
|
64
|
-
def
|
|
65
|
-
|
|
66
|
-
end
|
|
68
|
+
def processing_payment_for_renewal(subscription)
|
|
69
|
+
return unless subscription.transition_to_processing_payment
|
|
67
70
|
|
|
68
|
-
|
|
69
|
-
send_notification_to_listeners(subscription, :on_subscription_canceled, reason: reason, immediate: immediate)
|
|
71
|
+
notify(subscription, :renewal_payment_processing)
|
|
70
72
|
end
|
|
71
73
|
|
|
72
|
-
def
|
|
73
|
-
|
|
74
|
+
def payment_successful_for_renewal(subscription)
|
|
75
|
+
renew_subscription(subscription)
|
|
74
76
|
end
|
|
75
77
|
|
|
76
|
-
def
|
|
77
|
-
|
|
78
|
+
def payment_failed_for_renewal(subscription)
|
|
79
|
+
is_in_grace_period = subscription.in_grace_period?
|
|
80
|
+
if is_in_grace_period
|
|
81
|
+
subscription.transition_to_past_due
|
|
82
|
+
notify(subscription, :grace_period_started, days_remaining: subscription.days_remaining_in_grace_period)
|
|
83
|
+
else
|
|
84
|
+
subscription.transition_to_expired
|
|
85
|
+
notify(subscription, :expired)
|
|
86
|
+
end
|
|
78
87
|
end
|
|
79
88
|
|
|
80
|
-
def
|
|
81
|
-
|
|
89
|
+
def check_cancellations
|
|
90
|
+
Subscription.find_each do |subscription|
|
|
91
|
+
process_for_cancellation(subscription) if subscription.pending_cancellation?
|
|
92
|
+
end
|
|
82
93
|
end
|
|
83
94
|
|
|
84
|
-
def
|
|
85
|
-
|
|
86
|
-
end
|
|
95
|
+
def process_for_cancellation(subscription)
|
|
96
|
+
return unless subscription.transition_to_expired
|
|
87
97
|
|
|
88
|
-
|
|
89
|
-
send_notification_to_listeners(
|
|
90
|
-
subscription, :on_subscription_grace_period_started,
|
|
91
|
-
days_remaining: days_remaining
|
|
92
|
-
)
|
|
98
|
+
notify(subscription, :expired)
|
|
93
99
|
end
|
|
94
100
|
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def renew_subscription(subscription)
|
|
104
|
+
return unless subscription.transition_to_active
|
|
105
|
+
|
|
106
|
+
subscription.start_new_period
|
|
107
|
+
notify(subscription, :renewed)
|
|
97
108
|
end
|
|
98
109
|
|
|
99
110
|
def send_notification_to_listeners(subscription, method_name, **args)
|
|
@@ -16,7 +16,7 @@ module CoreMerchant
|
|
|
16
16
|
# - `introductory_price_cents`: The introductory price of the subscription plan in cents.
|
|
17
17
|
# - `introductory_duration`: The duration of the introductory price of the subscription plan.
|
|
18
18
|
#
|
|
19
|
-
#
|
|
19
|
+
# Usage:
|
|
20
20
|
# ```
|
|
21
21
|
# plan = CoreMerchant::SubscriptionPlan.new(name_key: "basic_monthly", price_cents: 7_99)
|
|
22
22
|
# plan.save
|
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.9.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-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -144,7 +144,7 @@ files:
|
|
|
144
144
|
- Rakefile
|
|
145
145
|
- core_merchant.gemspec
|
|
146
146
|
- lib/core_merchant.rb
|
|
147
|
-
- lib/core_merchant/concerns/
|
|
147
|
+
- lib/core_merchant/concerns/subscription_manager_notifications.rb
|
|
148
148
|
- lib/core_merchant/concerns/subscription_notifications.rb
|
|
149
149
|
- lib/core_merchant/concerns/subscription_state_machine.rb
|
|
150
150
|
- lib/core_merchant/customer_behavior.rb
|
|
@@ -1,53 +0,0 @@
|
|
|
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
|