Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ WORKDIR /usr/src/app
COPY --from=setup /usr/src/app/dist .
COPY --from=setup /usr/src/app/node_modules ./node_modules
EXPOSE $PORT
ENTRYPOINT [ "node", "server.js" ]
ENTRYPOINT [ "node", "server.js" ]
1 change: 0 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 24 additions & 1 deletion src/modules/billing/billing.controller.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { EventName, Paddle } from '@paddle/paddle-node-sdk';
import { Request } from 'express';
import { Body, JsonController, Post, Req } from 'routing-controllers';
import { Body, Get, JsonController, Post, Req } from 'routing-controllers';
import { Inject, Service } from 'typedi';

import { diConstants } from '@bonadocs/di';
import { BonadocsLogger } from '@bonadocs/logger';

import { ConfigService, Paddle as PaddleConfig } from '../configuration';
import { Subscription } from '../repositories/billing';
import { PlanSummaryDto } from '../repositories/projects';
import { JsonResponse } from '../shared';

import { BillingSubscriptionResponse } from './billing.interface';
Expand Down Expand Up @@ -82,4 +84,25 @@ export class BillingController {
message: 'Webhook event could not be handled',
};
}

@Get('/plans')
async getPlans(): Promise<JsonResponse<PlanSummaryDto[]>> {
const data = await this.billingService.getPlans();
return {
status: 'successful',
message: 'Plans successfully retrieved',
data,
};
}

@Get('/active-subscription')
async getActiveSubscription(@Req() request: Request): Promise<JsonResponse<Subscription>> {
const authData = request.auth;
const data = await this.billingService.getActiveSubscriptionPlan(authData.projectId!);
return {
status: 'successful',
message: 'Plans successfully retrieved',
data,
};
}
}
267 changes: 139 additions & 128 deletions src/modules/billing/billing.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { BonadocsLogger } from '@bonadocs/logger';

import { ApplicationError, applicationErrorCodes } from '../errors';
import { PaddleHttpClient } from '../http/paddle-http-client';
import { MailSender, MailTemplate } from '../mailing';
import { BillingRepository } from '../repositories/billing';
import { ProjectRepository } from '../repositories/projects';
import { MailgunSender, MailTemplate } from '../mailing';
import { BillingRepository, Subscription } from '../repositories/billing';
import { PlanSummaryDto, ProjectRepository } from '../repositories/projects';

