Skip to content

Commit

Permalink
#23 | add, update, cancel subscription
Browse files Browse the repository at this point in the history
  • Loading branch information
leophan07 committed Dec 1, 2020
1 parent e125ed1 commit 7f35b7e
Show file tree
Hide file tree
Showing 20 changed files with 343 additions and 67 deletions.
16 changes: 15 additions & 1 deletion api/graphql/resolvers/user-plan.resolver.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { combineResolvers } from 'graphql-resolvers';
import { getUserPlan } from '~/services/user/plans-user.service';
import { getUserPlan, createUserPlan, deleteUserPlan, updateUserPlan } from '~/services/user/plans-user.service';
import { isAuthenticated } from './authorization.resolver';

const resolvers = {
Expand All @@ -9,6 +9,20 @@ const resolvers = {
(_, args, { user }) => getUserPlan(user.id),
),
},
Mutation: {
createUserPlan: combineResolvers(
isAuthenticated,
(_, { paymentMethodToken, planName, billingType }, { user }) => createUserPlan(user.id, paymentMethodToken, planName, billingType),
),
updateUserPlan: combineResolvers(
isAuthenticated,
(_, { userPlanId, planName, billingType }) => updateUserPlan(userPlanId, planName, billingType),
),
deleteUserPlan: combineResolvers(
isAuthenticated,
(_, { userPlanId }) => deleteUserPlan(userPlanId),
),
},
};

