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: 2 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
AuthController,
BillingController,
ContractController,
CouponController,
getConfigService,
ProjectController,
SubscriptionController,
Expand Down Expand Up @@ -97,6 +98,7 @@ useExpressServer(app, {
ContractController,
TenderlyController,
BillingController,
CouponController,
],
middlewares: [AppErrorHandler, AuthMiddleware],
});
Expand Down
5 changes: 4 additions & 1 deletion src/middleware/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { diConstants } from '@bonadocs/di';
import type { BonadocsLogger } from '@bonadocs/logger';

import { AuthService } from '../modules/auth/auth.service';
import { ApplicationError } from '../modules/errors/ApplicationError';
import { ApplicationError, applicationErrorCodes } from '../modules/errors/ApplicationError';

@Service()
@Middleware({ type: 'before' })
Expand All @@ -34,6 +34,9 @@ export class AuthMiddleware implements ExpressMiddlewareInterface {
throw new ApplicationError({
message: 'Invalid token',
logger: request.logger,
errorCode: applicationErrorCodes.unauthenticated,
userFriendlyMessage: 'Invalid Token',
statusCode: 401,
});
}

Expand Down
8 changes: 8 additions & 0 deletions src/middleware/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ export default class AppErrorHandler implements ExpressErrorMiddlewareInterface
return;
}

if ((<Error>error).name === 'PayloadTooLargeError') {
response.status(StatusCodes.REQUEST_TOO_LONG).json({
status: 'failed',
message: 'Request payload too large',
});
return;
}

if (request.logger) {
request.logger?.error('Unexpected error occurred', error);
} else {
Expand Down
2 changes: 1 addition & 1 deletion src/modules/auth/auth.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ describe('AuthService', () => {
},
authData,
),
).rejects.toHaveProperty('errorCode', 'UNAUTHORIZED');
).rejects.toHaveProperty('errorCode', 'UNAUTHENTICATED');
});
});
});
Expand Down
28 changes: 22 additions & 6 deletions src/modules/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export class AuthService {
message: 'Failed to authenticate user',
logger: this.logger,
userFriendlyMessage: 'Please check your credentials and try again',
statusCode: 401,
});
}

Expand Down Expand Up @@ -155,7 +156,9 @@ export class AuthService {
throw new ApplicationError({
message: 'User is not authenticated',
logger: this.logger,
errorCode: applicationErrorCodes.unauthorized,
errorCode: applicationErrorCodes.unauthenticated,
statusCode: 401,
userFriendlyMessage: 'You must be logged in to access this service',
});
}
// check if projectId and collectionId are provided exits
Expand All @@ -165,6 +168,7 @@ export class AuthService {
message: 'Project not found',
logger: this.logger,
errorCode: applicationErrorCodes.unauthorized,
statusCode: 403,
});
}
const collectionExist = await this.projectRepository.checkIfCollectionExist(
Expand All @@ -176,6 +180,7 @@ export class AuthService {
message: 'Collection not found',
logger: this.logger,
errorCode: applicationErrorCodes.unauthorized,
statusCode: 403,
});
}
// check if user has permission to write to the collection
Expand All @@ -185,6 +190,7 @@ export class AuthService {
message: 'You do not have permission to write in this collection, contact your admin',
logger: this.logger,
errorCode: applicationErrorCodes.unauthorized,
statusCode: 403,
});
}
const validityPeriod = this.validityFromEnv ? Number(this.validityFromEnv) : 6 * 3600;
Expand Down Expand Up @@ -213,6 +219,7 @@ export class AuthService {
throw new ApplicationError({
message: 'You do not have permission to write in this collection, contact your admin',
errorCode: applicationErrorCodes.unauthorized,
statusCode: 403,
logger: this.logger,
});
}
Expand All @@ -225,7 +232,8 @@ export class AuthService {
throw new ApplicationError({
message: 'Unsupported auth source',
logger: this.logger,
errorCode: applicationErrorCodes.unauthorized,
errorCode: applicationErrorCodes.unauthenticated,
statusCode: 401,
});
}
return func;
Expand Down Expand Up @@ -316,7 +324,8 @@ export class AuthService {
throw new ApplicationError({
message: 'Invalid or expired API key',
logger: this.logger,
errorCode: applicationErrorCodes.unauthorized,
errorCode: applicationErrorCodes.unauthenticated,
statusCode: 401,
});
}

