From 596be24d92bfb4671113ee5ea6510a744d1fefd0 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Tue, 11 Apr 2023 22:45:12 +0200 Subject: [PATCH 1/3] feat: Add cancellation effective date to subscriptions --- ...llation_effective_date_to_subscriptions.ts | 18 ++++++++++++ .../src/helpers/billing/webhooks.ee.ts | 28 +++++++++++-------- .../backend/src/models/subscription.ee.ts | 2 ++ 3 files changed, 37 insertions(+), 11 deletions(-) create mode 100644 packages/backend/src/db/migrations/20230411203412_add_cancellation_effective_date_to_subscriptions.ts diff --git a/packages/backend/src/db/migrations/20230411203412_add_cancellation_effective_date_to_subscriptions.ts b/packages/backend/src/db/migrations/20230411203412_add_cancellation_effective_date_to_subscriptions.ts new file mode 100644 index 00000000..dc9ee247 --- /dev/null +++ b/packages/backend/src/db/migrations/20230411203412_add_cancellation_effective_date_to_subscriptions.ts @@ -0,0 +1,18 @@ +import { Knex } from 'knex'; +import appConfig from '../../config/app'; + +export async function up(knex: Knex): Promise { + if (!appConfig.isCloud) return; + + return knex.schema.table('subscriptions', (table) => { + table.date('cancellation_effective_date'); + }); +} + +export async function down(knex: Knex): Promise { + if (!appConfig.isCloud) return; + + return knex.schema.table('subscriptions', (table) => { + table.dropColumn('cancellation_effective_date'); + }); +} diff --git a/packages/backend/src/helpers/billing/webhooks.ee.ts b/packages/backend/src/helpers/billing/webhooks.ee.ts index b9556d42..78e2fa60 100644 --- a/packages/backend/src/helpers/billing/webhooks.ee.ts +++ b/packages/backend/src/helpers/billing/webhooks.ee.ts @@ -3,13 +3,16 @@ import Subscription from '../../models/subscription.ee'; import Billing from './index.ee'; const handleSubscriptionCreated = async (request: IRequest) => { - const subscription = await Subscription.query().insertAndFetch(formatSubscription(request)); - await subscription.$relatedQuery('usageData').insert(formatUsageData(request)); + const subscription = await Subscription.query().insertAndFetch( + formatSubscription(request) + ); + await subscription + .$relatedQuery('usageData') + .insert(formatUsageData(request)); }; const handleSubscriptionUpdated = async (request: IRequest) => { - await Subscription - .query() + await Subscription.query() .findOne({ paddle_subscription_id: request.body.subscription_id, }) @@ -17,15 +20,15 @@ const handleSubscriptionUpdated = async (request: IRequest) => { }; const handleSubscriptionCancelled = async (request: IRequest) => { - const subscription = await Subscription - .query() - .findOne({ - paddle_subscription_id: request.body.subscription_id, - }); + const subscription = await Subscription.query().findOne({ + paddle_subscription_id: request.body.subscription_id, + }); await subscription.$query().patchAndFetch(formatSubscription(request)); - await subscription.$query().delete(); + // Have a background job that deletes the subscription in cancellation effective date + // Architect in a way that it will behave as cron. + // await subscription.$query().delete(); }; const handleSubscriptionPaymentSucceeded = async (request: IRequest) => { @@ -45,7 +48,9 @@ const handleSubscriptionPaymentSucceeded = async (request: IRequest) => { lastBillDate: remoteSubscription.last_payment.date, }); - await subscription.$relatedQuery('usageData').insert(formatUsageData(request)); + await subscription + .$relatedQuery('usageData') + .insert(formatUsageData(request)); }; const formatSubscription = (request: IRequest) => { @@ -58,6 +63,7 @@ const formatSubscription = (request: IRequest) => { status: request.body.status, nextBillDate: request.body.next_bill_date, nextBillAmount: request.body.unit_price, + cancellationEffectiveDate: request.body.cancellation_effective_date, }; }; diff --git a/packages/backend/src/models/subscription.ee.ts b/packages/backend/src/models/subscription.ee.ts index 216e5e52..a8d9de60 100644 --- a/packages/backend/src/models/subscription.ee.ts +++ b/packages/backend/src/models/subscription.ee.ts @@ -14,6 +14,7 @@ class Subscription extends Base { nextBillAmount!: string; nextBillDate!: string; lastBillDate?: string; + cancellationEffectiveDate?: string; usageData?: UsageData[]; currentUsageData?: UsageData; @@ -43,6 +44,7 @@ class Subscription extends Base { nextBillAmount: { type: 'string' }, nextBillDate: { type: 'string' }, lastBillDate: { type: 'string' }, + cancellationEffectiveDate: { type: 'string' }, }, }; From d1344457dd9fd97cb1898c69ac511da1262e743d Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Tue, 11 Apr 2023 23:19:11 +0200 Subject: [PATCH 2/3] feat: Auto remove cancelled subscriptions --- .../queries/get-billing-and-usage.ee.ts | 6 ++- .../src/helpers/billing/webhooks.ee.ts | 4 -- .../src/helpers/create-bull-board-handler.ts | 2 + .../remove-cancelled-subscriptions.ee.ts | 35 ++++++++++++++ packages/backend/src/worker.ts | 2 + .../remove-cancelled-subscriptions.ee.ts | 46 +++++++++++++++++++ 6 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 packages/backend/src/queues/remove-cancelled-subscriptions.ee.ts create mode 100644 packages/backend/src/workers/remove-cancelled-subscriptions.ee.ts diff --git a/packages/backend/src/graphql/queries/get-billing-and-usage.ee.ts b/packages/backend/src/graphql/queries/get-billing-and-usage.ee.ts index 7c786b96..f8d99e18 100644 --- a/packages/backend/src/graphql/queries/get-billing-and-usage.ee.ts +++ b/packages/backend/src/graphql/queries/get-billing-and-usage.ee.ts @@ -44,7 +44,9 @@ const paidSubscription = (subscription: Subscription): TSubscription => { }, }, nextBillAmount: { - title: '€' + subscription.nextBillAmount, + title: subscription.nextBillAmount + ? '€' + subscription.nextBillAmount + : '---', action: { type: 'link', text: 'Update payment method', @@ -52,7 +54,7 @@ const paidSubscription = (subscription: Subscription): TSubscription => { }, }, nextBillDate: { - title: subscription.nextBillDate, + title: subscription.nextBillDate ? subscription.nextBillDate : '---', action: { type: 'text', text: '(monthly payment)', diff --git a/packages/backend/src/helpers/billing/webhooks.ee.ts b/packages/backend/src/helpers/billing/webhooks.ee.ts index 78e2fa60..8390153e 100644 --- a/packages/backend/src/helpers/billing/webhooks.ee.ts +++ b/packages/backend/src/helpers/billing/webhooks.ee.ts @@ -25,10 +25,6 @@ const handleSubscriptionCancelled = async (request: IRequest) => { }); await subscription.$query().patchAndFetch(formatSubscription(request)); - - // Have a background job that deletes the subscription in cancellation effective date - // Architect in a way that it will behave as cron. - // await subscription.$query().delete(); }; const handleSubscriptionPaymentSucceeded = async (request: IRequest) => { diff --git a/packages/backend/src/helpers/create-bull-board-handler.ts b/packages/backend/src/helpers/create-bull-board-handler.ts index 28a39330..6bab1db9 100644 --- a/packages/backend/src/helpers/create-bull-board-handler.ts +++ b/packages/backend/src/helpers/create-bull-board-handler.ts @@ -6,6 +6,7 @@ import triggerQueue from '../queues/trigger'; import actionQueue from '../queues/action'; import emailQueue from '../queues/email'; import deleteUserQueue from '../queues/delete-user.ee'; +import removeCancelledSubscriptionsQueue from '../queues/remove-cancelled-subscriptions.ee'; import appConfig from '../config/app'; const serverAdapter = new ExpressAdapter(); @@ -25,6 +26,7 @@ const createBullBoardHandler = async (serverAdapter: ExpressAdapter) => { new BullMQAdapter(actionQueue), new BullMQAdapter(emailQueue), new BullMQAdapter(deleteUserQueue), + new BullMQAdapter(removeCancelledSubscriptionsQueue), ], serverAdapter: serverAdapter, }); diff --git a/packages/backend/src/queues/remove-cancelled-subscriptions.ee.ts b/packages/backend/src/queues/remove-cancelled-subscriptions.ee.ts new file mode 100644 index 00000000..ec7983b3 --- /dev/null +++ b/packages/backend/src/queues/remove-cancelled-subscriptions.ee.ts @@ -0,0 +1,35 @@ +import process from 'process'; +import { Queue } from 'bullmq'; +import redisConfig from '../config/redis'; +import logger from '../helpers/logger'; + +const CONNECTION_REFUSED = 'ECONNREFUSED'; + +const redisConnection = { + connection: redisConfig, +}; + +const removeCancelledSubscriptionsQueue = new Queue( + 'remove-cancelled-subscriptions', + redisConnection +); + +process.on('SIGTERM', async () => { + await removeCancelledSubscriptionsQueue.close(); +}); + +removeCancelledSubscriptionsQueue.on('error', (err) => { + if ((err as any).code === CONNECTION_REFUSED) { + logger.error('Make sure you have installed Redis and it is running.', err); + process.exit(); + } +}); + +removeCancelledSubscriptionsQueue.add('remove-cancelled-subscriptions', null, { + jobId: 'remove-cancelled-subscriptions', + repeat: { + pattern: '0 1 * * *', + }, +}); + +export default removeCancelledSubscriptionsQueue; diff --git a/packages/backend/src/worker.ts b/packages/backend/src/worker.ts index 141ac78b..ae63d224 100644 --- a/packages/backend/src/worker.ts +++ b/packages/backend/src/worker.ts @@ -9,6 +9,8 @@ import './workers/trigger'; import './workers/action'; import './workers/email'; import './workers/delete-user.ee'; +import './workers/remove-cancelled-subscriptions.ee'; +import './queues/remove-cancelled-subscriptions.ee'; import telemetry from './helpers/telemetry'; telemetry.setServiceType('worker'); diff --git a/packages/backend/src/workers/remove-cancelled-subscriptions.ee.ts b/packages/backend/src/workers/remove-cancelled-subscriptions.ee.ts new file mode 100644 index 00000000..91430a6b --- /dev/null +++ b/packages/backend/src/workers/remove-cancelled-subscriptions.ee.ts @@ -0,0 +1,46 @@ +import { Worker } from 'bullmq'; +import { DateTime } from 'luxon'; +import * as Sentry from '../helpers/sentry.ee'; +import redisConfig from '../config/redis'; +import logger from '../helpers/logger'; +import Subscription from '../models/subscription.ee'; + +export const worker = new Worker( + 'remove-cancelled-subscriptions', + async () => { + await Subscription.query() + .delete() + .where({ + status: 'cancelled', + }) + .andWhere( + 'cancellation_effective_date', + '<=', + DateTime.now().startOf('day').toISODate() + ); + }, + { connection: redisConfig } +); + +worker.on('completed', (job) => { + logger.info( + `JOB ID: ${job.id} - The cancelled subscriptions have been removed!` + ); +}); + +worker.on('failed', (job, err) => { + const errorMessage = ` + JOB ID: ${job.id} - ERROR: The cancelled subscriptions can not be removed! ${err.message} + \n ${err.stack} + `; + logger.error(errorMessage); + Sentry.captureException(err, { + extra: { + jobId: job.id, + }, + }); +}); + +process.on('SIGTERM', async () => { + await worker.close(); +}); From d2163f180e7eee7f442ce07564ba44be3b0a544b Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Wed, 12 Apr 2023 21:29:36 +0200 Subject: [PATCH 3/3] feat: Add getSubscriptionStatus graphQL query --- .../queries/get-subscription-status.ee.ts | 22 +++++++++++++++++++ .../backend/src/graphql/query-resolvers.ts | 2 ++ packages/backend/src/graphql/schema.graphql | 5 +++++ .../remove-cancelled-subscriptions.ee.ts | 2 +- 4 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 packages/backend/src/graphql/queries/get-subscription-status.ee.ts diff --git a/packages/backend/src/graphql/queries/get-subscription-status.ee.ts b/packages/backend/src/graphql/queries/get-subscription-status.ee.ts new file mode 100644 index 00000000..980a4752 --- /dev/null +++ b/packages/backend/src/graphql/queries/get-subscription-status.ee.ts @@ -0,0 +1,22 @@ +import appConfig from '../../config/app'; +import Context from '../../types/express/context'; + +const getSubscriptionStatus = async ( + _parent: unknown, + _params: unknown, + context: Context +) => { + if (!appConfig.isCloud) return; + + const currentSubscription = await context.currentUser.$relatedQuery( + 'currentSubscription' + ); + + if (!currentSubscription?.cancellationEffectiveDate) return; + + return { + cancellationEffectiveDate: currentSubscription.cancellationEffectiveDate, + }; +}; + +export default getSubscriptionStatus; diff --git a/packages/backend/src/graphql/query-resolvers.ts b/packages/backend/src/graphql/query-resolvers.ts index 71e44020..8ecb7a95 100644 --- a/packages/backend/src/graphql/query-resolvers.ts +++ b/packages/backend/src/graphql/query-resolvers.ts @@ -18,6 +18,7 @@ import getBillingAndUsage from './queries/get-billing-and-usage.ee'; import getInvoices from './queries/get-invoices.ee'; import getAutomatischInfo from './queries/get-automatisch-info'; import getTrialStatus from './queries/get-trial-status.ee'; +import getSubscriptionStatus from './queries/get-subscription-status.ee'; import healthcheck from './queries/healthcheck'; const queryResolvers = { @@ -41,6 +42,7 @@ const queryResolvers = { getInvoices, getAutomatischInfo, getTrialStatus, + getSubscriptionStatus, healthcheck, }; diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index 614ca958..e07f5888 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -41,6 +41,7 @@ type Query { getInvoices: [Invoice] getAutomatischInfo: GetAutomatischInfo getTrialStatus: GetTrialStatus + getSubscriptionStatus: GetSubscriptionStatus healthcheck: AppHealth } @@ -487,6 +488,10 @@ type GetTrialStatus { expireAt: String } +type GetSubscriptionStatus { + cancellationEffectiveDate: String +} + type GetBillingAndUsage { subscription: Subscription usage: Usage diff --git a/packages/backend/src/workers/remove-cancelled-subscriptions.ee.ts b/packages/backend/src/workers/remove-cancelled-subscriptions.ee.ts index 91430a6b..ab288e8e 100644 --- a/packages/backend/src/workers/remove-cancelled-subscriptions.ee.ts +++ b/packages/backend/src/workers/remove-cancelled-subscriptions.ee.ts @@ -11,7 +11,7 @@ export const worker = new Worker( await Subscription.query() .delete() .where({ - status: 'cancelled', + status: 'deleted', }) .andWhere( 'cancellation_effective_date',