CoreMerchant
Caution
This gem is under development at pre-release stage
CoreMerchant is a library for customer, product, and subscription management in Rails applications. It's meant to be a starting point for building e-commerce and SaaS applications. It provides essential functionality for handling customers, products, and subscriptions. Does not include payment integrations.
To-do
Until 1.0.0 release
- Add customer behavior
- Add initializer generator
- Add SubscriptionPlan model
- Add Subscription model
- Implement subscription manager and callbacks
- Implement SubscriptionEvent model for logging
- Add Invoice model
- Add billing and invoicing service
For future releases
- Add Product model
- Add Cart model
Table of contents
- CoreMerchant
- To-do
- Table of contents
- Installation
- Usage
- Initialization
- Configuration
- 1. Customer class
- 2. Subscription listener class
- Subscription management
- Creating a subscription plan
- Creating a subscription
- Cancelling a subscription
- Handling subscription events
- Subscription History
- Public API
- SubscriptionManager
- SubscriptionPlan
- Subscription
- SubscriptionEvent
- Subclasses
- SubscriptionRenewalEvent
- SubscriptionStatusChangeEvent
- SubscriptionPlanChangeEvent
- SubscriptionCancellationEvent
- Subclasses
- Contributing
Installation
Add this line to your application's Gemfile:
gem 'core_merchant', '~> 0.10.0'
and run bundle install
.
Alternatively, you can install the gem manually:
$ gem install core_merchant
Usage
The steps below will guide you through setting up CoreMerchant in your Rails application. After completing these steps, you will be able to create subscription plans, manage subscriptions, and handle subscription events.
# Create a subscription plan
plan = CoreMerchant::SubscriptionPlan.create(name_key: 'basic', price_cents: 9_99, duration: '1m')
# Subscribe a customer to the plan
customer = current_user
subscription = CoreMerchant::Subscription.create(customer: customer, plan: plan)
# Start the subscription
CoreMerchant.subscription_manager.start_subscription(subscription)
# Use a listener to handle subscription events
class MySubscriptionListener
include CoreMerchant::SubscriptionListener
def on_subscription_started(subscription)
puts "Subscription started for #{subscription.customer.email}, subscribed to #{subscription.plan.name}"
end
def on_subscription_due_for_renewal(subscription)
puts "Subscription due for renewal for #{subscription.customer.email}, renewing until #{subscription.current_period_end_date}"
# After payment is processed
CoreMerchant.subscription_manager.payment_successful_for_renewal(subscription)
end
end
Initialization
Run the generator to create the initializer file and the migrations:
$ rails generate core_merchant:install
This will create the following files:
-
config/initializers/core_merchant.rb
- Configuration file -
config/locales/core_merchant.en.yml
- English translations for core merchant models -
db/migrate/xxxxxx_create_core_merchant_subscription_plans.rb
- Migration for subscription plans -
db/migrate/xxxxxx_create_core_merchant_subscriptions.rb
- Migration for subscriptions You can then run the migrations:
$ rails db:migrate
Configuration
The initializer file config/initializers/core_merchant.rb
contains the following configuration options:
1. Customer class
config.customer_class = 'User'
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:
# app/models/user.rb
class User < ApplicationRecord
include CoreMerchant::Customer
# ... the rest of your model code
end
2. Subscription listener class
config.subscription_listener_class = 'MySubscriptionListener'
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:
class MySubscriptionListener
include CoreMerchant::SubscriptionListener
def on_test_event_received
puts 'Test event received, hooray!'
end
end
More about subscription events in the Handling subscription events section.
Subscription management
Creating a subscription plan
You can create a subscription plan using the SubscriptionPlan
model:
CoreMerchant::SubscriptionPlan.create(name_key: 'basic', price_cents: 10_00, duration: '1m')
Creating a subscription
You can create a subscription for a customer using the Subscription
model:
customer = User.find(1)
plan = CoreMerchant::SubscriptionPlan.find_by(name_key: 'basic')
subscription = CoreMerchant::Subscription.create(customer: customer, plan: plan)
Note that the subscription will not be active until you start it:
subscription.start
Cancelling a subscription
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:
subscription.cancel(reason: 'Customer request', at_period_end: false)
Handling subscription events
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:
class MySubscriptionListener
include CoreMerchant::SubscriptionListener
def on_subscription_created(subscription)
FakeEmailService.send_email(subscription.customer.email, "We're happy to have you on board!")
# You can also start the subscription automatically
subscription.start
end
def on_subscription_started(subscription)
FakeEmailService.send_email(subscription.customer.email, "Your subscription has started!")
end
def on_subscription_due_for_renewal(subscription)
success = FakePaymentService.charge(subscription.customer, subscription.plan.price_cents)
if success
CoreMerchant.subscription_manager.payment_successful_for_renewal(subscription)
else
FakeEmailService.send_email(subscription.customer.email, "Payment failed, please update your payment method.")
CoreMerchant.subscription_manager.payment_failed_for_renewal(subscription)
end
end
def on_subscription_renewed(subscription)
FakeEmailService.send_email(subscription.customer.email, "Your subscription has been renewed until #{subscription.current_period_end_date}")
end
end
Available subscription events:
on_subscription_created(subscription)
on_subscription_destroyed(subscription)
on_subscription_started(subscription)
on_subscription_canceled(subscription, reason:, at_period_end:)
on_subscription_due_for_renewal(subscription)
on_subscription_renewed(subscription)
on_subscription_renewal_payment_processing(subscription)
on_subscription_grace_period_started(subscription, days_remaining:)
Subscription History
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.
To access a subscription's history:
subscription = CoreMerchant::Subscription.find(42)
# Get all events
subscription.subscription_events
# Get specific event types
latest_renewal = subscription.renewal_events.last
puts "Last renewed at: #{latest_renewal.created_at}"
puts "Renewal price: #{latest_renewal.price_cents} cents, renewed until: #{latest_renewal.renewed_until}"
latest_status_change = subscription.status_change_events.last
puts "Status changed from #{latest_status_change.from} to #{latest_status_change.to}"
latest_plan_change = subscription.plan_change_events.last
puts "Plan changed from #{latest_plan_change.from_plan.name} to #{latest_plan_change.to_plan.name}"
Public API
SubscriptionManager
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.
Attributes:
-
listeners
- An array of listeners that will be notified when subscription events occur.
Methods:
-
check_subscriptions
- Checks all subscriptions for renewals -
add_listener(listener)
- Adds a listener to the list of listeners -
no_payment_needed_for_renewal(subscription)
- Handles the case where no payment is needed for a renewal. Call when a subscription is renewed without payment. -
processing_payment_for_renewal(subscription)
- Handles the case where payment is being processed for a renewal. Call when payment is being processed for a renewal. -
payment_successful_for_renewal(subscription)
- Handles the case where payment was successful for a renewal. Call when payment was successful for a renewal. -
payment_failed_for_renewal(subscription)
- Handles the case where payment failed for a renewal. Call when payment failed for a renewal.
Usage:
manager = CoreMerchant.subscription_manager
manager.check_subscriptions
# ... somewhere else in the code ...
manager.payment_successful_for_renewal(subscription1)
manager.payment_failed_for_renewal(subscription2)
SubscriptionPlan
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.
Attributes:
-
name_key
: A unique key for the subscription plan. This key is used to identify the plan in the application, as well as the translation key for the plan name throughcore_merchant.subscription_plans
. -
price_cents
: The price of the subscription plan in cents. -
duration
: The duration of the subscription plan. Consists of a number and a letter representing the time unit as day, week, month, or year. For example,1w
for 1 week,3m
for 3 months,1y
for 1 year. -
introductory_price_cents
: The introductory price of the subscription plan in cents. -
introductory_duration
: The duration of the introductory price of the subscription plan.
Usage:
plan = CoreMerchant::SubscriptionPlan.new(name_key: "basic_monthly", price_cents: 7_99)
plan.save
Subscription
Represents a subscription in CoreMerchant. This class manages the lifecycle of a customer's subscription to a specific plan.
Subscriptions can transition through various statuses:
-
pending
: Subscription created but not yet started -
trial
: In a trial period -
active
: Currently active and paid -
past_due
: Payment failed but in grace period -
pending_cancellation
: Will be canceled at period end -
processing_renewal
: Renewal in progress -
processing_payment
: Payment processing -
canceled
: Canceled by user or due to payment failure -
expired
: Subscription period ended -
paused
: Temporarily halted, not yet implemented -
pending_change
: Plan change scheduled for next renewal, not yet implemented
Key features:
- Supports immediate and end-of-period cancellations
- Allows plan changes, effective immediately or at next renewal
- Handles subscription pausing and resuming
- Manages trial periods
- Supports variable pricing for renewals
Attributes:
-
customer
: Polymorphic association to the customer -
subscription_plan
: The current plan for this subscription -
status
: Current status of the subscription (see enum definition) -
start_date
: When the subscription started -
end_date
: When the subscription ended (or will end) -
trial_end_date
: End date of the trial period (if applicable) -
canceled_at
: When the subscription was canceled -
current_period_start
: Start of the current billing period -
current_period_end
: End of the current billing period -
pause_start_date
: When the subscription was paused -
pause_end_date
: When the paused subscription will resume -
current_period_price_cents
: Price for the current period -
next_renewal_price_cents
: Price for the next renewal (if different from plan) -
cancellation_reason
: Reason for cancellation (if applicable)
Methods:
-
start
- Starts the subscription -
cancel(reason:, at_period_end:)
- Cancels the subscription, optionally at the end of the current period
Usage:
subscription = CoreMerchant::Subscription.create(customer: user, subscription_plan: plan, status: :active)
subscription.start
subscription.cancel(reason: "Too expensive", at_period_end: true)
SubscriptionEvent
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.
This class has subclasses for specific event types, such as SubscriptionRenewalEvent
, SubscriptionStatusChangeEvent
, and SubscriptionPlanChangeEvent
. Each subclass has additional fields specific to the event type.
Attributes:
-
subscription
: Association to the related Subscription -
event_type
: Type of the event (e.g., 'created', 'renewed', 'canceled', 'status_changed', 'plan_changed') -
metadata
: JSON field for storing additional event-specific data
Usage:
# Automatically logged when a subscription is created
subscription = CoreMerchant::Subscription.create(customer: user, subscription_plan: plan)
# Logging a custom event
subscription.log_event('custom_event', key: 'value')
# Retrieve the last renewal event
latest_renewal = subscription.renewal_events.last
puts "Last renewed at: #{latest_renewal.created_at}"
puts "Renewal price: #{latest_renewal.price_cents} cents, renewed until: #{latest_renewal.renewed_until}"
# Retrieve the last event of any type
latest_event = subscription.subscription_events.last
puts "Last event type: #{latest_event.event_type}, metadata: #{latest_event.metadata}"
Subclasses
SubscriptionRenewalEvent
-
price_cents
: The price of the renewal in cents -
renewed_until
: The end date of the renewal -
renewed_at
: The date and time of the renewal
SubscriptionStatusChangeEvent
-
from
: The previous status of the subscription -
to
: The new status of the subscription
SubscriptionPlanChangeEvent
-
from_plan
: The previous plan of the subscription -
to_plan
: The new plan of the subscription
SubscriptionCancellationEvent
-
reason
: The reason for the cancellation -
at_period_end
: Whether the cancellation is scheduled for the end of the current period -
canceled_at
: The date and time of the cancellation
Note
Other models and features are being developed and will be added in future releases.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/theseyithan/core_merchant