core_merchant 0.5.1 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4f973a1881b08969b6c5ba44de3ea5c3a047f3e1b28d8fc76e856a3c61ad4101
|
4
|
+
data.tar.gz: 1927e4b2202dcf7c0f25c41edc4bcdcac4abc89bf3f90d12f167afb392700c84
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 98c6446f4aaa3337e281099ab45bba98fdeae286c8332a753b943c0bbb086e37c026a72d870fe557c92d817eb8c9928d49a148b7c5bfac472d44bae5a09bda1c
|
7
|
+
data.tar.gz: e18ff914eaf377f8606c6b4766364678088cd1035384bd580d0ccad3b8bd6b920e513d11d63e9678c47d5e7bf35af8f34a2655377761e0eb5c71a9e758fe72e3
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -31,8 +31,10 @@ $ gem install core_merchant
|
|
31
31
|
### Initialization
|
32
32
|
Run the generator to create the initializer file and the migrations:
|
33
33
|
```
|
34
|
-
$ rails generate core_merchant:install
|
34
|
+
$ rails generate core_merchant:install --customer_class=User
|
35
35
|
```
|
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
|
+
|
36
38
|
This will create the following files:
|
37
39
|
- `config/initializers/core_merchant.rb` - Configuration file
|
38
40
|
- `db/migrate/xxxxxx_create_core_merchant_subscription_plans.rb` - Migration for subscription plans
|
@@ -45,19 +47,10 @@ $ rails db:migrate
|
|
45
47
|
### Configuration
|
46
48
|
The initializer file `config/initializers/core_merchant.rb` contains the following configuration options:
|
47
49
|
```ruby
|
48
|
-
config.customer_class
|
49
|
-
```
|
50
|
-
|
51
|
-
### The cusomer class
|
52
|
-
The customer class is 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. To do this, you need to set the `customer_class` configuration option in the initializer file:
|
53
|
-
```ruby
|
54
|
-
# config/initializers/core_merchant.rb
|
55
|
-
CoreMerchant.configure do |config|
|
56
|
-
config.customer_class = 'User'
|
57
|
-
end
|
50
|
+
config.customer_class = 'User'
|
58
51
|
```
|
59
52
|
|
60
|
-
You need to
|
53
|
+
You need to include the `CoreMerchant::Customer` module in the customer class:
|
61
54
|
```ruby
|
62
55
|
# app/models/user.rb
|
63
56
|
class User < ApplicationRecord
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
|
5
|
+
module CoreMerchant
|
6
|
+
module Concerns
|
7
|
+
class InvalidTransitionError < CoreMerchant::Error; end
|
8
|
+
|
9
|
+
# Adds state machine logic to a subscription.
|
10
|
+
# This module defines the possible states and transitions for a subscription.
|
11
|
+
# Possible transitions:
|
12
|
+
# - `pending` -> `active`, `trial`
|
13
|
+
# - `trial` -> `active`, `pending_cancellation`, `canceled`
|
14
|
+
# - `active` -> `pending_cancellation`, `canceled`, `expired`
|
15
|
+
# - `pending_cancellation` -> `canceled`, `expired`
|
16
|
+
# - `canceled` -> `pending`, `active`
|
17
|
+
# - `expired` -> `pending`, `active`
|
18
|
+
module SubscriptionStateMachine
|
19
|
+
extend ActiveSupport::Concern
|
20
|
+
|
21
|
+
# List of possible transitions in the form of { to_state: [from_states] }
|
22
|
+
POSSIBLE_TRANSITIONS = {
|
23
|
+
pending: %i[canceled expired],
|
24
|
+
trial: %i[pending],
|
25
|
+
active: %i[pending trial canceled expired],
|
26
|
+
pending_cancellation: %i[active trial],
|
27
|
+
canceled: %i[active pending_cancellation trial],
|
28
|
+
expired: %i[active pending_cancellation canceled trial],
|
29
|
+
past_due: [],
|
30
|
+
paused: [],
|
31
|
+
pending_change: []
|
32
|
+
}.freeze
|
33
|
+
|
34
|
+
included do
|
35
|
+
POSSIBLE_TRANSITIONS.each do |to_state, from_states|
|
36
|
+
define_method("can_transition_to_#{to_state}?") do
|
37
|
+
from_states.include?(status.to_sym)
|
38
|
+
end
|
39
|
+
|
40
|
+
define_method("transition_to_#{to_state}") do
|
41
|
+
return false unless send("can_transition_to_#{to_state}?")
|
42
|
+
|
43
|
+
update(status: to_state)
|
44
|
+
end
|
45
|
+
|
46
|
+
define_method("transition_to_#{to_state}!") do
|
47
|
+
raise InvalidTransitionError unless send("transition_to_#{to_state}")
|
48
|
+
|
49
|
+
update!(status: to_state)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "core_merchant/concerns/subscription_state_machine"
|
4
|
+
|
5
|
+
module CoreMerchant
|
6
|
+
# Represents a subscription in CoreMerchant.
|
7
|
+
# This class manages the lifecycle of a customer's subscription to a specific plan.
|
8
|
+
#
|
9
|
+
# **Subscriptions can transition through various statuses**:
|
10
|
+
# - `pending`: Subscription created but not yet started
|
11
|
+
# - `trial`: In a trial period
|
12
|
+
# - `active`: Currently active and paid
|
13
|
+
# - `past_due`: Payment failed but in grace period, not yet implemented
|
14
|
+
# - `pending_cancellation`: Will be canceled at period end
|
15
|
+
# - `canceled`: Canceled by user or due to payment failure
|
16
|
+
# - `expired`: Subscription period ended
|
17
|
+
# - `paused`: Temporarily halted, not yet implemented
|
18
|
+
# - `pending_change`: Plan change scheduled for next renewal, not yet implemented
|
19
|
+
#
|
20
|
+
# **Key features**:
|
21
|
+
# - Supports immediate and end-of-period cancellations
|
22
|
+
# - Allows plan changes, effective immediately or at next renewal
|
23
|
+
# - Handles subscription pausing and resuming
|
24
|
+
# - Manages trial periods
|
25
|
+
# - Supports variable pricing for renewals
|
26
|
+
#
|
27
|
+
# **Attributes**:
|
28
|
+
# - `customer`: Polymorphic association to the customer
|
29
|
+
# - `subscription_plan`: The current plan for this subscription
|
30
|
+
# - `next_subscription_plan`: The plan to change to at next renewal (if any)
|
31
|
+
# - `status`: Current status of the subscription (see enum definition)
|
32
|
+
# - `start_date`: When the subscription started
|
33
|
+
# - `end_date`: When the subscription ended (or will end)
|
34
|
+
# - `trial_end_date`: End date of the trial period (if applicable)
|
35
|
+
# - `canceled_at`: When the subscription was canceled
|
36
|
+
# - `current_period_start`: Start of the current billing period
|
37
|
+
# - `current_period_end`: End of the current billing period
|
38
|
+
# - `pause_start_date`: When the subscription was paused
|
39
|
+
# - `pause_end_date`: When the paused subscription will resume
|
40
|
+
# - `next_renewal_price_cents`: Price for the next renewal (if different from plan)
|
41
|
+
# - `cancellation_reason`: Reason for cancellation (if applicable)
|
42
|
+
#
|
43
|
+
# **Usage**:
|
44
|
+
# ```ruby
|
45
|
+
# subscription = CoreMerchant::Subscription.create(customer: user, subscription_plan: plan, status: :active)
|
46
|
+
# subscription.cancel(reason: "Too expensive", at_period_end: true)
|
47
|
+
# subscription.change_plan(new_plan, at_period_end: false)
|
48
|
+
# subscription.pause(until_date: 1.month.from_now)
|
49
|
+
# subscription.resume
|
50
|
+
# subscription.renew(price_cents: 1999)
|
51
|
+
# ```
|
52
|
+
class Subscription < ActiveRecord::Base
|
53
|
+
include CoreMerchant::Concerns::SubscriptionStateMachine
|
54
|
+
|
55
|
+
self.table_name = "core_merchant_subscriptions"
|
56
|
+
|
57
|
+
belongs_to :customer, polymorphic: true
|
58
|
+
belongs_to :subscription_plan
|
59
|
+
|
60
|
+
enum status: {
|
61
|
+
pending: 0,
|
62
|
+
trial: 1,
|
63
|
+
active: 2,
|
64
|
+
past_due: 3, # Logic not yet implemented
|
65
|
+
pending_cancellation: 4,
|
66
|
+
canceled: 5,
|
67
|
+
expired: 6,
|
68
|
+
paused: 7, # Logic not yet implemented
|
69
|
+
pending_change: 8 # Logic not yet implemented
|
70
|
+
}
|
71
|
+
|
72
|
+
validates :customer, :subscription_plan, :status, :start_date, presence: true
|
73
|
+
validates :status, inclusion: { in: statuses.keys }
|
74
|
+
validate :end_date_after_start_date, if: :end_date
|
75
|
+
validate :canceled_at_with_reason, if: :canceled_at
|
76
|
+
|
77
|
+
def start
|
78
|
+
new_period_start = start_date
|
79
|
+
new_period_end = new_period_start + subscription_plan.duration_in_date
|
80
|
+
|
81
|
+
transition_to_active!
|
82
|
+
update!(
|
83
|
+
current_period_start: new_period_start,
|
84
|
+
current_period_end: new_period_end
|
85
|
+
)
|
86
|
+
end
|
87
|
+
|
88
|
+
def cancel(reason:, at_period_end:)
|
89
|
+
if at_period_end
|
90
|
+
transition_to_pending_cancellation!
|
91
|
+
else
|
92
|
+
transition_to_canceled!
|
93
|
+
end
|
94
|
+
update!(
|
95
|
+
canceled_at: at_period_end ? current_period_end : Time.current,
|
96
|
+
cancellation_reason: reason
|
97
|
+
)
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def set_period_dates
|
103
|
+
self.start_date ||= Time.current
|
104
|
+
self.current_period_start ||= start_date
|
105
|
+
self.current_period_end ||= start_date + subscription_plan.duration_in_date
|
106
|
+
end
|
107
|
+
|
108
|
+
def end_date_after_start_date
|
109
|
+
errors.add(:end_date, "must be after the `start date") if end_date <= start_date
|
110
|
+
end
|
111
|
+
|
112
|
+
def canceled_at_with_reason
|
113
|
+
errors.add(:cancellation_reason, "must be present when `canceled_at` is set") if cancellation_reason.blank?
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -33,7 +33,9 @@ module CoreMerchant
|
|
33
33
|
|
34
34
|
def show_post_install
|
35
35
|
say "CoreMerchant has been successfully installed.", :green
|
36
|
-
say
|
36
|
+
say <<~MESSAGE
|
37
|
+
Customer class: #{@customer_class}. Please update this model to include the CoreMerchant::CustomerBehavior module.
|
38
|
+
MESSAGE
|
37
39
|
say "Please run `rails db:migrate` to create the subscription plans table."
|
38
40
|
end
|
39
41
|
|
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.6.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-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -130,7 +130,9 @@ files:
|
|
130
130
|
- Rakefile
|
131
131
|
- core_merchant.gemspec
|
132
132
|
- lib/core_merchant.rb
|
133
|
+
- lib/core_merchant/concerns/subscription_state_machine.rb
|
133
134
|
- lib/core_merchant/customer_behavior.rb
|
135
|
+
- lib/core_merchant/subscription.rb
|
134
136
|
- lib/core_merchant/subscription_plan.rb
|
135
137
|
- lib/core_merchant/version.rb
|
136
138
|
- lib/generators/core_merchant/install_generator.rb
|