Skip to content

Commit

Permalink
Merge pull request #109 from debtcollective/feat/subscription-toggling
Browse files Browse the repository at this point in the history
Feat/subscription toggling
  • Loading branch information
orlando authored Oct 30, 2019
2 parents fe9a17d + 090147b commit b80f387
Show file tree
Hide file tree
Showing 22 changed files with 419 additions and 17 deletions.
47 changes: 47 additions & 0 deletions app/controllers/plan_changes_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# frozen_string_literal: true

class PlanChangesController < ApplicationController
before_action :set_current_user_plan, only: %i[index create]
before_action :not_authorized_user, only: %i[create]

# GET /users/:user_id/plan_change
def index
redirect_to root_path unless current_user&.active_subscription
@user = current_user
@plans = Plan.all
end

# GET /users/:user_id/plan_change/new
# GET /users/:user_id/plan_change/new.json
def new
@plan_change = UserPlanChange.new
end

# POST /users/:user_id/plan_change
# POST /users/:user_id/plan_change.json
def create
@plan = UserPlanChange.new(user_id: plan_change_params[:user_id], old_plan_id: plan_change_params[:old_plan_id], new_plan_id: plan_change_params[:new_plan_id], status: 'pending')

respond_to do |format|
if @plan.save
format.json { render json: @plan, status: :ok }
else
format.html { render :index }
end
end
end

private

def not_authorized_user
head :not_authorized unless current_user
end

def set_current_user_plan
@current_plan = current_user&.active_subscription
end

def plan_change_params
params.require(:plan_change).permit(:user_id, :old_plan_id, :new_plan_id)
end
end
1 change: 1 addition & 0 deletions app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class UsersController < ApplicationController
# GET /users/1.json
def show
redirect_to root_path unless current_user == @user
@is_subscription_changing = UserPlanChange.where(user_id: @user.id, status: 'pending').first
end

private
Expand Down
92 changes: 92 additions & 0 deletions app/javascript/bundles/User/components/CurrentPlan.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React, { useState } from 'react'
import numeral from 'numeral'
import { makeStyles } from '@material-ui/core/styles'
import Button from '@material-ui/core/Button'
import Paper from '@material-ui/core/Paper'

const useStyles = makeStyles(theme => ({
root: {
width: '100%',
marginTop: theme.spacing(3),
padding: theme.spacing(4),
overflowX: 'auto'
},
table: {
minWidth: 650
}
}))

const CHANGE_PLAN_ENDPOINT = id => `/users/${id}/plan_changes`

function CurrentPlanView ({ user, activePlan, plans }) {
const classes = useStyles()
const [changedPlan, setPlanChanged] = useState(false)

const changePlan = async selectedPlanId => {
try {
await fetch(CHANGE_PLAN_ENDPOINT(user.id), {
method: 'post',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
plan_change: {
user_id: user.id,
old_plan_id: activePlan.id,
new_plan_id: selectedPlanId
}
})
})
setPlanChanged(true)
} catch (error) {
console.error(error) // TODO: Replace with sentry
}
}

return (
<>
<Paper className={classes.root}>
<h3>Change your susbcription</h3>
<p>
The plan you're using to contribute is{' '}
<strong> {activePlan.name}</strong>.
</p>
</Paper>
<br />
<h2>Available plans</h2>
<p>You can change your current plan for another one at anytime.</p>
{changedPlan && (
<p className='notice--subscription'>
We have changed your subscription successfully.
</p>
)}
{plans.map(plan => {
function createMarkup () {
return { __html: plan.description.body }
}

function changeHandler () {
return changePlan(plan.id)
}

return (
<Paper className={classes.root} key={plan.id} id={`plan-${plan.id}`}>
<h4>{plan.name}</h4>
<p>Price: {numeral(plan.amount).format('$0,0.00')}</p>
<p dangerouslySetInnerHTML={createMarkup()} />
<Button
variant='contained'
color='primary'
disabled={activePlan.id === plan.id || changedPlan}
onClick={changeHandler}
>
Pick plan
</Button>
</Paper>
)
})}
</>
)
}