export default resolvers;
9 changes: 8 additions & 1 deletion api/graphql/schemas/user-plan.schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const { gql } = pkg;
export const UserPlanSchema = gql`
type ResponseUserPlan {
id: Int!
userId: Int!
productId: Int!
priceId: Int!
Expand All @@ -15,6 +16,12 @@ export const UserPlanSchema = gql`
}
extend type Query {
getUserPlan: ResponseUserPlan!
getUserPlan: ResponseUserPlan
}
extend type Mutation {
createUserPlan(paymentMethodToken: String!, planName: String!, billingType: BillingType!): Boolean!
updateUserPlan(userPlanId: Int!, planName: String!, billingType: BillingType!): Boolean!
deleteUserPlan(userPlanId: Int!): Boolean!
}
`;
2 changes: 1 addition & 1 deletion api/libs/mail.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ export default async function sendMail(to, subject, html) {
to,
subject,
html,
}).then(() => resolve()).catch(() => resolve())
}).then(() => resolve()).catch(() => resolve());
});
}
1 change: 1 addition & 0 deletions api/migrations/20201126173501_create-user-plans.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export function up(knex) {
t.integer('user_id').unsigned().notNullable();
t.integer('product_id').unsigned().notNullable();
t.integer('price_id').unsigned().notNullable();
t.string('subcription_id').notNullable();
t.dateTime('created_at')
.notNullable()
.defaultTo(knex.raw('CURRENT_TIMESTAMP'));
Expand Down
2 changes: 1 addition & 1 deletion api/repository/products.repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function findProductInType(types) {

export function findProductAndPriceByType(productType, priceType) {
return database(TABLE)
.join(TABLES.prices, productColumns.id, priceColumns.product_id)
.join(TABLES.prices, productColumns.id, priceColumns.productId)
.select(productColumns, `${priceColumns.id} as price_id`, priceColumns.amount, `${priceColumns.type} as price_type`, `${priceColumns.stripeId} as price_stripe_id`)
.where({ [productColumns.type]: productType, [priceColumns.type]: priceType })
.first();
Expand Down
23 changes: 20 additions & 3 deletions api/repository/user_plans.repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,36 @@ export const userPlanColumns = {
userId: 'user_plans.user_id',
productId: 'user_plans.product_id',
priceId: 'user_plans.price_id',
subcriptionId: 'user_plans.subcription_id',
createAt: 'user_plans.created_at',
updatedAt: 'user_plans.updated_at',
};

export async function insertUserPlan(data, transaction) {
return database(TABLE).insert(data).transacting(transaction);
export function insertUserPlan(data, transaction = null) {
const query = database(TABLE).insert(data);
if (!transaction) {
return query;
}
return query.transacting(transaction);
}

export async function getUserPlanByUserId(userId) {
export function getUserPlanById(id) {
return database(TABLE).where({ id }).first();
}

export function updateUserPlanById(id, data) {
return database(TABLE).where({ id }).update(data);
}

export function getUserPlanByUserId(userId) {
return database(TABLE)
.leftJoin(TABLES.products, userPlanColumns.productId, productColumns.id)
.leftJoin(TABLES.prices, userPlanColumns.priceId, priceColumns.id)
.select(userPlanColumns, productColumns.name, `${productColumns.type} as productType`, priceColumns.amount, `${priceColumns.type} as priceType`)
.where({ user_id: userId })
.first();
}

export function deleteUserPlanById(id) {
return database(TABLE).where({ id }).del();
}
18 changes: 9 additions & 9 deletions api/services/authentication/register.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,16 @@ async function registerUser(email, password, name, paymentMethodToken, planName,
throw new ApolloError('Can not find any plan');
}

const userPlanData = {
product_id: product.id,
price_id: product.price_id,
};
const subscriptionId = await createNewSubcription(paymentMethodToken, email, name, product.price_stripe_id);

const [userId] = await Promise.all([
createUser(userData, userPlanData),
createNewSubcription(paymentMethodToken, email, name, product.price_stripe_id),
]);
newUserId = userId;
if (subscriptionId) {
const userPlanData = {
product_id: product.id,
price_id: product.price_id,
subcription_id: subscriptionId,
};
newUserId = await createUser(userData, userPlanData);
}
} else {
newUserId = await createUser(userData);
}
Expand Down
34 changes: 33 additions & 1 deletion api/services/stripe/subcription.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,43 @@ export async function createNewSubcription(token, email, name, price_id) {
source: token,
});

await stripe.subscriptions.create({
const result = await stripe.subscriptions.create({
customer: customer.id,
items: [{ price: price_id }],
trial_end: dayjs().add(14, 'day').unix(),
});

return result.id;
} catch (error) {
logger.error(error);
throw new ApolloError('Something went wrong!');
}
}

export async function updateSubcription(subId, priceId) {
try {
const subscription = await stripe.subscriptions.retrieve(subId);

await stripe.subscriptions.update(subId, {
cancel_at_period_end: false,
proration_behavior: 'create_prorations',
items: [{
id: subscription.items.data[0].id,
price: priceId,
}],
});

return true;
} catch (error) {
logger.error(error);
throw new ApolloError('Something went wrong!');
}
}

export async function cancelSubcription(subId) {
try {
await stripe.subscriptions.del(subId);

return true;
} catch (error) {
logger.error(error);
Expand Down
85 changes: 84 additions & 1 deletion api/services/user/plans-user.service.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,90 @@
import Apollo from 'apollo-server-express';

import logger from '~/utils/logger';
import {
deleteUserPlanById,
getUserPlanById,
getUserPlanByUserId,
insertUserPlan,
updateUserPlanById,
} from '~/repository/user_plans.repository';
import { cancelSubcription, createNewSubcription, updateSubcription } from '~/services/stripe/subcription.service';
import { findProductAndPriceByType } from '~/repository/products.repository';
import { findUser } from '~/repository/user.repository';

export async function getUserPlan(userId) {
const { ApolloError } = Apollo;
export function getUserPlan(userId) {
return getUserPlanByUserId(userId);
}

export async function createUserPlan(userId, paymentMethodToken, planName, billingType) {
try {
const user = await findUser({ id: userId });
if (!user) {
throw new ApolloError('Can not find any user');
}

const product = await findProductAndPriceByType(planName, billingType);
if (!product) {
throw new ApolloError('Can not find any plan');
}

const subscriptionId = await createNewSubcription(paymentMethodToken, user.email, user.name, product.price_stripe_id);
const dataUserPlan = {
user_id: userId,
product_id: product.id,
price_id: product.price_id,
subcription_id: subscriptionId,
};
await insertUserPlan(dataUserPlan);

return true;
} catch (error) {
logger.error(error);
throw new ApolloError('Something went wrong!');
}
}

export async function updateUserPlan(usePlanId, planName, billingType) {
try {
const userPlan = await getUserPlanById(usePlanId);
if (!userPlan) {
throw new ApolloError('Can not find any user plan');
}

const product = await findProductAndPriceByType(planName, billingType);
if (!product) {
throw new ApolloError('Can not find any plan');
}

await updateSubcription(userPlan.subcription_id, product.price_stripe_id);

const dataUserPlan = {
product_id: product.id,
price_id: product.price_id,
};
await updateUserPlanById(usePlanId, dataUserPlan);

return true;
} catch (error) {
logger.error(error);
throw new ApolloError('Something went wrong!');
}
}

export async function deleteUserPlan(id) {
try {
const userPlan = await getUserPlanById(id);
if (!userPlan) {
throw new ApolloError('Can not find any user plan');
}

await cancelSubcription(userPlan.subcription_id);
await deleteUserPlanById(id);

return true;
} catch (error) {
logger.error(error);
throw new ApolloError('Something went wrong!');
}
}
3 changes: 2 additions & 1 deletion app/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module.exports = {
parser: 'babel-eslint',
extends: ['airbnb', 'prettier', 'prettier/react'],
plugins: ['prettier', 'react', 'react-hooks', 'jsx-a11y'],
plugins: ['prettier', 'react', 'react-hooks', 'jsx-a11y', 'lodash'],
env: {
browser: true,
node: true,
Expand Down Expand Up @@ -84,5 +84,6 @@ module.exports = {
custom: 'ignore',
},
],
'lodash/import-scope': [2, 'method'],
},
};
2 changes: 2 additions & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"fb": "^2.0.0",
"graphql": "^15.4.0",
"graphql.macro": "^1.4.2",
"lodash": "^4.17.20",
"prop-types": "^15.7.2",
"react": "^17.0.1",
"react-apollo-network-status": "^5.0.1",
Expand Down Expand Up @@ -88,6 +89,7 @@
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-lodash": "^7.1.0",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.21.5",
"eslint-plugin-react-hooks": "^4.2.0",
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/Stripe/StripeForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ const StripeForm = ({ onSubmitSuccess, className, onGoBack, apiLoading, apiError
<button
type="submit"
disabled={isSubmitting}
className="py-2 px-4 w-full border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-indigo-600 shadow-sm hover:bg-indigo-500 focus:outline-none focus:shadow-outline-blue active:bg-indigo-600 transition duration-150 ease-in-out"
className="py-2 px-4 w-full text-sm leading-5 font-medium rounded-md text-white bg-indigo-600 shadow-sm hover:bg-indigo-500 focus:outline-none focus:shadow-outline-blue active:bg-indigo-600 transition duration-150 ease-in-out"
>
{isSubmitting ? 'Please wait' : submitText}
</button>
Expand Down
2 changes: 1 addition & 1 deletion app/src/containers/Layout/Admin.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import AdminLayout from '@/components/Layout/Admin';
import { setProfileUser } from '@/features/auth/user';
import { setUserPlan } from '@/features/auth/userPlan';
import getProfileQuery from '@/queries/auth/getProfile';
import getUserPlanQuery from '@/queries/auth/getUserPlan';
import getUserPlanQuery from '@/queries/userPlans/getUserPlan';

function AdminLayoutContainer() {
const { data, loading } = useQuery(getProfileQuery);
Expand Down
Loading

0 comments on commit 7f35b7e

Please sign in to comment.