core_merchant 0.3.0 → 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: 57cd9272d6c0056fdfd69e31fa31d88ebcee7e0f707d45201564ff464a1ad1d8
4
- data.tar.gz: 8715a63c453c11936e17b98d9712073a1f083f9f35170e6723d03f7789eef67f
3
+ metadata.gz: 4f973a1881b08969b6c5ba44de3ea5c3a047f3e1b28d8fc76e856a3c61ad4101
4
+ data.tar.gz: 1927e4b2202dcf7c0f25c41edc4bcdcac4abc89bf3f90d12f167afb392700c84
5
5
  SHA512:
6
- metadata.gz: cd166c331713fe259316708d16aee09ed7144a54302a52bf5b3aae298e58fa9bee37cc6371f54b3e6bf622be5fcf76ba5b2449e1fafd39e0ce177a5a1be44851
7
- data.tar.gz: a9760cb3d38f36b196bf0f3c22abee20e357bd1cd176c1385e36d00febf5ddbf07135a03a961454d65743063d6cb528f296836a19e042d062a5f557c8892652b
6
+ metadata.gz: 98c6446f4aaa3337e281099ab45bba98fdeae286c8332a753b943c0bbb086e37c026a72d870fe557c92d817eb8c9928d49a148b7c5bfac472d44bae5a09bda1c
7
+ data.tar.gz: e18ff914eaf377f8606c6b4766364678088cd1035384bd580d0ccad3b8bd6b920e513d11d63e9678c47d5e7bf35af8f34a2655377761e0eb5c71a9e758fe72e3
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- core_merchant (0.2.2)
4
+ core_merchant (0.5.1)
5
5
  activesupport (~> 7.0)
6
6
  rails (~> 7.0)
7
7
 
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 then include the `CoreMerchant::Customer` module in the customer class:
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CoreMerchant
4
- VERSION = "0.3.0"
4
+ VERSION = "0.6.0"
5
5
  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
- template "core_merchant.rb", "config/initializers/core_merchant.rb"
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
@@ -0,0 +1,5 @@
1
+ # Created by: rails generate core_merchant:install --customer_class=<%= @customer_class %>
2
+
3
+ CoreMerchant.configure do |config|
4
+ config.customer_class = "<%= @customer_class %>"
5
+ 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.3.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-20 00:00:00.000000000 Z
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.rb
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
@@ -1,4 +0,0 @@
1
- # Created by: rails generate core_merchant:install
2
- CoreMerchant.configure do |config|
3
- # config.customer_class = "User" # Uncomment and set to your class name that can represent a customer
4
- end