Skip to content

Commit

Permalink
feat(backend): introduce Order module
Browse files Browse the repository at this point in the history
  • Loading branch information
Skona27 committed Aug 26, 2021
1 parent 174441a commit ce7c436
Show file tree
Hide file tree
Showing 22 changed files with 530 additions and 19 deletions.
2 changes: 2 additions & 0 deletions apps/backend/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { AuthModule } from '../auth/auth.module';
import { ApiKeyModule } from '../api-key/api-key.module';
import { CreditModule } from '../credit/credit.module';
import { UsageModule } from '../usage/usage.module';
import { OrderModule } from '../order/order.module';

@Module({
imports: [
Expand Down Expand Up @@ -44,6 +45,7 @@ import { UsageModule } from '../usage/usage.module';
ApiKeyModule,
CreditModule,
UsageModule,
OrderModule,
],
controllers: [AppController],
providers: [AppService],
Expand Down
5 changes: 3 additions & 2 deletions apps/backend/src/config/config.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { CreditLimit } from '../credit/interface/credit-limit.interface';
import { Config } from './config.interface';

@Injectable()
Expand Down Expand Up @@ -51,8 +52,8 @@ export class AppConfigService {

get creditsConfig(): Config['creditsConfig'] {
return {
FREE: 1000,
STANDARD: 1000000,
[CreditLimit.Free]: 1000,
[CreditLimit.Standard]: 1000000,
};
}
}
14 changes: 7 additions & 7 deletions apps/backend/src/credit/credit.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,15 @@ export class CreditService {
try {
const presentDate = new Date();

return await this.creditRepository.findOneOrFail({
return await this.creditRepository.findOne({
where: {
user,
fromDate: LessThan(presentDate),
toDate: MoreThan(presentDate),
},
});
} catch (error) {
this.loggerService.log(`No active credit is available. ${error}`);
this.loggerService.log(`Failed to read user's active credit. ${error}`);
throw new ReadActiveCreditError(error);
}
}
Expand Down Expand Up @@ -106,7 +106,7 @@ export class CreditService {
toDate: Date;
} {
switch (duration) {
case 'MONTHLY': {
case CreditDuration.Monthly: {
const fromDate = new Date();
const toDate = addMonths(fromDate, 1);

Expand All @@ -123,10 +123,10 @@ export class CreditService {
const creditsConfig = this.configService.creditsConfig;

switch (limit) {
case 'FREE':
return creditsConfig.FREE;
case 'STANDARD':
return creditsConfig.STANDARD;
case CreditLimit.Free:
return creditsConfig.Free;
case CreditLimit.Standard:
return creditsConfig.Standard;
default:
throw new Error(`Cannot read credit limit value for type ${limit}`);
}
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/credit/error/read-active-credit.error.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export class ReadActiveCreditError extends Error {
constructor(error) {
super(`No active credit is available. ${error}`);
super(`Failed to read user's active credit. ${error}`);
}
}
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export type CreditDuration = 'MONTHLY';
export enum CreditDuration {
Monthly = 'Monthly',
}
5 changes: 4 additions & 1 deletion apps/backend/src/credit/interface/credit-limit.interface.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export type CreditLimit = 'FREE' | 'STANDARD';
export enum CreditLimit {
Free = 'Free',
Standard = 'Standard',
}
39 changes: 39 additions & 0 deletions apps/backend/src/database/migrations/1629996471851-orders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class orders1629996471851 implements MigrationInterface {
name = 'orders1629996471851';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TYPE "order_status_enum" AS ENUM('Created', 'PaymentPending', 'PaymentSuccessful', 'PaymentFailed')`,
);
await queryRunner.query(
`CREATE TYPE "order_credit_limit_type_enum" AS ENUM('Free', 'Standard')`,
);
await queryRunner.query(
`CREATE TYPE "order_credit_duration_type_enum" AS ENUM('Monthly')`,
);
await queryRunner.query(
`CREATE TABLE "order" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "price" integer NOT NULL, "status" "order_status_enum" NOT NULL DEFAULT 'Created', "credit_limit_type" "order_credit_limit_type_enum" NOT NULL, "credit_duration_type" "order_credit_duration_type_enum" NOT NULL, "user_id" uuid, "credit_id" uuid, CONSTRAINT "REL_d8625eb6d6e1217afd9ca73cd7" UNIQUE ("credit_id"), CONSTRAINT "PK_1031171c13130102495201e3e20" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`ALTER TABLE "order" ADD CONSTRAINT "FK_199e32a02ddc0f47cd93181d8fd" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "order" ADD CONSTRAINT "FK_d8625eb6d6e1217afd9ca73cd75" FOREIGN KEY ("credit_id") REFERENCES "credit"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "order" DROP CONSTRAINT "FK_d8625eb6d6e1217afd9ca73cd75"`,
);
await queryRunner.query(
`ALTER TABLE "order" DROP CONSTRAINT "FK_199e32a02ddc0f47cd93181d8fd"`,
);
await queryRunner.query(`DROP TABLE "order"`);
await queryRunner.query(`DROP TYPE "order_credit_duration_type_enum"`);
await queryRunner.query(`DROP TYPE "order_credit_limit_type_enum"`);
await queryRunner.query(`DROP TYPE "order_status_enum"`);
}
}
15 changes: 15 additions & 0 deletions apps/backend/src/order/dto/create-order.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum } from 'class-validator';

import { CreditDuration } from '../../credit/interface/credit-duration.interface';
import { CreditLimit } from '../../credit/interface/credit-limit.interface';

export class CreateOrderDto {
@ApiProperty()
@IsEnum(CreditLimit)
creditLimit: CreditLimit;

@ApiProperty()
@IsEnum(CreditDuration)
creditDuration: CreditDuration;
}
51 changes: 51 additions & 0 deletions apps/backend/src/order/dto/order.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { IsUUID, IsDate, IsInt, IsEnum } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

import { Order } from '../order.entity';
import { CreditLimit } from '../../credit/interface/credit-limit.interface';
import { CreditDuration } from '../../credit/interface/credit-duration.interface';
import { OrderStatus } from '../interface/order-status.interface';

export class OrderDto {
@ApiProperty()
@IsUUID()
id: string;

@ApiProperty()
@IsDate()
createdAt: Date;

@ApiProperty()
@IsDate()
updatedAt: Date;

@ApiProperty()
@IsEnum(CreditLimit)
creditLimit: CreditLimit;

@ApiProperty()
@IsEnum(CreditDuration)
creditDuration: CreditDuration;

@ApiProperty()
@IsInt()
public price: number;

@ApiProperty()
@IsEnum(OrderStatus)
public status: OrderStatus;

public static createDtoFromEntity(order: Order): OrderDto {
const dto = new OrderDto();

dto.id = order.id;
dto.price = order.price;
dto.status = order.status;
dto.createdAt = order.createdAt;
dto.updatedAt = order.updatedAt;
dto.creditLimit = order.creditLimit;
dto.creditDuration = order.creditDuration;

return dto;
}
}
5 changes: 5 additions & 0 deletions apps/backend/src/order/error/create-order.error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class CreateOrderError extends Error {
constructor(error: Error) {
super(`Failed to create a new order. ${error}`);
}
}
5 changes: 5 additions & 0 deletions apps/backend/src/order/error/invalid-order-status.order.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class InvalidOrderStatusError extends Error {
constructor(message: string) {
super(`Invalid status for order. ${message}`);
}
}
5 changes: 5 additions & 0 deletions apps/backend/src/order/error/read-order.error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class ReadOrderError extends Error {
constructor(error: Error) {
super(`Failed to read a order. ${error}`);
}
}
5 changes: 5 additions & 0 deletions apps/backend/src/order/error/unathorized-order.error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class UnathorizedOrderError extends Error {
constructor() {
super(`Unathorized access to an Order.`);
}
}
5 changes: 5 additions & 0 deletions apps/backend/src/order/error/update-order.error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class UpdateOrderError extends Error {
constructor(error: Error) {
super(`Failed to update a order. ${error}`);
}
}
6 changes: 6 additions & 0 deletions apps/backend/src/order/interface/order-status.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export enum OrderStatus {
Created = 'Created',
PaymentPending = 'PaymentPending',
PaymentSuccessful = 'PaymentSuccessful',
PaymentFailed = 'PaymentFailed',
}
30 changes: 30 additions & 0 deletions apps/backend/src/order/order.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Test, TestingModule } from '@nestjs/testing';
import { OrderController } from './order.controller';
import { OrderService } from './order.service';

describe('OrderController', () => {
let controller: OrderController;

const orderServiceMock = {
createOrder: jest.fn(),
getOrders: jest.fn(),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [OrderController],
providers: [
{
provide: OrderService,
useValue: orderServiceMock,
},
],
}).compile();

controller = module.get<OrderController>(OrderController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
78 changes: 78 additions & 0 deletions apps/backend/src/order/order.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {
Controller,
Post,
HttpStatus,
HttpException,
Request,
Body,
Get,
} from '@nestjs/common';
import {
ApiTags,
ApiOkResponse,
ApiUnauthorizedResponse,
ApiCreatedResponse,
ApiInternalServerErrorResponse,
} from '@nestjs/swagger';

import { Authorised } from '../auth/auth.decorator';
import { RequestWithUser } from '../common/interface/request-with-user.interface';
import { CreateOrderDto } from './dto/create-order.dto';
import { OrderDto } from './dto/order.dto';
import { OrderService } from './order.service';

@ApiTags('orders')
@Controller('orders')
export class OrderController {
constructor(private readonly orderService: OrderService) {}

@Post('/')
@Authorised()
@ApiCreatedResponse()
@ApiUnauthorizedResponse()
@ApiInternalServerErrorResponse()
public async createOrder(
@Request() request: RequestWithUser,
@Body() createOrderDto: CreateOrderDto,
): Promise<OrderDto> {
try {
const user = request.user;
const { creditDuration, creditLimit } = createOrderDto;

const order = await this.orderService.createOrder(
creditLimit,
creditDuration,
user,
);

return OrderDto.createDtoFromEntity(order);
} catch (error) {
throw new HttpException(
'Internal server error',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}

@Get('/')
@Authorised()
@ApiOkResponse()
@ApiUnauthorizedResponse()
@ApiInternalServerErrorResponse()
public async getOrders(
@Request() request: RequestWithUser,
): Promise<OrderDto[]> {
try {
const user = request.user;

const orders = await this.orderService.getOrders(user);

return orders.map(OrderDto.createDtoFromEntity);
} catch (error) {
throw new HttpException(
'Internal server error',
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}
}
48 changes: 48 additions & 0 deletions apps/backend/src/order/order.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Column, ManyToOne, JoinColumn, Entity, OneToOne } from 'typeorm';

import { User } from '../user/user.entity';
import { Credit } from '../credit/credit.entity';
import { BaseEditableEntity } from '../common/model/base-editable.entity';
import { OrderStatus } from './interface/order-status.interface';
import { CreditLimit } from '../credit/interface/credit-limit.interface';
import { CreditDuration } from '../credit/interface/credit-duration.interface';

@Entity()
export class Order extends BaseEditableEntity {
@Column({ name: 'price', type: 'int' })
public price: number;

@Column({
name: 'status',
type: 'enum',
enum: OrderStatus,
default: OrderStatus.Created,
})
public status: OrderStatus;

@Column({
name: 'credit_limit_type',
type: 'enum',
enum: CreditLimit,
})
public creditLimit: CreditLimit;

@Column({
name: 'credit_duration_type',
type: 'enum',
enum: CreditDuration,
})
public creditDuration: CreditDuration;

@ManyToOne(() => User, { eager: true })
@JoinColumn({ name: 'user_id', referencedColumnName: 'id' })
public user: User;

@OneToOne(() => Credit, { eager: true, nullable: true })
@JoinColumn({ name: 'credit_id', referencedColumnName: 'id' })
public credit?: Credit;

public belongsTo(userId: string): boolean {
return this.user.id === userId;
}
}
Loading

0 comments on commit ce7c436

Please sign in to comment.