export const CurrentPlan = props => <CurrentPlanView {...props} />
6 changes: 0 additions & 6 deletions app/javascript/bundles/User/components/DonationsHistory.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,6 @@ function DonationsView ({ activePlan, donations }) {
return (
<Paper className={classes.root}>
<h3>Your Donations History</h3>
{activePlan && (
<p>
You're <strong>currently subscribed to {activePlan.name}</strong>.
</p>
)}
<p>
You have donated:{' '}
<strong>{numeral(donatedAmount).format('$0,0.00')}</strong>
Expand Down Expand Up @@ -90,7 +85,6 @@ function DonationsView ({ activePlan, donations }) {
}

DonationsView.propTypes = {
activePlan: PropTypes.object,
donations: PropTypes.array
}

Expand Down
48 changes: 42 additions & 6 deletions app/javascript/bundles/User/components/SubscriptionCancel.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,34 @@
import React, { useState } from 'react'
import { makeStyles } from '@material-ui/core/styles'
import Button from '@material-ui/core/Button'
import Dialog from '@material-ui/core/Dialog'
import DialogActions from '@material-ui/core/DialogActions'
import DialogContent from '@material-ui/core/DialogContent'
import DialogContentText from '@material-ui/core/DialogContentText'
import DialogTitle from '@material-ui/core/DialogTitle'
import Paper from '@material-ui/core/Paper'

const SUBSCRIPTION_CANCEL_URL = userID => `/users/${userID}/subscription`

function SubscriptionCancelView ({ user, subscription }) {
const useStyles = makeStyles(theme => ({
root: {
width: '100%',
marginTop: theme.spacing(3),
padding: theme.spacing(4),
overflowX: 'auto'
},
table: {
minWidth: 650
}
}))

function SubscriptionCancelView ({
user,
subscription,
activePlan,
isSubscriptionChanging
}) {
const classes = useStyles()
const [open, setOpen] = useState(false)
const [active, toggleActive] = useState(subscription.active)

Expand Down Expand Up @@ -38,10 +58,26 @@ function SubscriptionCancelView ({ user, subscription }) {
}

return (
<div>
<Button color='secondary' onClick={handleClickOpen}>
Cancel Subscription
</Button>
<>
<Paper className={classes.root}>
<h3>You're subscribed</h3>
<p>
The plan you're using to contribute is{' '}
<strong> {activePlan.name}</strong>.
</p>
{!isSubscriptionChanging && (
<Button
component='a'
href={`/users/${user.id}/plan_changes`}
color='primary'
>
Change Subscription
</Button>
)}
<Button color='secondary' onClick={handleClickOpen}>
Cancel Subscription
</Button>
</Paper>
{!active && 'No active subscription'}
<Dialog
open={open}
Expand Down Expand Up @@ -72,7 +108,7 @@ function SubscriptionCancelView ({ user, subscription }) {
</Button>
</DialogActions>
</Dialog>
</div>
</>
)
}

Expand Down
2 changes: 1 addition & 1 deletion app/javascript/bundles/User/components/UserShow.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function UserShowView ({ user, subscription, streak }) {
You have been subscribed for {streak}
</p>
)}
<Paper className={classes.root}>
<Paper className={classes.root} id='contact-data'>
<h3>Contact Data</h3>
<p>
<strong>Name:</strong>
Expand Down
4 changes: 3 additions & 1 deletion app/javascript/packs/user-bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import ReactOnRails from 'react-on-rails'
import { UserShow } from '../bundles/User/components/UserShow'
import { DonationsHistory } from '../bundles/User/components/DonationsHistory'
import { SubscriptionCancel } from '../bundles/User/components/SubscriptionCancel'
import { CurrentPlan } from '../bundles/User/components/CurrentPlan'

ReactOnRails.register({
UserShow,
DonationsHistory,
SubscriptionCancel
SubscriptionCancel,
CurrentPlan
})
12 changes: 12 additions & 0 deletions app/jobs/change_subscription_plans_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

class ChangeSubscriptionPlansJob < ApplicationJob
queue_as :default

def perform
pending_user_plan_changes = UserPlanChange.where(status: 'pending')
pending_user_plan_changes.each do |pending_user_plan_change|
ToggleUserActiveSubscriptionPlanJob.perform_later(pending_user_plan_change)
end
end
end
28 changes: 28 additions & 0 deletions app/jobs/toggle_user_active_subscription_plan_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

class ToggleUserActiveSubscriptionPlanJob < ApplicationJob
queue_as :default

def perform(user_plan_change)
# find the user
user = User.find(user_plan_change.user_id)

# disable current user subscription.
old_subscription = user.active_subscription
ActiveRecord::Base.transaction do
old_subscription.update(active: false)

# creates a new subscription with the new plan details.
new_subscription = Subscription.new(
user_id: user_plan_change.user_id,
plan_id: user_plan_change.new_plan_id,
last_charge_at: old_subscription.last_charge_at,
active: true
)

if new_subscription.save
UserPlanChange.find(user_plan_change.id).update(status: 'finished')
end
end
end
end
10 changes: 10 additions & 0 deletions app/models/user_plan_change.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

class UserPlanChange < ApplicationRecord
belongs_to :user

enum status: %i[finished pending archived failed]

validates :old_plan_id, :new_plan_id, :user_id, presence: true
validates :user_id, uniqueness: true
end
3 changes: 3 additions & 0 deletions app/views/plan_changes/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<section class="section-content">
<%= react_component("CurrentPlan", props: {user: @user, activePlan: @user.active_subscription&.plan, plans: @plans}) %>
</section>
4 changes: 2 additions & 2 deletions app/views/users/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
<p id="notice"><%= notice %></p>

<%= react_component("UserShow", props: {user: @user, streak: @user.current_streak, subscription: @user.active_subscription}) %>
<%= react_component("DonationsHistory", props: {activePlan: @user.active_subscription&.plan, donations: @user.donations}) %>
<%= react_component("SubscriptionCancel", props: {user: @user, subscription: @user.active_subscription}) %>
<%= react_component("SubscriptionCancel", props: {user: @user, subscription: @user.active_subscription, activePlan: @user.active_subscription&.plan, isSubscriptionChanging: @is_subscription_changing}) %>
<%= react_component("DonationsHistory", props: {donations: @user.donations}) %>
</section>
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
resource :streak, only: %i[show]
resources :cards, only: %i[index destroy]
resource :subscription, only: %i[destroy]
resources :plan_changes, only: %i[index new create]
end

get '/login' => 'sessions#login'
Expand Down
5 changes: 5 additions & 0 deletions config/sidekiq.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@
queue: default
class: FindOverdueSubscriptionsJob
description: 'This job finds all active overdue subscriptions and creates a sidekiq job to charge the subscriber'
find_subscription_plan_changes:
every: '45m'
queue: default
class: ChangeSubscriptionPlansJob
description: 'This job finds all pending subscription changes and creates a sidekiq job to perform the plan change'
13 changes: 13 additions & 0 deletions db/migrate/20191017102956_add_subscription_change.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

class AddSubscriptionChange < ActiveRecord::Migration[6.0]
def change
create_table :user_plan_changes, id: :uuid do |t|
t.string :old_plan_id
t.string :new_plan_id
t.string :user_id

t.timestamps
end
end
end
7 changes: 7 additions & 0 deletions db/migrate/20191018102113_add_status_to_plan_change.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class AddStatusToPlanChange < ActiveRecord::Migration[6.0]
def change
add_column :user_plan_changes, :status, :integer, default: 0
end
end
11 changes: 10 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2019_09_30_093321) do
ActiveRecord::Schema.define(version: 2019_10_18_102113) do

# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
Expand Down Expand Up @@ -105,6 +105,15 @@
t.index ["user_id"], name: "index_subscriptions_on_user_id"
end

create_table "user_plan_changes", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "old_plan_id"
t.string "new_plan_id"
t.string "user_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.integer "status", default: 0
end

create_table "users", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "email"
t.datetime "created_at", precision: 6, null: false
Expand Down
Loading

0 comments on commit b80f387

Please sign in to comment.