Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add billing #329

Merged
merged 41 commits into from
May 3, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
dafd449
Add trial period
riggraz Apr 19, 2024
bff353a
fix specs
riggraz Apr 19, 2024
20b53bd
fix specs
riggraz Apr 19, 2024
d748fe4
fix specs
riggraz Apr 19, 2024
b64bc74
fix specs
riggraz Apr 19, 2024
f7936bf
fix specs
riggraz Apr 19, 2024
a183ab7
fix application_controller check_tenant_subscription
riggraz Apr 19, 2024
f72bba1
fix
riggraz Apr 19, 2024
e3b223a
default to 7 days if TRIAL_PERIOD_DAYS env var not defined
riggraz Apr 20, 2024
e2bb684
refactor tenant_billings factory
riggraz Apr 20, 2024
16c0fc3
Add basic billing page
riggraz Apr 20, 2024
f819633
add basic payment workflow
riggraz Apr 20, 2024
07270de
Add webhook endpoint for fulfilling completed subscriptions
riggraz Apr 20, 2024
3d428b1
init subscription_ends_at
riggraz Apr 21, 2024
4cdc79f
fix specs
riggraz Apr 21, 2024
9c85a18
add webhook for subscription update + various improvements
riggraz Apr 21, 2024
bf1e85c
fix codeql alert
riggraz Apr 21, 2024
872d106
add tos and pp
riggraz Apr 21, 2024
ddb067f
Improve billing page style
riggraz Apr 24, 2024
5e78e58
Add authentication to billing pages
riggraz Apr 24, 2024
f5e5440
Show "choose another plan" link after 5 seconds
riggraz Apr 24, 2024
4463da4
Move billing page to billing subdomain
riggraz Apr 27, 2024
2132dd2
move everything related to billing to billing subdomain
riggraz Apr 29, 2024
9cc2257
move stripe public key to env variable
riggraz Apr 29, 2024
7bc74c1
disable user sign out on billing pages
riggraz Apr 29, 2024
ae835ba
refactor billing_controller
riggraz Apr 29, 2024
d6850f9
add trial period and tos acceptance in tenant signup
riggraz Apr 29, 2024
0f3e2be
refactor
riggraz Apr 30, 2024
8e89470
Add smooth scrolling to checkout form
riggraz Apr 30, 2024
542b2d8
add some tenant subscription confirmation emails
riggraz Apr 30, 2024
426575f
update some mailer settings
riggraz Apr 30, 2024
37087b7
add some mailers
riggraz May 1, 2024
e9a0db2
add 'owner' method to tenant
riggraz May 1, 2024
99e7b05
add cron jobs to notify tenants in trial period
riggraz May 2, 2024
dbf52bb
increase scrollto checkout delay
riggraz May 2, 2024
24749fd
improve emails
riggraz May 3, 2024
fabb9a1
remove whenever gem and cron from web container
riggraz May 3, 2024
d2d1e0f
minor fixes
riggraz May 3, 2024
836c6c8
add alert near billing when trial ended
riggraz May 3, 2024
5874313
update emails
riggraz May 3, 2024
5f7a814
update rake task
riggraz May 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ gem 'rack-attack', '6.7.0'
# Slugs
gem 'friendly_id', '5.5.1'

# Billing
gem 'stripe', '11.2.0'

# CORS
gem 'rack-cors', '2.0.2'

group :development, :test do
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]

Expand Down
5 changes: 5 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ GEM
rack (2.2.8.1)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-cors (2.0.2)
rack (>= 2.0.0)
rack-test (2.1.0)
rack (>= 1.3)
rails (6.1.7.7)
Expand Down Expand Up @@ -252,6 +254,7 @@ GEM
actionpack (>= 5.2)
activesupport (>= 5.2)
sprockets (>= 3.0.0)
stripe (11.2.0)
thor (1.3.1)
tilt (2.0.10)
timeout (0.4.1)
Expand Down Expand Up @@ -299,13 +302,15 @@ DEPENDENCIES
puma (= 5.6.8)
pundit (= 2.2.0)
rack-attack (= 6.7.0)
rack-cors (= 2.0.2)
rails (= 6.1.7.7)
rake (= 12.3.3)
react-rails (= 2.6.2)
rspec-rails (= 4.0.2)
selenium-webdriver (= 4.1.0)
spring (= 2.1.1)
spring-watcher-listen (= 2.0.1)
stripe (= 11.2.0)
turbolinks (= 5.2.1)
tzinfo-data
web-console (>= 3.3.0)
Expand Down
1 change: 1 addition & 0 deletions app/assets/stylesheets/application.sass.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
@import 'components/LikeButton';
@import 'components/Post';
@import 'components/Roadmap';
@import 'components/Billing';

