Merge pull request #1055 from automatisch/subscription-cancelled

feat: add subscription cancelled flow
This commit is contained in:
Ali BARIN
2023-04-12 21:33:22 +02:00
committed by GitHub
11 changed files with 152 additions and 14 deletions

View File

@@ -0,0 +1,18 @@
import { Knex } from 'knex';
import appConfig from '../../config/app';
export async function up(knex: Knex): Promise<void> {
if (!appConfig.isCloud) return;
return knex.schema.table('subscriptions', (table) => {
table.date('cancellation_effective_date');
});
}
export async function down(knex: Knex): Promise<void> {
if (!appConfig.isCloud) return;
return knex.schema.table('subscriptions', (table) => {
table.dropColumn('cancellation_effective_date');
});
}

View File

@@ -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)',

View File

@@ -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;

View File

@@ -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,
};

View File

@@ -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

View File

@@ -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,11 @@ const handleSubscriptionUpdated = async (request: IRequest) => {
};
const handleSubscriptionCancelled = async (request: IRequest) => {
const subscription = await Subscription
.query()
.findOne({
const subscription = await Subscription.query().findOne({
paddle_subscription_id: request.body.subscription_id,
});
await subscription.$query().patchAndFetch(formatSubscription(request));
await subscription.$query().delete();
};
const handleSubscriptionPaymentSucceeded = async (request: IRequest) => {
@@ -45,7 +44,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 +59,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,
};
};

View File

@@ -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,
});

View File

@@ -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' },
},
};

View File

@@ -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;

View File

@@ -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');

View File

@@ -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: 'deleted',
})
.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();
});