Expand All @@ -333,7 +342,8 @@ export class AuthService {
throw new ApplicationError({
message: 'Invalid JWT',
logger: this.logger,
errorCode: applicationErrorCodes.unauthorized,
errorCode: applicationErrorCodes.unauthenticated,
statusCode: 401,
});
}

Expand All @@ -342,14 +352,16 @@ export class AuthService {
throw new ApplicationError({
message: 'Invalid JWT - purpose ws server',
logger: this.logger,
errorCode: applicationErrorCodes.unauthorized,
errorCode: applicationErrorCodes.unauthenticated,
statusCode: 401,
});
}
if (!payload.userId) {
throw new ApplicationError({
message: 'Invalid JWT - missing user ID',
logger: this.logger,
errorCode: applicationErrorCodes.unauthorized,
errorCode: applicationErrorCodes.unauthenticated,
statusCode: 401,
});
}

Expand All @@ -369,6 +381,7 @@ export class AuthService {
message: 'Invalid auth data',
logger: this.logger,
errorCode: applicationErrorCodes.unauthenticated,
statusCode: 401,
});
}

Expand All @@ -379,6 +392,7 @@ export class AuthService {
message: 'Invalid Firebase ID Token',
errorCode: applicationErrorCodes.unauthenticated,
logger: this.logger,
statusCode: 401,
});
}

Expand Down Expand Up @@ -407,6 +421,7 @@ export class AuthService {
message: 'Invalid auth data',
logger: this.logger,
errorCode: applicationErrorCodes.unauthenticated,
statusCode: 401,
});
}

Expand All @@ -417,6 +432,7 @@ export class AuthService {
message: 'Invalid Firebase ID Token',
errorCode: applicationErrorCodes.unauthenticated,
logger: this.logger,
statusCode: 401,
});
}

Expand Down
75 changes: 75 additions & 0 deletions src/modules/coupon/coupon.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Request } from 'express';
import { Body, Get, JsonController, Param, Patch, Post, Req } from 'routing-controllers';
import { Inject, Service } from 'typedi';

import { JsonResponse } from '../shared';

import { CouponResponse } from './coupon.interface';
import { CouponService } from './coupon.service';
import { CreateCouponDto, UpdateCouponStatusDto } from './dto';

@Service()
@JsonController('/coupons')
export class CouponController {
constructor(@Inject() private readonly couponService: CouponService) {}

@Post('/')
async create(
@Body({ validate: true }) payload: CreateCouponDto,
@Req() request: Request,
): Promise<JsonResponse<string>> {
const authData = request.auth;
const response = await this.couponService.create(payload, authData);
return {
data: response,
status: 'successful',
message: 'Created Coupon successfully',
};
}

@Patch('/:couponId')
async change_status(
@Param('couponId') couponId: number,
@Body({ validate: true }) payload: UpdateCouponStatusDto,
): Promise<JsonResponse<string>> {
const response = await this.couponService.changeStatus({
id: couponId,
status: payload.status,
});
return {
data: response,
status: 'successful',
message: 'Updated Coupon successfully',
};
}

@Get('/get-by-projectId/:projectId')
async list(@Param('projectId') projectId: number): Promise<JsonResponse<CouponResponse[]>> {
const response = await this.couponService.listCouponByProjectId(projectId);
return {
data: response,
status: 'successful',
message: 'Coupon Get Successfully',
};
}

@Get('/:couponId')
async get(@Param('couponId') couponId: number): Promise<JsonResponse<CouponResponse>> {
const response = await this.couponService.getById(couponId);
return {
data: response,
status: 'successful',
message: 'Coupon Get Successfully',
};
}

@Get('/get-by-code/:code')
async getByCode(@Param('code') code: string): Promise<JsonResponse<CouponResponse>> {
const response = await this.couponService.getByCode(code);
return {
data: response,
status: 'successful',
message: 'Coupon Get Successfully',
};
}
}
23 changes: 23 additions & 0 deletions src/modules/coupon/coupon.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { SubscriptionStatus } from '../shared/types/subscriptions';

