core_merchant 0.3.0 → 0.6.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 +5 -12
- data/lib/core_merchant/concerns/subscription_state_machine.rb +55 -0
- data/lib/core_merchant/subscription.rb +116 -0
- data/lib/core_merchant/version.rb +1 -1
- data/lib/generators/core_merchant/install_generator.rb +24 -1
- data/lib/generators/core_merchant/templates/core_merchant.erb +5 -0
- metadata +5 -3
- data/lib/generators/core_merchant/templates/core_merchant.rb +0 -4
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
|
@@ -10,8 +10,12 @@ module CoreMerchant
|
|
10
10
|
|
11
11
|
source_root File.expand_path("templates", __dir__)
|
12
12
|
|
13
|
+
class_option :customer_class, type: :string, required: true,
|
14
|
+
desc: "Name of your existing customer class, e.g. User"
|
15
|
+
|
13
16
|
def copy_initializer
|
14
|
-
|
17
|
+
@customer_class = options[:customer_class].classify
|
18
|
+
template "core_merchant.erb", "config/initializers/core_merchant.rb"
|
15
19
|
end
|
16
20
|
|
17
21
|
def copy_locales
|
@@ -26,6 +30,25 @@ module CoreMerchant
|
|
26
30
|
migration_template "create_core_merchant_subscription_plans.erb",
|
27
31
|
"db/migrate/create_core_merchant_subscription_plans.rb"
|
28
32
|
end
|
33
|
+
|
34
|
+
def show_post_install
|
35
|
+
say "CoreMerchant has been successfully installed.", :green
|
36
|
+
say <<~MESSAGE
|
37
|
+
Customer class: #{@customer_class}. Please update this model to include the CoreMerchant::CustomerBehavior module.
|
38
|
+
MESSAGE
|
39
|
+
say "Please run `rails db:migrate` to create the subscription plans table."
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.banner
|
43
|
+
"rails generate core_merchant:install --customer_class=User"
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.description
|
47
|
+
<<~DESC
|
48
|
+
Installs CoreMerchant into your application with the specified customer class.
|
49
|
+
This could be User, Customer, or any other existing model in your application that represents a customer."
|
50
|
+
DESC
|
51
|
+
end
|
29
52
|
end
|
30
53
|
end
|
31
54
|
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.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,12 +130,14 @@ 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
|
137
139
|
- lib/generators/core_merchant/templates/core_merchant.en.yml
|
138
|
-
- lib/generators/core_merchant/templates/core_merchant.
|
140
|
+
- lib/generators/core_merchant/templates/core_merchant.erb
|
139
141
|
- lib/generators/core_merchant/templates/create_core_merchant_subscription_plans.erb
|
140
142
|
- sig/core_merchant.rbs
|
141
143
|
homepage: https://github.com/theseyithan/core_merchant
|