core_merchant 0.8.0 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|