export interface CreateCouponRequest {
project_id: number;
date_expire: Date;
plan_id: number;
}

export interface CouponResponse {
id: number;
project_id: number;
code: string;
date_created: Date;
date_expire: Date;
plan_id: number;
status: SubscriptionStatus;
subscription_id: number;
}

export interface UpdateCouponStatus {
id: number;
status: SubscriptionStatus;
}
101 changes: 101 additions & 0 deletions src/modules/coupon/coupon.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Inject, Service } from 'typedi';
import { v4 as uuidv4 } from 'uuid';

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

import { ApplicationError, applicationErrorCodes } from '../errors';
import { CouponDto, CouponRepository, CreateCouponDto } from '../repositories/coupon';
import { SubscriptionStatus } from '../shared/types/subscriptions';

import { CreateCouponRequest, UpdateCouponStatus } from './coupon.interface';

@Service()
export class CouponService {
constructor(
@Inject(diConstants.logger) private readonly logger: BonadocsLogger,
@Inject() private readonly couponRepository: CouponRepository,
) {}

async create(request: CreateCouponRequest, authData: AuthData): Promise<string> {
const coupon: CreateCouponDto = {
code: this.generateUUIDCouponCode('bonadocs'),
date_expire: request.date_expire,
plan_id: request.plan_id,
project_id: request.project_id,
status: SubscriptionStatus.Active,
creator_id: authData.userId!,
};
// validate if any active coupon exists for the project on the selected plan
const existingCoupons = await this.couponRepository.getCouponsByProjectIdAndPlanId(
request.project_id,
request.plan_id,
);
if (existingCoupons !== undefined) {
this.logger.error('Active coupon already exists for the project on the selected plan');
throw new ApplicationError({
logger: this.logger,
message: 'Active coupon already exists for the project on the selected plan',
errorCode: applicationErrorCodes.invalidRequest,
});
}
const couponCode = await this.couponRepository.createCoupon(coupon);
if (couponCode === undefined) {
this.logger.error('Error creating coupon');
throw new ApplicationError({
logger: this.logger,
message: 'Unable to create coupon',
errorCode: applicationErrorCodes.invalidRequest,
});
}
return couponCode;
}

async changeStatus(request: UpdateCouponStatus): Promise<string> {
const code = await this.couponRepository.changeCouponStatus(request.id, request.status);
if (code === undefined) {
this.logger.error('Error updating coupon status');
throw new ApplicationError({
logger: this.logger,
message: 'Unable to update coupon status',
errorCode: applicationErrorCodes.invalidRequest,
});
}
return code;
}

async getById(couponId: number): Promise<CouponDto> {
const coupon = await this.couponRepository.getCouponById(couponId);
if (coupon === undefined) {
this.logger.error('Error getting coupon by id');
throw new ApplicationError({
logger: this.logger,
message: 'Unable to get coupon by id',
errorCode: applicationErrorCodes.invalidRequest,
});
}
return coupon;
}

async getByCode(couponCode: string): Promise<CouponDto> {
const coupon = await this.couponRepository.getCouponByCode(couponCode);
if (coupon === undefined) {
this.logger.error('Error getting coupon by code');
throw new ApplicationError({
logger: this.logger,
message: 'Unable to get coupon by code',
errorCode: applicationErrorCodes.invalidRequest,
});
}
return coupon;
}

async listCouponByProjectId(projectId: number): Promise<CouponDto[]> {
return this.couponRepository.listCouponsByProjectId(projectId);
}

private generateUUIDCouponCode(prefix = ''): string {
const code = uuidv4().split('-')[0].toUpperCase();
return prefix ? `${prefix}-${code}` : code;
}
}
Loading
Loading