core_merchant 0.8.0 → 0.10.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/Gemfile.lock +1 -1
- data/README.md +287 -13
- data/lib/core_merchant/concerns/subscription_event_association.rb +27 -0
- data/lib/core_merchant/concerns/subscription_manager_notifications.rb +80 -0
- data/lib/core_merchant/subscription.rb +42 -9
- data/lib/core_merchant/subscription_event.rb +143 -0
- 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
- data/lib/generators/core_merchant/install_generator.rb +5 -2
- data/lib/generators/core_merchant/templates/migrate/create_core_merchant_subscription_events.erb +14 -0
- metadata +6 -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: 698e2da9b3b98b8e6c6275063af7d439c1c2a79384927e952e50419691331ce9
|
4
|
+
data.tar.gz: fde06b7ff60d2ac49bc36b5c89200c073a1f498eca046075c7bd0cfb626702de
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2cc9e74b4053e9a91435977f1b6d30222c96a69403654062a9c36dcfec6a18e2e4eebecfdc9c889cc3afbac1ac1a34e4f451cd78e6abbe64bf4ca7e795570543
|
7
|
+
data.tar.gz: 6a8cfc973fe959bfbb5cf29c0b68e3e7f644e1cee9f5e21fdc415a4d510b26ddf904e85d05c29188a176fec0225c6e763938713dcdb4d8521cf12d0aab4797c5
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -10,15 +10,44 @@ 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
|
-
- [
|
14
|
-
- [
|
13
|
+
- [X] Implement subscription manager and callbacks
|
14
|
+
- [X] Implement SubscriptionEvent model for logging
|
15
15
|
- [ ] Add Invoice model
|
16
16
|
- [ ] Add billing and invoicing service
|
17
17
|
|
18
|
-
##
|
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
|
+
- [Subscription History](#subscription-history)
|
34
|
+
- [Public API](#public-api)
|
35
|
+
- [SubscriptionManager](#subscriptionmanager)
|
36
|
+
- [SubscriptionPlan](#subscriptionplan)
|
37
|
+
- [Subscription](#subscription)
|
38
|
+
- [SubscriptionEvent](#subscriptionevent)
|
39
|
+
- [Subclasses](#subclasses)
|
40
|
+
- [SubscriptionRenewalEvent](#subscriptionrenewalevent)
|
41
|
+
- [SubscriptionStatusChangeEvent](#subscriptionstatuschangeevent)
|
42
|
+
- [SubscriptionPlanChangeEvent](#subscriptionplanchangeevent)
|
43
|
+
- [SubscriptionCancellationEvent](#subscriptioncancellationevent)
|
44
|
+
- [Contributing](#contributing)
|
45
|
+
|
46
|
+
|
47
|
+
# Installation
|
19
48
|
Add this line to your application's Gemfile:
|
20
49
|
```
|
21
|
-
gem 'core_merchant', '~> 0.
|
50
|
+
gem 'core_merchant', '~> 0.10.0'
|
22
51
|
```
|
23
52
|
and run `bundle install`.
|
24
53
|
|
@@ -27,30 +56,32 @@ Alternatively, you can install the gem manually:
|
|
27
56
|
$ gem install core_merchant
|
28
57
|
```
|
29
58
|
|
30
|
-
|
31
|
-
|
59
|
+
# Usage
|
60
|
+
## Initialization
|
32
61
|
Run the generator to create the initializer file and the migrations:
|
33
62
|
```
|
34
|
-
$ rails generate core_merchant:install
|
63
|
+
$ rails generate core_merchant:install
|
35
64
|
```
|
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
65
|
|
38
66
|
This will create the following files:
|
39
67
|
- `config/initializers/core_merchant.rb` - Configuration file
|
68
|
+
- `config/locales/core_merchant.en.yml` - English translations for core merchant models
|
40
69
|
- `db/migrate/xxxxxx_create_core_merchant_subscription_plans.rb` - Migration for subscription plans
|
41
|
-
|
70
|
+
- `db/migrate/xxxxxx_create_core_merchant_subscriptions.rb` - Migration for subscriptions
|
42
71
|
You can then run the migrations:
|
43
72
|
```
|
44
73
|
$ rails db:migrate
|
45
74
|
```
|
46
75
|
|
47
|
-
|
76
|
+
## Configuration
|
48
77
|
The initializer file `config/initializers/core_merchant.rb` contains the following configuration options:
|
78
|
+
|
79
|
+
### 1. Customer class
|
49
80
|
```ruby
|
50
81
|
config.customer_class = 'User'
|
51
82
|
```
|
52
83
|
|
53
|
-
|
84
|
+
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
85
|
```ruby
|
55
86
|
# app/models/user.rb
|
56
87
|
class User < ApplicationRecord
|
@@ -60,9 +91,149 @@ class User < ApplicationRecord
|
|
60
91
|
end
|
61
92
|
```
|
62
93
|
|
63
|
-
|
94
|
+
### 2. Subscription listener class
|
95
|
+
```ruby
|
96
|
+
config.subscription_listener_class = 'MySubscriptionListener'
|
97
|
+
```
|
98
|
+
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:
|
99
|
+
```ruby
|
100
|
+
class MySubscriptionListener
|
101
|
+
include CoreMerchant::SubscriptionListener
|
102
|
+
|
103
|
+
def on_test_event_received
|
104
|
+
puts 'Test event received, hooray!'
|
105
|
+
end
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
More about subscription events in the [Handling subscription events](#handling-subscription-events) section.
|
110
|
+
|
111
|
+
## Subscription management
|
112
|
+
|
113
|
+
### Creating a subscription plan
|
114
|
+
You can create a subscription plan using the `SubscriptionPlan` model:
|
115
|
+
```ruby
|
116
|
+
CoreMerchant::SubscriptionPlan.create(name_key: 'basic', price_cents: 10_00, duration: '1m')
|
117
|
+
```
|
118
|
+
|
119
|
+
### Creating a subscription
|
120
|
+
You can create a subscription for a customer using the `Subscription` model:
|
121
|
+
```ruby
|
122
|
+
customer = User.find(1)
|
123
|
+
plan = CoreMerchant::SubscriptionPlan.find_by(name_key: 'basic')
|
124
|
+
subscription = CoreMerchant::Subscription.create(customer: customer, plan: plan)
|
125
|
+
```
|
126
|
+
|
127
|
+
Note that the subscription will not be active until you start it:
|
128
|
+
```ruby
|
129
|
+
subscription.start
|
130
|
+
```
|
131
|
+
|
132
|
+
### Cancelling a subscription
|
133
|
+
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:
|
134
|
+
```ruby
|
135
|
+
subscription.cancel(reason: 'Customer request', at_period_end: false)
|
136
|
+
```
|
137
|
+
|
138
|
+
### Handling subscription events
|
139
|
+
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:
|
140
|
+
```ruby
|
141
|
+
class MySubscriptionListener
|
142
|
+
include CoreMerchant::SubscriptionListener
|
143
|
+
|
144
|
+
|
145
|
+
def on_subscription_created(subscription)
|
146
|
+
FakeEmailService.send_email(subscription.customer.email, "We're happy to have you on board!")
|
147
|
+
|
148
|
+
# You can also start the subscription automatically
|
149
|
+
subscription.start
|
150
|
+
end
|
151
|
+
|
152
|
+
def on_subscription_started(subscription)
|
153
|
+
FakeEmailService.send_email(subscription.customer.email, "Your subscription has started!")
|
154
|
+
end
|
155
|
+
|
156
|
+
def on_subscription_due_for_renewal(subscription)
|
157
|
+
success = FakePaymentService.charge(subscription.customer, subscription.plan.price_cents)
|
158
|
+
|
159
|
+
if success
|
160
|
+
CoreMerchant.subscription_manager.payment_successful_for_renewal(subscription)
|
161
|
+
else
|
162
|
+
FakeEmailService.send_email(subscription.customer.email, "Payment failed, please update your payment method.")
|
163
|
+
CoreMerchant.subscription_manager.payment_failed_for_renewal(subscription)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def on_subscription_renewed(subscription)
|
168
|
+
FakeEmailService.send_email(subscription.customer.email, "Your subscription has been renewed until #{subscription.current_period_end_date}")
|
169
|
+
end
|
170
|
+
end
|
171
|
+
```
|
172
|
+
|
173
|
+
Available subscription events:
|
174
|
+
- `on_subscription_created(subscription)`
|
175
|
+
- `on_subscription_destroyed(subscription)`
|
176
|
+
- `on_subscription_started(subscription)`
|
177
|
+
- `on_subscription_canceled(subscription, reason:, at_period_end:)`
|
178
|
+
- `on_subscription_due_for_renewal(subscription)`
|
179
|
+
- `on_subscription_renewed(subscription)`
|
180
|
+
- `on_subscription_renewal_payment_processing(subscription)`
|
181
|
+
- `on_subscription_grace_period_started(subscription, days_remaining:)`
|
182
|
+
|
183
|
+
### Subscription History
|
184
|
+
CoreMerchant now keeps a detailed history of subscription events, including creations, renewals, cancellations, status changes, and plan changes. This provides an audit trail and can be useful for debugging, customer support, and analytics.
|
185
|
+
|
186
|
+
To access a subscription's history:
|
187
|
+
```ruby
|
188
|
+
subscription = CoreMerchant::Subscription.find(42)
|
189
|
+
|
190
|
+
# Get all events
|
191
|
+
subscription.subscription_events
|
192
|
+
|
193
|
+
# Get specific event types
|
194
|
+
latest_renewal = subscription.renewal_events.last
|
195
|
+
puts "Last renewed at: #{latest_renewal.created_at}"
|
196
|
+
puts "Renewal price: #{latest_renewal.price_cents} cents, renewed until: #{latest_renewal.renewed_until}"
|
197
|
+
|
198
|
+
latest_status_change = subscription.status_change_events.last
|
199
|
+
puts "Status changed from #{latest_status_change.from} to #{latest_status_change.to}"
|
200
|
+
|
201
|
+
latest_plan_change = subscription.plan_change_events.last
|
202
|
+
puts "Plan changed from #{latest_plan_change.from_plan.name} to #{latest_plan_change.to_plan.name}"
|
203
|
+
```
|
204
|
+
|
205
|
+
## Public API
|
206
|
+
### SubscriptionManager
|
207
|
+
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.
|
208
|
+
|
209
|
+
**Attributes**:
|
210
|
+
- `listeners` - An array of listeners that will be notified when subscription events occur.
|
211
|
+
|
212
|
+
**Methods**:
|
213
|
+
- `check_subscriptions` - Checks all subscriptions for renewals
|
214
|
+
- `add_listener(listener)` - Adds a listener to the list of listeners
|
215
|
+
- `no_payment_needed_for_renewal(subscription)` - Handles the case where no payment is needed for a renewal.
|
216
|
+
Call when a subscription is renewed without payment.
|
217
|
+
- `processing_payment_for_renewal(subscription)` - Handles the case where payment is being processed for a renewal.
|
218
|
+
Call when payment is being processed for a renewal.
|
219
|
+
- `payment_successful_for_renewal(subscription)` - Handles the case where payment was successful for a renewal.
|
220
|
+
Call when payment was successful for a renewal.
|
221
|
+
- `payment_failed_for_renewal(subscription)` - Handles the case where payment failed for a renewal.
|
222
|
+
Call when payment failed for a renewal.
|
223
|
+
|
224
|
+
**Usage**:
|
225
|
+
```ruby
|
226
|
+
manager = CoreMerchant.subscription_manager
|
227
|
+
manager.check_subscriptions
|
228
|
+
# ... somewhere else in the code ...
|
229
|
+
manager.payment_successful_for_renewal(subscription1)
|
230
|
+
manager.payment_failed_for_renewal(subscription2)
|
231
|
+
```
|
232
|
+
|
64
233
|
### SubscriptionPlan
|
65
|
-
The `SubscriptionPlan` model represents a subscription plan in your application.
|
234
|
+
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.
|
235
|
+
|
236
|
+
**Attributes**:
|
66
237
|
- `name_key`: A unique key for the subscription plan.
|
67
238
|
This key is used to identify the plan in the application,
|
68
239
|
as well as the translation key for the plan name through `core_merchant.subscription_plans`.
|
@@ -73,6 +244,109 @@ The `SubscriptionPlan` model represents a subscription plan in your application.
|
|
73
244
|
- `introductory_price_cents`: The introductory price of the subscription plan in cents.
|
74
245
|
- `introductory_duration`: The duration of the introductory price of the subscription plan.
|
75
246
|
|
247
|
+
**Usage**:
|
248
|
+
``` ruby
|
249
|
+
plan = CoreMerchant::SubscriptionPlan.new(name_key: "basic_monthly", price_cents: 7_99)
|
250
|
+
plan.save
|
251
|
+
```
|
252
|
+
### Subscription
|
253
|
+
Represents a subscription in CoreMerchant.
|
254
|
+
This class manages the lifecycle of a customer's subscription to a specific plan.
|
255
|
+
|
256
|
+
**Subscriptions can transition through various statuses**:
|
257
|
+
- `pending`: Subscription created but not yet started
|
258
|
+
- `trial`: In a trial period
|
259
|
+
- `active`: Currently active and paid
|
260
|
+
- `past_due`: Payment failed but in grace period
|
261
|
+
- `pending_cancellation`: Will be canceled at period end
|
262
|
+
- `processing_renewal`: Renewal in progress
|
263
|
+
- `processing_payment`: Payment processing
|
264
|
+
- `canceled`: Canceled by user or due to payment failure
|
265
|
+
- `expired`: Subscription period ended
|
266
|
+
- `paused`: Temporarily halted, not yet implemented
|
267
|
+
- `pending_change`: Plan change scheduled for next renewal, not yet implemented
|
268
|
+
|
269
|
+
**Key features**:
|
270
|
+
- Supports immediate and end-of-period cancellations
|
271
|
+
- Allows plan changes, effective immediately or at next renewal
|
272
|
+
- Handles subscription pausing and resuming
|
273
|
+
- Manages trial periods
|
274
|
+
- Supports variable pricing for renewals
|
275
|
+
|
276
|
+
**Attributes**:
|
277
|
+
- `customer`: Polymorphic association to the customer
|
278
|
+
- `subscription_plan`: The current plan for this subscription
|
279
|
+
- `status`: Current status of the subscription (see enum definition)
|
280
|
+
- `start_date`: When the subscription started
|
281
|
+
- `end_date`: When the subscription ended (or will end)
|
282
|
+
- `trial_end_date`: End date of the trial period (if applicable)
|
283
|
+
- `canceled_at`: When the subscription was canceled
|
284
|
+
- `current_period_start`: Start of the current billing period
|
285
|
+
- `current_period_end`: End of the current billing period
|
286
|
+
- `pause_start_date`: When the subscription was paused
|
287
|
+
- `pause_end_date`: When the paused subscription will resume
|
288
|
+
- `current_period_price_cents`: Price for the current period
|
289
|
+
- `next_renewal_price_cents`: Price for the next renewal (if different from plan)
|
290
|
+
- `cancellation_reason`: Reason for cancellation (if applicable)
|
291
|
+
|
292
|
+
**Methods**:
|
293
|
+
- `start` - Starts the subscription
|
294
|
+
- `cancel(reason:, at_period_end:)` - Cancels the subscription, optionally at the end of the current period
|
295
|
+
|
296
|
+
**Usage**:
|
297
|
+
```ruby
|
298
|
+
subscription = CoreMerchant::Subscription.create(customer: user, subscription_plan: plan, status: :active)
|
299
|
+
subscription.start
|
300
|
+
subscription.cancel(reason: "Too expensive", at_period_end: true)
|
301
|
+
```
|
302
|
+
|
303
|
+
### SubscriptionEvent
|
304
|
+
The `SubscriptionEvent` model represents a historical log of events related to a subscription. It provides an audit trail of all significant actions and state changes for a subscription.
|
305
|
+
|
306
|
+
This class has subclasses for specific event types, such as `SubscriptionRenewalEvent`, `SubscriptionStatusChangeEvent`, and `SubscriptionPlanChangeEvent`. Each subclass has additional fields specific to the event type.
|
307
|
+
|
308
|
+
**Attributes**:
|
309
|
+
- `subscription`: Association to the related Subscription
|
310
|
+
- `event_type`: Type of the event (e.g., 'created', 'renewed', 'canceled', 'status_changed', 'plan_changed')
|
311
|
+
- `metadata`: JSON field for storing additional event-specific data
|
312
|
+
|
313
|
+
**Usage**:
|
314
|
+
```ruby
|
315
|
+
# Automatically logged when a subscription is created
|
316
|
+
subscription = CoreMerchant::Subscription.create(customer: user, subscription_plan: plan)
|
317
|
+
|
318
|
+
# Logging a custom event
|
319
|
+
subscription.log_event('custom_event', key: 'value')
|
320
|
+
|
321
|
+
# Retrieve the last renewal event
|
322
|
+
latest_renewal = subscription.renewal_events.last
|
323
|
+
puts "Last renewed at: #{latest_renewal.created_at}"
|
324
|
+
puts "Renewal price: #{latest_renewal.price_cents} cents, renewed until: #{latest_renewal.renewed_until}"
|
325
|
+
|
326
|
+
# Retrieve the last event of any type
|
327
|
+
latest_event = subscription.subscription_events.last
|
328
|
+
puts "Last event type: #{latest_event.event_type}, metadata: #{latest_event.metadata}"
|
329
|
+
```
|
330
|
+
|
331
|
+
#### Subclasses
|
332
|
+
##### SubscriptionRenewalEvent
|
333
|
+
- `price_cents`: The price of the renewal in cents
|
334
|
+
- `renewed_until`: The end date of the renewal
|
335
|
+
- `renewed_at`: The date and time of the renewal
|
336
|
+
|
337
|
+
##### SubscriptionStatusChangeEvent
|
338
|
+
- `from`: The previous status of the subscription
|
339
|
+
- `to`: The new status of the subscription
|
340
|
+
|
341
|
+
##### SubscriptionPlanChangeEvent
|
342
|
+
- `from_plan`: The previous plan of the subscription
|
343
|
+
- `to_plan`: The new plan of the subscription
|
344
|
+
|
345
|
+
##### SubscriptionCancellationEvent
|
346
|
+
- `reason`: The reason for the cancellation
|
347
|
+
- `at_period_end`: Whether the cancellation is scheduled for the end of the current period
|
348
|
+
- `canceled_at`: The date and time of the cancellation
|
349
|
+
|
76
350
|
> [!NOTE]
|
77
351
|
> Other models and features are being developed and will be added in future releases.
|
78
352
|
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CoreMerchant
|
4
|
+
module Concerns
|
5
|
+
# This concern adds a SubscriptionEvent association including creation and retrieval
|
6
|
+
module SubscriptionEventAssociation
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
EVENT_TYPES = %i[renewal status_change plan_change cancellation].freeze
|
10
|
+
|
11
|
+
included do
|
12
|
+
has_many :events, class_name: "SubscriptionEvent", dependent: :destroy
|
13
|
+
|
14
|
+
EVENT_TYPES.each do |event_type|
|
15
|
+
scope_name = "#{event_type}_events".to_sym
|
16
|
+
class_name = "Subscription#{event_type.to_s.camelize}Event"
|
17
|
+
|
18
|
+
has_many scope_name, -> { where(event_type: event_type) }, class_name: class_name
|
19
|
+
|
20
|
+
define_method "create_#{event_type}_event" do |**metadata|
|
21
|
+
events.create!(event_type: event_type, metadata: metadata)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -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
|
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
require "core_merchant/concerns/subscription_state_machine"
|
4
4
|
require "core_merchant/concerns/subscription_notifications"
|
5
|
+
require "core_merchant/concerns/subscription_event_association"
|
6
|
+
require "core_merchant/subscription_event"
|
5
7
|
|
6
8
|
module CoreMerchant
|
7
9
|
# Represents a subscription in CoreMerchant.
|
@@ -46,15 +48,13 @@ module CoreMerchant
|
|
46
48
|
# **Usage**:
|
47
49
|
# ```ruby
|
48
50
|
# subscription = CoreMerchant::Subscription.create(customer: user, subscription_plan: plan, status: :active)
|
51
|
+
# subscription.start
|
49
52
|
# 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
53
|
# ```
|
55
|
-
class Subscription < ActiveRecord::Base
|
54
|
+
class Subscription < ActiveRecord::Base # rubocop:disable Metrics/ClassLength
|
56
55
|
include CoreMerchant::Concerns::SubscriptionStateMachine
|
57
56
|
include CoreMerchant::Concerns::SubscriptionNotifications
|
57
|
+
include CoreMerchant::Concerns::SubscriptionEventAssociation
|
58
58
|
|
59
59
|
self.table_name = "core_merchant_subscriptions"
|
60
60
|
|
@@ -80,6 +80,12 @@ module CoreMerchant
|
|
80
80
|
validate :end_date_after_start_date, if: :end_date
|
81
81
|
validate :canceled_at_with_reason, if: :canceled_at
|
82
82
|
|
83
|
+
scope :due_for_renewal,
|
84
|
+
lambda {
|
85
|
+
where(status: %i[active trial past_due processing_renewal processing_payment])
|
86
|
+
.where("current_period_end <= ?", Time.current)
|
87
|
+
}
|
88
|
+
|
83
89
|
# Starts the subscription.
|
84
90
|
# Sets the current period start and end dates based on the plan's duration.
|
85
91
|
def start
|
@@ -89,8 +95,8 @@ module CoreMerchant
|
|
89
95
|
transaction do
|
90
96
|
transition_to_active!
|
91
97
|
update!(
|
92
|
-
current_period_start: new_period_start,
|
93
|
-
current_period_end: new_period_end
|
98
|
+
current_period_start: new_period_start.to_date,
|
99
|
+
current_period_end: new_period_end.to_date
|
94
100
|
)
|
95
101
|
end
|
96
102
|
|
@@ -119,6 +125,18 @@ module CoreMerchant
|
|
119
125
|
notify_subscription_manager(:canceled, reason: reason, immediate: !at_period_end)
|
120
126
|
end
|
121
127
|
|
128
|
+
# Starts a new period for the subscription.
|
129
|
+
# This is called by SubscriptionManager when a subscription renewal is successful.
|
130
|
+
def start_new_period
|
131
|
+
new_period_start = current_period_end
|
132
|
+
new_period_end = new_period_start + subscription_plan.duration_in_date
|
133
|
+
|
134
|
+
update!(
|
135
|
+
current_period_start: new_period_start.to_date,
|
136
|
+
current_period_end: new_period_end.to_date
|
137
|
+
)
|
138
|
+
end
|
139
|
+
|
122
140
|
# Returns the days remaining in the current period.
|
123
141
|
# Use to show the user how many days are left before the next renewal or
|
124
142
|
# to refund pro-rated amounts for early cancellations.
|
@@ -151,8 +169,23 @@ module CoreMerchant
|
|
151
169
|
end
|
152
170
|
|
153
171
|
def due_for_renewal?
|
154
|
-
|
155
|
-
|
172
|
+
renewable? && current_period_end <= Time.current
|
173
|
+
end
|
174
|
+
|
175
|
+
def expired_or_canceled?
|
176
|
+
expired? || canceled?
|
177
|
+
end
|
178
|
+
|
179
|
+
def processing?
|
180
|
+
processing_renewal? || processing_payment?
|
181
|
+
end
|
182
|
+
|
183
|
+
def ongoing?
|
184
|
+
active? || trial? || past_due? || processing_renewal? || processing_payment? || pending_cancellation?
|
185
|
+
end
|
186
|
+
|
187
|
+
def renewable?
|
188
|
+
active? || trial? || past_due? || processing?
|
156
189
|
end
|
157
190
|
|
158
191
|
private
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CoreMerchant
|
4
|
+
# The `SubscriptionEvent` model represents a historical log of events related to a subscription.
|
5
|
+
# It provides an audit trail of all significant actions and state changes for a subscription.
|
6
|
+
|
7
|
+
# This class has subclasses for specific event types, such as
|
8
|
+
# `SubscriptionRenewalEvent`, `SubscriptionStatusChangeEvent`, and `SubscriptionPlanChangeEvent`.
|
9
|
+
# Each subclass has additional fields specific to the event type.
|
10
|
+
|
11
|
+
# **Attributes**:
|
12
|
+
# - `subscription`: Association to the related Subscription
|
13
|
+
# - `event_type`: Type of the event (e.g., 'created', 'renewed', 'canceled', 'status_changed', 'plan_changed')
|
14
|
+
# - `metadata`: JSON field for storing additional event-specific data
|
15
|
+
|
16
|
+
# **Usage**:
|
17
|
+
# ```ruby
|
18
|
+
# # Automatically logged when a subscription is created
|
19
|
+
# subscription = CoreMerchant::Subscription.create(customer: user, subscription_plan: plan)
|
20
|
+
|
21
|
+
# # Logging a custom event
|
22
|
+
# subscription.log_event('custom_event', key: 'value')
|
23
|
+
|
24
|
+
# # Retrieve the last renewal event
|
25
|
+
# latest_renewal = subscription.renewal_events.last
|
26
|
+
# puts "Last renewed at: #{latest_renewal.created_at}"
|
27
|
+
# puts "Renewal price: #{latest_renewal.price_cents} cents, renewed until: #{latest_renewal.renewed_until}"
|
28
|
+
|
29
|
+
# # Retrieve the last event of any type
|
30
|
+
# latest_event = subscription.subscription_events.last
|
31
|
+
# puts "Last event type: #{latest_event.event_type}, metadata: #{latest_event.metadata}"
|
32
|
+
# ```
|
33
|
+
class SubscriptionEvent < ActiveRecord::Base
|
34
|
+
self.table_name = "core_merchant_subscription_events"
|
35
|
+
|
36
|
+
belongs_to :subscription, class_name: "CoreMerchant::Subscription"
|
37
|
+
|
38
|
+
validates :event_type, presence: true
|
39
|
+
|
40
|
+
def self.event_type
|
41
|
+
name.demodulize.underscore
|
42
|
+
end
|
43
|
+
|
44
|
+
def metadata
|
45
|
+
value = self[:metadata]
|
46
|
+
value.is_a?(Hash) ? value : JSON.parse(value || "{}")
|
47
|
+
end
|
48
|
+
|
49
|
+
def metadata=(value)
|
50
|
+
self[:metadata] = value.is_a?(Hash) ? value.to_json : value
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Represents a renewal event for a subscription.
|
55
|
+
class SubscriptionRenewalEvent < SubscriptionEvent
|
56
|
+
def price_cents
|
57
|
+
metadata["price_cents"]
|
58
|
+
end
|
59
|
+
|
60
|
+
def price_cents=(value)
|
61
|
+
self.metadata = metadata.merge(price_cents: value)
|
62
|
+
end
|
63
|
+
|
64
|
+
def renewed_from
|
65
|
+
metadata["renewed_from"].to_date
|
66
|
+
end
|
67
|
+
|
68
|
+
def renewed_from=(value)
|
69
|
+
self.metadata = metadata.merge(renewed_from: value)
|
70
|
+
end
|
71
|
+
|
72
|
+
def renewed_until
|
73
|
+
metadata["renewed_until"].to_date
|
74
|
+
end
|
75
|
+
|
76
|
+
def renewed_until=(value)
|
77
|
+
self.metadata = metadata.merge(renewed_until: value)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Represents a status change event for a subscription.
|
82
|
+
class SubscriptionStatusChangeEvent < SubscriptionEvent
|
83
|
+
def from
|
84
|
+
metadata["from_status"]
|
85
|
+
end
|
86
|
+
|
87
|
+
def from=(value)
|
88
|
+
self.metadata = metadata.merge(from_status: value)
|
89
|
+
end
|
90
|
+
|
91
|
+
def to
|
92
|
+
metadata["to_status"]
|
93
|
+
end
|
94
|
+
|
95
|
+
def to=(value)
|
96
|
+
self.metadata = metadata.merge(to_status: value)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Represents a plan change event for a subscription.
|
101
|
+
class SubscriptionPlanChangeEvent < SubscriptionEvent
|
102
|
+
def from_plan
|
103
|
+
id = metadata["from_plan_id"]
|
104
|
+
CoreMerchant::SubscriptionPlan.find(id) if id
|
105
|
+
end
|
106
|
+
|
107
|
+
def from_plan=(value)
|
108
|
+
self.metadata = metadata.merge(from_plan_id: value.id)
|
109
|
+
end
|
110
|
+
|
111
|
+
def to_plan
|
112
|
+
id = metadata["to_plan_id"]
|
113
|
+
CoreMerchant::SubscriptionPlan.find(id) if id
|
114
|
+
end
|
115
|
+
|
116
|
+
def to_plan=(value)
|
117
|
+
self.metadata = metadata.merge(to_plan_id: value.id)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Represents a cancellation event for a subscription.
|
122
|
+
class SubscriptionCancellationEvent < SubscriptionEvent
|
123
|
+
def canceled_at
|
124
|
+
created_at
|
125
|
+
end
|
126
|
+
|
127
|
+
def at_period_end?
|
128
|
+
metadata["at_period_end"]
|
129
|
+
end
|
130
|
+
|
131
|
+
def at_period_end=(value)
|
132
|
+
self.metadata = metadata.merge(at_period_end: value)
|
133
|
+
end
|
134
|
+
|
135
|
+
def reason
|
136
|
+
metadata["reason"]
|
137
|
+
end
|
138
|
+
|
139
|
+
def reason=(value)
|
140
|
+
self.metadata = metadata.merge(reason: value)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -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
|
@@ -28,6 +28,9 @@ module CoreMerchant
|
|
28
28
|
|
29
29
|
migration_template "migrate/create_core_merchant_subscriptions.erb",
|
30
30
|
"db/migrate/create_core_merchant_subscriptions.rb"
|
31
|
+
|
32
|
+
migration_template "migrate/create_core_merchant_subscription_events.erb",
|
33
|
+
"db/migrate/create_core_merchant_subscription_events.rb"
|
31
34
|
end
|
32
35
|
|
33
36
|
def show_post_install
|
@@ -36,7 +39,7 @@ module CoreMerchant
|
|
36
39
|
Next steps:
|
37
40
|
1. Set the customer class in the initializer file (config/initializers/core_merchant.rb) to the class you want to use for customers.
|
38
41
|
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
|
42
|
+
3. Run `rails db:migrate` to create the subscription, subscription plan, and subscription event tables.
|
40
43
|
MESSAGE
|
41
44
|
say next_steps, :yellow
|
42
45
|
end
|
@@ -47,7 +50,7 @@ module CoreMerchant
|
|
47
50
|
|
48
51
|
def self.description
|
49
52
|
<<~DESC
|
50
|
-
Installs CoreMerchant into your application. This generator will create an initializer file, migration files for the subscription and subscription
|
53
|
+
Installs CoreMerchant into your application. This generator will create an initializer file, migration files for the subscription, subscription plan, and subscription event tables, and a locale file.
|
51
54
|
DESC
|
52
55
|
end
|
53
56
|
end
|
data/lib/generators/core_merchant/templates/migrate/create_core_merchant_subscription_events.erb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# Created by: rails generate core_merchant:install
|
2
|
+
class CreateCoreMerchantSubscriptionEvents < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
3
|
+
def change
|
4
|
+
create_table :core_merchant_subscription_events do |t|
|
5
|
+
t.references :subscription, null: false, foreign_key: { to_table: :core_merchant_subscriptions }
|
6
|
+
t.string :event_type, null: false
|
7
|
+
t.jsonb :metadata, default: {}, null: false
|
8
|
+
|
9
|
+
t.timestamps
|
10
|
+
|
11
|
+
t.index :event_type
|
12
|
+
end
|
13
|
+
end
|
14
|
+
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.10.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-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -144,11 +144,13 @@ 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_event_association.rb
|
148
|
+
- lib/core_merchant/concerns/subscription_manager_notifications.rb
|
148
149
|
- lib/core_merchant/concerns/subscription_notifications.rb
|
149
150
|
- lib/core_merchant/concerns/subscription_state_machine.rb
|
150
151
|
- lib/core_merchant/customer_behavior.rb
|
151
152
|
- lib/core_merchant/subscription.rb
|
153
|
+
- lib/core_merchant/subscription_event.rb
|
152
154
|
- lib/core_merchant/subscription_listener.rb
|
153
155
|
- lib/core_merchant/subscription_manager.rb
|
154
156
|
- lib/core_merchant/subscription_plan.rb
|
@@ -156,6 +158,7 @@ files:
|
|
156
158
|
- lib/generators/core_merchant/install_generator.rb
|
157
159
|
- lib/generators/core_merchant/templates/core_merchant.en.yml
|
158
160
|
- lib/generators/core_merchant/templates/core_merchant.rb
|
161
|
+
- lib/generators/core_merchant/templates/migrate/create_core_merchant_subscription_events.erb
|
159
162
|
- lib/generators/core_merchant/templates/migrate/create_core_merchant_subscription_plans.erb
|
160
163
|
- lib/generators/core_merchant/templates/migrate/create_core_merchant_subscriptions.erb
|
161
164
|
- sig/core_merchant.rbs
|
@@ -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
|