├── src ├── auth │ ├── dto │ │ ├── index.ts │ │ └── auth.dto.ts │ ├── guard │ │ ├── index.ts │ │ └── jwt.guard.ts │ ├── strategy │ │ ├── index.ts │ │ └── jwt.strategy.ts │ ├── decorator │ │ ├── index.ts │ │ └── getUser.decorator.ts │ ├── auth.module.ts │ ├── auth.controller.ts │ └── auth.service.ts ├── movie │ ├── dto │ │ ├── index.ts │ │ └── movie.dto.ts │ ├── movie.module.ts │ ├── movie.controller.ts │ └── movie.service.ts ├── orders │ ├── dto │ │ ├── index.ts │ │ └── orders.dto.ts │ ├── orders.module.ts │ ├── orders.controller.ts │ └── orders.service.ts ├── ticket │ ├── dto │ │ ├── index.ts │ │ └── ticket.dto.ts │ ├── ticket.module.ts │ ├── ticket.controller.ts │ └── ticket.service.ts ├── balance │ ├── dto │ │ ├── index.ts │ │ └── balance.dto.ts │ ├── balance.module.ts │ ├── balance.controller.ts │ └── balance.service.ts ├── prisma │ ├── prisma.module.ts │ └── prisma.service.ts ├── user │ ├── user.module.ts │ ├── user.controller.ts │ └── user.service.ts ├── app.module.ts └── main.ts ├── .prettierrc ├── erd.png ├── tsconfig.build.json ├── prisma ├── migrations │ ├── migration_lock.toml │ └── 20240821145045_first │ │ └── migration.sql ├── seed.ts ├── schema backup.prisma ├── schema copy.prisma ├── schema.prisma └── movies.ts ├── nest-cli.json ├── test ├── jest-e2e.json └── app.e2e-spec.ts ├── ecosystem.config.js ├── Dockerfile ├── docker-compose.yml ├── .env.example ├── .gitignore ├── tsconfig.json ├── .eslintrc.js ├── Jenkinsfile ├── package.json ├── README.md ├── _Movie__202306282204.sql └── movies.json /src/auth/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.dto'; 2 | -------------------------------------------------------------------------------- /src/auth/guard/index.ts: -------------------------------------------------------------------------------- 1 | export * from './jwt.guard'; 2 | -------------------------------------------------------------------------------- /src/movie/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './movie.dto'; 2 | -------------------------------------------------------------------------------- /src/orders/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './orders.dto'; 2 | -------------------------------------------------------------------------------- /src/ticket/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ticket.dto'; 2 | -------------------------------------------------------------------------------- /src/auth/strategy/index.ts: -------------------------------------------------------------------------------- 1 | export * from './jwt.strategy'; 2 | -------------------------------------------------------------------------------- /src/balance/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './balance.dto'; 2 | -------------------------------------------------------------------------------- /src/auth/decorator/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getUser.decorator'; 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /erd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fzrsahi/movie-booking-app/HEAD/erd.png -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /src/movie/dto/movie.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from "@nestjs/swagger"; 2 | 3 | export class SearchQueryDto { 4 | @ApiProperty() 5 | title: string; 6 | } 7 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /src/auth/guard/jwt.guard.ts: -------------------------------------------------------------------------------- 1 | import { AuthGuard } from '@nestjs/passport'; 2 | 3 | export class JwtGuard extends AuthGuard('jwt') { 4 | constructor() { 5 | super(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: 'movie-booking-app', 5 | script: 'dist/src/main.js', 6 | watch: false, 7 | exec_mode: 'cluster', 8 | instances: 2, 9 | }, 10 | ], 11 | }; 12 | -------------------------------------------------------------------------------- /src/prisma/prisma.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | import { PrismaService } from './prisma.service'; 3 | 4 | @Global() 5 | @Module({ 6 | providers: [PrismaService], 7 | exports: [PrismaService], 8 | }) 9 | export class PrismaModule {} 10 | -------------------------------------------------------------------------------- /src/ticket/dto/ticket.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsArray, IsNotEmpty } from 'class-validator'; 3 | 4 | export class TicketDto { 5 | @ApiProperty({example : [1,2,3]}) 6 | @IsNotEmpty() 7 | @IsArray() 8 | seatNumber: number[]; 9 | } 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY package*.json ./ 6 | 7 | RUN npm install 8 | RUN npm install pm2 -g 9 | 10 | COPY . . 11 | 12 | RUN npx prisma generate 13 | RUN npm run build 14 | 15 | EXPOSE 3009 16 | 17 | CMD ["pm2-runtime", "ecosystem.config.js"] -------------------------------------------------------------------------------- /src/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UserController } from './user.controller'; 3 | import { UserService } from './user.service'; 4 | 5 | @Module({ 6 | controllers: [UserController], 7 | providers: [UserService] 8 | }) 9 | export class UserModule {} 10 | -------------------------------------------------------------------------------- /src/movie/movie.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MovieController } from './movie.controller'; 3 | import { MovieService } from './movie.service'; 4 | 5 | @Module({ 6 | controllers: [MovieController], 7 | providers: [MovieService], 8 | }) 9 | export class MovieModule {} 10 | -------------------------------------------------------------------------------- /src/orders/orders.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { OrdersController } from './orders.controller'; 3 | import { OrdersService } from './orders.service'; 4 | 5 | @Module({ 6 | controllers: [OrdersController], 7 | providers: [OrdersService] 8 | }) 9 | export class OrdersModule {} 10 | -------------------------------------------------------------------------------- /src/ticket/ticket.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TicketController } from './ticket.controller'; 3 | import { TicketService } from './ticket.service'; 4 | 5 | @Module({ 6 | controllers: [TicketController], 7 | providers: [TicketService] 8 | }) 9 | export class TicketModule {} 10 | -------------------------------------------------------------------------------- /src/orders/dto/orders.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsArray, IsNotEmpty } from 'class-validator'; 3 | 4 | export class cancelOrderDto { 5 | @ApiProperty({ example: ['a2220515-4bb8-46a6-aac1-93d5eb2d64da'] }) 6 | @IsNotEmpty() 7 | @IsArray() 8 | ticketsId: string[]; 9 | } 10 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | app: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | ports: 9 | - "${PORT}:3000" 10 | environment: 11 | - PORT 12 | - SECRET_AUTH_KEY 13 | - APP_MODE 14 | - HIDE_POST_ENDPOINTS 15 | - DATABASE_URL 16 | -------------------------------------------------------------------------------- /src/balance/balance.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { BalanceController } from './balance.controller'; 3 | import { BalanceService } from './balance.service'; 4 | 5 | @Module({ 6 | controllers: [BalanceController], 7 | providers: [BalanceService] 8 | }) 9 | export class BalanceModule {} 10 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | #App 2 | JWT_SECRET="movieapp" 3 | SECRET_TOKEN="movieapp" 4 | PORT=3001 5 | SWAGGER_PATH=docs 6 | 7 | #Db 8 | DATABASE_URL=postgresql://fzrsahi:password@localhost:5432/movie-booking-app?schema=public 9 | POSTGRES_USER=fzrsahi 10 | POSTGRES_PASSWORD=password 11 | POSTGRES_DB=movie-booking-app 12 | RATE_LIMITER_TTL=60000 13 | RATE_LIMITER_LIMIT=50 14 | -------------------------------------------------------------------------------- /src/balance/dto/balance.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { IsNotEmpty, IsNumber } from 'class-validator'; 3 | 4 | export class BalanceDto { 5 | @IsNotEmpty() 6 | @ApiProperty() 7 | @IsNumber() 8 | balance: bigint; 9 | } 10 | 11 | export class BalanceWithdrawalDto { 12 | @IsNotEmpty() 13 | @IsNumber() 14 | @ApiProperty() 15 | withdrawal: bigint; 16 | } 17 | -------------------------------------------------------------------------------- /src/auth/decorator/getUser.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 2 | 3 | export const GetUser = createParamDecorator( 4 | (data: string | undefined, ctx: ExecutionContext) => { 5 | const request: Express.Request = ctx.switchToHttp().getRequest(); 6 | if (data) { 7 | return request.user[data]; 8 | } 9 | 10 | return request.user; 11 | }, 12 | ); 13 | -------------------------------------------------------------------------------- /src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AuthController } from './auth.controller'; 3 | import { AuthService } from './auth.service'; 4 | import { JwtModule } from '@nestjs/jwt'; 5 | import { JwtStrategy } from './strategy'; 6 | 7 | @Module({ 8 | imports: [JwtModule.register({})], 9 | controllers: [AuthController], 10 | providers: [AuthService, JwtStrategy], 11 | }) 12 | export class AuthModule {} 13 | -------------------------------------------------------------------------------- /src/prisma/prisma.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { PrismaClient } from '@prisma/client'; 4 | 5 | @Injectable() 6 | export class PrismaService extends PrismaClient { 7 | constructor(config: ConfigService) { 8 | super({ 9 | datasources: { 10 | db: { 11 | url: config.get('DATABASE_URL'), 12 | }, 13 | }, 14 | }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | /.env 5 | /data 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | pnpm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | lerna-debug.log* 15 | 16 | # OS 17 | .DS_Store 18 | 19 | # Tests 20 | /coverage 21 | /.nyc_output 22 | 23 | # IDEs and editors 24 | /.idea 25 | .project 26 | .classpath 27 | .c9/ 28 | *.launch 29 | .settings/ 30 | *.sublime-workspace 31 | 32 | # IDE - VSCode 33 | .vscode/* 34 | !.vscode/settings.json 35 | !.vscode/tasks.json 36 | !.vscode/launch.json 37 | !.vscode/extensions.json 38 | .vercel 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, HttpCode, Post } from '@nestjs/common'; 2 | import { AuthService } from './auth.service'; 3 | import { CreateUserDto, UserLoginDto } from './dto/'; 4 | import { ApiTags } from '@nestjs/swagger'; 5 | 6 | @Controller('auth') 7 | @ApiTags("Auth") 8 | export class AuthController { 9 | constructor(private readonly authService: AuthService) {} 10 | 11 | @Post('signup') 12 | async createUser(@Body() dto: CreateUserDto) { 13 | return this.authService.createUser(dto); 14 | } 15 | 16 | @HttpCode(200) 17 | @Post('signin') 18 | userLogin(@Body() dto: UserLoginDto) { 19 | return this.authService.userLogin(dto); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /src/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Param, UseGuards } from '@nestjs/common'; 2 | import { UserService } from './user.service'; 3 | import { JwtGuard } from 'src/auth/guard'; 4 | import { GetUser } from 'src/auth/decorator'; 5 | import { User } from '@prisma/client'; 6 | import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; 7 | 8 | @UseGuards(JwtGuard) 9 | @Controller('users') 10 | @ApiTags("Users") 11 | @ApiBearerAuth('JWTAUTH') 12 | export class UserController { 13 | constructor(private readonly userService: UserService) {} 14 | 15 | @Get('me') 16 | getCurrentUserLogin(@GetUser() user: User) { 17 | return this.userService.getUserData(user); 18 | } 19 | 20 | @Get('tickets/:id') 21 | getTicketsById(@GetUser() user: User, @Param("id") ticketsId : string) { 22 | return this.userService.getTicketsById(user, ticketsId); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/auth/strategy/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { PassportStrategy } from '@nestjs/passport'; 4 | import { ExtractJwt, Strategy } from 'passport-jwt'; 5 | import { PrismaService } from '../../prisma/prisma.service'; 6 | 7 | @Injectable() 8 | export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { 9 | constructor(config: ConfigService, private prisma: PrismaService) { 10 | super({ 11 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 12 | secretOrKey: config.get('SECRET_TOKEN'), 13 | }); 14 | } 15 | 16 | async validate(payload: { sub: string; email: string }) { 17 | const user = await this.prisma.user.findUnique({ 18 | where: { 19 | id: payload.sub, 20 | }, 21 | }); 22 | 23 | delete user.hash; 24 | return user; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/auth/dto/auth.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | import { 3 | IsDate, 4 | IsEmail, 5 | IsNotEmpty, 6 | IsNumber, 7 | IsOptional, 8 | IsString, 9 | } from 'class-validator'; 10 | 11 | export class CreateUserDto { 12 | @ApiProperty() 13 | @IsString() 14 | @IsNotEmpty() 15 | name: string; 16 | 17 | @ApiProperty() 18 | @IsString() 19 | @IsNotEmpty() 20 | username: string; 21 | 22 | @IsEmail() 23 | @IsString() 24 | @IsNotEmpty() 25 | @ApiProperty() 26 | email: string; 27 | 28 | @IsString() 29 | @IsNotEmpty() 30 | @ApiProperty() 31 | hash: string; 32 | 33 | @IsNotEmpty() 34 | @ApiProperty({ example: '2003-03-17' }) 35 | birth: string; 36 | 37 | @IsNumber() 38 | @IsOptional() 39 | age?: number; 40 | } 41 | 42 | export class UserLoginDto { 43 | @IsString() 44 | @IsNotEmpty() 45 | @ApiProperty() 46 | username: string; 47 | 48 | @IsString() 49 | @IsNotEmpty() 50 | @ApiProperty() 51 | hash: string; 52 | } 53 | -------------------------------------------------------------------------------- /prisma/seed.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | 3 | import { movies } from './movies'; 4 | const prisma = new PrismaClient(); 5 | 6 | async function main() { 7 | const moviesSeed = await prisma.movie.createMany({ 8 | data: movies, 9 | }); 10 | 11 | const moviesCount = movies.length; 12 | const seatsData = []; 13 | 14 | for (let movieId = 0; movieId < moviesCount; movieId++) { 15 | for (let seatNumber = 1; seatNumber <= 64; seatNumber++) { 16 | const seat = { 17 | movieId: movieId, 18 | seatNumber: seatNumber, 19 | book: false, 20 | }; 21 | seatsData.push(seat); 22 | } 23 | } 24 | 25 | console.log(seatsData); 26 | 27 | // const seatsSeed = await prisma.seats.createMany({ 28 | // data: seatsData, 29 | // }); 30 | 31 | console.log(moviesSeed); 32 | } 33 | 34 | main() 35 | .then(async () => { 36 | await prisma.$disconnect(); 37 | }) 38 | .catch(async (e) => { 39 | console.error(e); 40 | await prisma.$disconnect(); 41 | process.exit(1); 42 | }); 43 | -------------------------------------------------------------------------------- /src/movie/movie.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | Controller, 4 | Get, 5 | Param, 6 | ParseIntPipe, 7 | Query, 8 | } from '@nestjs/common'; 9 | import { MovieService } from './movie.service'; 10 | import { SearchQueryDto } from './dto'; 11 | import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; 12 | 13 | @ApiTags("Movie") 14 | @Controller('/movies') 15 | export class MovieController { 16 | constructor(private movieService: MovieService) {} 17 | 18 | @Get('search') 19 | searchMovies(@Query() query: SearchQueryDto) { 20 | if (!query.title) { 21 | throw new BadRequestException({ 22 | statusCode: 400, 23 | message: 'Please Add The Movie Title', 24 | }); 25 | } 26 | 27 | return this.movieService.searchMovies(query); 28 | } 29 | 30 | @Get() 31 | getAllMovie( 32 | @Query('page', ParseIntPipe) page: number, 33 | @Query('limit', ParseIntPipe) limit: number, 34 | ) { 35 | return this.movieService.getAllMovies(page, limit); 36 | } 37 | 38 | @Get(':id') 39 | getMovieById(@Param('id', ParseIntPipe) movieId : number) { 40 | return this.movieService.getMovieById(movieId); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/orders/orders.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Delete, 5 | Get, 6 | Param, 7 | ParseIntPipe, 8 | Patch, 9 | Post, 10 | UseGuards, 11 | } from '@nestjs/common'; 12 | import { User } from '@prisma/client'; 13 | import { GetUser } from 'src/auth/decorator'; 14 | import { JwtGuard } from 'src/auth/guard'; 15 | import { OrdersService } from './orders.service'; 16 | import { cancelOrderDto } from './dto'; 17 | import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; 18 | 19 | @UseGuards(JwtGuard) 20 | @ApiBearerAuth('JWTAUTH') 21 | @ApiTags("Orders") 22 | @Controller('orders') 23 | export class OrdersController { 24 | constructor(private readonly orderService: OrdersService) {} 25 | 26 | @Get(':id') 27 | getOrderById(@GetUser() user, @Param('id') orderId: string) { 28 | return this.orderService.getOrderById(user, orderId); 29 | } 30 | 31 | @Get() 32 | getOrderByUserId(@GetUser() user: User) { 33 | return this.orderService.getUserOrders(user); 34 | } 35 | 36 | @Delete('cancel') 37 | cancelOrder(@GetUser() user: User, @Body() ticketsId: cancelOrderDto) { 38 | return this.orderService.cancelOrder(user, ticketsId.ticketsId); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/balance/balance.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | Body, 4 | Controller, 5 | Get, 6 | Post, 7 | UseGuards, 8 | } from '@nestjs/common'; 9 | import { BalanceService } from './balance.service'; 10 | import { BalanceDto, BalanceWithdrawalDto } from './dto'; 11 | import { JwtGuard } from 'src/auth/guard'; 12 | import { User } from '@prisma/client'; 13 | import { GetUser } from 'src/auth/decorator'; 14 | import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; 15 | 16 | @UseGuards(JwtGuard) 17 | @ApiTags("Balances") 18 | @ApiBearerAuth('JWTAUTH') 19 | @Controller('balances') 20 | export class BalanceController { 21 | constructor(private readonly balanceService: BalanceService) {} 22 | 23 | @Get() 24 | getBalance(@GetUser() user: User) { 25 | return this.balanceService.getBalance(user); 26 | } 27 | 28 | @Post() 29 | addBalance(@GetUser() user: User, @Body() dto: BalanceDto) { 30 | if (dto.balance > 999999999999999999) { 31 | throw new BadRequestException({ 32 | statusCode: 400, 33 | message: 'Cannot Top Up More Than RP.999.999.999.999.999.999', 34 | }); 35 | } 36 | return this.balanceService.addBalance(dto, user); 37 | } 38 | 39 | @Post('withdraw') 40 | balanceWithdrawal(@GetUser() user: User, @Body() dto: BalanceWithdrawalDto) { 41 | return this.balanceService.balanceWithdrawal(dto, user); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/ticket/ticket.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Param, 6 | ParseIntPipe, 7 | UseGuards, 8 | Body, 9 | BadRequestException, 10 | } from '@nestjs/common'; 11 | import { TicketService } from './ticket.service'; 12 | import { JwtGuard } from 'src/auth/guard'; 13 | import { GetUser } from 'src/auth/decorator'; 14 | import { User } from '@prisma/client'; 15 | import { TicketDto } from './dto'; 16 | import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; 17 | 18 | @Controller('tickets') 19 | @ApiTags('Tickets') 20 | export class TicketController { 21 | constructor(private readonly ticketService: TicketService) {} 22 | 23 | @Get('seat/:movie_id') 24 | getSeat(@Param('movie_id', ParseIntPipe) movieId: number) { 25 | return this.ticketService.getSeat(movieId); 26 | } 27 | 28 | @UseGuards(JwtGuard) 29 | @ApiBearerAuth('JWTAUTH') 30 | @Post('seat/:movie_id') 31 | bookSeat( 32 | @GetUser() user: User, 33 | @Param('movie_id', ParseIntPipe) movieId : number, 34 | @Body() dto: TicketDto, 35 | ) { 36 | if (!dto.seatNumber.length) { 37 | throw new BadRequestException('Array Tidak Boleh Kosong'); 38 | } 39 | 40 | if (dto.seatNumber.length > 6) { 41 | throw new BadRequestException( 42 | 'Sorry, you can only order a maximum of 6 tickets', 43 | ); 44 | } 45 | return this.ticketService.bookSeats(user, movieId, dto.seatNumber); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MovieModule } from './movie/movie.module'; 3 | import { PrismaService } from './prisma/prisma.service'; 4 | 5 | import { ConfigModule, ConfigService } from '@nestjs/config'; 6 | import { PrismaModule } from './prisma/prisma.module'; 7 | import { AuthModule } from './auth/auth.module'; 8 | import { UserModule } from './user/user.module'; 9 | import { BalanceModule } from './balance/balance.module'; 10 | import { TicketModule } from './ticket/ticket.module'; 11 | import { OrdersModule } from './orders/orders.module'; 12 | import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler'; 13 | import { APP_GUARD } from '@nestjs/core'; 14 | 15 | @Module({ 16 | imports: [ 17 | MovieModule, 18 | PrismaModule, 19 | ConfigModule.forRoot({ 20 | isGlobal: true, 21 | }), 22 | AuthModule, 23 | UserModule, 24 | BalanceModule, 25 | TicketModule, 26 | OrdersModule, 27 | ThrottlerModule.forRootAsync({ 28 | imports: [ConfigModule], 29 | inject: [ConfigService], 30 | useFactory: (config: ConfigService) => [ 31 | { 32 | ttl: config.get('RATE_LIMITER_TTL'), 33 | limit: config.get('RATE_LIMITER_LIMIT'), 34 | }, 35 | ], 36 | }), 37 | ], 38 | controllers: [], 39 | providers: [ 40 | PrismaService, 41 | { 42 | provide: APP_GUARD, 43 | useClass: ThrottlerGuard, 44 | }, 45 | ], 46 | }) 47 | export class AppModule {} 48 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent any 3 | 4 | stages { 5 | stage('verify tooling') { 6 | steps { 7 | sh ''' 8 | docker version 9 | docker info 10 | docker info 11 | docker compose version 12 | curl --version 13 | ''' 14 | } 15 | } 16 | 17 | stage('check docker status') { 18 | steps { 19 | sh ''' 20 | docker ps 21 | docker images 22 | ''' 23 | } 24 | } 25 | 26 | stage('Build') { 27 | steps { 28 | script { 29 | sh ''' 30 | docker compose build 31 | docker compose run --rm app npx prisma migrate deploy 32 | ''' 33 | } 34 | } 35 | } 36 | 37 | stage('Test') { 38 | steps { 39 | script { 40 | // sh 'docker compose run --rm app npm run test' 41 | sh 'echo simulatiion running test... && sleep 3 && echo test OK!' 42 | } 43 | } 44 | } 45 | 46 | stage('Deploy') { 47 | steps { 48 | script { 49 | sh 'docker compose up -d' 50 | } 51 | } 52 | } 53 | } 54 | 55 | post { 56 | always { 57 | script { 58 | // sh 'docker compose down' 59 | echo 'OK' 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { ValidationPipe } from '@nestjs/common'; 4 | import { ConfigService } from '@nestjs/config'; 5 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 6 | 7 | async function bootstrap() { 8 | const app = await NestFactory.create(AppModule); 9 | const configService = new ConfigService(); 10 | const PORT = configService.get('PORT') || 3000; 11 | const SWAGGER_PATH = configService.get('SWAGGER_PATH'); 12 | const config = new DocumentBuilder() 13 | .setTitle('MOvie Booking APP REST API Documentation') 14 | .setDescription('fazrul anugrah sahi backend portofolio') 15 | .setVersion('1.0') 16 | .addBearerAuth( 17 | { 18 | type: 'http', 19 | scheme: 'bearer', 20 | bearerFormat: 'JWT', 21 | name: 'JWT', 22 | description: 'Enter Jwt Token', 23 | in: 'header', 24 | }, 25 | 'JWTAUTH', 26 | ) 27 | .build(); 28 | const document = SwaggerModule.createDocument(app, config); 29 | SwaggerModule.setup('docs', app, document); 30 | 31 | app.useGlobalPipes(new ValidationPipe()); 32 | app.enableCors(); 33 | 34 | await app.listen(PORT, '0.0.0.0'); 35 | console.log( 36 | '======================================================================', 37 | ); 38 | console.log(`Server Berjalan DI : http://localhost:${PORT}/`); 39 | console.log(`Swagger Berjalan DI : http://localhost:${PORT}/${SWAGGER_PATH}`); 40 | console.log( 41 | '======================================================================', 42 | ); 43 | } 44 | bootstrap(); 45 | -------------------------------------------------------------------------------- /src/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { User } from '@prisma/client'; 3 | import { PrismaService } from 'src/prisma/prisma.service'; 4 | 5 | @Injectable() 6 | export class UserService { 7 | constructor(private readonly prisma: PrismaService) {} 8 | async getUserData(user: User) { 9 | const userData = await this.prisma.user.findUnique({ 10 | where: { 11 | id: user.id, 12 | }, 13 | include: { 14 | balance: { 15 | select: { 16 | balance: true, 17 | }, 18 | }, 19 | Tickets: { 20 | select: { 21 | id: true, 22 | bookAt: true, 23 | cancelAt: true, 24 | isCancel: true, 25 | Seats: { 26 | select: { 27 | seatNumber: true, 28 | id: true, 29 | isBook: true, 30 | Movie: { 31 | select: { 32 | title: true, 33 | }, 34 | }, 35 | }, 36 | }, 37 | }, 38 | }, 39 | }, 40 | }); 41 | 42 | const balance = userData.balance.balance.toString(); 43 | 44 | delete userData.balance.balance; 45 | delete userData.hash; 46 | return { 47 | statusCode: 200, 48 | message: `Success Fetch ${user.username} data `, 49 | data: { ...userData, balance }, 50 | }; 51 | } 52 | 53 | async getTicketsById(user: User, ticketsId: string) { 54 | const tickets = await this.prisma.tickets.findUnique({ 55 | where: { 56 | id: ticketsId, 57 | }, 58 | include: { 59 | Seats: { 60 | include: { 61 | Movie: true, 62 | }, 63 | }, 64 | User: true, 65 | }, 66 | }); 67 | 68 | delete tickets.User.hash; 69 | 70 | return { 71 | statusCode: 200, 72 | message: `Success Fetch ${user.username} data`, 73 | data: tickets, 74 | }; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/movie/movie.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NotFoundException } from '@nestjs/common'; 2 | 3 | import { PrismaService } from 'src/prisma/prisma.service'; 4 | import { SearchQueryDto } from './dto'; 5 | 6 | @Injectable() 7 | export class MovieService { 8 | constructor(private prisma: PrismaService) {} 9 | 10 | async searchMovies(query: SearchQueryDto) { 11 | const querySplit = query.title.split(' '); 12 | const modifiedString = querySplit.join(' | '); 13 | 14 | try { 15 | const movies = await this.prisma.movie.findMany({ 16 | where: { 17 | title: { 18 | search: modifiedString, 19 | }, 20 | }, 21 | }); 22 | 23 | return { 24 | statusCode: 200, 25 | message: 'Success Search Movies', 26 | data: movies, 27 | }; 28 | } catch (error) { 29 | throw error; 30 | } 31 | } 32 | 33 | async getAllMovies(page: number, limit: number) { 34 | try { 35 | const skip: number = (page - 1) * limit; 36 | const totalCount = await this.prisma.movie.count(); 37 | const movies = await this.prisma.movie.findMany({ 38 | take: limit, 39 | skip, 40 | include: { 41 | _count: true, 42 | }, 43 | }); 44 | 45 | const totalPage = Math.ceil(totalCount / limit); 46 | 47 | return { 48 | statusCode: 200, 49 | message: 'Success Fetch All Movies', 50 | page, 51 | length: movies.length, 52 | totalData: totalCount, 53 | totalPage, 54 | data: movies, 55 | }; 56 | } catch (error) { 57 | throw error; 58 | } 59 | } 60 | 61 | async getMovieById(movieId: number) { 62 | const movie = await this.prisma.movie.findFirst({ 63 | where: { 64 | id: movieId, 65 | }, 66 | }); 67 | 68 | if (!movie) throw new NotFoundException(); 69 | delete movie.id; 70 | return { 71 | statusCode: 200, 72 | message: `Success Get Movie id : ${movieId} Details`, 73 | data: movie, 74 | }; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /prisma/schema backup.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | previewFeatures = ["fullTextSearch"] 4 | } 5 | 6 | datasource db { 7 | provider = "postgresql" 8 | url = env("DATABASE_URL") 9 | } 10 | 11 | model User { 12 | id String @id @default(uuid()) 13 | name String 14 | username String @unique 15 | email String @unique 16 | birth String 17 | hash String 18 | age Int 19 | createdAt DateTime @default(now()) 20 | updatedAt DateTime @updatedAt 21 | balance Balance? 22 | seats Seats[] 23 | orders Orders[] 24 | 25 | @@map("Users") 26 | } 27 | 28 | model Balance { 29 | id String @id @default(uuid()) 30 | balance Int 31 | user User @relation(fields: [userId], references: [id]) 32 | userId String @unique 33 | 34 | createdAt DateTime @default(now()) 35 | updatedAt DateTime @updatedAt 36 | } 37 | 38 | model Movie { 39 | id Int @id @default(autoincrement()) 40 | title String 41 | description String 42 | price Int 43 | releaseDate String 44 | ageRating Int 45 | poster String 46 | seats Seats[] 47 | orders Orders[] 48 | 49 | createdAt DateTime @default(now()) 50 | updatedAt DateTime @updatedAt 51 | 52 | // @@fulltext([title]) 53 | @@map("Movies") 54 | } 55 | 56 | model Seats { 57 | id String @id @default(uuid()) 58 | seatNumber Int 59 | book Boolean 60 | Movie Movie @relation(fields: [movieId], references: [id]) 61 | movieId Int 62 | 63 | User User? @relation(fields: [userId], references: [id]) 64 | userId String? 65 | createdAt DateTime @default(now()) 66 | updatedAt DateTime @updatedAt 67 | 68 | cancelAt DateTime? 69 | } 70 | 71 | model Orders { 72 | id String @id @default(uuid()) 73 | User User? @relation(fields: [userId], references: [id]) 74 | userId String? 75 | Movie Movie? @relation(fields: [movieId], references: [id]) 76 | movieId Int? 77 | seats Int[] 78 | 79 | total Int 80 | 81 | createdAt DateTime @default(now()) 82 | updatedAt DateTime @updatedAt 83 | } 84 | -------------------------------------------------------------------------------- /prisma/schema copy.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | previewFeatures = ["fullTextSearch"] 4 | } 5 | 6 | datasource db { 7 | provider = "postgresql" 8 | url = env("DATABASE_URL") 9 | } 10 | 11 | model User { 12 | id String @id @default(uuid()) 13 | name String 14 | username String @unique 15 | email String @unique 16 | birth String 17 | hash String 18 | age Int 19 | createdAt DateTime @default(now()) 20 | updatedAt DateTime @updatedAt 21 | balance Balance? 22 | seats Seats[] 23 | orders Orders[] 24 | 25 | @@map("Users") 26 | } 27 | 28 | model Balance { 29 | id String @id @default(uuid()) 30 | balance Int 31 | user User @relation(fields: [userId], references: [id]) 32 | userId String @unique 33 | 34 | createdAt DateTime @default(now()) 35 | updatedAt DateTime @updatedAt 36 | } 37 | 38 | model Movie { 39 | id Int @id @default(autoincrement()) 40 | title String 41 | description String 42 | price Int 43 | releaseDate String 44 | ageRating Int 45 | poster String 46 | seats Seats[] 47 | orders Orders[] 48 | 49 | createdAt DateTime @default(now()) 50 | updatedAt DateTime @updatedAt 51 | 52 | @@map("Movies") 53 | } 54 | 55 | model Seats { 56 | id String @id @default(uuid()) 57 | seatNumber Int 58 | book Boolean 59 | Movie Movie @relation(fields: [movieId], references: [id]) 60 | movieId Int 61 | 62 | User User? @relation(fields: [userId], references: [id]) 63 | userId String? 64 | createdAt DateTime @default(now()) 65 | updatedAt DateTime @updatedAt 66 | 67 | bookAt DateTime? 68 | 69 | orders Orders[] 70 | 71 | cancelAt DateTime? 72 | } 73 | 74 | model Orders { 75 | id String @id @default(uuid()) 76 | User User? @relation(fields: [userId], references: [id]) 77 | userId String? 78 | Movie Movie? @relation(fields: [movieId], references: [id]) 79 | movieId Int? 80 | 81 | seats Seats[] 82 | 83 | total Int 84 | 85 | createdAt DateTime @default(now()) 86 | updatedAt DateTime @updatedAt 87 | } 88 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | previewFeatures = ["fullTextSearch"] 4 | } 5 | 6 | datasource db { 7 | provider = "postgresql" 8 | url = env("DATABASE_URL") 9 | } 10 | 11 | model User { 12 | id String @id @default(uuid()) 13 | name String 14 | username String @unique 15 | email String @unique 16 | birth String 17 | hash String 18 | age Int 19 | createdAt DateTime @default(now()) 20 | updatedAt DateTime @updatedAt 21 | balance Balance? 22 | 23 | orders Orders[] 24 | Tickets Tickets[] 25 | 26 | @@map("Users") 27 | } 28 | 29 | model Balance { 30 | id String @id @default(uuid()) 31 | balance BigInt 32 | user User @relation(fields: [userId], references: [id]) 33 | userId String @unique 34 | 35 | createdAt DateTime @default(now()) 36 | updatedAt DateTime @updatedAt 37 | } 38 | 39 | model Movie { 40 | id Int @id @default(autoincrement()) 41 | title String 42 | description String 43 | price Int 44 | releaseDate String 45 | ageRating Int 46 | poster String 47 | seats Seats[] 48 | orders Orders[] 49 | 50 | createdAt DateTime @default(now()) 51 | updatedAt DateTime @updatedAt 52 | 53 | @@map("Movies") 54 | } 55 | 56 | model Seats { 57 | id String @id @default(uuid()) 58 | seatNumber Int 59 | isBook Boolean 60 | Movie Movie @relation(fields: [movieId], references: [id]) 61 | movieId Int 62 | 63 | tickets Tickets[] 64 | } 65 | 66 | model Orders { 67 | id String @id @default(uuid()) 68 | User User? @relation(fields: [userId], references: [id]) 69 | userId String? 70 | Movie Movie? @relation(fields: [movieId], references: [id]) 71 | movieId Int? 72 | 73 | total Int 74 | 75 | ticket Tickets[] 76 | createdAt DateTime @default(now()) 77 | updatedAt DateTime @updatedAt 78 | } 79 | 80 | model Tickets { 81 | id String @id @default(uuid()) 82 | 83 | Orders Orders? @relation(fields: [ordersId], references: [id]) 84 | ordersId String? 85 | 86 | isCancel Boolean? 87 | 88 | User User? @relation(fields: [userId], references: [id]) 89 | userId String? 90 | Seats Seats? @relation(fields: [seatsId], references: [id]) 91 | seatsId String? 92 | 93 | bookAt DateTime 94 | 95 | cancelAt DateTime? 96 | } 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "movie-booking-app", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "prisma": { 9 | "seed": "ts-node prisma/seed.ts" 10 | }, 11 | "scripts": { 12 | "build": "nest build", 13 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 14 | "start": "nest start", 15 | "start:dev": "nest start --watch", 16 | "start:debug": "nest start --debug --watch", 17 | "start:prod": "node dist/main", 18 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 19 | "test": "jest", 20 | "test:watch": "jest --watch", 21 | "test:cov": "jest --coverage", 22 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 23 | "test:e2e": "jest --config ./test/jest-e2e.json", 24 | "migrate:db": "npx prisma migrate deploy" 25 | }, 26 | "dependencies": { 27 | "@nestjs/common": "^9.0.0", 28 | "@nestjs/config": "^3.0.0", 29 | "@nestjs/core": "^9.0.0", 30 | "@nestjs/jwt": "^10.1.0", 31 | "@nestjs/mapped-types": "*", 32 | "@nestjs/passport": "^10.0.0", 33 | "@nestjs/platform-express": "^9.0.0", 34 | "@nestjs/swagger": "^7.1.17", 35 | "@nestjs/throttler": "^6.1.1", 36 | "@prisma/client": "^4.16.1", 37 | "bcrypt": "^5.1.0", 38 | "class-transformer": "^0.5.1", 39 | "class-validator": "^0.14.0", 40 | "passport": "^0.6.0", 41 | "passport-jwt": "^4.0.1", 42 | "reflect-metadata": "^0.1.13", 43 | "rxjs": "^7.2.0" 44 | }, 45 | "devDependencies": { 46 | "@nestjs/cli": "^9.0.0", 47 | "@nestjs/schematics": "^9.0.0", 48 | "@nestjs/testing": "^9.0.0", 49 | "@types/bcrypt": "^5.0.0", 50 | "@types/express": "^4.17.13", 51 | "@types/jest": "29.5.1", 52 | "@types/node": "^18.16.18", 53 | "@types/passport-jwt": "^3.0.8", 54 | "@types/supertest": "^2.0.11", 55 | "@typescript-eslint/eslint-plugin": "^5.0.0", 56 | "@typescript-eslint/parser": "^5.0.0", 57 | "eslint": "^8.0.1", 58 | "eslint-config-prettier": "^8.3.0", 59 | "eslint-plugin-prettier": "^4.0.0", 60 | "jest": "29.5.0", 61 | "prettier": "^2.3.2", 62 | "prisma": "^4.16.1", 63 | "source-map-support": "^0.5.20", 64 | "supertest": "^6.1.3", 65 | "ts-jest": "29.1.0", 66 | "ts-loader": "^9.2.3", 67 | "ts-node": "^10.9.1", 68 | "tsconfig-paths": "4.2.0", 69 | "typescript": "^5.1.6" 70 | }, 71 | "jest": { 72 | "moduleFileExtensions": [ 73 | "js", 74 | "json", 75 | "ts" 76 | ], 77 | "rootDir": "src", 78 | "testRegex": ".*\\.spec\\.ts$", 79 | "transform": { 80 | "^.+\\.(t|j)s$": "ts-jest" 81 | }, 82 | "collectCoverageFrom": [ 83 | "**/*.(t|j)s" 84 | ], 85 | "coverageDirectory": "../coverage", 86 | "testEnvironment": "node" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { ForbiddenException, Injectable } from '@nestjs/common'; 2 | import { PrismaService } from 'src/prisma/prisma.service'; 3 | import { CreateUserDto, UserLoginDto } from './dto'; 4 | import { Prisma } from '@prisma/client'; 5 | import * as bcrypt from 'bcrypt'; 6 | import { ConfigService } from '@nestjs/config'; 7 | import { JwtService } from '@nestjs/jwt'; 8 | 9 | @Injectable() 10 | export class AuthService { 11 | constructor( 12 | private readonly prisma: PrismaService, 13 | private readonly config: ConfigService, 14 | private readonly jwt: JwtService, 15 | ) {} 16 | async createUser(dto: CreateUserDto) { 17 | const saltOrRounds = 10; 18 | const hash = await bcrypt.hash(dto.hash, saltOrRounds); 19 | const age = this.getAge(dto.birth); 20 | 21 | try { 22 | const user = await this.prisma.user.create({ 23 | data: { 24 | name: dto.name, 25 | username: dto.username, 26 | email: dto.email, 27 | birth: dto.birth, 28 | age, 29 | hash, 30 | balance: { 31 | create: { 32 | balance: BigInt(0), 33 | }, 34 | }, 35 | }, 36 | }); 37 | 38 | delete user.hash; 39 | return { 40 | statusCode: 201, 41 | message: 'User Created', 42 | data: user, 43 | }; 44 | } catch (e) { 45 | if (e instanceof Prisma.PrismaClientKnownRequestError) { 46 | // The .code property can be accessed in a type-safe manner 47 | if (e.code === 'P2002') { 48 | throw new ForbiddenException( 49 | 'There is a unique constraint violation, a new user cannot be created with this email or this username', 50 | ); 51 | } 52 | } 53 | throw e; 54 | } 55 | } 56 | 57 | async userLogin(dto: UserLoginDto) { 58 | const user = await this.prisma.user.findUnique({ 59 | where: { 60 | username: dto.username, 61 | }, 62 | }); 63 | 64 | try { 65 | if (!user) throw new ForbiddenException('Account Not Found!'); 66 | const pwMatches = await bcrypt.compare(dto.hash, user.hash); 67 | if (!pwMatches) throw new ForbiddenException('Account Not Found!'); 68 | delete user.hash; 69 | const token = await this.signToken(user.id, user.email); 70 | return { 71 | statusCode: 200, 72 | message: 'Log In!', 73 | data: user, 74 | token, 75 | }; 76 | } catch (error) { 77 | throw error; 78 | } 79 | } 80 | 81 | getAge(dateString) { 82 | var today = new Date(); 83 | var birthDate = new Date(dateString); 84 | var age = today.getFullYear() - birthDate.getFullYear(); 85 | var m = today.getMonth() - birthDate.getMonth(); 86 | if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) { 87 | age--; 88 | } 89 | return age; 90 | } 91 | 92 | async signToken(id: string, email: string): Promise { 93 | const payload = { sub: id, email }; 94 | return this.jwt.signAsync(payload, { 95 | expiresIn: '1d', 96 | secret: this.config.get('SECRET_TOKEN'), 97 | }); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /prisma/migrations/20240821145045_first/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Users" ( 3 | "id" TEXT NOT NULL, 4 | "name" TEXT NOT NULL, 5 | "username" TEXT NOT NULL, 6 | "email" TEXT NOT NULL, 7 | "birth" TEXT NOT NULL, 8 | "hash" TEXT NOT NULL, 9 | "age" INTEGER NOT NULL, 10 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 11 | "updatedAt" TIMESTAMP(3) NOT NULL, 12 | 13 | CONSTRAINT "Users_pkey" PRIMARY KEY ("id") 14 | ); 15 | 16 | -- CreateTable 17 | CREATE TABLE "Balance" ( 18 | "id" TEXT NOT NULL, 19 | "balance" BIGINT NOT NULL, 20 | "userId" TEXT NOT NULL, 21 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 22 | "updatedAt" TIMESTAMP(3) NOT NULL, 23 | 24 | CONSTRAINT "Balance_pkey" PRIMARY KEY ("id") 25 | ); 26 | 27 | -- CreateTable 28 | CREATE TABLE "Movies" ( 29 | "id" SERIAL NOT NULL, 30 | "title" TEXT NOT NULL, 31 | "description" TEXT NOT NULL, 32 | "price" INTEGER NOT NULL, 33 | "releaseDate" TEXT NOT NULL, 34 | "ageRating" INTEGER NOT NULL, 35 | "poster" TEXT NOT NULL, 36 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 37 | "updatedAt" TIMESTAMP(3) NOT NULL, 38 | 39 | CONSTRAINT "Movies_pkey" PRIMARY KEY ("id") 40 | ); 41 | 42 | -- CreateTable 43 | CREATE TABLE "Seats" ( 44 | "id" TEXT NOT NULL, 45 | "seatNumber" INTEGER NOT NULL, 46 | "isBook" BOOLEAN NOT NULL, 47 | "movieId" INTEGER NOT NULL, 48 | 49 | CONSTRAINT "Seats_pkey" PRIMARY KEY ("id") 50 | ); 51 | 52 | -- CreateTable 53 | CREATE TABLE "Orders" ( 54 | "id" TEXT NOT NULL, 55 | "userId" TEXT, 56 | "movieId" INTEGER, 57 | "total" INTEGER NOT NULL, 58 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 59 | "updatedAt" TIMESTAMP(3) NOT NULL, 60 | 61 | CONSTRAINT "Orders_pkey" PRIMARY KEY ("id") 62 | ); 63 | 64 | -- CreateTable 65 | CREATE TABLE "Tickets" ( 66 | "id" TEXT NOT NULL, 67 | "ordersId" TEXT, 68 | "isCancel" BOOLEAN, 69 | "userId" TEXT, 70 | "seatsId" TEXT, 71 | "bookAt" TIMESTAMP(3) NOT NULL, 72 | "cancelAt" TIMESTAMP(3), 73 | 74 | CONSTRAINT "Tickets_pkey" PRIMARY KEY ("id") 75 | ); 76 | 77 | -- CreateIndex 78 | CREATE UNIQUE INDEX "Users_username_key" ON "Users"("username"); 79 | 80 | -- CreateIndex 81 | CREATE UNIQUE INDEX "Users_email_key" ON "Users"("email"); 82 | 83 | -- CreateIndex 84 | CREATE UNIQUE INDEX "Balance_userId_key" ON "Balance"("userId"); 85 | 86 | -- AddForeignKey 87 | ALTER TABLE "Balance" ADD CONSTRAINT "Balance_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 88 | 89 | -- AddForeignKey 90 | ALTER TABLE "Seats" ADD CONSTRAINT "Seats_movieId_fkey" FOREIGN KEY ("movieId") REFERENCES "Movies"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 91 | 92 | -- AddForeignKey 93 | ALTER TABLE "Orders" ADD CONSTRAINT "Orders_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Users"("id") ON DELETE SET NULL ON UPDATE CASCADE; 94 | 95 | -- AddForeignKey 96 | ALTER TABLE "Orders" ADD CONSTRAINT "Orders_movieId_fkey" FOREIGN KEY ("movieId") REFERENCES "Movies"("id") ON DELETE SET NULL ON UPDATE CASCADE; 97 | 98 | -- AddForeignKey 99 | ALTER TABLE "Tickets" ADD CONSTRAINT "Tickets_ordersId_fkey" FOREIGN KEY ("ordersId") REFERENCES "Orders"("id") ON DELETE SET NULL ON UPDATE CASCADE; 100 | 101 | -- AddForeignKey 102 | ALTER TABLE "Tickets" ADD CONSTRAINT "Tickets_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Users"("id") ON DELETE SET NULL ON UPDATE CASCADE; 103 | 104 | -- AddForeignKey 105 | ALTER TABLE "Tickets" ADD CONSTRAINT "Tickets_seatsId_fkey" FOREIGN KEY ("seatsId") REFERENCES "Seats"("id") ON DELETE SET NULL ON UPDATE CASCADE; 106 | -------------------------------------------------------------------------------- /src/orders/orders.service.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, Injectable } from '@nestjs/common'; 2 | import { Balance, User } from '@prisma/client'; 3 | import { PrismaService } from 'src/prisma/prisma.service'; 4 | import { cancelOrderDto } from './dto'; 5 | 6 | @Injectable() 7 | export class OrdersService { 8 | constructor(private readonly prisma: PrismaService) {} 9 | 10 | async getUserOrders(user: User) { 11 | try { 12 | const orders = await this.prisma.orders.findMany({ 13 | where: { 14 | userId: user.id, 15 | }, 16 | include: { 17 | User: { 18 | select: { 19 | username: true, 20 | name: true, 21 | }, 22 | }, 23 | ticket: { 24 | select: { 25 | Seats: true, 26 | isCancel: true, 27 | cancelAt: true, 28 | }, 29 | }, 30 | Movie: { 31 | select: { 32 | title: true, 33 | }, 34 | }, 35 | }, 36 | }); 37 | 38 | return { 39 | statusCode: 200, 40 | message: `Success Get All Of ${user.id} Order History `, 41 | length: orders.length, 42 | data: orders, 43 | }; 44 | } catch (error) { 45 | throw error; 46 | } 47 | } 48 | 49 | async cancelOrder(user: User, ticketsId: string[]) { 50 | try { 51 | const userData = await this.prisma.user.findUnique({ 52 | where: { 53 | id: user.id, 54 | }, 55 | select: { 56 | balance: { 57 | select: { 58 | balance: true, 59 | }, 60 | }, 61 | orders: { 62 | select: { 63 | ticket: { 64 | where: { 65 | id: { 66 | in: ticketsId, 67 | }, 68 | }, 69 | }, 70 | Movie: { 71 | select: { 72 | price: true, 73 | }, 74 | }, 75 | }, 76 | }, 77 | }, 78 | }); 79 | 80 | const seatsId = userData.orders.flatMap((seat) => { 81 | return seat.ticket.map((seat) => seat.seatsId); 82 | }); 83 | 84 | const seatsPrice = userData.orders.flatMap((seat) => { 85 | return seat.Movie.price; 86 | }); 87 | 88 | const updateTickets = await this.prisma.tickets.updateMany({ 89 | where: { 90 | id: { 91 | in: ticketsId, 92 | }, 93 | }, 94 | data: { 95 | isCancel: true, 96 | cancelAt: new Date(), 97 | }, 98 | }); 99 | 100 | const updateSeats = await this.prisma.seats.updateMany({ 101 | where: { 102 | id: { 103 | in: seatsId, 104 | }, 105 | }, 106 | data: { 107 | isBook: false, 108 | }, 109 | }); 110 | const moviePrice = seatsPrice[0] * ticketsId.length; 111 | const userBalance = userData.balance.balance; 112 | 113 | const updateBalance = userBalance + BigInt(moviePrice); 114 | 115 | const updateUserBalance = await this.prisma.balance.update({ 116 | where: { 117 | userId: user.id, 118 | }, 119 | data: { 120 | balance: updateBalance, 121 | }, 122 | }); 123 | 124 | const balance = updateUserBalance.balance.toString(); 125 | delete updateUserBalance.balance; 126 | return { 127 | statusCode: 201, 128 | message: `Success Cancel Seats`, 129 | data: { ...updateUserBalance, balance }, 130 | }; 131 | } catch (error) { 132 | return error; 133 | } 134 | } 135 | 136 | async getOrderById(user: User, orderId: string) { 137 | try { 138 | const order = await this.prisma.orders.findUnique({ 139 | where: { 140 | id: orderId, 141 | }, 142 | include: { 143 | User: true, 144 | Movie: true, 145 | ticket: { 146 | select: { 147 | Seats: true, 148 | isCancel: true, 149 | cancelAt: true, 150 | id: true, 151 | }, 152 | }, 153 | }, 154 | }); 155 | 156 | return { 157 | statusCode: 200, 158 | message: `Success Get ${user.name} order ${orderId} details`, 159 | data: order, 160 | }; 161 | } catch (error) { 162 | throw error; 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/balance/balance.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | ForbiddenException, 4 | Injectable, 5 | } from '@nestjs/common'; 6 | import { PrismaService } from 'src/prisma/prisma.service'; 7 | import { BalanceDto, BalanceWithdrawalDto } from './dto'; 8 | import { User } from '@prisma/client'; 9 | 10 | @Injectable() 11 | export class BalanceService { 12 | constructor(private readonly prisma: PrismaService) {} 13 | 14 | async getBalance(user: User) { 15 | try { 16 | const userBalance = await this.prisma.balance.findUnique({ 17 | where: { 18 | userId: user.id, 19 | }, 20 | include: { 21 | user: { 22 | select: { 23 | username: true, 24 | }, 25 | }, 26 | }, 27 | }); 28 | 29 | const balanceInRp = this.toRupiah(userBalance.balance); 30 | const balance = userBalance.balance.toString(); 31 | 32 | return { 33 | statusCode: 200, 34 | message: 'Success Get User Balance', 35 | balance: balanceInRp, 36 | data: { ...userBalance, balance }, 37 | }; 38 | } catch (error) { 39 | return error; 40 | } 41 | } 42 | 43 | async addBalance(dto: BalanceDto, user: User) { 44 | try { 45 | const userBalance = await this.prisma.balance.findUnique({ 46 | where: { 47 | userId: user.id, 48 | }, 49 | }); 50 | 51 | const updateBalance = await this.prisma.balance.update({ 52 | where: { 53 | userId: user.id, 54 | }, 55 | data: { 56 | balance: userBalance.balance + BigInt(dto.balance), 57 | }, 58 | select: { 59 | balance: true, 60 | user: { 61 | select: { 62 | username: true, 63 | }, 64 | }, 65 | }, 66 | }); 67 | 68 | const balance = this.balanceToString(updateBalance.balance); 69 | 70 | const rupiah = this.toRupiah(dto.balance); 71 | const currentBalance = this.toRupiah(updateBalance.balance); 72 | 73 | return { 74 | statusCode: 201, 75 | message: `Success Add ${rupiah} to your balance!`, 76 | addedBalance: rupiah, 77 | currentBalance, 78 | data: { ...updateBalance, balance }, 79 | }; 80 | } catch (error) { 81 | throw error; 82 | } 83 | } 84 | 85 | async balanceWithdrawal(dto, user) { 86 | const balance = await this.prisma.balance.findUnique({ 87 | where: { 88 | userId: user.id, 89 | }, 90 | }); 91 | const currentBalance = balance.balance; 92 | 93 | const toWithdrawn = BigInt(dto.withdrawal); 94 | 95 | if (currentBalance - toWithdrawn < 0) { 96 | throw new BadRequestException('insufficient balance'); 97 | } 98 | 99 | const maximumWithdrawal = this.calculateMaximumWithdrawal(currentBalance); 100 | 101 | let withdrawalAmount = toWithdrawn; 102 | 103 | let messageWarning; 104 | if (toWithdrawn > maximumWithdrawal) { 105 | withdrawalAmount = BigInt(500000); 106 | messageWarning = 107 | 'The maximum amount that can be withdrawn is Rp.500.000,00 for each withdrawal.'; 108 | } 109 | 110 | const updatedBalance = currentBalance - withdrawalAmount; 111 | if (updatedBalance < 0) { 112 | throw new BadRequestException('insufficient balance'); 113 | } 114 | 115 | try { 116 | const balanceWithdrawal = await this.prisma.balance.update({ 117 | where: { 118 | userId: user.id, 119 | }, 120 | data: { 121 | balance: updatedBalance, 122 | }, 123 | include: { 124 | user: { 125 | select: { 126 | username: true, 127 | }, 128 | }, 129 | }, 130 | }); 131 | 132 | const balance = this.balanceToString(balanceWithdrawal.balance); 133 | delete balanceWithdrawal.balance; 134 | const withdrawalAmountInRp = this.toRupiah(withdrawalAmount); 135 | 136 | return { 137 | statusCode: 201, 138 | message: `Successfully Withdraw Balance by amount ${withdrawalAmountInRp}`, 139 | messageWarning, 140 | data: { ...balanceWithdrawal, balance }, 141 | }; 142 | } catch (error) { 143 | throw error; 144 | } 145 | } 146 | 147 | toRupiah(number: bigint) { 148 | return new Intl.NumberFormat('id-ID', { 149 | style: 'currency', 150 | currency: 'IDR', 151 | }).format(number); 152 | } 153 | 154 | calculateMaximumWithdrawal(currentBalance: bigint): bigint { 155 | const maximumWithdrawal = BigInt(Math.min(Number(currentBalance), 500000)); 156 | return maximumWithdrawal; 157 | } 158 | 159 | balanceToString(balance: bigint) { 160 | return balance.toString(); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/ticket/ticket.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | HttpException, 4 | HttpStatus, 5 | Injectable, 6 | } from '@nestjs/common'; 7 | import { User } from '@prisma/client'; 8 | import { PrismaService } from 'src/prisma/prisma.service'; 9 | import { TicketDto } from './dto'; 10 | 11 | @Injectable() 12 | export class TicketService { 13 | constructor(private readonly prisma: PrismaService) {} 14 | 15 | async getSeat(movieId: number) { 16 | const seats = await this.prisma.movie.findUnique({ 17 | where: { 18 | id: movieId, 19 | }, 20 | include: { 21 | seats: { 22 | orderBy: { 23 | seatNumber: 'asc', 24 | }, 25 | }, 26 | }, 27 | }); 28 | 29 | return { 30 | statusCode: 200, 31 | message: `Success Get MovieId ${movieId} seats`, 32 | data: seats, 33 | }; 34 | } 35 | 36 | async bookSeats(user: User, movieId: number, seats: number[]) { 37 | const userData = await this.prisma.user.findUnique({ 38 | where: { 39 | id: user.id, 40 | }, 41 | select: { 42 | id: true, 43 | age: true, 44 | balance: true, 45 | }, 46 | }); 47 | 48 | const movieData = await this.prisma.movie.findUnique({ 49 | where: { 50 | id: movieId, 51 | }, 52 | include: { 53 | seats: true, 54 | }, 55 | }); 56 | 57 | const userBalance: bigint = userData.balance.balance; 58 | const userAge = userData.age; 59 | 60 | const moviePrice = movieData.price; 61 | const movieRating = movieData.ageRating; 62 | 63 | const totalMoviePrice = moviePrice * seats.length; 64 | 65 | if (userAge < movieRating) { 66 | throw new HttpException( 67 | { 68 | statusCode: 400, 69 | message: 'Failed to book seats. Age requirement not met', 70 | }, 71 | HttpStatus.BAD_REQUEST, 72 | ); 73 | } 74 | 75 | if (userBalance < totalMoviePrice) { 76 | throw new HttpException( 77 | { 78 | statusCode: 400, 79 | message: 'Failed to book seats. Insufficient balance', 80 | }, 81 | HttpStatus.BAD_REQUEST, 82 | ); 83 | } 84 | 85 | const updateBalance = userBalance - BigInt(totalMoviePrice); 86 | 87 | try { 88 | const updateSeats = []; 89 | for (const seat of seats) { 90 | let seatMovie = movieData.seats.find( 91 | (movieSeat) => movieSeat.seatNumber === seat, 92 | ); 93 | if (!seatMovie) { 94 | const seatMovie = await this.prisma.seats.create({ 95 | data: { 96 | isBook: true, 97 | seatNumber: seat, 98 | movieId, 99 | }, 100 | select: { 101 | id: true, 102 | }, 103 | }); 104 | updateSeats.push(seatMovie); 105 | continue; 106 | } 107 | const updateSeatMovie = await this.prisma.seats.update({ 108 | where: { 109 | id: seatMovie.id, 110 | }, 111 | data: { 112 | isBook: true, 113 | seatNumber: seat, 114 | movieId, 115 | }, 116 | select: { 117 | id: true, 118 | }, 119 | }); 120 | updateSeats.push(updateSeatMovie); 121 | } 122 | 123 | const updateUserBalance = await this.prisma.balance.update({ 124 | where: { 125 | userId: user.id, 126 | }, 127 | data: { 128 | balance: updateBalance, 129 | }, 130 | }); 131 | 132 | const createOrder = await this.prisma.orders.create({ 133 | data: { 134 | total: totalMoviePrice, 135 | userId: user.id, 136 | movieId, 137 | ticket: { 138 | create: updateSeats.map((seat) => ({ 139 | isCancel: false, 140 | seatsId: seat.id, 141 | userId: user.id, 142 | bookAt: new Date(), 143 | })), 144 | }, 145 | }, 146 | include: { 147 | User: { 148 | select: { 149 | username: true, 150 | name: true, 151 | }, 152 | }, 153 | ticket: { 154 | select: { 155 | Seats: true, 156 | }, 157 | }, 158 | Movie: { 159 | select: { 160 | title: true, 161 | }, 162 | }, 163 | }, 164 | }); 165 | 166 | return { 167 | statusCode: 201, 168 | message: `Success Book Seats Number ${seats} `, 169 | total: totalMoviePrice, 170 | data: createOrder, 171 | }; 172 | } catch (error) { 173 | throw error; 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | This is a repository for the task given by Compfest as a requirement to join the Software Engineer Academy. 28 | 29 | The Application is Online On [Vercel](https://seacinema.vercel.app/) 30 | 31 | This REST API Application use Nest Js, Posgtree SQL, and deploy into [Railway](https://movie-booking-app-production.up.railway.app/api/v1/movies?page=1&limit=10). Click to see the REST API 32 | 33 | ## Installation 34 | 35 | 1. Clone this repository 36 | 2. Install Dependencies 37 | 38 | ```bash 39 | $ npm install 40 | ``` 41 | 42 | ## Configure The database 43 | 44 | This App use docker compose to configure the Postgree SQL database. 45 | configure the docker compose : 46 | 47 | rename the docker-compose copy.yml to docker-compose.yml 48 | 49 | ```bash 50 | 51 | version: '3.8' 52 | services: 53 | dev-db: 54 | image: postgres:alpine3.18 55 | ports: 56 | - 5434:5432 57 | environment: 58 | POSTGRES_USER: yourhostname 59 | POSTGRES_PASSWORD: yourpassword 60 | POSTGRES_DB: yourdbname 61 | 62 | # change the value to your configuration 63 | 64 | ``` 65 | 66 | ## Configure the .env file 67 | 68 | rename the .env copy to .env 69 | 70 | ```bash 71 | example .env file : 72 | - DATABASE_URL="postgresql://yourhostname:yourpassword@localhost:5434/yourdbname?schema=public" 73 | - JWT_SECRET="yoursecretjwttoken" 74 | - SECRET_TOKEN="yoursecretjwttoken" 75 | - PORT =3000 # default 76 | 77 | #NOTE : if you use the docker compose , please make sure the value you use in the .env DATABASE_URL same with value you use in docker-compose.yml 78 | ``` 79 | 80 | ## Build the database 81 | 82 | ```bash 83 | # generate prisma 84 | $ npx prisma generate 85 | 86 | # push the schema 87 | $ npx prisma db push 88 | 89 | # seed the database 90 | $ npx prisma db seed 91 | ``` 92 | 93 | ## Running the app 94 | 95 | ```bash 96 | # development 97 | $ npm run start 98 | 99 | # watch mode 100 | $ npm run start:dev 101 | 102 | # production mode 103 | $ npm run start:prod 104 | ``` 105 | 106 | #### Run localhost:3000/api/v1/movies?page=1&limit=10 in your browser 107 | 108 | ## Rest Api Documentation 109 | 110 | you can see the endpoint documentation in here 111 | 112 | [LINK DOCUMENTATION](https://documenter.getpostman.com/view/21962409/2s946e9tW4#sea-cinema-rest-api) 113 | 114 | ## ERD 115 | 116 | ![Alt text](erd.png 'ERD') 117 | 118 | ## Features 119 | 120 | - Login 121 | - Register 122 | - Movie list 123 | - Movie detail 124 | - Ticket booking 125 | - Transaction detail 126 | - Show tickets in transaction detail 127 | - Top up balance 128 | - Withdraw balance 129 | - Search movie by title 130 | - Transaction history 131 | - Cancel order/transaction 132 | - Cancel ticket 133 | 134 | ## Stay in touch 135 | 136 | - Author - [Fazrul Anugrah Sahi](https://instagram.com/fzrsahi) 137 | 138 | ## Support 139 | 140 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 141 | 142 | ## License 143 | 144 | Nest is [MIT licensed](LICENSE). 145 | -------------------------------------------------------------------------------- /_Movie__202306282204.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO public."Movie" (title,description,price,"releaseDate","ageRating",poster) VALUES 2 | ('Fast X','Dom Toretto dan keluarganya menjadi sasaran putra gembong narkoba Hernan Reyes yang pendendam.',53000,'2023-05-17',15,'https://image.tmdb.org/t/p/w500/fiVW06jE7z9YnO4trhaMEdclSiC.jpg'), 3 | ('John Wick: Chapter 4','ohn Wick mengungkap jalan untuk mengalahkan The High Table. Tapi sebelum dia bisa mendapatkan kebebasannya, Wick harus berhadapan dengan musuh baru dengan aliansi kuat di seluruh dunia dan kekuatan yang mengubah teman lama menjadi musuh.',60000,'2023-03-22',10,'https://image.tmdb.org/t/p/w500/vZloFAK7NmvMGKE7VkF5UHaz0I.jpg'), 4 | ('The Super Mario Bros. Movie','Ketika sedang bekerja di bawah tanah untuk memperbaiki pipa air, Mario dan Luigi, yang merupakan tukang ledeng dari Brooklyn, tiba-tiba terhisap ke dalam pipa misterius dan masuk ke dunia yang sangat berbeda. Mereka berada di tempat yang ajaib dan aneh. Tapi sayangnya, mereka terpisah satu sama lain. Mario memulai petualangan besar untuk mencari dan menemukan Luigi.',49000,'2023-04-05',14,'https://image.tmdb.org/t/p/w500/qNBAXBIQlnOThrVvA6mA2B5ggV6.jpg'), 5 | ('Avatar: The Way of Water','Jake Sully tinggal bersama keluarga barunya di planet Pandora. Setelah ancaman kembali datang, Jake harus bekerja dengan Neytiri dan pasukan ras Na''vi untuk melindungi planet mereka.',53000,'2022-12-14',12,'https://image.tmdb.org/t/p/w500/t6HIqrRAclMCA60NsSmeqe9RmNV.jpg'), 6 | ('Guardians of the Galaxy Vol. 3','Peter Quill masih trauma karena kehilangan Gamora. Ia perlu mengumpulkan timnya untuk melindungi alam semesta dan salah satu anggota mereka. Jika mereka gagal, Guardian akan berakhir.',41000,'2023-05-03',12,'https://image.tmdb.org/t/p/w500/nAbpLidFdbbi3efFQKMPQJkaZ1r.jpg'), 7 | ('Ant-Man and the Wasp: Quantumania','Scott Lang dan Hope van Dyne adalah pasangan pahlawan super. Mereka pergi bersama orang tua Hope, Janet van Dyne dan Hank Pym, serta anak perempuan Scott, Cassie Lang, untuk menjelajahi Alam Kuantum. Di sana, mereka bertemu dengan makhluk-makhluk aneh dan memulai petualangan yang tak terduga. Petualangan ini akan menguji batas-batas mereka.',51000,'2023-02-15',12,'https://image.tmdb.org/t/p/w500/g0OWGM7HoIt866Lu7yKohYO31NU.jpg'), 8 | ('The Pope''s Exorcist','Pastor Gabriele Amorth, yang memimpin tim pengusir setan di Vatikan, menginvestigasi kasus kekerasan roh jahat yang menghantui seorang anak laki-laki. Dalam penyelidikannya, ia secara tak terduga menemukan rahasia tua yang disembunyikan oleh Vatikan selama berabad-abad.',51000,'2023-04-05',13,'https://image.tmdb.org/t/p/w500/gNPqcv1tAifbN7PRNgqpzY8sEJZ.jpg'), 9 | ('To Catch a Killer','Baltimore. Malam tahun baru. Seorang petugas polisi yang berbakat tetapi bermasalah (Shailene Woodley) direkrut oleh kepala penyelidik FBI (Ben Mendelsohn) untuk membantu membuat profil dan melacak individu yang terganggu yang meneror kota.',47000,'2023-04-06',15,'https://image.tmdb.org/t/p/w500/mFp3l4lZg1NSEsyxKrdi0rNK8r1.jpg'), 10 | ('Transformers: Age of Extinction','Lima tahun setelah Chicago dihancurkan, manusia berbalik melawan robot. Namun seorang ayah tunggal dan penemu membangkitkan robot yang dapat menyelamatkan dunia.',54000,'2014-06-25',11,'https://image.tmdb.org/t/p/w500/jyzrfx2WaeY60kYZpPYepSjGz4S.jpg'), 11 | ('Puss in Boots: The Last Wish','Puss in Boots menemukan fakta bahwa kecintaannya pada petualangan telah merenggut nyawanya: dia telah menghabiskan delapan dari sembilan nyawanya. Puss kini memulai petualangan epik untuk menemukan harapan terakhir untuk memulihkan sembilan nyawanya.',51000,'2022-12-07',11,'https://image.tmdb.org/t/p/w500/kuf6dutpsT0vSVehic3EZIqkOBt.jpg'); 12 | INSERT INTO public."Movie" (title,description,price,"releaseDate","ageRating",poster) VALUES 13 | ('Scream VI','Setelah pembunuhan terbaru oleh Ghostface, keempat orang yang selamat pergi dari Woodsboro dan memulai hidup baru.',36000,'2023-03-08',12,'https://image.tmdb.org/t/p/w500/wDWwtvkRRlgTiUr6TyLSMX8FCuZ.jpg'), 14 | ('Black Adam','Hampir 5.000 tahun setelah dia dianugerahi kekuatan maha kuasa para dewa Mesir—dan dipenjara dengan cepat—Black Adam dibebaskan dari makam duniawinya, siap untuk melepaskan bentuk keadilannya yang unik di dunia modern.',42000,'2022-10-19',10,'https://image.tmdb.org/t/p/w500/A5imhXiFF3AL9RRA4FBzNDFmfgW.jpg'), 15 | ('Dungeons & Dragons: Honor Among Thieves','Seorang pencuri menawan dan sekelompok petualang yang unik melakukan pencurian besar-besaran untuk mencuri relik yang hilang. Namun, segalanya menjadi kacau ketika mereka berjumpa dengan orang yang salah.',38000,'2023-03-23',12,'https://image.tmdb.org/t/p/w500/A7AoNT06aRAc4SV89Dwxj3EYAgC.jpg'), 16 | ('Peter Pan & Wendy','Wendy Darling adalah seorang gadis kecil yang takut pergi dari rumah masa kecilnya. Suatu hari, dia bertemu dengan Peter Pan, seorang anak laki-laki yang tidak mau tumbuh dewasa. Mereka bersama saudara-saudaranya dan peri kecil bernama Tinker Bell pergi ke dunia ajaib yang disebut Neverland. Di sana, mereka menghadapi Kapten Hook, seorang bajak laut jahat, dan mengalami petualangan seru yang akan mengubah hidup Wendy selamanya.',35000,'2023-04-20',13,'https://image.tmdb.org/t/p/w500/9NXAlFEE7WDssbXSMgdacsUD58Y.jpg'), 17 | ('Spider-Man: No Way Home','Peter Parker menghadapi masalah besar. Hal ini terjadi setelah identitasnya sebagai Spiderman terungkap. Dengan kepergian Tony Stark, Peter Parker pun harus meminta bantuan Doctor Strange agar semua orang bisa melupakan identitasnya sebagai manusia laba-laba.',56000,'2021-12-15',15,'https://image.tmdb.org/t/p/w500/uJYYizSuA9Y3DCs0qS4qWvHfZg4.jpg'), 18 | ('Black Panther: Wakanda Forever','Rakyat Wakanda kali ini akan berjuang untuk melindungi negerinya dari campur tangan kekuatan dunia setelah kematian sang Raja T''Challa.',39000,'2022-11-09',13,'https://image.tmdb.org/t/p/w500/sv1xJUazXeYqALzczSZ3O6nkH75.jpg'), 19 | ('Transformers: The Last Knight','Di tengah ketidakhadiran Optimus Prime, umat manusia berperang melawanTransformers untuk mempertahankan eksistensinya. Cade Yeager membentuk kerjasama dengan Bumblebee, seorang bangsawan Inggris dan seorang professor dari Oxford untuk mempelajari mengapa Transformers selalu kembali ke planet bumi.',52000,'2017-06-16',12,'https://image.tmdb.org/t/p/w500/s5HQf2Gb3lIO2cRcFwNL9sn1o1o.jpg'), 20 | ('Renfield','Setelah bertahun-tahun sebagai hamba Dracula yang merasa jenuh dan lelah, Renfield menemukan harapan baru dalam hidupnya. Dia jatuh cinta pada Rebecca Quincy, seorang polisi lalu lintas yang energik dan sering marah. Kesempatan ini bisa menjadi penebusan baginya.',51000,'2023-04-07',14,'https://image.tmdb.org/t/p/w500/2OaprROMZZeiWsydjGUIkXrv2Z3.jpg'), 21 | ('Cocaine Bear','Sekelompok polisi, penjahat, turis, dan remaja eksentrik berkumpul di hutan Georgia tempat beruang hitam besar mengamuk setelah menelan kokain secara tidak sengaja.',53000,'2023-02-22',12,'https://image.tmdb.org/t/p/w500/gOnmaxHo0412UVr1QM5Nekv1xPi.jpg'), 22 | ('Prey','Di Comanche Nation pada tahun 1717, seorang pejuang yang ganas dan sangat terampil bernama Naru mengetahui bahwa mangsa yang dia intai adalah alien yang sangat berkembang dengan persenjataan berteknologi maju.',42000,'2022-08-02',10,'https://image.tmdb.org/t/p/w500/ujr5pztc1oitbe7ViMUOilFaJ7s.jpg'); 23 | INSERT INTO public."Movie" (title,description,price,"releaseDate","ageRating",poster) VALUES 24 | ('Fall','Untuk sahabat Becky dan Hunter, hidup adalah tentang menaklukkan ketakutan dan mendorong batas. Tetapi setelah mereka mendaki 2.000 kaki ke puncak menara radio terpencil yang ditinggalkan, mereka menemukan diri mereka terdampar tanpa jalan turun. Sekarang keterampilan panjat ahli Becky dan Hunter akan diuji saat mereka mati-matian berjuang untuk bertahan hidup dari unsur-unsur, kurangnya persediaan, dan ketinggian yang menyebabkan vertigo.',39000,'2022-08-11',11,'https://image.tmdb.org/t/p/w500/v28T5F1IygM8vXWZIycfNEm3xcL.jpg'), 25 | ('Avatar','Pada abad ke-22, seorang Marinir lumpuh dikirim ke Pandora bulan pada misi yang unik, tetapi menjadi terpecah antara mengikuti perintah dan melindungi peradaban alien.',37000,'2009-12-15',13,'https://image.tmdb.org/t/p/w500/kyeqWdyUXW608qlYkRqosgbbJyK.jpg'), 26 | ('Split','Ketika ketiga gadis remaja sedang menunggu ayah mereka di dalam mobil, seorang pria misterius menculik dan membawa mereka ke dalam sebuah bunker. Sang penculik yang bernama Kevin (James McAvoy) adalah seorang pria dengan gangguan jiwa yang membuatnya memiliki 23 kepribadian yang berbeda, yang diantaranya adalah seorang wanita dan anak berumur 9 tahun yang bernama Hedwig. Sebagai salah satu gadis yang diculik, Casey berusaha meloloskan diri dengan meyakinkan salah satu kepribadian Kevin untuk melepaskan mereka. Akan tetapi hal tersebut tidaklah mudah, terlebih setelah Hedwig memperingatkan mereka akan the Beast yang merupakan kepribadian Kevin yang paling berbahaya.',45000,'2017-01-19',10,'https://image.tmdb.org/t/p/w500/lli31lYTFpvxVBeFHWoe5PMfW5s.jpg'), 27 | ('Top Gun: Maverick','Setelah lebih dari tiga puluh tahun mengabdi sebagai salah satu penerbang top Angkatan Laut, dan menghindari kenaikan pangkat yang akan menjatuhkannya, Pete "Maverick" Mitchell mendapati dirinya melatih satu detasemen lulusan TOP GUN untuk misi khusus yang tidak ada kehidupan. pilot pernah melihat.',57000,'2022-05-24',14,'https://image.tmdb.org/t/p/w500/jeGvNOVMs5QIU1VaoGvnd3gSv0G.jpg'), 28 | ('Thor: Love and Thunder','"Thor: Love and Thunder"menceritakan Thor (Chris Hemsworth) dalam sebuah perjalanan yang belum pernah ia jalani – pencariankedamaian batin. Namun, masa pensiunnya terganggu oleh seorang pembunuh galaksi yang dikenal sebagai Gorr sang Dewa Jagal (Christian Bale), yang ingin memusnahkan para dewa. Untuk mengatasi ancaman, Thor meminta bantuan Raja Valkyrie (Tessa Thompson), Korg (Taika Waititi), dan mantan kekasihnya Jane Foster (Natalie Portman), yang secara mengejutkan dan misterius berhasil menggunakan palu ajaibnya, Mjolnir, sebagai Mighty Thor. Bersama, mereka memulai petualangan kosmik yang mendebarkan untuk mengungkap misteri pembalasan Dewa Jagal dan menghentikannya sebelum terlambat.',35000,'2022-07-06',12,'https://image.tmdb.org/t/p/w500/pIkRyD18kl4FhoCNQuWxWu5cBLM.jpg'), 29 | ('Sonic the Hedgehog 2','Alur cerita film Sonic the Hedgehog 2 bermula ketika Sonic menetap di Green Hills. Ia memutuskan menetap di sana agar bisa merasakan lebih banyak kebebasan. Ditambah lagi, Tom dan Maddie setuju untuk meninggalakannya di rumah ketika mereka pergi untuk berlibur. Namun sayangnya, tidak lama setelah mereka pergi Dr. Robotnik sang musuh bubuyutan si landak biru itu kembali ke bumi. Kali ini Dr. Robotnik datang dengan pasukan baru, Knuckles. Tujuan mereka datang kembali adalah untuk mencari Master Emerald yang memiliki kekuatan super. Kekuatan super itu bisa membangun dan menghancurkan peradaban di bumi. Atas hal ini, Sonic pun mencari strategi agar bisa menggagalkan rencara Dr. Robotnik. Strategi yang dilakukan oleh Sonic ialah bekerjasama dengan sahabatnya, Tails. Kemudian bersama dengan Tails, Sonic memulai perjalanan untuk menemukan Master Emerald. Semua itu dilakukan dengan cepat, sebelum Master Emerald jatuh ke tangan yang salah.',45000,'2022-04-08',12,'https://image.tmdb.org/t/p/w500/6DrHO1jr3qVrViUO6s6kFiAGM7.jpg'), 30 | ('Avengers: Infinity War','Karena Avengers dan sekutunya terus melindungi dunia dari ancaman yang terlalu besar untuk ditangani oleh seorang pahlawan, bahaya baru telah muncul dari bayangan kosmik: Thanos. Seorang lalim penghujatan intergalaksi, tujuannya adalah untuk mengumpulkan semua enam Batu Infinity, artefak kekuatan yang tak terbayangkan, dan menggunakannya untuk menimbulkan kehendak memutar pada semua realitas. Segala sesuatu yang telah diperjuangkan oleh Avengers telah berkembang hingga saat ini - nasib Bumi dan keberadaannya sendiri tidak pernah lebih pasti.',46000,'2018-04-25',10,'https://image.tmdb.org/t/p/w500/7WsyChQLEftFiDOVTGkv3hFpyyt.jpg'), 31 | ('The Whale','Seorang guru bahasa Inggris yang tertutup dan gemuk mencoba untuk berhubungan kembali dengan putri remajanya yang terasing.',55000,'2022-12-09',15,'https://image.tmdb.org/t/p/w500/jQ0gylJMxWSL490sy0RrPj1Lj7e.jpg'), 32 | ('The Batman','Ketika seorang pembunuh berantai sadis mulai membunuh tokoh-tokoh politik penting di Gotham, Batman terpaksa menyelidiki korupsi tersembunyi di kota itu dan mempertanyakan keterlibatan keluarganya.',53000,'2022-03-01',13,'https://image.tmdb.org/t/p/w500/seyWFgGInaLqW7nOZvu0ZC95rtx.jpg'), 33 | ('Smile','Setelah menyaksikan kejadian aneh dan traumatis yang melibatkan seorang pasien, Dr. Rose Cotter mulai mengalami kejadian menakutkan yang tidak dapat dia jelaskan. Saat teror luar biasa mulai mengambil alih hidupnya, Rose harus menghadapi masa lalunya yang bermasalah untuk bertahan hidup dan melarikan diri dari kenyataan barunya yang mengerikan.',38000,'2022-09-23',11,'https://image.tmdb.org/t/p/w500/67Myda9zANAnlS54rRjQF4dHNNG.jpg'); 34 | INSERT INTO public."Movie" (title,description,price,"releaseDate","ageRating",poster) VALUES 35 | ('Encanto','menceritakan tentang keluarga Madrigals, sebuah keluarga yang tinggal di rumah ajaib dan masing-masing anggota keluarga memiliki keajaibannya tersendiri. Pada jaman dahulu kala, Abuela bersama suami dan anak-anaknya melarikan diri dari kerusuhan di desa.',44000,'2021-10-13',12,'https://image.tmdb.org/t/p/w500/4j0PNHkMr5ax3IA8tjtxcmPU3QT.jpg'); 36 | -------------------------------------------------------------------------------- /prisma/movies.ts: -------------------------------------------------------------------------------- 1 | export const movies = [ 2 | { 3 | id: 0, 4 | title: 'Fast X', 5 | description: 6 | 'Dom Toretto dan keluarganya menjadi sasaran putra gembong narkoba Hernan Reyes yang pendendam.', 7 | releaseDate: '2023-05-17', 8 | poster: 'https://image.tmdb.org/t/p/w500/fiVW06jE7z9YnO4trhaMEdclSiC.jpg', 9 | ageRating: 15, 10 | price: 53000, 11 | }, 12 | { 13 | id: 1, 14 | title: 'John Wick: Chapter 4', 15 | description: 16 | 'ohn Wick mengungkap jalan untuk mengalahkan The High Table. Tapi sebelum dia bisa mendapatkan kebebasannya, Wick harus berhadapan dengan musuh baru dengan aliansi kuat di seluruh dunia dan kekuatan yang mengubah teman lama menjadi musuh.', 17 | releaseDate: '2023-03-22', 18 | poster: 'https://image.tmdb.org/t/p/w500/vZloFAK7NmvMGKE7VkF5UHaz0I.jpg', 19 | ageRating: 10, 20 | price: 60000, 21 | }, 22 | { 23 | id: 2, 24 | title: 'The Super Mario Bros. Movie', 25 | description: 26 | 'Ketika sedang bekerja di bawah tanah untuk memperbaiki pipa air, Mario dan Luigi, yang merupakan tukang ledeng dari Brooklyn, tiba-tiba terhisap ke dalam pipa misterius dan masuk ke dunia yang sangat berbeda. Mereka berada di tempat yang ajaib dan aneh. Tapi sayangnya, mereka terpisah satu sama lain. Mario memulai petualangan besar untuk mencari dan menemukan Luigi.', 27 | releaseDate: '2023-04-05', 28 | poster: 'https://image.tmdb.org/t/p/w500/qNBAXBIQlnOThrVvA6mA2B5ggV6.jpg', 29 | ageRating: 14, 30 | price: 49000, 31 | }, 32 | { 33 | id: 3, 34 | title: 'Avatar: The Way of Water', 35 | description: 36 | "Jake Sully tinggal bersama keluarga barunya di planet Pandora. Setelah ancaman kembali datang, Jake harus bekerja dengan Neytiri dan pasukan ras Na'vi untuk melindungi planet mereka.", 37 | releaseDate: '2022-12-14', 38 | poster: 'https://image.tmdb.org/t/p/w500/t6HIqrRAclMCA60NsSmeqe9RmNV.jpg', 39 | ageRating: 12, 40 | price: 53000, 41 | }, 42 | { 43 | id: 4, 44 | title: 'Guardians of the Galaxy Vol. 3', 45 | description: 46 | 'Peter Quill masih trauma karena kehilangan Gamora. Ia perlu mengumpulkan timnya untuk melindungi alam semesta dan salah satu anggota mereka. Jika mereka gagal, Guardian akan berakhir.', 47 | releaseDate: '2023-05-03', 48 | poster: 'https://image.tmdb.org/t/p/w500/nAbpLidFdbbi3efFQKMPQJkaZ1r.jpg', 49 | ageRating: 12, 50 | price: 41000, 51 | }, 52 | { 53 | id: 5, 54 | title: 'Ant-Man and the Wasp: Quantumania', 55 | description: 56 | 'Scott Lang dan Hope van Dyne adalah pasangan pahlawan super. Mereka pergi bersama orang tua Hope, Janet van Dyne dan Hank Pym, serta anak perempuan Scott, Cassie Lang, untuk menjelajahi Alam Kuantum. Di sana, mereka bertemu dengan makhluk-makhluk aneh dan memulai petualangan yang tak terduga. Petualangan ini akan menguji batas-batas mereka.', 57 | releaseDate: '2023-02-15', 58 | poster: 'https://image.tmdb.org/t/p/w500/g0OWGM7HoIt866Lu7yKohYO31NU.jpg', 59 | ageRating: 12, 60 | price: 51000, 61 | }, 62 | { 63 | id: 6, 64 | title: "The Pope's Exorcist", 65 | description: 66 | 'Pastor Gabriele Amorth, yang memimpin tim pengusir setan di Vatikan, menginvestigasi kasus kekerasan roh jahat yang menghantui seorang anak laki-laki. Dalam penyelidikannya, ia secara tak terduga menemukan rahasia tua yang disembunyikan oleh Vatikan selama berabad-abad.', 67 | releaseDate: '2023-04-05', 68 | poster: 'https://image.tmdb.org/t/p/w500/gNPqcv1tAifbN7PRNgqpzY8sEJZ.jpg', 69 | ageRating: 13, 70 | price: 51000, 71 | }, 72 | { 73 | id: 7, 74 | title: 'To Catch a Killer', 75 | description: 76 | 'Baltimore. Malam tahun baru. Seorang petugas polisi yang berbakat tetapi bermasalah (Shailene Woodley) direkrut oleh kepala penyelidik FBI (Ben Mendelsohn) untuk membantu membuat profil dan melacak individu yang terganggu yang meneror kota.', 77 | releaseDate: '2023-04-06', 78 | poster: 'https://image.tmdb.org/t/p/w500/mFp3l4lZg1NSEsyxKrdi0rNK8r1.jpg', 79 | ageRating: 15, 80 | price: 47000, 81 | }, 82 | { 83 | id: 8, 84 | title: 'Transformers: Age of Extinction', 85 | description: 86 | 'Lima tahun setelah Chicago dihancurkan, manusia berbalik melawan robot. Namun seorang ayah tunggal dan penemu membangkitkan robot yang dapat menyelamatkan dunia.', 87 | releaseDate: '2014-06-25', 88 | poster: 'https://image.tmdb.org/t/p/w500/jyzrfx2WaeY60kYZpPYepSjGz4S.jpg', 89 | ageRating: 11, 90 | price: 54000, 91 | }, 92 | { 93 | id: 9, 94 | title: 'Puss in Boots: The Last Wish', 95 | description: 96 | 'Puss in Boots menemukan fakta bahwa kecintaannya pada petualangan telah merenggut nyawanya: dia telah menghabiskan delapan dari sembilan nyawanya. Puss kini memulai petualangan epik untuk menemukan harapan terakhir untuk memulihkan sembilan nyawanya.', 97 | releaseDate: '2022-12-07', 98 | poster: 'https://image.tmdb.org/t/p/w500/kuf6dutpsT0vSVehic3EZIqkOBt.jpg', 99 | ageRating: 11, 100 | price: 51000, 101 | }, 102 | { 103 | id: 10, 104 | title: 'Scream VI', 105 | description: 106 | 'Setelah pembunuhan terbaru oleh Ghostface, keempat orang yang selamat pergi dari Woodsboro dan memulai hidup baru.', 107 | releaseDate: '2023-03-08', 108 | poster: 'https://image.tmdb.org/t/p/w500/wDWwtvkRRlgTiUr6TyLSMX8FCuZ.jpg', 109 | ageRating: 12, 110 | price: 36000, 111 | }, 112 | { 113 | id: 11, 114 | title: 'Black Adam', 115 | description: 116 | 'Hampir 5.000 tahun setelah dia dianugerahi kekuatan maha kuasa para dewa Mesir—dan dipenjara dengan cepat—Black Adam dibebaskan dari makam duniawinya, siap untuk melepaskan bentuk keadilannya yang unik di dunia modern.', 117 | releaseDate: '2022-10-19', 118 | poster: 'https://image.tmdb.org/t/p/w500/A5imhXiFF3AL9RRA4FBzNDFmfgW.jpg', 119 | ageRating: 10, 120 | price: 42000, 121 | }, 122 | { 123 | id: 12, 124 | title: 'Dungeons & Dragons: Honor Among Thieves', 125 | description: 126 | 'Seorang pencuri menawan dan sekelompok petualang yang unik melakukan pencurian besar-besaran untuk mencuri relik yang hilang. Namun, segalanya menjadi kacau ketika mereka berjumpa dengan orang yang salah.', 127 | releaseDate: '2023-03-23', 128 | poster: 'https://image.tmdb.org/t/p/w500/A7AoNT06aRAc4SV89Dwxj3EYAgC.jpg', 129 | ageRating: 12, 130 | price: 38000, 131 | }, 132 | { 133 | id: 13, 134 | title: 'Peter Pan & Wendy', 135 | description: 136 | 'Wendy Darling adalah seorang gadis kecil yang takut pergi dari rumah masa kecilnya. Suatu hari, dia bertemu dengan Peter Pan, seorang anak laki-laki yang tidak mau tumbuh dewasa. Mereka bersama saudara-saudaranya dan peri kecil bernama Tinker Bell pergi ke dunia ajaib yang disebut Neverland. Di sana, mereka menghadapi Kapten Hook, seorang bajak laut jahat, dan mengalami petualangan seru yang akan mengubah hidup Wendy selamanya.', 137 | releaseDate: '2023-04-20', 138 | poster: 'https://image.tmdb.org/t/p/w500/9NXAlFEE7WDssbXSMgdacsUD58Y.jpg', 139 | ageRating: 13, 140 | price: 35000, 141 | }, 142 | { 143 | id: 14, 144 | title: 'Spider-Man: No Way Home', 145 | description: 146 | 'Peter Parker menghadapi masalah besar. Hal ini terjadi setelah identitasnya sebagai Spiderman terungkap. Dengan kepergian Tony Stark, Peter Parker pun harus meminta bantuan Doctor Strange agar semua orang bisa melupakan identitasnya sebagai manusia laba-laba.', 147 | releaseDate: '2021-12-15', 148 | poster: 'https://image.tmdb.org/t/p/w500/uJYYizSuA9Y3DCs0qS4qWvHfZg4.jpg', 149 | ageRating: 15, 150 | price: 56000, 151 | }, 152 | { 153 | id: 15, 154 | title: 'Black Panther: Wakanda Forever', 155 | description: 156 | "Rakyat Wakanda kali ini akan berjuang untuk melindungi negerinya dari campur tangan kekuatan dunia setelah kematian sang Raja T'Challa.", 157 | releaseDate: '2022-11-09', 158 | poster: 'https://image.tmdb.org/t/p/w500/sv1xJUazXeYqALzczSZ3O6nkH75.jpg', 159 | ageRating: 13, 160 | price: 39000, 161 | }, 162 | { 163 | id: 16, 164 | title: 'Transformers: The Last Knight', 165 | description: 166 | 'Di tengah ketidakhadiran Optimus Prime, umat manusia berperang melawanTransformers untuk mempertahankan eksistensinya. Cade Yeager membentuk kerjasama dengan Bumblebee, seorang bangsawan Inggris dan seorang professor dari Oxford untuk mempelajari mengapa Transformers selalu kembali ke planet bumi.', 167 | releaseDate: '2017-06-16', 168 | poster: 'https://image.tmdb.org/t/p/w500/s5HQf2Gb3lIO2cRcFwNL9sn1o1o.jpg', 169 | ageRating: 12, 170 | price: 52000, 171 | }, 172 | { 173 | id: 17, 174 | title: 'Renfield', 175 | description: 176 | 'Setelah bertahun-tahun sebagai hamba Dracula yang merasa jenuh dan lelah, Renfield menemukan harapan baru dalam hidupnya. Dia jatuh cinta pada Rebecca Quincy, seorang polisi lalu lintas yang energik dan sering marah. Kesempatan ini bisa menjadi penebusan baginya.', 177 | releaseDate: '2023-04-07', 178 | poster: 'https://image.tmdb.org/t/p/w500/2OaprROMZZeiWsydjGUIkXrv2Z3.jpg', 179 | ageRating: 14, 180 | price: 51000, 181 | }, 182 | { 183 | id: 18, 184 | title: 'Cocaine Bear', 185 | description: 186 | 'Sekelompok polisi, penjahat, turis, dan remaja eksentrik berkumpul di hutan Georgia tempat beruang hitam besar mengamuk setelah menelan kokain secara tidak sengaja.', 187 | releaseDate: '2023-02-22', 188 | poster: 'https://image.tmdb.org/t/p/w500/gOnmaxHo0412UVr1QM5Nekv1xPi.jpg', 189 | ageRating: 12, 190 | price: 53000, 191 | }, 192 | { 193 | id: 19, 194 | title: 'Prey', 195 | description: 196 | 'Di Comanche Nation pada tahun 1717, seorang pejuang yang ganas dan sangat terampil bernama Naru mengetahui bahwa mangsa yang dia intai adalah alien yang sangat berkembang dengan persenjataan berteknologi maju.', 197 | releaseDate: '2022-08-02', 198 | poster: 'https://image.tmdb.org/t/p/w500/ujr5pztc1oitbe7ViMUOilFaJ7s.jpg', 199 | ageRating: 10, 200 | price: 42000, 201 | }, 202 | { 203 | id: 20, 204 | title: 'Fall', 205 | description: 206 | 'Untuk sahabat Becky dan Hunter, hidup adalah tentang menaklukkan ketakutan dan mendorong batas. Tetapi setelah mereka mendaki 2.000 kaki ke puncak menara radio terpencil yang ditinggalkan, mereka menemukan diri mereka terdampar tanpa jalan turun. Sekarang keterampilan panjat ahli Becky dan Hunter akan diuji saat mereka mati-matian berjuang untuk bertahan hidup dari unsur-unsur, kurangnya persediaan, dan ketinggian yang menyebabkan vertigo.', 207 | releaseDate: '2022-08-11', 208 | poster: 'https://image.tmdb.org/t/p/w500/v28T5F1IygM8vXWZIycfNEm3xcL.jpg', 209 | ageRating: 11, 210 | price: 39000, 211 | }, 212 | { 213 | id: 21, 214 | title: 'Avatar', 215 | description: 216 | 'Pada abad ke-22, seorang Marinir lumpuh dikirim ke Pandora bulan pada misi yang unik, tetapi menjadi terpecah antara mengikuti perintah dan melindungi peradaban alien.', 217 | releaseDate: '2009-12-15', 218 | poster: 'https://image.tmdb.org/t/p/w500/kyeqWdyUXW608qlYkRqosgbbJyK.jpg', 219 | ageRating: 13, 220 | price: 37000, 221 | }, 222 | { 223 | id: 22, 224 | title: 'Split', 225 | description: 226 | 'Ketika ketiga gadis remaja sedang menunggu ayah mereka di dalam mobil, seorang pria misterius menculik dan membawa mereka ke dalam sebuah bunker. Sang penculik yang bernama Kevin (James McAvoy) adalah seorang pria dengan gangguan jiwa yang membuatnya memiliki 23 kepribadian yang berbeda, yang diantaranya adalah seorang wanita dan anak berumur 9 tahun yang bernama Hedwig. Sebagai salah satu gadis yang diculik, Casey berusaha meloloskan diri dengan meyakinkan salah satu kepribadian Kevin untuk melepaskan mereka. Akan tetapi hal tersebut tidaklah mudah, terlebih setelah Hedwig memperingatkan mereka akan the Beast yang merupakan kepribadian Kevin yang paling berbahaya.', 227 | releaseDate: '2017-01-19', 228 | poster: 'https://image.tmdb.org/t/p/w500/lli31lYTFpvxVBeFHWoe5PMfW5s.jpg', 229 | ageRating: 10, 230 | price: 45000, 231 | }, 232 | { 233 | id: 23, 234 | title: 'Top Gun: Maverick', 235 | description: 236 | 'Setelah lebih dari tiga puluh tahun mengabdi sebagai salah satu penerbang top Angkatan Laut, dan menghindari kenaikan pangkat yang akan menjatuhkannya, Pete "Maverick" Mitchell mendapati dirinya melatih satu detasemen lulusan TOP GUN untuk misi khusus yang tidak ada kehidupan. pilot pernah melihat.', 237 | releaseDate: '2022-05-24', 238 | poster: 'https://image.tmdb.org/t/p/w500/jeGvNOVMs5QIU1VaoGvnd3gSv0G.jpg', 239 | ageRating: 14, 240 | price: 57000, 241 | }, 242 | { 243 | id: 24, 244 | title: 'Thor: Love and Thunder', 245 | description: 246 | '"Thor: Love and Thunder"menceritakan Thor (Chris Hemsworth) dalam sebuah perjalanan yang belum pernah ia jalani – pencariankedamaian batin. Namun, masa pensiunnya terganggu oleh seorang pembunuh galaksi yang dikenal sebagai Gorr sang Dewa Jagal (Christian Bale), yang ingin memusnahkan para dewa. Untuk mengatasi ancaman, Thor meminta bantuan Raja Valkyrie (Tessa Thompson), Korg (Taika Waititi), dan mantan kekasihnya Jane Foster (Natalie Portman), yang secara mengejutkan dan misterius berhasil menggunakan palu ajaibnya, Mjolnir, sebagai Mighty Thor. Bersama, mereka memulai petualangan kosmik yang mendebarkan untuk mengungkap misteri pembalasan Dewa Jagal dan menghentikannya sebelum terlambat.', 247 | releaseDate: '2022-07-06', 248 | poster: 'https://image.tmdb.org/t/p/w500/pIkRyD18kl4FhoCNQuWxWu5cBLM.jpg', 249 | ageRating: 12, 250 | price: 35000, 251 | }, 252 | { 253 | id: 25, 254 | title: 'Sonic the Hedgehog 2', 255 | description: 256 | 'Alur cerita film Sonic the Hedgehog 2 bermula ketika Sonic menetap di Green Hills. Ia memutuskan menetap di sana agar bisa merasakan lebih banyak kebebasan. Ditambah lagi, Tom dan Maddie setuju untuk meninggalakannya di rumah ketika mereka pergi untuk berlibur. Namun sayangnya, tidak lama setelah mereka pergi Dr. Robotnik sang musuh bubuyutan si landak biru itu kembali ke bumi. Kali ini Dr. Robotnik datang dengan pasukan baru, Knuckles. Tujuan mereka datang kembali adalah untuk mencari Master Emerald yang memiliki kekuatan super. Kekuatan super itu bisa membangun dan menghancurkan peradaban di bumi. Atas hal ini, Sonic pun mencari strategi agar bisa menggagalkan rencara Dr. Robotnik. Strategi yang dilakukan oleh Sonic ialah bekerjasama dengan sahabatnya, Tails. Kemudian bersama dengan Tails, Sonic memulai perjalanan untuk menemukan Master Emerald. Semua itu dilakukan dengan cepat, sebelum Master Emerald jatuh ke tangan yang salah.', 257 | releaseDate: '2022-04-08', 258 | poster: 'https://image.tmdb.org/t/p/w500/6DrHO1jr3qVrViUO6s6kFiAGM7.jpg', 259 | ageRating: 12, 260 | price: 45000, 261 | }, 262 | { 263 | id: 26, 264 | title: 'Avengers: Infinity War', 265 | description: 266 | 'Karena Avengers dan sekutunya terus melindungi dunia dari ancaman yang terlalu besar untuk ditangani oleh seorang pahlawan, bahaya baru telah muncul dari bayangan kosmik: Thanos. Seorang lalim penghujatan intergalaksi, tujuannya adalah untuk mengumpulkan semua enam Batu Infinity, artefak kekuatan yang tak terbayangkan, dan menggunakannya untuk menimbulkan kehendak memutar pada semua realitas. Segala sesuatu yang telah diperjuangkan oleh Avengers telah berkembang hingga saat ini - nasib Bumi dan keberadaannya sendiri tidak pernah lebih pasti.', 267 | releaseDate: '2018-04-25', 268 | poster: 'https://image.tmdb.org/t/p/w500/7WsyChQLEftFiDOVTGkv3hFpyyt.jpg', 269 | ageRating: 10, 270 | price: 46000, 271 | }, 272 | { 273 | id: 27, 274 | title: 'The Whale', 275 | description: 276 | 'Seorang guru bahasa Inggris yang tertutup dan gemuk mencoba untuk berhubungan kembali dengan putri remajanya yang terasing.', 277 | releaseDate: '2022-12-09', 278 | poster: 'https://image.tmdb.org/t/p/w500/jQ0gylJMxWSL490sy0RrPj1Lj7e.jpg', 279 | ageRating: 15, 280 | price: 55000, 281 | }, 282 | { 283 | id: 28, 284 | title: 'The Batman', 285 | description: 286 | 'Ketika seorang pembunuh berantai sadis mulai membunuh tokoh-tokoh politik penting di Gotham, Batman terpaksa menyelidiki korupsi tersembunyi di kota itu dan mempertanyakan keterlibatan keluarganya.', 287 | releaseDate: '2022-03-01', 288 | poster: 'https://image.tmdb.org/t/p/w500/seyWFgGInaLqW7nOZvu0ZC95rtx.jpg', 289 | ageRating: 13, 290 | price: 53000, 291 | }, 292 | { 293 | id: 29, 294 | title: 'Smile', 295 | description: 296 | 'Setelah menyaksikan kejadian aneh dan traumatis yang melibatkan seorang pasien, Dr. Rose Cotter mulai mengalami kejadian menakutkan yang tidak dapat dia jelaskan. Saat teror luar biasa mulai mengambil alih hidupnya, Rose harus menghadapi masa lalunya yang bermasalah untuk bertahan hidup dan melarikan diri dari kenyataan barunya yang mengerikan.', 297 | releaseDate: '2022-09-23', 298 | poster: 'https://image.tmdb.org/t/p/w500/67Myda9zANAnlS54rRjQF4dHNNG.jpg', 299 | ageRating: 11, 300 | price: 38000, 301 | }, 302 | { 303 | id: 30, 304 | title: 'Encanto', 305 | description: 306 | 'menceritakan tentang keluarga Madrigals, sebuah keluarga yang tinggal di rumah ajaib dan masing-masing anggota keluarga memiliki keajaibannya tersendiri. Pada jaman dahulu kala, Abuela bersama suami dan anak-anaknya melarikan diri dari kerusuhan di desa.', 307 | releaseDate: '2021-10-13', 308 | poster: 'https://image.tmdb.org/t/p/w500/4j0PNHkMr5ax3IA8tjtxcmPU3QT.jpg', 309 | ageRating: 12, 310 | price: 44000, 311 | }, 312 | { 313 | id: 31, 314 | title: 'Azab Pemakan Daging kurban sebelum kurban dilaksanakan', 315 | description: 316 | 'pada suatu hari hiduplah seorang manusia yang makan daging kurban padahal hari raya idul adha saja belum dilaksanakan', 317 | price: 9999999, 318 | ageRating: 60, 319 | poster: 320 | 'https://ichef.bbci.co.uk/news/640/cpsprodpb/CB16/production/_103909915_c558208e-40b7-4ff7-aa68-b712d45403d9.jpg', 321 | releaseDate: '2077-01-01', 322 | }, 323 | ]; 324 | -------------------------------------------------------------------------------- /movies.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 0, 4 | "title": "Fast X", 5 | "description": "Dom Toretto dan keluarganya menjadi sasaran putra gembong narkoba Hernan Reyes yang pendendam.", 6 | "releaseDate": "2023-05-17", 7 | "poster": "https://image.tmdb.org/t/p/w500/fiVW06jE7z9YnO4trhaMEdclSiC.jpg", 8 | "ageRating": 15, 9 | "price": 53000 10 | }, 11 | { 12 | "id": 1, 13 | "title": "John Wick: Chapter 4", 14 | "description": "ohn Wick mengungkap jalan untuk mengalahkan The High Table. Tapi sebelum dia bisa mendapatkan kebebasannya, Wick harus berhadapan dengan musuh baru dengan aliansi kuat di seluruh dunia dan kekuatan yang mengubah teman lama menjadi musuh.", 15 | "releaseDate": "2023-03-22", 16 | "poster": "https://image.tmdb.org/t/p/w500/vZloFAK7NmvMGKE7VkF5UHaz0I.jpg", 17 | "ageRating": 10, 18 | "price": 60000 19 | }, 20 | { 21 | "id": 2, 22 | "title": "The Super Mario Bros. Movie", 23 | "description": "Ketika sedang bekerja di bawah tanah untuk memperbaiki pipa air, Mario dan Luigi, yang merupakan tukang ledeng dari Brooklyn, tiba-tiba terhisap ke dalam pipa misterius dan masuk ke dunia yang sangat berbeda. Mereka berada di tempat yang ajaib dan aneh. Tapi sayangnya, mereka terpisah satu sama lain. Mario memulai petualangan besar untuk mencari dan menemukan Luigi.", 24 | "releaseDate": "2023-04-05", 25 | "poster": "https://image.tmdb.org/t/p/w500/qNBAXBIQlnOThrVvA6mA2B5ggV6.jpg", 26 | "ageRating": 14, 27 | "price": 49000 28 | }, 29 | { 30 | "id": 3, 31 | "title": "Avatar: The Way of Water", 32 | "description": "Jake Sully tinggal bersama keluarga barunya di planet Pandora. Setelah ancaman kembali datang, Jake harus bekerja dengan Neytiri dan pasukan ras Na'vi untuk melindungi planet mereka.", 33 | "releaseDate": "2022-12-14", 34 | "poster": "https://image.tmdb.org/t/p/w500/t6HIqrRAclMCA60NsSmeqe9RmNV.jpg", 35 | "ageRating": 12, 36 | "price": 53000 37 | }, 38 | { 39 | "id": 4, 40 | "title": "Guardians of the Galaxy Vol. 3", 41 | "description": "Peter Quill masih trauma karena kehilangan Gamora. Ia perlu mengumpulkan timnya untuk melindungi alam semesta dan salah satu anggota mereka. Jika mereka gagal, Guardian akan berakhir.", 42 | "releaseDate": "2023-05-03", 43 | "poster": "https://image.tmdb.org/t/p/w500/nAbpLidFdbbi3efFQKMPQJkaZ1r.jpg", 44 | "ageRating": 12, 45 | "price": 41000 46 | }, 47 | { 48 | "id": 5, 49 | "title": "Ant-Man and the Wasp: Quantumania", 50 | "description": "Scott Lang dan Hope van Dyne adalah pasangan pahlawan super. Mereka pergi bersama orang tua Hope, Janet van Dyne dan Hank Pym, serta anak perempuan Scott, Cassie Lang, untuk menjelajahi Alam Kuantum. Di sana, mereka bertemu dengan makhluk-makhluk aneh dan memulai petualangan yang tak terduga. Petualangan ini akan menguji batas-batas mereka.", 51 | "releaseDate": "2023-02-15", 52 | "poster": "https://image.tmdb.org/t/p/w500/g0OWGM7HoIt866Lu7yKohYO31NU.jpg", 53 | "ageRating": 12, 54 | "price": 51000 55 | }, 56 | { 57 | "id": 6, 58 | "title": "The Pope's Exorcist", 59 | "description": "Pastor Gabriele Amorth, yang memimpin tim pengusir setan di Vatikan, menginvestigasi kasus kekerasan roh jahat yang menghantui seorang anak laki-laki. Dalam penyelidikannya, ia secara tak terduga menemukan rahasia tua yang disembunyikan oleh Vatikan selama berabad-abad.", 60 | "releaseDate": "2023-04-05", 61 | "poster": "https://image.tmdb.org/t/p/w500/gNPqcv1tAifbN7PRNgqpzY8sEJZ.jpg", 62 | "ageRating": 13, 63 | "price": 51000 64 | }, 65 | { 66 | "id": 7, 67 | "title": "To Catch a Killer", 68 | "description": "Baltimore. Malam tahun baru. Seorang petugas polisi yang berbakat tetapi bermasalah (Shailene Woodley) direkrut oleh kepala penyelidik FBI (Ben Mendelsohn) untuk membantu membuat profil dan melacak individu yang terganggu yang meneror kota.", 69 | "releaseDate": "2023-04-06", 70 | "poster": "https://image.tmdb.org/t/p/w500/mFp3l4lZg1NSEsyxKrdi0rNK8r1.jpg", 71 | "ageRating": 15, 72 | "price": 47000 73 | }, 74 | { 75 | "id": 8, 76 | "title": "Transformers: Age of Extinction", 77 | "description": "Lima tahun setelah Chicago dihancurkan, manusia berbalik melawan robot. Namun seorang ayah tunggal dan penemu membangkitkan robot yang dapat menyelamatkan dunia.", 78 | "releaseDate": "2014-06-25", 79 | "poster": "https://image.tmdb.org/t/p/w500/jyzrfx2WaeY60kYZpPYepSjGz4S.jpg", 80 | "ageRating": 11, 81 | "price": 54000 82 | }, 83 | { 84 | "id": 9, 85 | "title": "Puss in Boots: The Last Wish", 86 | "description": "Puss in Boots menemukan fakta bahwa kecintaannya pada petualangan telah merenggut nyawanya: dia telah menghabiskan delapan dari sembilan nyawanya. Puss kini memulai petualangan epik untuk menemukan harapan terakhir untuk memulihkan sembilan nyawanya.", 87 | "releaseDate": "2022-12-07", 88 | "poster": "https://image.tmdb.org/t/p/w500/kuf6dutpsT0vSVehic3EZIqkOBt.jpg", 89 | "ageRating": 11, 90 | "price": 51000 91 | }, 92 | { 93 | "id": 10, 94 | "title": "Scream VI", 95 | "description": "Setelah pembunuhan terbaru oleh Ghostface, keempat orang yang selamat pergi dari Woodsboro dan memulai hidup baru.", 96 | "releaseDate": "2023-03-08", 97 | "poster": "https://image.tmdb.org/t/p/w500/wDWwtvkRRlgTiUr6TyLSMX8FCuZ.jpg", 98 | "ageRating": 12, 99 | "price": 36000 100 | }, 101 | { 102 | "id": 11, 103 | "title": "Black Adam", 104 | "description": "Hampir 5.000 tahun setelah dia dianugerahi kekuatan maha kuasa para dewa Mesir—dan dipenjara dengan cepat—Black Adam dibebaskan dari makam duniawinya, siap untuk melepaskan bentuk keadilannya yang unik di dunia modern.", 105 | "releaseDate": "2022-10-19", 106 | "poster": "https://image.tmdb.org/t/p/w500/A5imhXiFF3AL9RRA4FBzNDFmfgW.jpg", 107 | "ageRating": 10, 108 | "price": 42000 109 | }, 110 | { 111 | "id": 12, 112 | "title": "Dungeons & Dragons: Honor Among Thieves", 113 | "description": "Seorang pencuri menawan dan sekelompok petualang yang unik melakukan pencurian besar-besaran untuk mencuri relik yang hilang. Namun, segalanya menjadi kacau ketika mereka berjumpa dengan orang yang salah.", 114 | "releaseDate": "2023-03-23", 115 | "poster": "https://image.tmdb.org/t/p/w500/A7AoNT06aRAc4SV89Dwxj3EYAgC.jpg", 116 | "ageRating": 12, 117 | "price": 38000 118 | }, 119 | { 120 | "id": 13, 121 | "title": "Peter Pan & Wendy", 122 | "description": "Wendy Darling adalah seorang gadis kecil yang takut pergi dari rumah masa kecilnya. Suatu hari, dia bertemu dengan Peter Pan, seorang anak laki-laki yang tidak mau tumbuh dewasa. Mereka bersama saudara-saudaranya dan peri kecil bernama Tinker Bell pergi ke dunia ajaib yang disebut Neverland. Di sana, mereka menghadapi Kapten Hook, seorang bajak laut jahat, dan mengalami petualangan seru yang akan mengubah hidup Wendy selamanya.", 123 | "releaseDate": "2023-04-20", 124 | "poster": "https://image.tmdb.org/t/p/w500/9NXAlFEE7WDssbXSMgdacsUD58Y.jpg", 125 | "ageRating": 13, 126 | "price": 35000 127 | }, 128 | { 129 | "id": 14, 130 | "title": "Spider-Man: No Way Home", 131 | "description": "Peter Parker menghadapi masalah besar. Hal ini terjadi setelah identitasnya sebagai Spiderman terungkap. Dengan kepergian Tony Stark, Peter Parker pun harus meminta bantuan Doctor Strange agar semua orang bisa melupakan identitasnya sebagai manusia laba-laba.", 132 | "releaseDate": "2021-12-15", 133 | "poster": "https://image.tmdb.org/t/p/w500/uJYYizSuA9Y3DCs0qS4qWvHfZg4.jpg", 134 | "ageRating": 15, 135 | "price": 56000 136 | }, 137 | { 138 | "id": 15, 139 | "title": "Black Panther: Wakanda Forever", 140 | "description": "Rakyat Wakanda kali ini akan berjuang untuk melindungi negerinya dari campur tangan kekuatan dunia setelah kematian sang Raja T'Challa.", 141 | "releaseDate": "2022-11-09", 142 | "poster": "https://image.tmdb.org/t/p/w500/sv1xJUazXeYqALzczSZ3O6nkH75.jpg", 143 | "ageRating": 13, 144 | "price": 39000 145 | }, 146 | { 147 | "id": 16, 148 | "title": "Transformers: The Last Knight", 149 | "description": "Di tengah ketidakhadiran Optimus Prime, umat manusia berperang melawanTransformers untuk mempertahankan eksistensinya. Cade Yeager membentuk kerjasama dengan Bumblebee, seorang bangsawan Inggris dan seorang professor dari Oxford untuk mempelajari mengapa Transformers selalu kembali ke planet bumi.", 150 | "releaseDate": "2017-06-16", 151 | "poster": "https://image.tmdb.org/t/p/w500/s5HQf2Gb3lIO2cRcFwNL9sn1o1o.jpg", 152 | "ageRating": 12, 153 | "price": 52000 154 | }, 155 | { 156 | "id": 17, 157 | "title": "Renfield", 158 | "description": "Setelah bertahun-tahun sebagai hamba Dracula yang merasa jenuh dan lelah, Renfield menemukan harapan baru dalam hidupnya. Dia jatuh cinta pada Rebecca Quincy, seorang polisi lalu lintas yang energik dan sering marah. Kesempatan ini bisa menjadi penebusan baginya.", 159 | "releaseDate": "2023-04-07", 160 | "poster": "https://image.tmdb.org/t/p/w500/2OaprROMZZeiWsydjGUIkXrv2Z3.jpg", 161 | "ageRating": 14, 162 | "price": 51000 163 | }, 164 | { 165 | "id": 18, 166 | "title": "Cocaine Bear", 167 | "description": "Sekelompok polisi, penjahat, turis, dan remaja eksentrik berkumpul di hutan Georgia tempat beruang hitam besar mengamuk setelah menelan kokain secara tidak sengaja.", 168 | "releaseDate": "2023-02-22", 169 | "poster": "https://image.tmdb.org/t/p/w500/gOnmaxHo0412UVr1QM5Nekv1xPi.jpg", 170 | "ageRating": 12, 171 | "price": 53000 172 | }, 173 | { 174 | "id": 19, 175 | "title": "Prey", 176 | "description": "Di Comanche Nation pada tahun 1717, seorang pejuang yang ganas dan sangat terampil bernama Naru mengetahui bahwa mangsa yang dia intai adalah alien yang sangat berkembang dengan persenjataan berteknologi maju.", 177 | "releaseDate": "2022-08-02", 178 | "poster": "https://image.tmdb.org/t/p/w500/ujr5pztc1oitbe7ViMUOilFaJ7s.jpg", 179 | "ageRating": 10, 180 | "price": 42000 181 | }, 182 | { 183 | "id": 20, 184 | "title": "Fall", 185 | "description": "Untuk sahabat Becky dan Hunter, hidup adalah tentang menaklukkan ketakutan dan mendorong batas. Tetapi setelah mereka mendaki 2.000 kaki ke puncak menara radio terpencil yang ditinggalkan, mereka menemukan diri mereka terdampar tanpa jalan turun. Sekarang keterampilan panjat ahli Becky dan Hunter akan diuji saat mereka mati-matian berjuang untuk bertahan hidup dari unsur-unsur, kurangnya persediaan, dan ketinggian yang menyebabkan vertigo.", 186 | "releaseDate": "2022-08-11", 187 | "poster": "https://image.tmdb.org/t/p/w500/v28T5F1IygM8vXWZIycfNEm3xcL.jpg", 188 | "ageRating": 11, 189 | "price": 39000 190 | }, 191 | { 192 | "id": 21, 193 | "title": "Avatar", 194 | "description": "Pada abad ke-22, seorang Marinir lumpuh dikirim ke Pandora bulan pada misi yang unik, tetapi menjadi terpecah antara mengikuti perintah dan melindungi peradaban alien.", 195 | "releaseDate": "2009-12-15", 196 | "poster": "https://image.tmdb.org/t/p/w500/kyeqWdyUXW608qlYkRqosgbbJyK.jpg", 197 | "ageRating": 13, 198 | "price": 37000 199 | }, 200 | { 201 | "id": 22, 202 | "title": "Split", 203 | "description": "Ketika ketiga gadis remaja sedang menunggu ayah mereka di dalam mobil, seorang pria misterius menculik dan membawa mereka ke dalam sebuah bunker. Sang penculik yang bernama Kevin (James McAvoy) adalah seorang pria dengan gangguan jiwa yang membuatnya memiliki 23 kepribadian yang berbeda, yang diantaranya adalah seorang wanita dan anak berumur 9 tahun yang bernama Hedwig. Sebagai salah satu gadis yang diculik, Casey berusaha meloloskan diri dengan meyakinkan salah satu kepribadian Kevin untuk melepaskan mereka. Akan tetapi hal tersebut tidaklah mudah, terlebih setelah Hedwig memperingatkan mereka akan the Beast yang merupakan kepribadian Kevin yang paling berbahaya.", 204 | "releaseDate": "2017-01-19", 205 | "poster": "https://image.tmdb.org/t/p/w500/lli31lYTFpvxVBeFHWoe5PMfW5s.jpg", 206 | "ageRating": 10, 207 | "price": 45000 208 | }, 209 | { 210 | "id": 23, 211 | "title": "Top Gun: Maverick", 212 | "description": "Setelah lebih dari tiga puluh tahun mengabdi sebagai salah satu penerbang top Angkatan Laut, dan menghindari kenaikan pangkat yang akan menjatuhkannya, Pete \"Maverick\" Mitchell mendapati dirinya melatih satu detasemen lulusan TOP GUN untuk misi khusus yang tidak ada kehidupan. pilot pernah melihat.", 213 | "releaseDate": "2022-05-24", 214 | "poster": "https://image.tmdb.org/t/p/w500/jeGvNOVMs5QIU1VaoGvnd3gSv0G.jpg", 215 | "ageRating": 14, 216 | "price": 57000 217 | }, 218 | { 219 | "id": 24, 220 | "title": "Thor: Love and Thunder", 221 | "description": "\"Thor: Love and Thunder\"menceritakan Thor (Chris Hemsworth) dalam sebuah perjalanan yang belum pernah ia jalani – pencariankedamaian batin. Namun, masa pensiunnya terganggu oleh seorang pembunuh galaksi yang dikenal sebagai Gorr sang Dewa Jagal (Christian Bale), yang ingin memusnahkan para dewa. Untuk mengatasi ancaman, Thor meminta bantuan Raja Valkyrie (Tessa Thompson), Korg (Taika Waititi), dan mantan kekasihnya Jane Foster (Natalie Portman), yang secara mengejutkan dan misterius berhasil menggunakan palu ajaibnya, Mjolnir, sebagai Mighty Thor. Bersama, mereka memulai petualangan kosmik yang mendebarkan untuk mengungkap misteri pembalasan Dewa Jagal dan menghentikannya sebelum terlambat.", 222 | "releaseDate": "2022-07-06", 223 | "poster": "https://image.tmdb.org/t/p/w500/pIkRyD18kl4FhoCNQuWxWu5cBLM.jpg", 224 | "ageRating": 12, 225 | "price": 35000 226 | }, 227 | { 228 | "id": 25, 229 | "title": "Sonic the Hedgehog 2", 230 | "description": "Alur cerita film Sonic the Hedgehog 2 bermula ketika Sonic menetap di Green Hills. Ia memutuskan menetap di sana agar bisa merasakan lebih banyak kebebasan. Ditambah lagi, Tom dan Maddie setuju untuk meninggalakannya di rumah ketika mereka pergi untuk berlibur. Namun sayangnya, tidak lama setelah mereka pergi Dr. Robotnik sang musuh bubuyutan si landak biru itu kembali ke bumi. Kali ini Dr. Robotnik datang dengan pasukan baru, Knuckles. Tujuan mereka datang kembali adalah untuk mencari Master Emerald yang memiliki kekuatan super. Kekuatan super itu bisa membangun dan menghancurkan peradaban di bumi. Atas hal ini, Sonic pun mencari strategi agar bisa menggagalkan rencara Dr. Robotnik. Strategi yang dilakukan oleh Sonic ialah bekerjasama dengan sahabatnya, Tails. Kemudian bersama dengan Tails, Sonic memulai perjalanan untuk menemukan Master Emerald. Semua itu dilakukan dengan cepat, sebelum Master Emerald jatuh ke tangan yang salah.", 231 | "releaseDate": "2022-04-08", 232 | "poster": "https://image.tmdb.org/t/p/w500/6DrHO1jr3qVrViUO6s6kFiAGM7.jpg", 233 | "ageRating": 12, 234 | "price": 45000 235 | }, 236 | { 237 | "id": 26, 238 | "title": "Avengers: Infinity War", 239 | "description": "Karena Avengers dan sekutunya terus melindungi dunia dari ancaman yang terlalu besar untuk ditangani oleh seorang pahlawan, bahaya baru telah muncul dari bayangan kosmik: Thanos. Seorang lalim penghujatan intergalaksi, tujuannya adalah untuk mengumpulkan semua enam Batu Infinity, artefak kekuatan yang tak terbayangkan, dan menggunakannya untuk menimbulkan kehendak memutar pada semua realitas. Segala sesuatu yang telah diperjuangkan oleh Avengers telah berkembang hingga saat ini - nasib Bumi dan keberadaannya sendiri tidak pernah lebih pasti.", 240 | "releaseDate": "2018-04-25", 241 | "poster": "https://image.tmdb.org/t/p/w500/7WsyChQLEftFiDOVTGkv3hFpyyt.jpg", 242 | "ageRating": 10, 243 | "price": 46000 244 | }, 245 | { 246 | "id": 27, 247 | "title": "The Whale", 248 | "description": "Seorang guru bahasa Inggris yang tertutup dan gemuk mencoba untuk berhubungan kembali dengan putri remajanya yang terasing.", 249 | "releaseDate": "2022-12-09", 250 | "poster": "https://image.tmdb.org/t/p/w500/jQ0gylJMxWSL490sy0RrPj1Lj7e.jpg", 251 | "ageRating": 15, 252 | "price": 55000 253 | }, 254 | { 255 | "id": 28, 256 | "title": "The Batman", 257 | "description": "Ketika seorang pembunuh berantai sadis mulai membunuh tokoh-tokoh politik penting di Gotham, Batman terpaksa menyelidiki korupsi tersembunyi di kota itu dan mempertanyakan keterlibatan keluarganya.", 258 | "releaseDate": "2022-03-01", 259 | "poster": "https://image.tmdb.org/t/p/w500/seyWFgGInaLqW7nOZvu0ZC95rtx.jpg", 260 | "ageRating": 13, 261 | "price": 53000 262 | }, 263 | { 264 | "id": 29, 265 | "title": "Smile", 266 | "description": "Setelah menyaksikan kejadian aneh dan traumatis yang melibatkan seorang pasien, Dr. Rose Cotter mulai mengalami kejadian menakutkan yang tidak dapat dia jelaskan. Saat teror luar biasa mulai mengambil alih hidupnya, Rose harus menghadapi masa lalunya yang bermasalah untuk bertahan hidup dan melarikan diri dari kenyataan barunya yang mengerikan.", 267 | "releaseDate": "2022-09-23", 268 | "poster": "https://image.tmdb.org/t/p/w500/67Myda9zANAnlS54rRjQF4dHNNG.jpg", 269 | "ageRating": 11, 270 | "price": 38000 271 | }, 272 | { 273 | "id": 30, 274 | "title": "Encanto", 275 | "description": "menceritakan tentang keluarga Madrigals, sebuah keluarga yang tinggal di rumah ajaib dan masing-masing anggota keluarga memiliki keajaibannya tersendiri. Pada jaman dahulu kala, Abuela bersama suami dan anak-anaknya melarikan diri dari kerusuhan di desa.", 276 | "releaseDate": "2021-10-13", 277 | "poster": "https://image.tmdb.org/t/p/w500/4j0PNHkMr5ax3IA8tjtxcmPU3QT.jpg", 278 | "ageRating": 12, 279 | "price": 44000 280 | } 281 | ] --------------------------------------------------------------------------------