/* Site Settings Components */
@import 'components/SiteSettings';
Expand Down
3 changes: 2 additions & 1 deletion app/assets/stylesheets/common/_custom_texts.scss
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
.smallMutedText {
@extend
.mutedText,
.m-0;
.mt-1,
.mb-0;

font-size: smaller;
}
Expand Down
16 changes: 15 additions & 1 deletion app/assets/stylesheets/common/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,14 @@ body {
}

.link {
color: var(--astuto-black);
text-decoration: underline;
cursor: pointer;
&:hover { text-decoration: underline; }

&:hover {
color: var(--astuto-black);
text-decoration: underline;
}
}

.actionLink {
Expand Down Expand Up @@ -301,4 +307,12 @@ body {

a { color: var(--astuto-grey); }
a:hover { text-decoration: underline; }
}

.noActiveSubscriptionBanner {
@extend
.alert,
.alert-warning,
.text-center,
.m-0;
}
122 changes: 122 additions & 0 deletions app/assets/stylesheets/components/Billing.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
.billingContainer {
max-width: 640px;
margin: 0 auto;

h2 { @extend .mb-4; }

.billingStatusBadge {
@extend
.badge,
.badgeLight,
.p-2,
.ml-1,
.mr-1;

width: fit-content;
text-transform: uppercase;
}

.billingStatusBadgeExpired {
color: white;
background-color: $danger;
}

.pricingTable {
@extend
.d-flex,
.flex-column,
.mt-4,
.mb-4;

h3 { text-align: center; }

.pricingPlansNav {
@extend
.nav,
.nav-pills,
.align-self-center,
.px-2,
.py-1,
.mt-2;

.yearlyPlanDiscount {
@extend .ml-2;
color: red;
}

background-color: var(--astuto-grey-light);
border-radius: 0.5rem;

li.nav-item {
width: 130px;
}

a {
@extend
.px-3,
.py-1;

color: var(--astuto-black);
cursor: pointer;
text-align: center;

&::first-letter { text-transform: uppercase; }
}

a.nav-link.active {
color: var(--astuto-black);
background-color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
}

.pricingTableColumn {
@extend
.card,
.my-2,
.p-4;

width: 100%;
max-width: 400px;
margin: 0 auto;

.priceContainer {
@extend
.d-flex,
.justify-content-between;

.price {
.amount { font-size: 36px; }
}
.priceYearly {
@extend .align-self-end;

.amount { font-size: 26px; }
}

.price, .priceYearly {
.amount { font-weight: 700; }
.currency { text-transform: uppercase; }
}
}
}
}

.checkoutContainer {
@extend .mt-4;

a { display: block; text-align: center; }

#checkout { @extend .my-2; }
}

.billingUsefulLinks {
@extend
.d-flex,
.mt-4;

margin: 0 auto;

a { @extend .mx-2; }
}
}
9 changes: 9 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def load_tenant_data
# Load tenant data
@tenant = Current.tenant_or_raise!
@tenant_setting = TenantSetting.first_or_create
@tenant_billing = TenantBilling.first_or_create
@boards = Board.select(:id, :name, :slug).order(order: :asc)

# Setup locale
Expand All @@ -48,6 +49,14 @@ def load_oauths
.order(created_at: :asc)
end

def check_tenant_subscription
return if Current.tenant.tenant_billing.has_active_subscription?

render json: {
error: 'Your subscription has expired. Please renew it to continue using the service.'
}, status: :forbidden
end

private

def user_not_authorized
Expand Down
115 changes: 115 additions & 0 deletions app/controllers/billing_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
require 'stripe'

class BillingController < ApplicationController
before_action :check_multi_tenancy
before_action :authenticate_owner, only: [:request_billing_page, :return]
skip_before_action :verify_authenticity_token, only: [:create_checkout_session, :webhook]
Fixed Show fixed Hide fixed

def request_billing_page
tb = Current.tenant.tenant_billing
tenant_id = tb.slug
auth_token = tb.generate_auth_token

redirect_to billing_url(tenant_id: tenant_id, auth_token: auth_token)
end

def index
return unless params[:tenant_id] and params[:auth_token]

tb = TenantBilling.unscoped.find_by(slug: params[:tenant_id])
Current.tenant = tb.tenant

if (user_signed_in? && current_user.owner?) || (tb.auth_token == params[:auth_token])
# needed because ApplicationController#load_tenant_data is not called for this action
@tenant = tb.tenant
@tenant_setting = @tenant.tenant_setting
@tenant_billing = tb
@boards = Board.select(:id, :name, :slug).order(order: :asc)
I18n.locale = @tenant.locale

owner = User.find_by(role: "owner")
sign_in owner
tb.invalidate_auth_token

@page_title = t('billing.title')
@prices = Stripe::Price.list({limit: 2}).data
else
redirect_to get_url_for(method(:root_url))
end
end

def return
@page_title = t('billing.title')

session = Stripe::Checkout::Session.retrieve(params[:session_id])
Current.tenant.tenant_billing.update!(customer_id: session.customer)
end

def create_checkout_session
session = Stripe::Checkout::Session.create({
ui_mode: 'embedded',
line_items: [{
price: params[:price_id],
quantity: 1,
}],
mode: 'subscription',
return_url: "#{get_url_for(method(:billing_return_url))}?session_id={CHECKOUT_SESSION_ID}",
customer: Current.tenant.tenant_billing.customer_id,
})

render json: { clientSecret: session.client_secret }
end

def session_status
session = Stripe::Checkout::Session.retrieve(params[:session_id])
render json: { status: session.status, session: session }
end

def webhook
event = nil

# Verify webhook signature and extract the event
# See https://stripe.com/docs/webhooks#verify-events for more information.
begin
sig_header = request.env['HTTP_STRIPE_SIGNATURE']
payload = request.body.read
event = Stripe::Webhook.construct_event(payload, sig_header, Rails.application.stripe_endpoint_secret)
rescue JSON::ParserError => e
# Invalid payload
return head :bad_request
rescue Stripe::SignatureVerificationError => e
# Invalid signature
return head :bad_request
end

if event['type'] == 'invoice.paid'
Current.tenant = get_tenant_from_customer_id(event.data.object.customer)

subscription_type = event.data.object.lines.data[0].price.lookup_key
return head :bad_request unless subscription_type == 'monthly' || subscription_type == 'yearly'

subscription_duration = subscription_type == 'monthly' ? 1.month : 1.year
Current.tenant.tenant_billing.update!(
status: 'active',
subscription_ends_at: Time.current + subscription_duration
)
elsif event['type'] == 'customer.subscription.updated'
Current.tenant = get_tenant_from_customer_id(event.data.object.customer)

has_canceled = event.data.object.cancel_at_period_end
Current.tenant.tenant_billing.update!(status: has_canceled ? 'canceled' : 'active')
end

return head :ok
end

private

def check_multi_tenancy
redirect_to root_path unless Rails.application.multi_tenancy?
end

def get_tenant_from_customer_id(customer_id)
TenantBilling.unscoped.find_by(customer_id: customer_id).tenant
end
end
1 change: 1 addition & 0 deletions app/controllers/comments_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
class CommentsController < ApplicationController
before_action :authenticate_user!, only: [:create, :update, :destroy]
before_action :check_tenant_subscription, only: [:create, :update, :destroy]

def index
comments = Comment
Expand Down
1 change: 1 addition & 0 deletions app/controllers/follows_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
class FollowsController < ApplicationController
before_action :authenticate_user!, only: [:create, :destroy]
before_action :check_tenant_subscription, only: [:create, :destroy]

def index
unless user_signed_in?
Expand Down
1 change: 1 addition & 0 deletions app/controllers/likes_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
class LikesController < ApplicationController
before_action :authenticate_user!, only: [:create, :destroy]
before_action :check_tenant_subscription, only: [:create, :destroy]

def index
likes = Like
Expand Down
1 change: 1 addition & 0 deletions app/controllers/posts_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
class PostsController < ApplicationController
before_action :authenticate_user!, only: [:create, :update, :destroy]
before_action :check_tenant_subscription, only: [:create, :update, :destroy]

def index
start_date = params[:start_date] ? Date.parse(params[:start_date]) : Date.parse('1970-01-01')
Expand Down
2 changes: 2 additions & 0 deletions app/controllers/tenants_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ def create

@user.save!

@tenant.tenant_billing = TenantBilling.create!

CreateWelcomeEntitiesWorkflow.new().run

logger.info { "New tenant registration: #{Current.tenant.inspect}" }
Expand Down
Loading
Loading