From d08bc6d3fe8e723b64c7cb4a07dee9d084aff8d6 Mon Sep 17 00:00:00 2001 From: marshal Date: Thu, 28 May 2026 11:34:24 +0300 Subject: [PATCH 1/3] add service-types and cargo-types CRUD modules with pagination, filtering --- apps/edr-freight-api/package.json | 1 + apps/edr-freight-api/pnpm-lock.yaml | 206 ++++++++++++++++ apps/edr-freight-api/src/app.module.ts | 4 + ...8427600000-AddServiceTypesAndCargoTypes.ts | 228 ++++++++++++++++++ .../cargo-types/cargo-types.controller.ts | 64 +++++ .../modules/cargo-types/cargo-types.module.ts | 23 ++ .../cargo-types/cargo-types.repository.ts | 31 +++ .../cargo-types/cargo-types.service.ts | 135 +++++++++++ .../cargo-types/dto/create-cargo-type.dto.ts | 35 +++ .../cargo-types/dto/filter-cargo-type.dto.ts | 51 ++++ .../cargo-types/dto/update-cargo-type.dto.ts | 5 + .../cargo-types/entities/cargo-type.entity.ts | 36 +++ .../cargo-types.repository.interface.ts | 14 ++ .../dto/create-service-type.dto.ts | 51 ++++ .../dto/filter-service-type.dto.ts | 46 ++++ .../dto/update-service-type.dto.ts | 5 + .../entities/service-type.entity.ts | 34 +++ .../service-types.repository.interface.ts | 14 ++ .../service-types/service-types.controller.ts | 64 +++++ .../service-types/service-types.module.ts | 23 ++ .../service-types/service-types.repository.ts | 30 +++ .../service-types/service-types.service.ts | 111 +++++++++ 22 files changed, 1211 insertions(+) create mode 100644 apps/edr-freight-api/pnpm-lock.yaml create mode 100644 apps/edr-freight-api/src/migrations/1748427600000-AddServiceTypesAndCargoTypes.ts create mode 100644 apps/edr-freight-api/src/modules/cargo-types/cargo-types.controller.ts create mode 100644 apps/edr-freight-api/src/modules/cargo-types/cargo-types.module.ts create mode 100644 apps/edr-freight-api/src/modules/cargo-types/cargo-types.repository.ts create mode 100644 apps/edr-freight-api/src/modules/cargo-types/cargo-types.service.ts create mode 100644 apps/edr-freight-api/src/modules/cargo-types/dto/create-cargo-type.dto.ts create mode 100644 apps/edr-freight-api/src/modules/cargo-types/dto/filter-cargo-type.dto.ts create mode 100644 apps/edr-freight-api/src/modules/cargo-types/dto/update-cargo-type.dto.ts create mode 100644 apps/edr-freight-api/src/modules/cargo-types/entities/cargo-type.entity.ts create mode 100644 apps/edr-freight-api/src/modules/cargo-types/interfaces/cargo-types.repository.interface.ts create mode 100644 apps/edr-freight-api/src/modules/service-types/dto/create-service-type.dto.ts create mode 100644 apps/edr-freight-api/src/modules/service-types/dto/filter-service-type.dto.ts create mode 100644 apps/edr-freight-api/src/modules/service-types/dto/update-service-type.dto.ts create mode 100644 apps/edr-freight-api/src/modules/service-types/entities/service-type.entity.ts create mode 100644 apps/edr-freight-api/src/modules/service-types/interfaces/service-types.repository.interface.ts create mode 100644 apps/edr-freight-api/src/modules/service-types/service-types.controller.ts create mode 100644 apps/edr-freight-api/src/modules/service-types/service-types.module.ts create mode 100644 apps/edr-freight-api/src/modules/service-types/service-types.repository.ts create mode 100644 apps/edr-freight-api/src/modules/service-types/service-types.service.ts diff --git a/apps/edr-freight-api/package.json b/apps/edr-freight-api/package.json index 3b80db1bf..5114e20da 100644 --- a/apps/edr-freight-api/package.json +++ b/apps/edr-freight-api/package.json @@ -18,6 +18,7 @@ "@nestjs/common": "^11.0.0", "@nestjs/config": "^4.0.0", "@nestjs/core": "^11.0.0", + "@nestjs/mapped-types": "^2.1.1", "@nestjs/microservices": "^11.0.0", "@nestjs/platform-express": "^11.0.0", "@nestjs/swagger": "^11.4.2", diff --git a/apps/edr-freight-api/pnpm-lock.yaml b/apps/edr-freight-api/pnpm-lock.yaml new file mode 100644 index 000000000..7a7a3beb2 --- /dev/null +++ b/apps/edr-freight-api/pnpm-lock.yaml @@ -0,0 +1,206 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@edr/api-common': + specifier: workspace:* + version: link:../../packages/api-common + '@edr/types': + specifier: workspace:* + version: link:../../packages/types + '@nestjs/common': + specifier: ^11.0.0 + version: 11.1.24(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': + specifier: ^2.1.1 + version: 2.1.1(@nestjs/common@11.1.24(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2) + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + rxjs: + specifier: ^7.8.1 + version: 7.8.2 + devDependencies: + '@edr/eslint-config': + specifier: workspace:* + version: link:../../packages/config/eslint-config + '@edr/tsconfig': + specifier: workspace:* + version: link:../../packages/config/tsconfig + +packages: + + '@borewit/text-codec@0.2.2': + resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==} + + '@lukeed/csprng@1.1.0': + resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} + engines: {node: '>=8'} + + '@nestjs/common@11.1.24': + resolution: {integrity: sha512-9zHxaDDM+oXW9As6UsP5yYB+UqczBmpeSCIFWdPEtEukMnZhxODG1BBjaUcdBB8Sc1uzojSJSJlp3yFp853t1g==} + peerDependencies: + class-transformer: '>=0.4.1' + class-validator: '>=0.13.2' + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + + '@nestjs/mapped-types@2.1.1': + resolution: {integrity: sha512-SCCoMEJ6jdeI5h/N+KCVF1+pmg/hmEkNA5nHTS8Gvww7T/LCl4o1gFLinw2iQ60w7slFkszHcGLKGdazVI4F8A==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + class-transformer: ^0.4.0 || ^0.5.0 + class-validator: ^0.13.0 || ^0.14.0 || ^0.15.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + + '@tokenizer/inflate@0.4.1': + resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} + engines: {node: '>=18'} + + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + file-type@21.3.4: + resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==} + engines: {node: '>=20'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + iterare@1.2.1: + resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} + engines: {node: '>=6'} + + load-esm@1.0.3: + resolution: {integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==} + engines: {node: '>=13.2.0'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + strtok3@10.3.5: + resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} + engines: {node: '>=18'} + + token-types@6.1.2: + resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} + engines: {node: '>=14.16'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + uid@2.0.2: + resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} + engines: {node: '>=8'} + + uint8array-extras@1.5.0: + resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} + engines: {node: '>=18'} + +snapshots: + + '@borewit/text-codec@0.2.2': {} + + '@lukeed/csprng@1.1.0': {} + + '@nestjs/common@11.1.24(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + file-type: 21.3.4 + iterare: 1.2.1 + load-esm: 1.0.3 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + uid: 2.0.2 + transitivePeerDependencies: + - supports-color + + '@nestjs/mapped-types@2.1.1(@nestjs/common@11.1.24(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 11.1.24(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + + '@tokenizer/inflate@0.4.1': + dependencies: + debug: 4.4.3 + token-types: 6.1.2 + transitivePeerDependencies: + - supports-color + + '@tokenizer/token@0.3.0': {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + file-type@21.3.4: + dependencies: + '@tokenizer/inflate': 0.4.1 + strtok3: 10.3.5 + token-types: 6.1.2 + uint8array-extras: 1.5.0 + transitivePeerDependencies: + - supports-color + + ieee754@1.2.1: {} + + iterare@1.2.1: {} + + load-esm@1.0.3: {} + + ms@2.1.3: {} + + reflect-metadata@0.2.2: {} + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + strtok3@10.3.5: + dependencies: + '@tokenizer/token': 0.3.0 + + token-types@6.1.2: + dependencies: + '@borewit/text-codec': 0.2.2 + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + + tslib@2.8.1: {} + + uid@2.0.2: + dependencies: + '@lukeed/csprng': 1.1.0 + + uint8array-extras@1.5.0: {} diff --git a/apps/edr-freight-api/src/app.module.ts b/apps/edr-freight-api/src/app.module.ts index d1fdc085f..8403cb0bf 100644 --- a/apps/edr-freight-api/src/app.module.ts +++ b/apps/edr-freight-api/src/app.module.ts @@ -18,6 +18,8 @@ import { NotificationsModule } from "./modules/notifications/notifications.modul import { FileUploadSettingsModule } from "./modules/file-upload-settings/file-upload-settings.module"; import { DropdownSettingsModule } from "./modules/dropdown-settings/dropdown-settings.module"; import { OtpModule } from './modules/otp/otp.module'; +import { ServiceTypesModule } from "./modules/service-types/service-types.module"; +import { CargoTypesModule } from "./modules/cargo-types/cargo-types.module"; import { DropdownSettingsService } from "./modules/dropdown-settings/dropdown-settings.service"; @Module({ @@ -44,6 +46,8 @@ import { DropdownSettingsService } from "./modules/dropdown-settings/dropdown-se FileUploadSettingsModule, DropdownSettingsModule, OtpModule, + ServiceTypesModule, + CargoTypesModule, ], }) export class AppModule implements OnApplicationBootstrap { diff --git a/apps/edr-freight-api/src/migrations/1748427600000-AddServiceTypesAndCargoTypes.ts b/apps/edr-freight-api/src/migrations/1748427600000-AddServiceTypesAndCargoTypes.ts new file mode 100644 index 000000000..54ce5b656 --- /dev/null +++ b/apps/edr-freight-api/src/migrations/1748427600000-AddServiceTypesAndCargoTypes.ts @@ -0,0 +1,228 @@ +import { MigrationInterface, QueryRunner, Table, TableIndex, TableForeignKey } from "typeorm"; + +export class AddServiceTypesAndCargoTypes1748427600000 implements MigrationInterface { + name = "AddServiceTypesAndCargoTypes1748427600000"; + + public async up(queryRunner: QueryRunner): Promise { + // Create service_types table + await queryRunner.createTable( + new Table({ + name: "service_types", + schema: "freight", + columns: [ + { + name: "id", + type: "uuid", + isPrimary: true, + generationStrategy: "uuid", + default: "uuid_generate_v4()", + }, + { + name: "service_name", + type: "varchar", + length: "255", + isNullable: false, + }, + { + name: "description", + type: "text", + isNullable: true, + }, + { + name: "can_be_booked_alone", + type: "boolean", + default: true, + isNullable: false, + }, + { + name: "includes_first_mile", + type: "boolean", + default: false, + isNullable: false, + }, + { + name: "includes_last_mile", + type: "boolean", + default: false, + isNullable: false, + }, + { + name: "includes_customs", + type: "boolean", + default: false, + isNullable: false, + }, + { + name: "priority_bonus_points", + type: "int", + default: 0, + isNullable: false, + }, + { + name: "is_active", + type: "boolean", + default: true, + isNullable: false, + }, + { + name: "display_order", + type: "int", + default: 1, + isNullable: false, + }, + { + name: "created_at", + type: "timestamptz", + default: "now()", + isNullable: false, + }, + { + name: "updated_at", + type: "timestamptz", + default: "now()", + isNullable: false, + }, + { + name: "deleted_at", + type: "timestamptz", + isNullable: true, + }, + ], + }), + true, + ); + + // Create indexes for service_types + await queryRunner.createIndex( + "freight.service_types", + new TableIndex({ + name: "IDX_SERVICE_TYPES_IS_ACTIVE", + columnNames: ["is_active"], + }), + ); + await queryRunner.createIndex( + "freight.service_types", + new TableIndex({ + name: "IDX_SERVICE_TYPES_DISPLAY_ORDER", + columnNames: ["display_order"], + }), + ); + + // Create cargo_types table + await queryRunner.createTable( + new Table({ + name: "cargo_types", + schema: "freight", + columns: [ + { + name: "id", + type: "uuid", + isPrimary: true, + generationStrategy: "uuid", + default: "uuid_generate_v4()", + }, + { + name: "cargo_type_name", + type: "varchar", + length: "255", + isNullable: false, + }, + { + name: "parent_group_id", + type: "uuid", + isNullable: true, + }, + { + name: "show_free_text_box", + type: "boolean", + default: false, + isNullable: false, + }, + { + name: "requires_director_approval", + type: "boolean", + default: false, + isNullable: false, + }, + { + name: "is_active", + type: "boolean", + default: true, + isNullable: false, + }, + { + name: "display_order", + type: "int", + default: 1, + isNullable: false, + }, + { + name: "created_at", + type: "timestamptz", + default: "now()", + isNullable: false, + }, + { + name: "updated_at", + type: "timestamptz", + default: "now()", + isNullable: false, + }, + { + name: "deleted_at", + type: "timestamptz", + isNullable: true, + }, + ], + }), + true, + ); + + // Create indexes for cargo_types + await queryRunner.createIndex( + "freight.cargo_types", + new TableIndex({ + name: "IDX_CARGO_TYPES_IS_ACTIVE", + columnNames: ["is_active"], + }), + ); + await queryRunner.createIndex( + "freight.cargo_types", + new TableIndex({ + name: "IDX_CARGO_TYPES_DISPLAY_ORDER", + columnNames: ["display_order"], + }), + ); + await queryRunner.createIndex( + "freight.cargo_types", + new TableIndex({ + name: "IDX_CARGO_TYPES_PARENT_GROUP_ID", + columnNames: ["parent_group_id"], + }), + ); + + // Create self-referencing foreign key for cargo_types + await queryRunner.createForeignKey( + "freight.cargo_types", + new TableForeignKey({ + name: "FK_CARGO_TYPES_PARENT_GROUP", + columnNames: ["parent_group_id"], + referencedSchema: "freight", + referencedTableName: "cargo_types", + referencedColumnNames: ["id"], + onDelete: "SET NULL", + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + // Drop foreign key first + await queryRunner.dropForeignKey("freight.cargo_types", "FK_CARGO_TYPES_PARENT_GROUP"); + + // Drop cargo_types table + await queryRunner.dropTable("freight.cargo_types", true); + + // Drop service_types table + await queryRunner.dropTable("freight.service_types", true); + } +} diff --git a/apps/edr-freight-api/src/modules/cargo-types/cargo-types.controller.ts b/apps/edr-freight-api/src/modules/cargo-types/cargo-types.controller.ts new file mode 100644 index 000000000..03b9e6594 --- /dev/null +++ b/apps/edr-freight-api/src/modules/cargo-types/cargo-types.controller.ts @@ -0,0 +1,64 @@ +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Param, + ParseUUIDPipe, + Patch, + Post, + Query, +} from "@nestjs/common"; +import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger"; + +import { CargoTypesService } from "./cargo-types.service"; +import { CreateCargoTypeDto } from "./dto/create-cargo-type.dto"; +import { FilterCargoTypeDto } from "./dto/filter-cargo-type.dto"; +import { UpdateCargoTypeDto } from "./dto/update-cargo-type.dto"; + +@ApiTags("cargo-types") +@Controller("cargo-types") +// @UseGuards(JwtAuthGuard) — TODO: add when auth package integrated +@ApiBearerAuth() +export class CargoTypesController { + constructor(private readonly service: CargoTypesService) {} + + @Get() + @ApiOperation({ + summary: "List cargo types", + description: "Paginated list with optional filtering by isActive, requiresDirectorApproval, parentGroupId, and name search.", + }) + findAll(@Query() filter: FilterCargoTypeDto) { + return this.service.findAll(filter); + } + + @Get(":id") + @ApiOperation({ summary: "Get a cargo type by ID", description: "Returns the cargo type with parent and children relations." }) + findOne(@Param("id", ParseUUIDPipe) id: string) { + return this.service.findById(id); + } + + @Post() + @ApiOperation({ summary: "Create a new cargo type" }) + create(@Body() dto: CreateCargoTypeDto) { + return this.service.create(dto); + } + + @Patch(":id") + @ApiOperation({ summary: "Update a cargo type" }) + update( + @Param("id", ParseUUIDPipe) id: string, + @Body() dto: UpdateCargoTypeDto, + ) { + return this.service.update(id, dto); + } + + @Delete(":id") + @HttpCode(HttpStatus.NO_CONTENT) + @ApiOperation({ summary: "Soft-delete a cargo type" }) + remove(@Param("id", ParseUUIDPipe) id: string) { + return this.service.remove(id); + } +} diff --git a/apps/edr-freight-api/src/modules/cargo-types/cargo-types.module.ts b/apps/edr-freight-api/src/modules/cargo-types/cargo-types.module.ts new file mode 100644 index 000000000..a79759dfd --- /dev/null +++ b/apps/edr-freight-api/src/modules/cargo-types/cargo-types.module.ts @@ -0,0 +1,23 @@ +import { Module } from "@nestjs/common"; +import { TypeOrmModule } from "@nestjs/typeorm"; + +import { CargoType } from "./entities/cargo-type.entity"; +import { CARGO_TYPES_REPOSITORY } from "./interfaces/cargo-types.repository.interface"; +import { CargoTypesRepository } from "./cargo-types.repository"; +import { CargoTypesController } from "./cargo-types.controller"; +import { CargoTypesService } from "./cargo-types.service"; + +@Module({ + imports: [TypeOrmModule.forFeature([CargoType])], + controllers: [CargoTypesController], + providers: [ + CargoTypesRepository, + { + provide: CARGO_TYPES_REPOSITORY, + useExisting: CargoTypesRepository, + }, + CargoTypesService, + ], + exports: [CargoTypesService], +}) +export class CargoTypesModule {} diff --git a/apps/edr-freight-api/src/modules/cargo-types/cargo-types.repository.ts b/apps/edr-freight-api/src/modules/cargo-types/cargo-types.repository.ts new file mode 100644 index 000000000..4f71c61a1 --- /dev/null +++ b/apps/edr-freight-api/src/modules/cargo-types/cargo-types.repository.ts @@ -0,0 +1,31 @@ +import { BaseRepository } from "@edr/api-common"; +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { FindManyOptions, Repository } from "typeorm"; + +import { CargoType } from "./entities/cargo-type.entity"; +import { ICargoTypesRepository } from "./interfaces/cargo-types.repository.interface"; + +@Injectable() +export class CargoTypesRepository + extends BaseRepository + implements ICargoTypesRepository +{ + constructor( + @InjectRepository(CargoType) + repository: Repository, + ) { + super(repository); + } + + override findById(id: string): Promise { + return this.repository.findOne({ + where: { id }, + relations: { parent: true, children: true }, + }); + } + + override findAndCount(options?: FindManyOptions): Promise<[CargoType[], number]> { + return this.repository.findAndCount(options); + } +} diff --git a/apps/edr-freight-api/src/modules/cargo-types/cargo-types.service.ts b/apps/edr-freight-api/src/modules/cargo-types/cargo-types.service.ts new file mode 100644 index 000000000..2ad4339c5 --- /dev/null +++ b/apps/edr-freight-api/src/modules/cargo-types/cargo-types.service.ts @@ -0,0 +1,135 @@ +import { ConflictException, Inject, Injectable, NotFoundException } from "@nestjs/common"; +import { ILike } from "typeorm"; + +import { CreateCargoTypeDto } from "./dto/create-cargo-type.dto"; +import { FilterCargoTypeDto } from "./dto/filter-cargo-type.dto"; +import { UpdateCargoTypeDto } from "./dto/update-cargo-type.dto"; +import { CargoType } from "./entities/cargo-type.entity"; +import { + CARGO_TYPES_REPOSITORY, + ICargoTypesRepository, +} from "./interfaces/cargo-types.repository.interface"; + +@Injectable() +export class CargoTypesService { + constructor( + @Inject(CARGO_TYPES_REPOSITORY) + private readonly repository: ICargoTypesRepository, + ) {} + + /** + * Find all cargo types with pagination and optional filtering. + */ + async findAll(filter: FilterCargoTypeDto): Promise<{ + data: CargoType[]; + meta: { + total: number; + page: number; + pageSize: number; + totalPages: number; + }; + }> { + const where: Record = {}; + + if (filter.isActive !== undefined) { + where.isActive = filter.isActive; + } + + if (filter.requiresDirectorApproval !== undefined) { + where.requiresDirectorApproval = filter.requiresDirectorApproval; + } + + if (filter.parentGroupId !== undefined) { + where.parentGroupId = filter.parentGroupId; + } + + if (filter.search) { + where.cargoTypeName = ILike(`%${filter.search}%`); + } + + const [data, total] = await this.repository.findAndCount({ + where, + order: { [filter.sortBy!]: filter.sortOrder }, + skip: (filter.page! - 1) * filter.pageSize!, + take: filter.pageSize, + relations: { parent: true }, + }); + + return { + data, + meta: { + total, + page: filter.page!, + pageSize: filter.pageSize!, + totalPages: Math.ceil(total / filter.pageSize!), + }, + }; + } + + /** + * Get a single cargo type by ID. + */ + async findById(id: string): Promise { + const entity = await this.repository.findById(id); + if (!entity) { + throw new NotFoundException(`Cargo type ${id} not found`); + } + return entity; + } + + /** + * Create a new cargo type. + */ + async create(dto: CreateCargoTypeDto): Promise { + // Validate parent exists if provided + if (dto.parentGroupId) { + const parent = await this.repository.findById(dto.parentGroupId); + if (!parent) { + throw new NotFoundException(`Parent cargo type ${dto.parentGroupId} not found`); + } + } + + return this.repository.create({ + cargoTypeName: dto.cargoTypeName, + parentGroupId: dto.parentGroupId ?? null, + showFreeTextBox: dto.showFreeTextBox ?? false, + requiresDirectorApproval: dto.requiresDirectorApproval ?? false, + isActive: dto.isActive ?? true, + displayOrder: dto.displayOrder ?? 1, + }); + } + + /** + * Update an existing cargo type. + */ + async update(id: string, dto: UpdateCargoTypeDto): Promise { + await this.findById(id); + + // Validate parent exists if provided + const parentGroupId = dto.parentGroupId; + if (parentGroupId) { + const parent = await this.repository.findById(parentGroupId); + if (!parent) { + throw new NotFoundException(`Parent cargo type ${parentGroupId} not found`); + } + // Prevent circular reference + if (parentGroupId === id) { + throw new ConflictException("A cargo type cannot be its own parent"); + } + } + + const updated = await this.repository.update(id, dto); + if (!updated) { + throw new NotFoundException(`Cargo type ${id} not found`); + } + return this.findById(id); + } + + /** + * Soft-delete a cargo type. + */ + async remove(id: string): Promise { + await this.findById(id); + await this.repository.softDelete(id); + } +} diff --git a/apps/edr-freight-api/src/modules/cargo-types/dto/create-cargo-type.dto.ts b/apps/edr-freight-api/src/modules/cargo-types/dto/create-cargo-type.dto.ts new file mode 100644 index 000000000..add968ed5 --- /dev/null +++ b/apps/edr-freight-api/src/modules/cargo-types/dto/create-cargo-type.dto.ts @@ -0,0 +1,35 @@ +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { IsBoolean, IsInt, IsOptional, IsString, IsUUID, MaxLength, Min } from "class-validator"; + +export class CreateCargoTypeDto { + @ApiProperty({ description: "Cargo type name", maxLength: 255 }) + @IsString() + @MaxLength(255) + cargoTypeName!: string; + + @ApiPropertyOptional({ description: "Parent cargo type group ID (UUID) for hierarchical structure" }) + @IsOptional() + @IsUUID() + parentGroupId?: string; + + @ApiPropertyOptional({ description: "Show free text box for this cargo type", default: false }) + @IsOptional() + @IsBoolean() + showFreeTextBox?: boolean = false; + + @ApiPropertyOptional({ description: "Requires director approval for this cargo type", default: false }) + @IsOptional() + @IsBoolean() + requiresDirectorApproval?: boolean = false; + + @ApiPropertyOptional({ description: "Is the cargo type active", default: true }) + @IsOptional() + @IsBoolean() + isActive?: boolean = true; + + @ApiPropertyOptional({ description: "Display order for UI", default: 1 }) + @IsOptional() + @IsInt() + @Min(1) + displayOrder?: number = 1; +} diff --git a/apps/edr-freight-api/src/modules/cargo-types/dto/filter-cargo-type.dto.ts b/apps/edr-freight-api/src/modules/cargo-types/dto/filter-cargo-type.dto.ts new file mode 100644 index 000000000..69a6141a9 --- /dev/null +++ b/apps/edr-freight-api/src/modules/cargo-types/dto/filter-cargo-type.dto.ts @@ -0,0 +1,51 @@ +import { ApiPropertyOptional } from "@nestjs/swagger"; +import { Transform, Type } from "class-transformer"; +import { IsBoolean, IsIn, IsInt, IsOptional, IsString, IsUUID, Min } from "class-validator"; + +export class FilterCargoTypeDto { + @ApiPropertyOptional({ description: "Filter by active status" }) + @IsOptional() + @IsBoolean() + @Transform(({ value }) => value === "true" || value === true) + isActive?: boolean; + + @ApiPropertyOptional({ description: "Filter by requires director approval" }) + @IsOptional() + @IsBoolean() + @Transform(({ value }) => value === "true" || value === true) + requiresDirectorApproval?: boolean; + + @ApiPropertyOptional({ description: "Filter by parent group ID (or 'root' for top-level only)" }) + @IsOptional() + @IsUUID() + parentGroupId?: string; + + @ApiPropertyOptional({ description: "Search by cargo type name (case-insensitive)" }) + @IsOptional() + @IsString() + search?: string; + + @ApiPropertyOptional({ enum: ["cargoTypeName", "displayOrder", "createdAt"], default: "displayOrder" }) + @IsOptional() + @IsIn(["cargoTypeName", "displayOrder", "createdAt"]) + sortBy?: string = "displayOrder"; + + @ApiPropertyOptional({ enum: ["ASC", "DESC"], default: "ASC" }) + @IsOptional() + @IsIn(["ASC", "DESC"]) + sortOrder?: "ASC" | "DESC" = "ASC"; + + @ApiPropertyOptional({ description: "Page number", default: 1, minimum: 1 }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + page?: number = 1; + + @ApiPropertyOptional({ description: "Items per page", default: 20, minimum: 1 }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + pageSize?: number = 20; +} diff --git a/apps/edr-freight-api/src/modules/cargo-types/dto/update-cargo-type.dto.ts b/apps/edr-freight-api/src/modules/cargo-types/dto/update-cargo-type.dto.ts new file mode 100644 index 000000000..638422fd7 --- /dev/null +++ b/apps/edr-freight-api/src/modules/cargo-types/dto/update-cargo-type.dto.ts @@ -0,0 +1,5 @@ +import { PartialType } from "@nestjs/mapped-types"; + +import { CreateCargoTypeDto } from "./create-cargo-type.dto"; + +export class UpdateCargoTypeDto extends PartialType(CreateCargoTypeDto) {} diff --git a/apps/edr-freight-api/src/modules/cargo-types/entities/cargo-type.entity.ts b/apps/edr-freight-api/src/modules/cargo-types/entities/cargo-type.entity.ts new file mode 100644 index 000000000..752e9e965 --- /dev/null +++ b/apps/edr-freight-api/src/modules/cargo-types/entities/cargo-type.entity.ts @@ -0,0 +1,36 @@ +import { BaseEntity } from "@edr/api-common"; +import { Column, Entity, Index, ManyToOne, OneToMany, JoinColumn } from "typeorm"; + +@Entity({ schema: "freight", name: "cargo_types" }) +@Index(["is_active"]) +@Index(["display_order"]) +@Index(["parent_group_id"]) +export class CargoType extends BaseEntity { + @Column({ name: "cargo_type_name", type: "varchar", length: 255, nullable: false }) + cargoTypeName!: string; + + @Column({ name: "parent_group_id", type: "uuid", nullable: true }) + parentGroupId?: string | null; + + @Column({ name: "show_free_text_box", type: "boolean", default: false }) + showFreeTextBox!: boolean; + + @Column({ name: "requires_director_approval", type: "boolean", default: false }) + requiresDirectorApproval!: boolean; + + @Column({ name: "is_active", type: "boolean", default: true }) + isActive!: boolean; + + @Column({ name: "display_order", type: "int", default: 1 }) + displayOrder!: number; + + @ManyToOne(() => CargoType, (cargoType) => cargoType.children, { + nullable: true, + onDelete: "SET NULL", + }) + @JoinColumn({ name: "parent_group_id" }) + parent?: CargoType | null; + + @OneToMany(() => CargoType, (cargoType) => cargoType.parent) + children?: CargoType[]; +} diff --git a/apps/edr-freight-api/src/modules/cargo-types/interfaces/cargo-types.repository.interface.ts b/apps/edr-freight-api/src/modules/cargo-types/interfaces/cargo-types.repository.interface.ts new file mode 100644 index 000000000..21f501511 --- /dev/null +++ b/apps/edr-freight-api/src/modules/cargo-types/interfaces/cargo-types.repository.interface.ts @@ -0,0 +1,14 @@ +import { FindManyOptions } from "typeorm"; + +import { CargoType } from "../entities/cargo-type.entity"; + +export interface ICargoTypesRepository { + findById(id: string): Promise; + findAll(options?: FindManyOptions): Promise; + findAndCount(options?: FindManyOptions): Promise<[CargoType[], number]>; + create(data: Partial): Promise; + update(id: string, data: Partial): Promise; + softDelete(id: string): Promise; +} + +export const CARGO_TYPES_REPOSITORY = Symbol("CARGO_TYPES_REPOSITORY"); diff --git a/apps/edr-freight-api/src/modules/service-types/dto/create-service-type.dto.ts b/apps/edr-freight-api/src/modules/service-types/dto/create-service-type.dto.ts new file mode 100644 index 000000000..c4da74794 --- /dev/null +++ b/apps/edr-freight-api/src/modules/service-types/dto/create-service-type.dto.ts @@ -0,0 +1,51 @@ +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { IsBoolean, IsInt, IsOptional, IsString, MaxLength, Min } from "class-validator"; + +export class CreateServiceTypeDto { + @ApiProperty({ description: "Service type name", maxLength: 255 }) + @IsString() + @MaxLength(255) + serviceName!: string; + + @ApiPropertyOptional({ description: "Detailed description of the service" }) + @IsOptional() + @IsString() + description?: string; + + @ApiPropertyOptional({ description: "Can be booked alone", default: true }) + @IsOptional() + @IsBoolean() + canBeBookedAlone?: boolean = true; + + @ApiPropertyOptional({ description: "Includes first mile service", default: false }) + @IsOptional() + @IsBoolean() + includesFirstMile?: boolean = false; + + @ApiPropertyOptional({ description: "Includes last mile service", default: false }) + @IsOptional() + @IsBoolean() + includesLastMile?: boolean = false; + + @ApiPropertyOptional({ description: "Includes customs clearance", default: false }) + @IsOptional() + @IsBoolean() + includesCustoms?: boolean = false; + + @ApiPropertyOptional({ description: "Priority bonus points for booking priority", default: 0 }) + @IsOptional() + @IsInt() + @Min(0) + priorityBonusPoints?: number = 0; + + @ApiPropertyOptional({ description: "Is the service type active", default: true }) + @IsOptional() + @IsBoolean() + isActive?: boolean = true; + + @ApiPropertyOptional({ description: "Display order for UI", default: 1 }) + @IsOptional() + @IsInt() + @Min(1) + displayOrder?: number = 1; +} diff --git a/apps/edr-freight-api/src/modules/service-types/dto/filter-service-type.dto.ts b/apps/edr-freight-api/src/modules/service-types/dto/filter-service-type.dto.ts new file mode 100644 index 000000000..cd0992c96 --- /dev/null +++ b/apps/edr-freight-api/src/modules/service-types/dto/filter-service-type.dto.ts @@ -0,0 +1,46 @@ +import { ApiPropertyOptional } from "@nestjs/swagger"; +import { Transform, Type } from "class-transformer"; +import { IsBoolean, IsIn, IsInt, IsOptional, IsString, Min } from "class-validator"; + +export class FilterServiceTypeDto { + @ApiPropertyOptional({ description: "Filter by active status" }) + @IsOptional() + @IsBoolean() + @Transform(({ value }) => value === "true" || value === true) + isActive?: boolean; + + @ApiPropertyOptional({ description: "Filter by can be booked alone" }) + @IsOptional() + @IsBoolean() + @Transform(({ value }) => value === "true" || value === true) + canBeBookedAlone?: boolean; + + @ApiPropertyOptional({ description: "Search by service name (case-insensitive)" }) + @IsOptional() + @IsString() + search?: string; + + @ApiPropertyOptional({ enum: ["serviceName", "displayOrder", "createdAt"], default: "displayOrder" }) + @IsOptional() + @IsIn(["serviceName", "displayOrder", "createdAt"]) + sortBy?: string = "displayOrder"; + + @ApiPropertyOptional({ enum: ["ASC", "DESC"], default: "ASC" }) + @IsOptional() + @IsIn(["ASC", "DESC"]) + sortOrder?: "ASC" | "DESC" = "ASC"; + + @ApiPropertyOptional({ description: "Page number", default: 1, minimum: 1 }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + page?: number = 1; + + @ApiPropertyOptional({ description: "Items per page", default: 20, minimum: 1 }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + pageSize?: number = 20; +} diff --git a/apps/edr-freight-api/src/modules/service-types/dto/update-service-type.dto.ts b/apps/edr-freight-api/src/modules/service-types/dto/update-service-type.dto.ts new file mode 100644 index 000000000..25cd87146 --- /dev/null +++ b/apps/edr-freight-api/src/modules/service-types/dto/update-service-type.dto.ts @@ -0,0 +1,5 @@ +import { PartialType } from "@nestjs/mapped-types"; + +import { CreateServiceTypeDto } from "./create-service-type.dto"; + +export class UpdateServiceTypeDto extends PartialType(CreateServiceTypeDto) {} diff --git a/apps/edr-freight-api/src/modules/service-types/entities/service-type.entity.ts b/apps/edr-freight-api/src/modules/service-types/entities/service-type.entity.ts new file mode 100644 index 000000000..986add2c1 --- /dev/null +++ b/apps/edr-freight-api/src/modules/service-types/entities/service-type.entity.ts @@ -0,0 +1,34 @@ +import { BaseEntity } from "@edr/api-common"; +import { Column, Entity, Index } from "typeorm"; + +@Entity({ schema: "freight", name: "service_types" }) +@Index(["is_active"]) +@Index(["display_order"]) +export class ServiceType extends BaseEntity { + @Column({ name: "service_name", type: "varchar", length: 255, nullable: false }) + serviceName!: string; + + @Column({ name: "description", type: "text", nullable: true }) + description?: string | null; + + @Column({ name: "can_be_booked_alone", type: "boolean", default: true }) + canBeBookedAlone!: boolean; + + @Column({ name: "includes_first_mile", type: "boolean", default: false }) + includesFirstMile!: boolean; + + @Column({ name: "includes_last_mile", type: "boolean", default: false }) + includesLastMile!: boolean; + + @Column({ name: "includes_customs", type: "boolean", default: false }) + includesCustoms!: boolean; + + @Column({ name: "priority_bonus_points", type: "int", default: 0 }) + priorityBonusPoints!: number; + + @Column({ name: "is_active", type: "boolean", default: true }) + isActive!: boolean; + + @Column({ name: "display_order", type: "int", default: 1 }) + displayOrder!: number; +} diff --git a/apps/edr-freight-api/src/modules/service-types/interfaces/service-types.repository.interface.ts b/apps/edr-freight-api/src/modules/service-types/interfaces/service-types.repository.interface.ts new file mode 100644 index 000000000..8d4efd672 --- /dev/null +++ b/apps/edr-freight-api/src/modules/service-types/interfaces/service-types.repository.interface.ts @@ -0,0 +1,14 @@ +import { FindManyOptions } from "typeorm"; + +import { ServiceType } from "../entities/service-type.entity"; + +export interface IServiceTypesRepository { + findById(id: string): Promise; + findAll(options?: FindManyOptions): Promise; + findAndCount(options?: FindManyOptions): Promise<[ServiceType[], number]>; + create(data: Partial): Promise; + update(id: string, data: Partial): Promise; + softDelete(id: string): Promise; +} + +export const SERVICE_TYPES_REPOSITORY = Symbol("SERVICE_TYPES_REPOSITORY"); diff --git a/apps/edr-freight-api/src/modules/service-types/service-types.controller.ts b/apps/edr-freight-api/src/modules/service-types/service-types.controller.ts new file mode 100644 index 000000000..90203a493 --- /dev/null +++ b/apps/edr-freight-api/src/modules/service-types/service-types.controller.ts @@ -0,0 +1,64 @@ +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Param, + ParseUUIDPipe, + Patch, + Post, + Query, +} from "@nestjs/common"; +import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger"; + +import { CreateServiceTypeDto } from "./dto/create-service-type.dto"; +import { FilterServiceTypeDto } from "./dto/filter-service-type.dto"; +import { UpdateServiceTypeDto } from "./dto/update-service-type.dto"; +import { ServiceTypesService } from "./service-types.service"; + +@ApiTags("service-types") +@Controller("service-types") +// @UseGuards(JwtAuthGuard) — TODO: add when auth package integrated +@ApiBearerAuth() +export class ServiceTypesController { + constructor(private readonly service: ServiceTypesService) {} + + @Get() + @ApiOperation({ + summary: "List service types", + description: "Paginated list with optional filtering by isActive, canBeBookedAlone, and name search.", + }) + findAll(@Query() filter: FilterServiceTypeDto) { + return this.service.findAll(filter); + } + + @Get(":id") + @ApiOperation({ summary: "Get a service type by ID" }) + findOne(@Param("id", ParseUUIDPipe) id: string) { + return this.service.findById(id); + } + + @Post() + @ApiOperation({ summary: "Create a new service type" }) + create(@Body() dto: CreateServiceTypeDto) { + return this.service.create(dto); + } + + @Patch(":id") + @ApiOperation({ summary: "Update a service type" }) + update( + @Param("id", ParseUUIDPipe) id: string, + @Body() dto: UpdateServiceTypeDto, + ) { + return this.service.update(id, dto); + } + + @Delete(":id") + @HttpCode(HttpStatus.NO_CONTENT) + @ApiOperation({ summary: "Soft-delete a service type" }) + remove(@Param("id", ParseUUIDPipe) id: string) { + return this.service.remove(id); + } +} diff --git a/apps/edr-freight-api/src/modules/service-types/service-types.module.ts b/apps/edr-freight-api/src/modules/service-types/service-types.module.ts new file mode 100644 index 000000000..db4408af1 --- /dev/null +++ b/apps/edr-freight-api/src/modules/service-types/service-types.module.ts @@ -0,0 +1,23 @@ +import { Module } from "@nestjs/common"; +import { TypeOrmModule } from "@nestjs/typeorm"; + +import { ServiceType } from "./entities/service-type.entity"; +import { SERVICE_TYPES_REPOSITORY } from "./interfaces/service-types.repository.interface"; +import { ServiceTypesRepository } from "./service-types.repository"; +import { ServiceTypesController } from "./service-types.controller"; +import { ServiceTypesService } from "./service-types.service"; + +@Module({ + imports: [TypeOrmModule.forFeature([ServiceType])], + controllers: [ServiceTypesController], + providers: [ + ServiceTypesRepository, + { + provide: SERVICE_TYPES_REPOSITORY, + useExisting: ServiceTypesRepository, + }, + ServiceTypesService, + ], + exports: [ServiceTypesService], +}) +export class ServiceTypesModule {} diff --git a/apps/edr-freight-api/src/modules/service-types/service-types.repository.ts b/apps/edr-freight-api/src/modules/service-types/service-types.repository.ts new file mode 100644 index 000000000..299610e28 --- /dev/null +++ b/apps/edr-freight-api/src/modules/service-types/service-types.repository.ts @@ -0,0 +1,30 @@ +import { BaseRepository } from "@edr/api-common"; +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { FindManyOptions, Repository } from "typeorm"; + +import { ServiceType } from "./entities/service-type.entity"; +import { IServiceTypesRepository } from "./interfaces/service-types.repository.interface"; + +@Injectable() +export class ServiceTypesRepository + extends BaseRepository + implements IServiceTypesRepository +{ + constructor( + @InjectRepository(ServiceType) + repository: Repository, + ) { + super(repository); + } + + override findById(id: string): Promise { + return this.repository.findOne({ + where: { id }, + }); + } + + override findAndCount(options?: FindManyOptions): Promise<[ServiceType[], number]> { + return this.repository.findAndCount(options); + } +} diff --git a/apps/edr-freight-api/src/modules/service-types/service-types.service.ts b/apps/edr-freight-api/src/modules/service-types/service-types.service.ts new file mode 100644 index 000000000..b625eb72d --- /dev/null +++ b/apps/edr-freight-api/src/modules/service-types/service-types.service.ts @@ -0,0 +1,111 @@ +import { Inject, Injectable, NotFoundException } from "@nestjs/common"; +import { ILike } from "typeorm"; + +import { CreateServiceTypeDto } from "./dto/create-service-type.dto"; +import { FilterServiceTypeDto } from "./dto/filter-service-type.dto"; +import { UpdateServiceTypeDto } from "./dto/update-service-type.dto"; +import { ServiceType } from "./entities/service-type.entity"; +import { + IServiceTypesRepository, + SERVICE_TYPES_REPOSITORY, +} from "./interfaces/service-types.repository.interface"; + +@Injectable() +export class ServiceTypesService { + constructor( + @Inject(SERVICE_TYPES_REPOSITORY) + private readonly repository: IServiceTypesRepository, + ) {} + + /** + * Find all service types with pagination and optional filtering. + */ + async findAll(filter: FilterServiceTypeDto): Promise<{ + data: ServiceType[]; + meta: { + total: number; + page: number; + pageSize: number; + totalPages: number; + }; + }> { + const where: Record = {}; + + if (filter.isActive !== undefined) { + where.isActive = filter.isActive; + } + + if (filter.canBeBookedAlone !== undefined) { + where.canBeBookedAlone = filter.canBeBookedAlone; + } + + if (filter.search) { + where.serviceName = ILike(`%${filter.search}%`); + } + + const [data, total] = await this.repository.findAndCount({ + where, + order: { [filter.sortBy!]: filter.sortOrder }, + skip: (filter.page! - 1) * filter.pageSize!, + take: filter.pageSize, + }); + + return { + data, + meta: { + total, + page: filter.page!, + pageSize: filter.pageSize!, + totalPages: Math.ceil(total / filter.pageSize!), + }, + }; + } + + /** + * Get a single service type by ID. + */ + async findById(id: string): Promise { + const entity = await this.repository.findById(id); + if (!entity) { + throw new NotFoundException(`Service type ${id} not found`); + } + return entity; + } + + /** + * Create a new service type. + */ + async create(dto: CreateServiceTypeDto): Promise { + return this.repository.create({ + serviceName: dto.serviceName, + description: dto.description ?? null, + canBeBookedAlone: dto.canBeBookedAlone ?? true, + includesFirstMile: dto.includesFirstMile ?? false, + includesLastMile: dto.includesLastMile ?? false, + includesCustoms: dto.includesCustoms ?? false, + priorityBonusPoints: dto.priorityBonusPoints ?? 0, + isActive: dto.isActive ?? true, + displayOrder: dto.displayOrder ?? 1, + }); + } + + /** + * Update an existing service type. + */ + async update(id: string, dto: UpdateServiceTypeDto): Promise { + await this.findById(id); + const updated = await this.repository.update(id, dto); + if (!updated) { + throw new NotFoundException(`Service type ${id} not found`); + } + return this.findById(id); + } + + /** + * Soft-delete a service type. + */ + async remove(id: string): Promise { + await this.findById(id); + await this.repository.softDelete(id); + } +} From e1d05d54756244e4c96b7cbe40e5c25cc2d7fbbe Mon Sep 17 00:00:00 2001 From: marshal Date: Thu, 28 May 2026 11:52:29 +0300 Subject: [PATCH 2/3] add service-types and cargo-types CRUD modules with pagination, filtering --- apps/edr-freight-api/src/app.module.ts | 1 - .../src/modules/bookings/dto/update-booking.dto.ts | 2 +- .../src/modules/customers/dto/update-customer.dto.ts | 2 +- .../src/modules/customers2/dto/update-customer.dto.ts | 2 +- .../modules/dropdown-settings/dto/update-dropdown-option.dto.ts | 2 +- .../dropdown-settings/dto/update-dropdown-setting.dto.ts | 2 +- .../file-upload-settings/dto/update-file-upload-field.dto.ts | 2 +- .../file-upload-settings/dto/update-file-upload-setting.dto.ts | 2 +- 8 files changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/edr-freight-api/src/app.module.ts b/apps/edr-freight-api/src/app.module.ts index a770c2adf..6b0f072e8 100644 --- a/apps/edr-freight-api/src/app.module.ts +++ b/apps/edr-freight-api/src/app.module.ts @@ -20,7 +20,6 @@ import { DropdownSettingsModule } from "./modules/dropdown-settings/dropdown-set import { OtpModule } from './modules/otp/otp.module'; import { ServiceTypesModule } from "./modules/service-types/service-types.module"; import { CargoTypesModule } from "./modules/cargo-types/cargo-types.module"; -import { DropdownSettingsService } from "./modules/dropdown-settings/dropdown-settings.service"; import { BackofficeModule } from "./modules/backoffice/backoffice.module"; import { EdrOrgSeeder } from "./seed/edr-org.seeder"; diff --git a/apps/edr-freight-api/src/modules/bookings/dto/update-booking.dto.ts b/apps/edr-freight-api/src/modules/bookings/dto/update-booking.dto.ts index b97a624fb..2b97debc6 100644 --- a/apps/edr-freight-api/src/modules/bookings/dto/update-booking.dto.ts +++ b/apps/edr-freight-api/src/modules/bookings/dto/update-booking.dto.ts @@ -1,4 +1,4 @@ -import { PartialType } from "@nestjs/swagger"; +import { PartialType } from "@nestjs/mapped-types"; import { CreateBookingDto } from "./create-booking.dto"; diff --git a/apps/edr-freight-api/src/modules/customers/dto/update-customer.dto.ts b/apps/edr-freight-api/src/modules/customers/dto/update-customer.dto.ts index 3651f4b44..f8cefe046 100644 --- a/apps/edr-freight-api/src/modules/customers/dto/update-customer.dto.ts +++ b/apps/edr-freight-api/src/modules/customers/dto/update-customer.dto.ts @@ -1,5 +1,5 @@ // src/modules/customers/dto/update-customer.dto.ts -import { PartialType } from '@nestjs/swagger'; +import { PartialType } from '@nestjs/mapped-types'; import { CreateCustomerDto } from './create-customer.dto'; export class UpdateCustomerDto extends PartialType(CreateCustomerDto) { diff --git a/apps/edr-freight-api/src/modules/customers2/dto/update-customer.dto.ts b/apps/edr-freight-api/src/modules/customers2/dto/update-customer.dto.ts index 94b49d0fd..499d0ef9b 100644 --- a/apps/edr-freight-api/src/modules/customers2/dto/update-customer.dto.ts +++ b/apps/edr-freight-api/src/modules/customers2/dto/update-customer.dto.ts @@ -1,4 +1,4 @@ -import { PartialType } from "@nestjs/swagger"; +import { PartialType } from "@nestjs/mapped-types"; import { CreateCustomerDto } from "./create-customer.dto"; diff --git a/apps/edr-freight-api/src/modules/dropdown-settings/dto/update-dropdown-option.dto.ts b/apps/edr-freight-api/src/modules/dropdown-settings/dto/update-dropdown-option.dto.ts index aaa34dd2e..bbe0a3ecc 100644 --- a/apps/edr-freight-api/src/modules/dropdown-settings/dto/update-dropdown-option.dto.ts +++ b/apps/edr-freight-api/src/modules/dropdown-settings/dto/update-dropdown-option.dto.ts @@ -1,4 +1,4 @@ -import { PartialType } from "@nestjs/swagger"; +import { PartialType } from "@nestjs/mapped-types"; import { CreateDropdownOptionDto } from "./create-dropdown-option.dto"; diff --git a/apps/edr-freight-api/src/modules/dropdown-settings/dto/update-dropdown-setting.dto.ts b/apps/edr-freight-api/src/modules/dropdown-settings/dto/update-dropdown-setting.dto.ts index 8ad26229f..35908c419 100644 --- a/apps/edr-freight-api/src/modules/dropdown-settings/dto/update-dropdown-setting.dto.ts +++ b/apps/edr-freight-api/src/modules/dropdown-settings/dto/update-dropdown-setting.dto.ts @@ -1,4 +1,4 @@ -import { OmitType, PartialType } from "@nestjs/swagger"; +import { OmitType, PartialType } from "@nestjs/mapped-types"; import { CreateDropdownSettingDto } from "./create-dropdown-setting.dto"; diff --git a/apps/edr-freight-api/src/modules/file-upload-settings/dto/update-file-upload-field.dto.ts b/apps/edr-freight-api/src/modules/file-upload-settings/dto/update-file-upload-field.dto.ts index dae3ca4ca..abef2b5b7 100644 --- a/apps/edr-freight-api/src/modules/file-upload-settings/dto/update-file-upload-field.dto.ts +++ b/apps/edr-freight-api/src/modules/file-upload-settings/dto/update-file-upload-field.dto.ts @@ -1,4 +1,4 @@ -import { PartialType } from "@nestjs/swagger"; +import { PartialType } from "@nestjs/mapped-types"; import { CreateFileUploadFieldDto } from "./create-file-upload-field.dto"; diff --git a/apps/edr-freight-api/src/modules/file-upload-settings/dto/update-file-upload-setting.dto.ts b/apps/edr-freight-api/src/modules/file-upload-settings/dto/update-file-upload-setting.dto.ts index d8b6eb8ca..f055f4ebf 100644 --- a/apps/edr-freight-api/src/modules/file-upload-settings/dto/update-file-upload-setting.dto.ts +++ b/apps/edr-freight-api/src/modules/file-upload-settings/dto/update-file-upload-setting.dto.ts @@ -1,4 +1,4 @@ -import { OmitType, PartialType } from "@nestjs/swagger"; +import { OmitType, PartialType } from "@nestjs/mapped-types"; import { CreateFileUploadSettingDto } from "./create-file-upload-setting.dto"; From 3b53042cf86006f916c59c9912ffbdb3e2e51970 Mon Sep 17 00:00:00 2001 From: marshal Date: Thu, 28 May 2026 11:58:04 +0300 Subject: [PATCH 3/3] add service-types and cargo-types CRUD modules with pagination, filtering --- .../src/modules/cargo-types/entities/cargo-type.entity.ts | 6 +++--- .../modules/service-types/entities/service-type.entity.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/edr-freight-api/src/modules/cargo-types/entities/cargo-type.entity.ts b/apps/edr-freight-api/src/modules/cargo-types/entities/cargo-type.entity.ts index 752e9e965..14589ae4b 100644 --- a/apps/edr-freight-api/src/modules/cargo-types/entities/cargo-type.entity.ts +++ b/apps/edr-freight-api/src/modules/cargo-types/entities/cargo-type.entity.ts @@ -2,9 +2,9 @@ import { BaseEntity } from "@edr/api-common"; import { Column, Entity, Index, ManyToOne, OneToMany, JoinColumn } from "typeorm"; @Entity({ schema: "freight", name: "cargo_types" }) -@Index(["is_active"]) -@Index(["display_order"]) -@Index(["parent_group_id"]) +@Index(["isActive"]) +@Index(["displayOrder"]) +@Index(["parentGroupId"]) export class CargoType extends BaseEntity { @Column({ name: "cargo_type_name", type: "varchar", length: 255, nullable: false }) cargoTypeName!: string; diff --git a/apps/edr-freight-api/src/modules/service-types/entities/service-type.entity.ts b/apps/edr-freight-api/src/modules/service-types/entities/service-type.entity.ts index 986add2c1..756ece6e0 100644 --- a/apps/edr-freight-api/src/modules/service-types/entities/service-type.entity.ts +++ b/apps/edr-freight-api/src/modules/service-types/entities/service-type.entity.ts @@ -2,8 +2,8 @@ import { BaseEntity } from "@edr/api-common"; import { Column, Entity, Index } from "typeorm"; @Entity({ schema: "freight", name: "service_types" }) -@Index(["is_active"]) -@Index(["display_order"]) +@Index(["isActive"]) +@Index(["displayOrder"]) export class ServiceType extends BaseEntity { @Column({ name: "service_name", type: "varchar", length: 255, nullable: false }) serviceName!: string;