@Service()
export class BillingService {
Expand All @@ -16,7 +16,7 @@ export class BillingService {
@Inject() private readonly billingRepository: BillingRepository,
@Inject() private readonly projectRepository: ProjectRepository,
@Inject() private readonly paddleHttpClient: PaddleHttpClient,
@Inject() private readonly mailService: MailSender,
@Inject() private readonly mailService: MailgunSender,
) {}

async upgradePlan(projectId: number, planId: number): Promise<void> {
Expand Down Expand Up @@ -113,6 +113,141 @@ export class BillingService {
);
}

async getPlans(): Promise<PlanSummaryDto[]> {
const plans = this.billingRepository.getPlans();
return plans;
}

async getActiveSubscriptionPlan(projectId: number): Promise<Subscription> {
const activeSubscription = await this.billingRepository.getProjectActiveSubscription(projectId);
if (!activeSubscription) {
throw new ApplicationError({
message: `Project with id ${projectId} has not active subscription`,
logger: this.logger,
errorCode: applicationErrorCodes.notFound,
});
}
return activeSubscription;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
async handleTransactionPaidWebhook(data: Record<string, any>): Promise<void> {
// handle new subscription charge
const transactionId = data.id;
const transaction = await this.paddleHttpClient.getTransactionById(transactionId);
const origin = data?.origin || null;
const bonadocsType = data.custom_data.bonadocs_type;
const invoiceId = Number(transaction.custom_data.invoice_id);
const invoicePaymentId = Number(transaction.custom_data.invoice_payment_id);
const subscriptionId = Number(transaction.custom_data.subscription_id);

if (
(origin === 'api' && bonadocsType === 'new_subscription') ||
bonadocsType === 'new_subscription'
) {
const paddleSubscriptionId = data.subscription_id;
let updatedMetadata: Record<string, string> | null = null;

// get new subscription
const newSubscription = await this.billingRepository.getSubscriptionById(subscriptionId);
if (newSubscription === null) {
throw new ApplicationError({
message: 'Subscription not found',
logger: this.logger,
errorCode: applicationErrorCodes.notFound,
});
}
const activeSubscriptionId = newSubscription.metadata.previousSubscriptionId;
if (paddleSubscriptionId) {
updatedMetadata = {
...newSubscription.metadata,
paddleSubscriptionId,
};
}

await this.billingRepository.updateOnTransactionPaidForNewAndRecurringSubscription(
newSubscription.id,
activeSubscriptionId,
invoiceId,
'paid',
invoicePaymentId,
'successful',
updatedMetadata,
);
}

if (origin === 'subscription_recurring') {
const paddleTransactionId = data.id;
let updatedMetadata: Record<string, string> | null = null;

// get new subscription
const newSubscription = await this.billingRepository.getSubscriptionById(
Number(subscriptionId),
);
if (newSubscription === null) {
throw new ApplicationError({
message: 'Subscription not found',
logger: this.logger,
errorCode: applicationErrorCodes.notFound,
});
}
const activeSubscriptionId = newSubscription.metadata.previousSubscriptionId;
if (paddleTransactionId) {
updatedMetadata = {
...newSubscription.metadata,
paddleTransactionId,
};
}

await this.billingRepository.updateOnTransactionPaidForNewAndRecurringSubscription(
newSubscription.id,
activeSubscriptionId,
invoiceId,
'paid',
invoicePaymentId,
'successful',
updatedMetadata,
);
}

if (origin === 'subscription_charge') {
await this.billingRepository.updateTransactionPaidForSubscriptionOverage(
invoiceId,
'paid',
invoicePaymentId,
'successful',
);
}
}

async handleTransactionPaymentFailedWebhook(): Promise<void> {}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
async handleSubscriptionCreatedWebhook(data: Record<string, any>): Promise<void> {
const transactionId = data.id;
const transaction = await this.paddleHttpClient.getTransactionById(transactionId);
const subscriptionId = Number(transaction.custom_data.subscription_id);
const paddleSubscriptionId = data.id;
let updatedMetadata: Record<string, string> = {};

// get new subscription
const newSubscription = await this.billingRepository.getSubscriptionById(subscriptionId);
if (newSubscription === null) {
throw new ApplicationError({
message: 'Subscription not found',
logger: this.logger,
errorCode: applicationErrorCodes.notFound,
});
}
if (paddleSubscriptionId) {
updatedMetadata = {
...newSubscription.metadata,
paddleSubscriptionId,
};
}
await this.billingRepository.updateSubscriptionMetadata(subscriptionId, updatedMetadata);
}

async handlerOverageCharge(projectId: number): Promise<void> {
// get project
const project = await this.projectRepository.getProjectById(projectId);
Expand Down Expand Up @@ -246,130 +381,6 @@ export class BillingService {
});
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
async handleTransactionPaidWebhook(data: Record<string, any>): Promise<void> {
// handle new subscription charge
const transactionId = data.id;
const transaction = await this.paddleHttpClient.getTransactionById(transactionId);
const origin = data?.origin || null;
const bonadocsType = data.custom_data.bonadocs_type;
const invoiceId = Number(transaction.custom_data.invoice_id);
const invoicePaymentId = Number(transaction.custom_data.invoice_payment_id);
const subscriptionId = Number(transaction.custom_data.subscription_id);

if (
(origin === 'api' && bonadocsType === 'new_subscription') ||
bonadocsType === 'new_subscription'
) {
const paddleSubscriptionId = data.subscription_id;
let updatedMetadata: Record<string, string> | null = null;

// get new subscription
const newSubscription = await this.billingRepository.getSubscriptionById(subscriptionId);
if (newSubscription === null) {
throw new ApplicationError({
message: 'Subscription not found',
logger: this.logger,
errorCode: applicationErrorCodes.notFound,
});
}
const activeSubscriptionId = newSubscription.metadata.previousSubscriptionId;
if (paddleSubscriptionId) {
updatedMetadata = {
...newSubscription.metadata,
paddleSubscriptionId,
};
}

await this.billingRepository.updateOnTransactionPaidForNewAndRecurringSubscription(
newSubscription.id,
activeSubscriptionId,
invoiceId,
'paid',
invoicePaymentId,
'successful',
updatedMetadata,
);
}

if (origin === 'subscription_recurring') {
const paddleTransactionId = data.id;
let updatedMetadata: Record<string, string> | null = null;

// get new subscription
const newSubscription = await this.billingRepository.getSubscriptionById(
Number(subscriptionId),
);
if (newSubscription === null) {
throw new ApplicationError({
message: 'Subscription not found',
logger: this.logger,
errorCode: applicationErrorCodes.notFound,
});
}
const activeSubscriptionId = newSubscription.metadata.previousSubscriptionId;
if (paddleTransactionId) {
updatedMetadata = {
...newSubscription.metadata,
paddleTransactionId,
};
}

await this.billingRepository.updateOnTransactionPaidForNewAndRecurringSubscription(
newSubscription.id,
activeSubscriptionId,
invoiceId,
'paid',
invoicePaymentId,
'successful',
updatedMetadata,
);
}

if (origin === 'subscription_charge') {
await this.billingRepository.updateTransactionPaidForSubscriptionOverage(
invoiceId,
'paid',
invoicePaymentId,
'successful',
);
}
}

async handleTransactionPaymentFailedWebhook(): Promise<void> {}

async handleTransactionCompletedWebhook(): Promise<void> {}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
async handleSubscriptionCreatedWebhook(data: Record<string, any>): Promise<void> {
const transactionId = data.id;
const transaction = await this.paddleHttpClient.getTransactionById(transactionId);
const subscriptionId = Number(transaction.custom_data.subscription_id);
const paddleSubscriptionId = data.id;
let updatedMetadata: Record<string, string> = {};

// get new subscription
const newSubscription = await this.billingRepository.getSubscriptionById(subscriptionId);
if (newSubscription === null) {
throw new ApplicationError({
message: 'Subscription not found',
logger: this.logger,
errorCode: applicationErrorCodes.notFound,
});
}
if (paddleSubscriptionId) {
updatedMetadata = {
...newSubscription.metadata,
paddleSubscriptionId,
};
}
await this.billingRepository.updateSubscriptionMetadata(subscriptionId, updatedMetadata);
}

async handleSubscriptionCancelledWebhook(): Promise<void> {}

async handleSubscriptionUpdatedWebhook(): Promise<void> {}

generateInvoiceRef(): string {
const now = new Date();
const pad2 = (n: number) => n.toString().padStart(2, '0');
Expand Down
5 changes: 2 additions & 3 deletions src/modules/repositories/billing/billing.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { BonadocsLogger } from '@bonadocs/logger';

import { DbContext, withDbContext } from '../../connection/dbcontext';
import { SubscriptionStatus } from '../../shared/types/subscriptions';
import { PlanResourceDetails, PlanWithMetadataDto } from '../projects';
import { PlanResourceDetails, PlanSummaryDto, PlanWithMetadataDto } from '../projects';

import { queries } from './queries';
import {
Expand Down Expand Up @@ -281,7 +281,7 @@ export class BillingRepository {
}

@withDbContext
async getPlans(context?: DbContext): Promise<PlanWithMetadataDto[]> {
async getPlans(context?: DbContext): Promise<PlanSummaryDto[]> {
try {
const getPlansResult = await context?.query({
text: queries.getAllPlans,
Expand All @@ -297,7 +297,6 @@ export class BillingRepository {
duration: row.duration,
active: row.active,
dateCreated: row.date_created,
metadata: row.metadata,
})) || []
);
} catch (error) {
Expand Down
2 changes: 1 addition & 1 deletion src/modules/repositories/billing/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const queries = {
getInvoicePaymentsByInvoiceIds:
'SELECT ip.invoice_id, ip.payment_channel, ip.payment_reference, ip.amount, ip.currency, ip.date_created, ip.status FROM "bonadocs"."invoice_payments" ip WHERE invoice_id = ANY($1)',
getAllPlans:
'SELECT id, name, price, currency, duration, active, date_created, metadata FROM "bonadocs"."plans"',
'SELECT p.id, p.name, p.price, p.currency, p.duration, p.active, p.date_created, FROM "bonadocs"."plans" AS p',
getPlanById:
'SELECT id, name, price, currency, duration, active, date_created, metadata FROM "bonadocs"."plans" WHERE id = $1 LIMIT 1',
updateSubscriptionMetadata: 'UPDATE "bonadocs"."subscriptions" SET metadata = $2 WHERE id = $1',
Expand Down
Loading
Loading