feat: add screening service
This commit is contained in:
commit
13c322653a
56
.gitignore
vendored
Normal file
56
.gitignore
vendored
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# compiled output
|
||||||
|
/dist
|
||||||
|
/node_modules
|
||||||
|
/build
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
/coverage
|
||||||
|
/.nyc_output
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# temp directory
|
||||||
|
.temp
|
||||||
|
.tmp
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
98
README.md
Normal file
98
README.md
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||||
|
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||||
|
|
||||||
|
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||||
|
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||||
|
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||||
|
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||||
|
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
|
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||||
|
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
|
||||||
|
</p>
|
||||||
|
<!--[](https://opencollective.com/nest#backer)
|
||||||
|
[](https://opencollective.com/nest#sponsor)-->
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compile and run the project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# development
|
||||||
|
$ yarn run start
|
||||||
|
|
||||||
|
# watch mode
|
||||||
|
$ yarn run start:dev
|
||||||
|
|
||||||
|
# production mode
|
||||||
|
$ yarn run start:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# unit tests
|
||||||
|
$ yarn run test
|
||||||
|
|
||||||
|
# e2e tests
|
||||||
|
$ yarn run test:e2e
|
||||||
|
|
||||||
|
# test coverage
|
||||||
|
$ yarn run test:cov
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
|
||||||
|
|
||||||
|
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ yarn install -g @nestjs/mau
|
||||||
|
$ mau deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
Check out a few resources that may come in handy when working with NestJS:
|
||||||
|
|
||||||
|
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
|
||||||
|
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
|
||||||
|
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
|
||||||
|
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
|
||||||
|
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
|
||||||
|
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
|
||||||
|
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
|
||||||
|
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
## Stay in touch
|
||||||
|
|
||||||
|
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
|
||||||
|
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||||
|
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
|
||||||
35
eslint.config.mjs
Normal file
35
eslint.config.mjs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// @ts-check
|
||||||
|
import eslint from '@eslint/js';
|
||||||
|
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||||
|
import globals from 'globals';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{
|
||||||
|
ignores: ['eslint.config.mjs'],
|
||||||
|
},
|
||||||
|
eslint.configs.recommended,
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
eslintPluginPrettierRecommended,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
...globals.jest,
|
||||||
|
},
|
||||||
|
sourceType: 'commonjs',
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-floating-promises': 'warn',
|
||||||
|
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||||
|
"prettier/prettier": ["error", { endOfLine: "auto" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
8
nest-cli.json
Normal file
8
nest-cli.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"deleteOutDir": true
|
||||||
|
}
|
||||||
|
}
|
||||||
80
package.json
Normal file
80
package.json
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
{
|
||||||
|
"name": "screening-service",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"author": "",
|
||||||
|
"private": true,
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nest build",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
|
"start": "nest start",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
|
"start:prod": "node dist/main",
|
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage",
|
||||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@grpc/grpc-js": "^1.14.3",
|
||||||
|
"@nestjs/common": "^11.0.1",
|
||||||
|
"@nestjs/config": "^4.0.4",
|
||||||
|
"@nestjs/core": "^11.0.1",
|
||||||
|
"@nestjs/microservices": "^11.1.19",
|
||||||
|
"@nestjs/mongoose": "^11.0.4",
|
||||||
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
|
"@teacinema/common": "^1.6.0",
|
||||||
|
"@teacinema/contracts": "^1.5.0",
|
||||||
|
"@teacinema/core": "^1.0.9",
|
||||||
|
"mongoose": "^9.5.0",
|
||||||
|
"nanoid": "^5.1.9",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"rxjs": "^7.8.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
|
"@eslint/js": "^9.18.0",
|
||||||
|
"@nestjs/cli": "^11.0.0",
|
||||||
|
"@nestjs/schematics": "^11.0.0",
|
||||||
|
"@nestjs/testing": "^11.0.1",
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/jest": "^30.0.0",
|
||||||
|
"@types/node": "^22.10.7",
|
||||||
|
"@types/supertest": "^6.0.2",
|
||||||
|
"eslint": "^9.18.0",
|
||||||
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
"eslint-plugin-prettier": "^5.2.2",
|
||||||
|
"globals": "^16.0.0",
|
||||||
|
"jest": "^30.0.0",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"supertest": "^7.0.0",
|
||||||
|
"ts-jest": "^29.2.5",
|
||||||
|
"ts-loader": "^9.5.2",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"typescript-eslint": "^8.20.0"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
prettier.config.mjs
Normal file
7
prettier.config.mjs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import config from '@teacinema/core/prettier'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...config,
|
||||||
|
tabWidth: 2,
|
||||||
|
useTabs: false,
|
||||||
|
};
|
||||||
14
src/app.module.ts
Normal file
14
src/app.module.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Module } from '@nestjs/common'
|
||||||
|
import { ConfigModule } from '@nestjs/config'
|
||||||
|
|
||||||
|
import { DatabaseModule } from './infra/database/database.module'
|
||||||
|
import { ScreeningModule } from './modules/screening/infra/screening.module'
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({ isGlobal: true }),
|
||||||
|
DatabaseModule,
|
||||||
|
ScreeningModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
11
src/config/mongoose.config.ts
Normal file
11
src/config/mongoose.config.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { ConfigService } from '@nestjs/config'
|
||||||
|
import { MongooseModuleOptions } from '@nestjs/mongoose'
|
||||||
|
|
||||||
|
export function getMongooseConfig(
|
||||||
|
config: ConfigService
|
||||||
|
): MongooseModuleOptions {
|
||||||
|
return {
|
||||||
|
uri: config.getOrThrow<string>('MONGO_URI'),
|
||||||
|
serverSelectionTimeoutMS: 5000
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/infra/database/database.module.ts
Normal file
15
src/infra/database/database.module.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Module } from '@nestjs/common'
|
||||||
|
import { ConfigService } from '@nestjs/config'
|
||||||
|
import { MongooseModule } from '@nestjs/mongoose'
|
||||||
|
|
||||||
|
import { getMongooseConfig } from '../../config/mongoose.config'
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
MongooseModule.forRootAsync({
|
||||||
|
useFactory: getMongooseConfig,
|
||||||
|
inject: [ConfigService]
|
||||||
|
})
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class DatabaseModule {}
|
||||||
34
src/main.ts
Normal file
34
src/main.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { ConfigService } from '@nestjs/config'
|
||||||
|
import { NestFactory } from '@nestjs/core'
|
||||||
|
import { MicroserviceOptions, Transport } from '@nestjs/microservices'
|
||||||
|
import { PROTO_PATHS } from '@teacinema/contracts'
|
||||||
|
|
||||||
|
import { AppModule } from './app.module'
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule)
|
||||||
|
const config = app.get(ConfigService)
|
||||||
|
const host = config.getOrThrow<string>('GRPC_HOST')
|
||||||
|
const port = config.getOrThrow<string>('GRPC_PORT')
|
||||||
|
const url = `${host}:${port}`
|
||||||
|
|
||||||
|
app.connectMicroservice<MicroserviceOptions>({
|
||||||
|
transport: Transport.GRPC,
|
||||||
|
options: {
|
||||||
|
url,
|
||||||
|
package: ['screening.v1'],
|
||||||
|
protoPath: [PROTO_PATHS.SCREENING],
|
||||||
|
loader: {
|
||||||
|
keepCase: false,
|
||||||
|
longs: String,
|
||||||
|
enums: String,
|
||||||
|
defaults: true,
|
||||||
|
oneofs: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await app.startAllMicroservices()
|
||||||
|
await app.init()
|
||||||
|
}
|
||||||
|
bootstrap()
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
import { Injectable } from '@nestjs/common'
|
||||||
|
import { RpcException } from '@nestjs/microservices'
|
||||||
|
import { RpcStatus } from '@teacinema/common'
|
||||||
|
import { nanoid } from 'nanoid'
|
||||||
|
|
||||||
|
import { ScreeningEntity } from '../../domain/entities/screening.entity'
|
||||||
|
import { HallPort } from '../../domain/ports/hall.port'
|
||||||
|
import { ScreeningRepositoryPort } from '../../domain/ports/screening.repository.port'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CreateScreeningUsecase {
|
||||||
|
public constructor(
|
||||||
|
private readonly repository: ScreeningRepositoryPort,
|
||||||
|
private readonly hallPort: HallPort
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async execute(input: {
|
||||||
|
movieId: string
|
||||||
|
hallId: string
|
||||||
|
startAt: string | Date
|
||||||
|
endAt: string | Date
|
||||||
|
}) {
|
||||||
|
const start = new Date(input.startAt)
|
||||||
|
const end = new Date(input.endAt)
|
||||||
|
|
||||||
|
if (!(start < end)) {
|
||||||
|
throw new RpcException({
|
||||||
|
code: RpcStatus.INVALID_ARGUMENTS,
|
||||||
|
details: 'Invalid time range'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const hall = await this.hallPort.findById(input.hallId)
|
||||||
|
if (!hall) {
|
||||||
|
throw new RpcException({
|
||||||
|
code: RpcStatus.NOT_FOUND,
|
||||||
|
details: 'Hall not found'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const overlap = await this.repository.findOverlap(hall.id, start, end)
|
||||||
|
if (overlap) {
|
||||||
|
throw new RpcException({
|
||||||
|
code: RpcStatus.ALREADY_EXISTS,
|
||||||
|
details: 'Screening overlap with existing screening'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const screening = new ScreeningEntity(
|
||||||
|
nanoid(),
|
||||||
|
hall.id,
|
||||||
|
input.movieId,
|
||||||
|
start,
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
await this.repository.create(screening)
|
||||||
|
|
||||||
|
return { ok: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
import { Injectable } from '@nestjs/common'
|
||||||
|
import { RpcException } from '@nestjs/microservices'
|
||||||
|
import { RpcStatus } from '@teacinema/common'
|
||||||
|
|
||||||
|
import { HallPort } from '../../domain/ports/hall.port'
|
||||||
|
import { MoviePort } from '../../domain/ports/movie.port'
|
||||||
|
import { ScreeningRepositoryPort } from '../../domain/ports/screening.repository.port'
|
||||||
|
import { SeatPort } from '../../domain/ports/seat.port'
|
||||||
|
import { TheaterPort } from '../../domain/ports/theater.port'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GetScreeningUsecase {
|
||||||
|
public constructor(
|
||||||
|
private readonly repository: ScreeningRepositoryPort,
|
||||||
|
private readonly hallPort: HallPort,
|
||||||
|
private readonly theaterPort: TheaterPort,
|
||||||
|
private readonly moviePort: MoviePort,
|
||||||
|
private readonly seatPort: SeatPort
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async execute(id: string) {
|
||||||
|
const screening = await this.repository.findById(id)
|
||||||
|
|
||||||
|
if (!screening) {
|
||||||
|
throw new RpcException({
|
||||||
|
code: RpcStatus.NOT_FOUND,
|
||||||
|
datails: 'Screening not found'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const [hall, movie] = await Promise.all([
|
||||||
|
this.hallPort.findById(screening.hallId),
|
||||||
|
this.moviePort.findById(screening.movieId)
|
||||||
|
])
|
||||||
|
|
||||||
|
if (!hall || !movie) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const [theater, seatTypes] = await Promise.all([
|
||||||
|
this.theaterPort.findById(hall.theaterId),
|
||||||
|
this.seatPort.listSeatTypes(screening.hallId, screening.id)
|
||||||
|
])
|
||||||
|
if (!theater) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: screening.id,
|
||||||
|
startAt: screening.startAt,
|
||||||
|
endAt: screening.endAt,
|
||||||
|
hall,
|
||||||
|
theater,
|
||||||
|
movie,
|
||||||
|
seatTypes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,128 @@
|
|||||||
|
import { Injectable } from '@nestjs/common'
|
||||||
|
|
||||||
|
import { resolveDayRange } from '../../../../shared/utils/resolve-day-range'
|
||||||
|
import { Hall, HallPort } from '../../domain/ports/hall.port'
|
||||||
|
import { Movie, MoviePort } from '../../domain/ports/movie.port'
|
||||||
|
import { ScreeningRepositoryPort } from '../../domain/ports/screening.repository.port'
|
||||||
|
import { SeatPort } from '../../domain/ports/seat.port'
|
||||||
|
import { Theater, TheaterPort } from '../../domain/ports/theater.port'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GetScreeningsUsecase {
|
||||||
|
public constructor(
|
||||||
|
private readonly repository: ScreeningRepositoryPort,
|
||||||
|
private readonly hallPort: HallPort,
|
||||||
|
private readonly theaterPort: TheaterPort,
|
||||||
|
private readonly moviePort: MoviePort,
|
||||||
|
private readonly seatPort: SeatPort
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async execute(input: { date?: string; theaterId?: string }) {
|
||||||
|
const { start, end } = resolveDayRange(input.date)
|
||||||
|
|
||||||
|
let hallIds: string[] | undefined
|
||||||
|
|
||||||
|
if (input.theaterId) {
|
||||||
|
const halls = await this.hallPort.listByTheater(input.theaterId)
|
||||||
|
if (halls.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
hallIds = halls.map(hall => hall.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const screenings = await this.repository.findByDateRange(
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
hallIds
|
||||||
|
)
|
||||||
|
|
||||||
|
if (screenings.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const theaterCache = new Map<string, Promise<Theater | null>>()
|
||||||
|
const hallCache = new Map<string, Promise<Hall | null>>()
|
||||||
|
const movieCache = new Map<string, Promise<Movie | null>>()
|
||||||
|
|
||||||
|
const enriched = await Promise.all(
|
||||||
|
screenings.map(async screening => {
|
||||||
|
const hall = await this.getHallCached(screening.hallId, hallCache)
|
||||||
|
if (!hall) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const theater = await this.getTheaterCached(
|
||||||
|
hall.theaterId,
|
||||||
|
theaterCache
|
||||||
|
)
|
||||||
|
if (!theater) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const movie = await this.getMovieCached(screening.movieId, movieCache)
|
||||||
|
if (!movie) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const seatTypes = await this.seatPort.listSeatTypes(
|
||||||
|
screening.hallId,
|
||||||
|
screening.id
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: screening.id,
|
||||||
|
startAt: screening.startAt,
|
||||||
|
endAt: screening.endAt,
|
||||||
|
hall,
|
||||||
|
theater,
|
||||||
|
movie,
|
||||||
|
seatTypes
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return enriched.filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTheaterCached(
|
||||||
|
theaterId: string,
|
||||||
|
cache: Map<string, Promise<Theater | null>>
|
||||||
|
): Promise<Theater | null> {
|
||||||
|
let promise = cache.get(theaterId)
|
||||||
|
|
||||||
|
if (!promise) {
|
||||||
|
promise = this.theaterPort.findById(theaterId)
|
||||||
|
cache.set(theaterId, promise)
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
private getHallCached(
|
||||||
|
hallId: string,
|
||||||
|
cache: Map<string, Promise<Hall | null>>
|
||||||
|
): Promise<Hall | null> {
|
||||||
|
let promise = cache.get(hallId)
|
||||||
|
|
||||||
|
if (!promise) {
|
||||||
|
promise = this.hallPort.findById(hallId)
|
||||||
|
cache.set(hallId, promise)
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMovieCached(
|
||||||
|
movieId: string,
|
||||||
|
cache: Map<string, Promise<Movie | null>>
|
||||||
|
): Promise<Movie | null> {
|
||||||
|
let promise = cache.get(movieId)
|
||||||
|
|
||||||
|
if (!promise) {
|
||||||
|
promise = this.moviePort.findById(movieId)
|
||||||
|
cache.set(movieId, promise)
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,95 @@
|
|||||||
|
import { Injectable } from '@nestjs/common'
|
||||||
|
|
||||||
|
import { resolveDayRange } from '../../../../shared/utils/resolve-day-range'
|
||||||
|
import { Hall, HallPort } from '../../domain/ports/hall.port'
|
||||||
|
import { ScreeningRepositoryPort } from '../../domain/ports/screening.repository.port'
|
||||||
|
import { SeatPort } from '../../domain/ports/seat.port'
|
||||||
|
import { Theater, TheaterPort } from '../../domain/ports/theater.port'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GetScreeningsByMovieUsecase {
|
||||||
|
public constructor(
|
||||||
|
private readonly repository: ScreeningRepositoryPort,
|
||||||
|
private readonly theaterPort: TheaterPort,
|
||||||
|
private readonly hallPort: HallPort,
|
||||||
|
private readonly seatPort: SeatPort
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async execute(input: { movieId: string; date?: string }) {
|
||||||
|
const { start, end } = resolveDayRange(input.date)
|
||||||
|
|
||||||
|
const screenings = await this.repository.findManyByMovie(
|
||||||
|
input.movieId,
|
||||||
|
start,
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
if (screenings.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const theaterCache = new Map<string, Promise<Theater | null>>()
|
||||||
|
const hallCache = new Map<string, Promise<Hall | null>>()
|
||||||
|
|
||||||
|
const enriched = await Promise.all(
|
||||||
|
screenings.map(async screening => {
|
||||||
|
const hall = await this.getHallCached(screening.hallId, hallCache)
|
||||||
|
if (!hall) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const theater = await this.getTheaterCached(
|
||||||
|
hall.theaterId,
|
||||||
|
theaterCache
|
||||||
|
)
|
||||||
|
if (!theater) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const seatTypes = await this.seatPort.listSeatTypes(
|
||||||
|
screening.hallId,
|
||||||
|
screening.id
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: screening.id,
|
||||||
|
startAt: screening.startAt,
|
||||||
|
endAt: screening.endAt,
|
||||||
|
hall,
|
||||||
|
theater,
|
||||||
|
seatTypes
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return enriched.filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTheaterCached(
|
||||||
|
theaterId: string,
|
||||||
|
cache: Map<string, Promise<Theater | null>>
|
||||||
|
): Promise<Theater | null> {
|
||||||
|
let promise = cache.get(theaterId)
|
||||||
|
|
||||||
|
if (!promise) {
|
||||||
|
promise = this.theaterPort.findById(theaterId)
|
||||||
|
cache.set(theaterId, promise)
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
private getHallCached(
|
||||||
|
hallId: string,
|
||||||
|
cache: Map<string, Promise<Hall | null>>
|
||||||
|
): Promise<Hall | null> {
|
||||||
|
let promise = cache.get(hallId)
|
||||||
|
|
||||||
|
if (!promise) {
|
||||||
|
promise = this.hallPort.findById(hallId)
|
||||||
|
cache.set(hallId, promise)
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
export class ScreeningEntity {
|
||||||
|
public constructor(
|
||||||
|
public readonly id: string,
|
||||||
|
public readonly hallId: string,
|
||||||
|
public readonly movieId: string,
|
||||||
|
public readonly startAt: Date,
|
||||||
|
public readonly endAt: Date
|
||||||
|
) {}
|
||||||
|
}
|
||||||
11
src/modules/screening/domain/ports/hall.port.ts
Normal file
11
src/modules/screening/domain/ports/hall.port.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export interface Hall {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
theaterId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class HallPort {
|
||||||
|
public abstract findById(id: string): Promise<Hall | null>
|
||||||
|
|
||||||
|
public abstract listByTheater(theaterId: string): Promise<Hall[]>
|
||||||
|
}
|
||||||
12
src/modules/screening/domain/ports/movie.port.ts
Normal file
12
src/modules/screening/domain/ports/movie.port.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export interface Movie {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
slug: string
|
||||||
|
poster: string
|
||||||
|
banner: string
|
||||||
|
duration: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class MoviePort {
|
||||||
|
public abstract findById(id: string): Promise<Movie | null>
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
import { ScreeningEntity } from '../entities/screening.entity'
|
||||||
|
|
||||||
|
export abstract class ScreeningRepositoryPort {
|
||||||
|
public abstract findById(id: string): Promise<ScreeningEntity | null>
|
||||||
|
public abstract findOverlap(
|
||||||
|
hallId: string,
|
||||||
|
startAt: Date,
|
||||||
|
endAt: Date
|
||||||
|
): Promise<ScreeningEntity | null>
|
||||||
|
|
||||||
|
public abstract findByDateRange(
|
||||||
|
dayStart: Date,
|
||||||
|
dayEnd: Date,
|
||||||
|
hallIds?: string[]
|
||||||
|
): Promise<ScreeningEntity[]>
|
||||||
|
|
||||||
|
public abstract findManyByMovie(
|
||||||
|
movieId: string,
|
||||||
|
dateStart?: Date,
|
||||||
|
dateEnd?: Date
|
||||||
|
): Promise<ScreeningEntity[]>
|
||||||
|
|
||||||
|
public abstract create(screening: ScreeningEntity): Promise<ScreeningEntity>
|
||||||
|
}
|
||||||
11
src/modules/screening/domain/ports/seat.port.ts
Normal file
11
src/modules/screening/domain/ports/seat.port.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export interface SeatType {
|
||||||
|
type: string
|
||||||
|
price: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class SeatPort {
|
||||||
|
public abstract listSeatTypes(
|
||||||
|
hallId: string,
|
||||||
|
screeningId: string
|
||||||
|
): Promise<SeatType[]>
|
||||||
|
}
|
||||||
9
src/modules/screening/domain/ports/theater.port.ts
Normal file
9
src/modules/screening/domain/ports/theater.port.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export interface Theater {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
address: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class TheaterPort {
|
||||||
|
public abstract findById(id: string): Promise<Theater | null>
|
||||||
|
}
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
import { InjectModel } from '@nestjs/mongoose'
|
||||||
|
import { ClientSession, Model, QueryFilter } from 'mongoose'
|
||||||
|
|
||||||
|
import { ScreeningEntity } from '../../../domain/entities/screening.entity'
|
||||||
|
import { ScreeningRepositoryPort } from '../../../domain/ports/screening.repository.port'
|
||||||
|
import { ScreeningMapper } from '../../mapper/screening.mapper'
|
||||||
|
import { ScreeningDocument, ScreeningModel } from '../schemas/screening.schema'
|
||||||
|
|
||||||
|
export class ScreeningMongoRepository implements ScreeningRepositoryPort {
|
||||||
|
public constructor(
|
||||||
|
@InjectModel(ScreeningModel.name)
|
||||||
|
private readonly screening: Model<ScreeningDocument>
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public async findOverlap(
|
||||||
|
hallId: string,
|
||||||
|
startAt: Date,
|
||||||
|
endAt: Date
|
||||||
|
): Promise<ScreeningEntity | null> {
|
||||||
|
const doc = await this.screening
|
||||||
|
.findOne({
|
||||||
|
hallId,
|
||||||
|
startAt: { $lt: endAt },
|
||||||
|
endAt: { $gt: startAt }
|
||||||
|
})
|
||||||
|
.lean()
|
||||||
|
.exec()
|
||||||
|
|
||||||
|
return doc ? ScreeningMapper.toEntity(doc) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
public async create(
|
||||||
|
screening: ScreeningEntity,
|
||||||
|
session?: ClientSession
|
||||||
|
): Promise<ScreeningEntity> {
|
||||||
|
const persistence = ScreeningMapper.toPersistence(screening)
|
||||||
|
const doc = new this.screening(persistence)
|
||||||
|
await doc.save({ session })
|
||||||
|
|
||||||
|
return ScreeningMapper.toEntity(doc.toObject())
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findById(id: string): Promise<ScreeningEntity | null> {
|
||||||
|
const doc = await this.screening.findById(id).lean().exec()
|
||||||
|
|
||||||
|
return doc ? ScreeningMapper.toEntity(doc) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findByDateRange(
|
||||||
|
dayStart: Date,
|
||||||
|
dayEnd: Date,
|
||||||
|
hallIds?: string[]
|
||||||
|
): Promise<ScreeningEntity[]> {
|
||||||
|
const query: QueryFilter<ScreeningDocument> = {
|
||||||
|
startAt: { $gte: dayStart, $lte: dayEnd },
|
||||||
|
...(hallIds?.length ? { hallId: { $in: hallIds } } : {})
|
||||||
|
}
|
||||||
|
|
||||||
|
const docs = await this.screening
|
||||||
|
.find(query)
|
||||||
|
.sort({ startAt: 1 })
|
||||||
|
.lean()
|
||||||
|
.exec()
|
||||||
|
|
||||||
|
return docs.map(doc => ScreeningMapper.toEntity(doc))
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findManyByMovie(
|
||||||
|
movieId: string,
|
||||||
|
dateStart?: Date,
|
||||||
|
dateEnd?: Date
|
||||||
|
): Promise<ScreeningEntity[]> {
|
||||||
|
const query: QueryFilter<ScreeningDocument> = {
|
||||||
|
movieId,
|
||||||
|
...(dateStart && dateEnd
|
||||||
|
? { startAt: { $gte: dateStart, $lte: dateEnd } }
|
||||||
|
: {})
|
||||||
|
}
|
||||||
|
|
||||||
|
const docs = await this.screening
|
||||||
|
.find(query)
|
||||||
|
.sort({ startAt: 1 })
|
||||||
|
.lean()
|
||||||
|
.exec()
|
||||||
|
return docs.map(doc => ScreeningMapper.toEntity(doc))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'
|
||||||
|
import { HydratedDocument } from 'mongoose'
|
||||||
|
import { nanoid } from 'nanoid'
|
||||||
|
|
||||||
|
export type ScreeningDocument = HydratedDocument<ScreeningModel>
|
||||||
|
|
||||||
|
@Schema({
|
||||||
|
_id: false,
|
||||||
|
collection: 'screenings',
|
||||||
|
timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' }
|
||||||
|
})
|
||||||
|
export class ScreeningModel {
|
||||||
|
@Prop({
|
||||||
|
type: String,
|
||||||
|
default: nanoid,
|
||||||
|
required: true
|
||||||
|
})
|
||||||
|
public _id: string
|
||||||
|
|
||||||
|
@Prop({
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
alias: 'hall_id'
|
||||||
|
})
|
||||||
|
public hallId: string
|
||||||
|
|
||||||
|
@Prop({
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
alias: 'movie_id'
|
||||||
|
})
|
||||||
|
public movieId: string
|
||||||
|
|
||||||
|
@Prop({
|
||||||
|
type: Date,
|
||||||
|
required: true,
|
||||||
|
alias: 'start_at'
|
||||||
|
})
|
||||||
|
public startAt: Date
|
||||||
|
|
||||||
|
@Prop({
|
||||||
|
type: Date,
|
||||||
|
required: true,
|
||||||
|
alias: 'end_at'
|
||||||
|
})
|
||||||
|
public endAt: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ScreeningSchema = SchemaFactory.createForClass(ScreeningModel)
|
||||||
|
|
||||||
|
ScreeningSchema.index({ hallId: 1 })
|
||||||
|
ScreeningSchema.index({ movieId: 1 })
|
||||||
30
src/modules/screening/infra/grpc/hall.grpc.adapter.ts
Normal file
30
src/modules/screening/infra/grpc/hall.grpc.adapter.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { Inject, OnModuleInit } from '@nestjs/common'
|
||||||
|
import type { ClientGrpc } from '@nestjs/microservices'
|
||||||
|
import { HallServiceClient } from '@teacinema/contracts/gen/ts/Hall'
|
||||||
|
import { lastValueFrom } from 'rxjs'
|
||||||
|
|
||||||
|
import { Hall, HallPort } from '../../domain/ports/hall.port'
|
||||||
|
|
||||||
|
export class HallGrpcAdapter implements HallPort, OnModuleInit {
|
||||||
|
public service: HallServiceClient
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
@Inject('HALL_PACKAGE') private readonly client: ClientGrpc
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public onModuleInit() {
|
||||||
|
this.service = this.client.getService<HallServiceClient>('HallService')
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findById(id: string): Promise<Hall | null> {
|
||||||
|
const { hall } = await lastValueFrom(this.service.getHall({ id }))
|
||||||
|
return hall ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
public async listByTheater(id: string): Promise<Hall[]> {
|
||||||
|
const { halls } = await lastValueFrom(
|
||||||
|
this.service.listHallsByTheater({ theaterId: id })
|
||||||
|
)
|
||||||
|
return halls ?? []
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/modules/screening/infra/grpc/movie.grpc.adapter.ts
Normal file
23
src/modules/screening/infra/grpc/movie.grpc.adapter.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { Inject, OnModuleInit } from '@nestjs/common'
|
||||||
|
import type { ClientGrpc } from '@nestjs/microservices'
|
||||||
|
import { MovieServiceClient } from '@teacinema/contracts/gen/ts/Movie'
|
||||||
|
import { lastValueFrom } from 'rxjs'
|
||||||
|
|
||||||
|
import { Movie, MoviePort } from '../../domain/ports/movie.port'
|
||||||
|
|
||||||
|
export class MovieGrpcAdapter implements MoviePort, OnModuleInit {
|
||||||
|
public service: MovieServiceClient
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
@Inject('MOVIE_PACKAGE') private readonly client: ClientGrpc
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public onModuleInit() {
|
||||||
|
this.service = this.client.getService<MovieServiceClient>('MovieService')
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findById(id: string): Promise<Movie | null> {
|
||||||
|
const { movie } = await lastValueFrom(this.service.getMovie({ id }))
|
||||||
|
return (movie as unknown as Movie) ?? null
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/modules/screening/infra/grpc/seat.grpc.adapter.ts
Normal file
41
src/modules/screening/infra/grpc/seat.grpc.adapter.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { Inject, OnModuleInit } from '@nestjs/common'
|
||||||
|
import type { ClientGrpc } from '@nestjs/microservices'
|
||||||
|
import { SeatServiceClient } from '@teacinema/contracts/gen/ts/Seat'
|
||||||
|
import { lastValueFrom } from 'rxjs'
|
||||||
|
|
||||||
|
import { SeatPort, SeatType } from '../../domain/ports/seat.port'
|
||||||
|
|
||||||
|
export class SeatGrpcAdapter implements SeatPort, OnModuleInit {
|
||||||
|
public service: SeatServiceClient
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
@Inject('SEAT_PACKAGE') private readonly client: ClientGrpc
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public onModuleInit() {
|
||||||
|
this.service = this.client.getService<SeatServiceClient>('SeatService')
|
||||||
|
}
|
||||||
|
|
||||||
|
public async listSeatTypes(
|
||||||
|
hallId: string,
|
||||||
|
screeningId: string
|
||||||
|
): Promise<SeatType[]> {
|
||||||
|
const res = await lastValueFrom(
|
||||||
|
this.service.listSeatsByHall({ hallId, screeningId })
|
||||||
|
)
|
||||||
|
const map = new Map<string, SeatType>()
|
||||||
|
|
||||||
|
for (const seat of res.seats) {
|
||||||
|
const key = `${seat.type}-${seat.price}`
|
||||||
|
|
||||||
|
if (!map.has(key)) {
|
||||||
|
map.set(key, {
|
||||||
|
type: seat.type,
|
||||||
|
price: seat.price
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(map.values())
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/modules/screening/infra/grpc/theater.grpc.adapter.ts
Normal file
24
src/modules/screening/infra/grpc/theater.grpc.adapter.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Inject, OnModuleInit } from '@nestjs/common'
|
||||||
|
import type { ClientGrpc } from '@nestjs/microservices'
|
||||||
|
import { TheaterServiceClient } from '@teacinema/contracts/gen/ts/theater'
|
||||||
|
import { lastValueFrom } from 'rxjs'
|
||||||
|
|
||||||
|
import { Theater, TheaterPort } from '../../domain/ports/theater.port'
|
||||||
|
|
||||||
|
export class TheaterGrpcAdapter implements TheaterPort, OnModuleInit {
|
||||||
|
public service: TheaterServiceClient
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
@Inject('THEATER_PACKAGE') private readonly client: ClientGrpc
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public onModuleInit() {
|
||||||
|
this.service =
|
||||||
|
this.client.getService<TheaterServiceClient>('TheaterService')
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findById(id: string): Promise<Theater | null> {
|
||||||
|
const { theater } = await lastValueFrom(this.service.getTheater({ id }))
|
||||||
|
return theater ?? null
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/modules/screening/infra/mapper/screening.mapper.ts
Normal file
31
src/modules/screening/infra/mapper/screening.mapper.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { ScreeningEntity } from '../../domain/entities/screening.entity'
|
||||||
|
|
||||||
|
interface ScreeningPersistence {
|
||||||
|
_id: string
|
||||||
|
hallId: string
|
||||||
|
movieId: string
|
||||||
|
startAt: Date
|
||||||
|
endAt: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ScreeningMapper {
|
||||||
|
public static toEntity(raw: ScreeningPersistence): ScreeningEntity {
|
||||||
|
return new ScreeningEntity(
|
||||||
|
raw._id.toString(),
|
||||||
|
raw.hallId,
|
||||||
|
raw.movieId,
|
||||||
|
raw.startAt,
|
||||||
|
raw.endAt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static toPersistence(entity: ScreeningEntity): ScreeningPersistence {
|
||||||
|
return {
|
||||||
|
_id: entity.id,
|
||||||
|
hallId: entity.hallId,
|
||||||
|
movieId: entity.movieId,
|
||||||
|
startAt: entity.startAt,
|
||||||
|
endAt: entity.endAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
102
src/modules/screening/infra/screening.module.ts
Normal file
102
src/modules/screening/infra/screening.module.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { Module } from '@nestjs/common'
|
||||||
|
import { ClientsModule, Transport } from '@nestjs/microservices'
|
||||||
|
import { MongooseModule } from '@nestjs/mongoose'
|
||||||
|
import { PROTO_PATHS } from '@teacinema/contracts'
|
||||||
|
|
||||||
|
import { CreateScreeningUsecase } from '../application/commands/create-screening.usecase'
|
||||||
|
import { GetScreeningUsecase } from '../application/queries/get-screening.usecase'
|
||||||
|
import { GetScreeningsUsecase } from '../application/queries/get-screenings.usecase'
|
||||||
|
import { GetScreeningsByMovieUsecase } from '../application/queries/get=screenings-by-movie.usecase'
|
||||||
|
import { HallPort } from '../domain/ports/hall.port'
|
||||||
|
import { MoviePort } from '../domain/ports/movie.port'
|
||||||
|
import { ScreeningRepositoryPort } from '../domain/ports/screening.repository.port'
|
||||||
|
import { SeatPort } from '../domain/ports/seat.port'
|
||||||
|
import { TheaterPort } from '../domain/ports/theater.port'
|
||||||
|
import { ScreeningGrpcController } from '../interfaces/grpc/screening.grpc.controller'
|
||||||
|
|
||||||
|
import { ScreeningMongoRepository } from './dataabse/repositories/screening.mongo.repository'
|
||||||
|
import {
|
||||||
|
ScreeningModel,
|
||||||
|
ScreeningSchema
|
||||||
|
} from './dataabse/schemas/screening.schema'
|
||||||
|
import { HallGrpcAdapter } from './grpc/hall.grpc.adapter'
|
||||||
|
import { MovieGrpcAdapter } from './grpc/movie.grpc.adapter'
|
||||||
|
import { SeatGrpcAdapter } from './grpc/seat.grpc.adapter'
|
||||||
|
import { TheaterGrpcAdapter } from './grpc/theater.grpc.adapter'
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
MongooseModule.forFeature([
|
||||||
|
{
|
||||||
|
name: ScreeningModel.name,
|
||||||
|
schema: ScreeningSchema
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
ClientsModule.register([
|
||||||
|
{
|
||||||
|
name: 'THEATER_PACKAGE',
|
||||||
|
transport: Transport.GRPC,
|
||||||
|
options: {
|
||||||
|
package: 'theater.v1',
|
||||||
|
protoPath: PROTO_PATHS.THEATER,
|
||||||
|
url: 'localhost:50054'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'HALL_PACKAGE',
|
||||||
|
transport: Transport.GRPC,
|
||||||
|
options: {
|
||||||
|
package: 'hall.v1',
|
||||||
|
protoPath: PROTO_PATHS.HALL,
|
||||||
|
url: 'localhost:50054'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'SEAT_PACKAGE',
|
||||||
|
transport: Transport.GRPC,
|
||||||
|
options: {
|
||||||
|
package: 'seat.v1',
|
||||||
|
protoPath: PROTO_PATHS.SEAT,
|
||||||
|
url: 'localhost:50054'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'MOVIE_PACKAGE',
|
||||||
|
transport: Transport.GRPC,
|
||||||
|
options: {
|
||||||
|
package: 'movie.v1',
|
||||||
|
protoPath: PROTO_PATHS.MOVIE,
|
||||||
|
url: 'localhost:50053'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
],
|
||||||
|
controllers: [ScreeningGrpcController],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: ScreeningRepositoryPort,
|
||||||
|
useClass: ScreeningMongoRepository
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: HallPort,
|
||||||
|
useClass: HallGrpcAdapter
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: SeatPort,
|
||||||
|
useClass: SeatGrpcAdapter
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: MoviePort,
|
||||||
|
useClass: MovieGrpcAdapter
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: TheaterPort,
|
||||||
|
useClass: TheaterGrpcAdapter
|
||||||
|
},
|
||||||
|
CreateScreeningUsecase,
|
||||||
|
GetScreeningsByMovieUsecase,
|
||||||
|
GetScreeningUsecase,
|
||||||
|
GetScreeningsUsecase
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class ScreeningModule {}
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
import { Controller } from '@nestjs/common'
|
||||||
|
import { GrpcMethod } from '@nestjs/microservices'
|
||||||
|
import type {
|
||||||
|
CreateScreeningRequest,
|
||||||
|
CreateScreeningResponse,
|
||||||
|
GetScreeningRequest,
|
||||||
|
GetScreeningResponse,
|
||||||
|
GetScreeningsByMovieRequest,
|
||||||
|
GetScreeningsByMovieResponse,
|
||||||
|
GetScreeningsRequest,
|
||||||
|
GetScreeningsResponse
|
||||||
|
} from '@teacinema/contracts/gen/ts/screening'
|
||||||
|
|
||||||
|
import { CreateScreeningUsecase } from '../../application/commands/create-screening.usecase'
|
||||||
|
import { GetScreeningUsecase } from '../../application/queries/get-screening.usecase'
|
||||||
|
import { GetScreeningsUsecase } from '../../application/queries/get-screenings.usecase'
|
||||||
|
import { GetScreeningsByMovieUsecase } from '../../application/queries/get=screenings-by-movie.usecase'
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class ScreeningGrpcController {
|
||||||
|
public constructor(
|
||||||
|
private readonly getAllByMovieUC: GetScreeningsByMovieUsecase,
|
||||||
|
private readonly getByIdUC: GetScreeningUsecase,
|
||||||
|
private readonly getAllUC: GetScreeningsUsecase,
|
||||||
|
private readonly createUC: CreateScreeningUsecase
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@GrpcMethod('ScreeningService', 'CreateScreening')
|
||||||
|
public async create(
|
||||||
|
data: CreateScreeningRequest
|
||||||
|
): Promise<CreateScreeningResponse> {
|
||||||
|
return this.createUC.execute(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
@GrpcMethod('ScreeningService', 'GetScreening')
|
||||||
|
public async getById(
|
||||||
|
data: GetScreeningRequest
|
||||||
|
): Promise<GetScreeningResponse> {
|
||||||
|
const screening = await this.getByIdUC.execute(data.id)
|
||||||
|
// @ts-expect-error -- todo
|
||||||
|
return { screening }
|
||||||
|
}
|
||||||
|
|
||||||
|
@GrpcMethod('ScreeningService', 'GetScreenings')
|
||||||
|
public async getAll(
|
||||||
|
data: GetScreeningsRequest
|
||||||
|
): Promise<GetScreeningsResponse> {
|
||||||
|
const screenings = await this.getAllUC.execute(data)
|
||||||
|
// @ts-expect-error -- todo
|
||||||
|
return { screenings }
|
||||||
|
}
|
||||||
|
|
||||||
|
@GrpcMethod('ScreeningService', 'GetScreeningsByMovie')
|
||||||
|
public async getAllByMovie(
|
||||||
|
data: GetScreeningsByMovieRequest
|
||||||
|
): Promise<GetScreeningsByMovieResponse> {
|
||||||
|
const screenings = await this.getAllByMovieUC.execute(data)
|
||||||
|
// @ts-expect-error -- todo
|
||||||
|
return { screenings }
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/shared/utils/resolve-day-range.ts
Normal file
11
src/shared/utils/resolve-day-range.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export function resolveDayRange(dateString?: string) {
|
||||||
|
const date = dateString ? new Date(dateString) : new Date()
|
||||||
|
|
||||||
|
const start = new Date(date)
|
||||||
|
start.setHours(0, 0, 0, 0)
|
||||||
|
|
||||||
|
const end = new Date(start)
|
||||||
|
end.setHours(23, 59, 59, 999)
|
||||||
|
|
||||||
|
return { start, end }
|
||||||
|
}
|
||||||
4
tsconfig.build.json
Normal file
4
tsconfig.build.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
||||||
25
tsconfig.json
Normal file
25
tsconfig.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "nodenext",
|
||||||
|
"moduleResolution": "nodenext",
|
||||||
|
"resolvePackageJsonExports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "ES2023",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"strictBindCallApply": false,
|
||||||
|
"noFallthroughCasesInSwitch": false
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user