Merge branch 'update-ad' into 'release-2.6'
Draft: Ad update See merge request mobicoop/v3/service/ad!45
This commit is contained in:
		
						commit
						bd4a755f12
					
				| 
						 | 
				
			
			@ -232,6 +232,9 @@ The app exposes the following [gRPC](https://grpc.io/) services :
 | 
			
		|||
    -   waypoints: an array of addresses that represent the waypoints of the journey (only first and last waypoints are used for passenger ads). Note that positions are **required** and **must** be consecutives
 | 
			
		||||
    -   comment: optional freetext comment / description about the ad
 | 
			
		||||
 | 
			
		||||
-   **Update** : Replace the content of an ad
 | 
			
		||||
    Accepts the same data as the `Create` function + an ad id, and replace the given ad with the given data.
 | 
			
		||||
 | 
			
		||||
-   **Delete** : Delete permanently an ad
 | 
			
		||||
 | 
			
		||||
    ```json
 | 
			
		||||
| 
						 | 
				
			
			@ -246,13 +249,16 @@ The app exposes the following [gRPC](https://grpc.io/) services :
 | 
			
		|||
 | 
			
		||||
The service listens to these RabbitMQ messages:
 | 
			
		||||
 | 
			
		||||
-   **matcher-ad.created** (to update the status of pending ads)
 | 
			
		||||
-   **matcher-ad.creation-failed** (to update the status of pending ads)
 | 
			
		||||
-   **user.deleted** (to delete the associated ads)
 | 
			
		||||
 | 
			
		||||
### Emitted
 | 
			
		||||
 | 
			
		||||
As mentionned earlier, RabbitMQ messages are sent after these events :
 | 
			
		||||
 | 
			
		||||
-   **ad.created** (message: the created ad informations)
 | 
			
		||||
-   **ad.created** (message: the created ad information)
 | 
			
		||||
-   **ad.updated** (message: the updated ad information)
 | 
			
		||||
-   **ad.deleted** (message: the id of the deleted ad)
 | 
			
		||||
 | 
			
		||||
## Tests / ESLint / Prettier
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,7 +11,7 @@
 | 
			
		|||
      "dependencies": {
 | 
			
		||||
        "@grpc/grpc-js": "^1.9.14",
 | 
			
		||||
        "@grpc/proto-loader": "^0.7.10",
 | 
			
		||||
        "@mobicoop/ddd-library": "^2.4.3",
 | 
			
		||||
        "@mobicoop/ddd-library": "^2.5.0",
 | 
			
		||||
        "@mobicoop/health-module": "^2.3.2",
 | 
			
		||||
        "@mobicoop/message-broker-module": "^2.1.2",
 | 
			
		||||
        "@nestjs/common": "^10.3.0",
 | 
			
		||||
| 
						 | 
				
			
			@ -1852,9 +1852,9 @@
 | 
			
		|||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@mobicoop/ddd-library": {
 | 
			
		||||
      "version": "2.4.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@mobicoop/ddd-library/-/ddd-library-2.4.3.tgz",
 | 
			
		||||
      "integrity": "sha512-HxNtAfov8ne7XsFTSIDI811r3L1VDV9YUikgX7HPjrB8u2gQh6FQFnIz3Fjb/zWOGxrDEIy8HEM0AYmXkf8ULA==",
 | 
			
		||||
      "version": "2.5.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@mobicoop/ddd-library/-/ddd-library-2.5.0.tgz",
 | 
			
		||||
      "integrity": "sha512-dTx7KTILs53HCqNx0BDVTzIZxfPW3pi0fZ4UMw/vDNm3oTqGA+jg7YBfNxn8yadM+j2dDIN5Kum43CmKGH8yYA==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@nestjs/event-emitter": "^2.0.3",
 | 
			
		||||
        "@nestjs/microservices": "^10.3.0",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -34,7 +34,7 @@
 | 
			
		|||
    "@grpc/grpc-js": "^1.9.14",
 | 
			
		||||
    "@grpc/proto-loader": "^0.7.10",
 | 
			
		||||
    "@songkeys/nestjs-redis": "^10.0.0",
 | 
			
		||||
    "@mobicoop/ddd-library": "^2.4.3",
 | 
			
		||||
    "@mobicoop/ddd-library": "^2.5.0",
 | 
			
		||||
    "@mobicoop/health-module": "^2.3.2",
 | 
			
		||||
    "@mobicoop/message-broker-module": "^2.1.2",
 | 
			
		||||
    "@nestjs/common": "^10.3.0",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,7 @@ export const GRPC_SERVICE_NAME = 'AdService';
 | 
			
		|||
 | 
			
		||||
// messaging output
 | 
			
		||||
export const AD_CREATED_ROUTING_KEY = 'ad.created';
 | 
			
		||||
// messaging output
 | 
			
		||||
export const AD_UPDATED_ROUTING_KEY = 'ad.updated';
 | 
			
		||||
export const AD_DELETED_ROUTING_KEY = 'ad.deleted';
 | 
			
		||||
 | 
			
		||||
// messaging input
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,19 +1,21 @@
 | 
			
		|||
import { Mapper } from '@mobicoop/ddd-library';
 | 
			
		||||
import { AdResponseDto } from './interface/dtos/ad.response.dto';
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { AdEntity } from './core/domain/ad.entity';
 | 
			
		||||
import {
 | 
			
		||||
  AdWriteModel,
 | 
			
		||||
  AdReadModel,
 | 
			
		||||
  WaypointModel,
 | 
			
		||||
  ScheduleItemModel,
 | 
			
		||||
} from './infrastructure/ad.repository';
 | 
			
		||||
import { Frequency, Status } from './core/domain/ad.types';
 | 
			
		||||
import { WaypointProps } from './core/domain/value-objects/waypoint.value-object';
 | 
			
		||||
import { v4 } from 'uuid';
 | 
			
		||||
import { ScheduleItemProps } from './core/domain/value-objects/schedule-item.value-object';
 | 
			
		||||
import { OUTPUT_DATETIME_TRANSFORMER } from './ad.di-tokens';
 | 
			
		||||
import { DateTimeTransformerPort } from './core/application/ports/datetime-transformer.port';
 | 
			
		||||
import { AdEntity } from './core/domain/ad.entity';
 | 
			
		||||
import { Frequency, Status } from './core/domain/ad.types';
 | 
			
		||||
import { ScheduleItemProps } from './core/domain/value-objects/schedule-item.value-object';
 | 
			
		||||
import { WaypointProps } from './core/domain/value-objects/waypoint.value-object';
 | 
			
		||||
import {
 | 
			
		||||
  AdReadModel,
 | 
			
		||||
  AdWriteModel,
 | 
			
		||||
  ScheduleItemModel,
 | 
			
		||||
  ScheduleWriteModel,
 | 
			
		||||
  WaypointModel,
 | 
			
		||||
  WaypointWriteModel,
 | 
			
		||||
} from './infrastructure/ad.repository';
 | 
			
		||||
import { AdResponseDto } from './interface/dtos/ad.response.dto';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Mapper constructs objects that are used in different layers:
 | 
			
		||||
| 
						 | 
				
			
			@ -31,9 +33,8 @@ export class AdMapper
 | 
			
		|||
    private readonly outputDatetimeTransformer: DateTimeTransformerPort,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  toPersistence = (entity: AdEntity): AdWriteModel => {
 | 
			
		||||
  toPersistence = (entity: AdEntity, update?: boolean): AdWriteModel => {
 | 
			
		||||
    const copy = entity.getProps();
 | 
			
		||||
    const now = new Date();
 | 
			
		||||
    const record: AdWriteModel = {
 | 
			
		||||
      uuid: copy.id,
 | 
			
		||||
      userUuid: copy.userId,
 | 
			
		||||
| 
						 | 
				
			
			@ -43,50 +44,80 @@ export class AdMapper
 | 
			
		|||
      frequency: copy.frequency,
 | 
			
		||||
      fromDate: new Date(copy.fromDate),
 | 
			
		||||
      toDate: new Date(copy.toDate),
 | 
			
		||||
      schedule: copy.schedule
 | 
			
		||||
        ? {
 | 
			
		||||
            create: copy.schedule.map((scheduleItem: ScheduleItemProps) => ({
 | 
			
		||||
              uuid: v4(),
 | 
			
		||||
              day: scheduleItem.day as number,
 | 
			
		||||
              time: new Date(
 | 
			
		||||
                1970,
 | 
			
		||||
                0,
 | 
			
		||||
                1,
 | 
			
		||||
                parseInt(scheduleItem.time.split(':')[0]),
 | 
			
		||||
                parseInt(scheduleItem.time.split(':')[1]),
 | 
			
		||||
              ),
 | 
			
		||||
              margin: scheduleItem.margin as number,
 | 
			
		||||
              createdAt: now,
 | 
			
		||||
              updatedAt: now,
 | 
			
		||||
            })),
 | 
			
		||||
          }
 | 
			
		||||
        : undefined,
 | 
			
		||||
      schedule: this.toScheduleItemWriteModel(copy.schedule, update),
 | 
			
		||||
      seatsProposed: copy.seatsProposed as number,
 | 
			
		||||
      seatsRequested: copy.seatsRequested as number,
 | 
			
		||||
      strict: copy.strict as boolean,
 | 
			
		||||
      waypoints: copy.waypoints
 | 
			
		||||
        ? {
 | 
			
		||||
            create: copy.waypoints.map((waypoint: WaypointProps) => ({
 | 
			
		||||
              uuid: v4(),
 | 
			
		||||
              position: waypoint.position,
 | 
			
		||||
              name: waypoint.address.name,
 | 
			
		||||
              houseNumber: waypoint.address.houseNumber,
 | 
			
		||||
              street: waypoint.address.street,
 | 
			
		||||
              locality: waypoint.address.locality,
 | 
			
		||||
              postalCode: waypoint.address.postalCode,
 | 
			
		||||
              country: waypoint.address.country,
 | 
			
		||||
              lon: waypoint.address.coordinates.lon,
 | 
			
		||||
              lat: waypoint.address.coordinates.lat,
 | 
			
		||||
              createdAt: now,
 | 
			
		||||
              updatedAt: now,
 | 
			
		||||
            })),
 | 
			
		||||
          }
 | 
			
		||||
        : undefined,
 | 
			
		||||
      waypoints: this.toWaypointWriteModel(copy.waypoints, update),
 | 
			
		||||
      comment: copy.comment,
 | 
			
		||||
    };
 | 
			
		||||
    return record;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  toScheduleItemWriteModel = (
 | 
			
		||||
    schedule: ScheduleItemProps[],
 | 
			
		||||
    update?: boolean,
 | 
			
		||||
  ): ScheduleWriteModel | undefined => {
 | 
			
		||||
    if (!schedule) {
 | 
			
		||||
      return undefined;
 | 
			
		||||
    }
 | 
			
		||||
    const now = new Date();
 | 
			
		||||
    const record: ScheduleWriteModel = {
 | 
			
		||||
      create: schedule.map((scheduleItem: ScheduleItemProps) => ({
 | 
			
		||||
        uuid: v4(),
 | 
			
		||||
        day: scheduleItem.day,
 | 
			
		||||
        time: new Date(
 | 
			
		||||
          1970,
 | 
			
		||||
          0,
 | 
			
		||||
          1,
 | 
			
		||||
          parseInt(scheduleItem.time.split(':')[0]),
 | 
			
		||||
          parseInt(scheduleItem.time.split(':')[1]),
 | 
			
		||||
        ),
 | 
			
		||||
        margin: scheduleItem.margin,
 | 
			
		||||
        createdAt: now,
 | 
			
		||||
        updatedAt: now,
 | 
			
		||||
      })),
 | 
			
		||||
    };
 | 
			
		||||
    if (update) {
 | 
			
		||||
      record.deleteMany = {
 | 
			
		||||
        createdAt: { lt: now },
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
    return record;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  toWaypointWriteModel = (
 | 
			
		||||
    waypoints: WaypointProps[],
 | 
			
		||||
    update?: boolean,
 | 
			
		||||
  ): WaypointWriteModel | undefined => {
 | 
			
		||||
    if (!waypoints) {
 | 
			
		||||
      return undefined;
 | 
			
		||||
    }
 | 
			
		||||
    const now = new Date();
 | 
			
		||||
    const record: WaypointWriteModel = {
 | 
			
		||||
      create: waypoints.map((waypoint: WaypointProps) => ({
 | 
			
		||||
        uuid: v4(),
 | 
			
		||||
        position: waypoint.position,
 | 
			
		||||
        name: waypoint.address.name,
 | 
			
		||||
        houseNumber: waypoint.address.houseNumber,
 | 
			
		||||
        street: waypoint.address.street,
 | 
			
		||||
        locality: waypoint.address.locality,
 | 
			
		||||
        postalCode: waypoint.address.postalCode,
 | 
			
		||||
        country: waypoint.address.country,
 | 
			
		||||
        lon: waypoint.address.coordinates.lon,
 | 
			
		||||
        lat: waypoint.address.coordinates.lat,
 | 
			
		||||
        createdAt: now,
 | 
			
		||||
        updatedAt: now,
 | 
			
		||||
      })),
 | 
			
		||||
    };
 | 
			
		||||
    if (update) {
 | 
			
		||||
      record.deleteMany = {
 | 
			
		||||
        createdAt: { lt: now },
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
    return record;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  toDomain = (record: AdReadModel): AdEntity => {
 | 
			
		||||
    const entity = new AdEntity({
 | 
			
		||||
      id: record.uuid,
 | 
			
		||||
| 
						 | 
				
			
			@ -141,6 +172,7 @@ export class AdMapper
 | 
			
		|||
    response.userId = props.userId;
 | 
			
		||||
    response.driver = props.driver as boolean;
 | 
			
		||||
    response.passenger = props.passenger as boolean;
 | 
			
		||||
    response.strict = props.strict;
 | 
			
		||||
    response.status = props.status;
 | 
			
		||||
    response.frequency = props.frequency;
 | 
			
		||||
    response.fromDate = this.outputDatetimeTransformer.fromDate(
 | 
			
		||||
| 
						 | 
				
			
			@ -163,7 +195,7 @@ export class AdMapper
 | 
			
		|||
    response.schedule = props.schedule.map(
 | 
			
		||||
      (scheduleItem: ScheduleItemProps) => ({
 | 
			
		||||
        day: this.outputDatetimeTransformer.day(
 | 
			
		||||
          scheduleItem.day as number,
 | 
			
		||||
          scheduleItem.day,
 | 
			
		||||
          {
 | 
			
		||||
            date: props.fromDate,
 | 
			
		||||
            time: scheduleItem.time,
 | 
			
		||||
| 
						 | 
				
			
			@ -179,7 +211,7 @@ export class AdMapper
 | 
			
		|||
          },
 | 
			
		||||
          props.frequency,
 | 
			
		||||
        ),
 | 
			
		||||
        margin: scheduleItem.margin as number,
 | 
			
		||||
        margin: scheduleItem.margin,
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
    response.seatsProposed = props.seatsProposed as number;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,9 +14,11 @@ import { CreateAdService } from './core/application/commands/create-ad/create-ad
 | 
			
		|||
import { DeleteAdService } from './core/application/commands/delete-ad/delete-ad.service';
 | 
			
		||||
import { DeleteUserAdsService } from './core/application/commands/delete-user-ads/delete-user-ads.service';
 | 
			
		||||
import { InvalidateAdService } from './core/application/commands/invalidate-ad/invalidate-ad.service';
 | 
			
		||||
import { UpdateAdService } from './core/application/commands/update-ad/update-ad.service';
 | 
			
		||||
import { ValidateAdService } from './core/application/commands/validate-ad/validate-ad.service';
 | 
			
		||||
import { PublishMessageWhenAdIsDeletedDomainEventHandler } from './core/application/event-handlers/publish-message-when-ad-deleted.domain-event-handler';
 | 
			
		||||
import { PublishMessageWhenAdIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-ad-is-created.domain-event-handler';
 | 
			
		||||
import { PublishMessageWhenAdIsUpdatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-ad-is-updated.domain-event-handler';
 | 
			
		||||
import { FindAdByIdQueryHandler } from './core/application/queries/find-ad-by-id/find-ad-by-id.query-handler';
 | 
			
		||||
import { FindAdsByIdsQueryHandler } from './core/application/queries/find-ads-by-ids/find-ads-by-ids.query-handler';
 | 
			
		||||
import { FindAdsByUserIdQueryHandler } from './core/application/queries/find-ads-by-user-id/find-ads-by-user-id.query-handler';
 | 
			
		||||
| 
						 | 
				
			
			@ -31,12 +33,14 @@ import { DeleteAdGrpcController } from './interface/grpc-controllers/delete-ad.g
 | 
			
		|||
import { FindAdByIdGrpcController } from './interface/grpc-controllers/find-ad-by-id.grpc.controller';
 | 
			
		||||
import { FindAdsByIdsGrpcController } from './interface/grpc-controllers/find-ads-by-ids.grpc.controller';
 | 
			
		||||
import { FindAdsByUserIdGrpcController } from './interface/grpc-controllers/find-ads-by-user-id.grpc.controller';
 | 
			
		||||
import { UpdateAdGrpcController } from './interface/grpc-controllers/update-ad.grpc.controller';
 | 
			
		||||
import { MatcherAdCreatedMessageHandler } from './interface/message-handlers/matcher-ad-created.message-handler';
 | 
			
		||||
import { MatcherAdCreationFailedMessageHandler } from './interface/message-handlers/matcher-ad-creation-failed.message-handler';
 | 
			
		||||
import { UserDeletedMessageHandler } from './interface/message-handlers/user-deleted.message-handler';
 | 
			
		||||
 | 
			
		||||
const grpcControllers = [
 | 
			
		||||
  CreateAdGrpcController,
 | 
			
		||||
  UpdateAdGrpcController,
 | 
			
		||||
  DeleteAdGrpcController,
 | 
			
		||||
  FindAdByIdGrpcController,
 | 
			
		||||
  FindAdsByIdsGrpcController,
 | 
			
		||||
| 
						 | 
				
			
			@ -51,11 +55,13 @@ const messageHandlers = [
 | 
			
		|||
 | 
			
		||||
const eventHandlers: Provider[] = [
 | 
			
		||||
  PublishMessageWhenAdIsCreatedDomainEventHandler,
 | 
			
		||||
  PublishMessageWhenAdIsUpdatedDomainEventHandler,
 | 
			
		||||
  PublishMessageWhenAdIsDeletedDomainEventHandler,
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const commandHandlers: Provider[] = [
 | 
			
		||||
  CreateAdService,
 | 
			
		||||
  UpdateAdService,
 | 
			
		||||
  DeleteAdService,
 | 
			
		||||
  DeleteUserAdsService,
 | 
			
		||||
  ValidateAdService,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
import { ScheduleItem } from '../../types/schedule-item';
 | 
			
		||||
import { Waypoint } from '../../types/waypoint';
 | 
			
		||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
 | 
			
		||||
import { Command, CommandProps } from '@mobicoop/ddd-library';
 | 
			
		||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
 | 
			
		||||
import { ScheduleItemProps } from '@modules/ad/core/domain/value-objects/schedule-item.value-object';
 | 
			
		||||
import { Waypoint } from '../../types/waypoint';
 | 
			
		||||
 | 
			
		||||
export class CreateAdCommand extends Command {
 | 
			
		||||
  readonly userId: string;
 | 
			
		||||
| 
						 | 
				
			
			@ -10,7 +10,7 @@ export class CreateAdCommand extends Command {
 | 
			
		|||
  readonly frequency: Frequency;
 | 
			
		||||
  readonly fromDate: string;
 | 
			
		||||
  readonly toDate: string;
 | 
			
		||||
  readonly schedule: ScheduleItem[];
 | 
			
		||||
  readonly schedule: ScheduleItemProps[];
 | 
			
		||||
  readonly seatsProposed?: number;
 | 
			
		||||
  readonly seatsRequested?: number;
 | 
			
		||||
  readonly strict: boolean;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,17 +1,98 @@
 | 
			
		|||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
 | 
			
		||||
import { CreateAdCommand } from './create-ad.command';
 | 
			
		||||
import { Inject } from '@nestjs/common';
 | 
			
		||||
import { AggregateID, ConflictException } from '@mobicoop/ddd-library';
 | 
			
		||||
import {
 | 
			
		||||
  AD_REPOSITORY,
 | 
			
		||||
  INPUT_DATETIME_TRANSFORMER,
 | 
			
		||||
} from '@modules/ad/ad.di-tokens';
 | 
			
		||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
 | 
			
		||||
import { Waypoint } from '../../types/waypoint';
 | 
			
		||||
import { AdRepositoryPort } from '../../ports/ad.repository.port';
 | 
			
		||||
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
 | 
			
		||||
import { AggregateID, ConflictException } from '@mobicoop/ddd-library';
 | 
			
		||||
import { ScheduleItem } from '../../types/schedule-item';
 | 
			
		||||
import { ScheduleItemProps } from '@modules/ad/core/domain/value-objects/schedule-item.value-object';
 | 
			
		||||
import { Inject } from '@nestjs/common';
 | 
			
		||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
 | 
			
		||||
import { AdRepositoryPort } from '../../ports/ad.repository.port';
 | 
			
		||||
import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port';
 | 
			
		||||
import { Waypoint } from '../../types/waypoint';
 | 
			
		||||
import { CreateAdCommand } from './create-ad.command';
 | 
			
		||||
 | 
			
		||||
export function createPropsFromCommand(
 | 
			
		||||
  command: CreateAdCommand,
 | 
			
		||||
  datetimeTransformer: DateTimeTransformerPort,
 | 
			
		||||
) {
 | 
			
		||||
  return {
 | 
			
		||||
    userId: command.userId,
 | 
			
		||||
    driver: command.driver,
 | 
			
		||||
    passenger: command.passenger,
 | 
			
		||||
    frequency: command.frequency,
 | 
			
		||||
    //TODO Shouldn't that kind of logic be in the domain layer?
 | 
			
		||||
    fromDate: datetimeTransformer.fromDate(
 | 
			
		||||
      {
 | 
			
		||||
        date: command.fromDate,
 | 
			
		||||
        time: command.schedule[0].time,
 | 
			
		||||
        coordinates: {
 | 
			
		||||
          lon: command.waypoints[0].lon,
 | 
			
		||||
          lat: command.waypoints[0].lat,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      command.frequency,
 | 
			
		||||
    ),
 | 
			
		||||
    toDate: datetimeTransformer.toDate(
 | 
			
		||||
      command.toDate,
 | 
			
		||||
      {
 | 
			
		||||
        date: command.fromDate,
 | 
			
		||||
        time: command.schedule[0].time,
 | 
			
		||||
        coordinates: {
 | 
			
		||||
          lon: command.waypoints[0].lon,
 | 
			
		||||
          lat: command.waypoints[0].lat,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      command.frequency,
 | 
			
		||||
    ),
 | 
			
		||||
    schedule: command.schedule.map((scheduleItem: ScheduleItemProps) => ({
 | 
			
		||||
      day: datetimeTransformer.day(
 | 
			
		||||
        scheduleItem.day,
 | 
			
		||||
        {
 | 
			
		||||
          date: command.fromDate,
 | 
			
		||||
          time: scheduleItem.time,
 | 
			
		||||
          coordinates: {
 | 
			
		||||
            lon: command.waypoints[0].lon,
 | 
			
		||||
            lat: command.waypoints[0].lat,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        command.frequency,
 | 
			
		||||
      ),
 | 
			
		||||
      time: datetimeTransformer.time(
 | 
			
		||||
        {
 | 
			
		||||
          date: command.fromDate,
 | 
			
		||||
          time: scheduleItem.time,
 | 
			
		||||
          coordinates: {
 | 
			
		||||
            lon: command.waypoints[0].lon,
 | 
			
		||||
            lat: command.waypoints[0].lat,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        command.frequency,
 | 
			
		||||
      ),
 | 
			
		||||
      margin: scheduleItem.margin,
 | 
			
		||||
    })),
 | 
			
		||||
    seatsProposed: command.seatsProposed ?? 0,
 | 
			
		||||
    seatsRequested: command.seatsRequested ?? 0,
 | 
			
		||||
    strict: command.strict,
 | 
			
		||||
    waypoints: command.waypoints.map((waypoint: Waypoint) => ({
 | 
			
		||||
      position: waypoint.position,
 | 
			
		||||
      address: {
 | 
			
		||||
        name: waypoint.name,
 | 
			
		||||
        houseNumber: waypoint.houseNumber,
 | 
			
		||||
        street: waypoint.street,
 | 
			
		||||
        postalCode: waypoint.postalCode,
 | 
			
		||||
        locality: waypoint.locality,
 | 
			
		||||
        country: waypoint.country,
 | 
			
		||||
        coordinates: {
 | 
			
		||||
          lon: waypoint.lon,
 | 
			
		||||
          lat: waypoint.lat,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    })),
 | 
			
		||||
    comment: command.comment,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@CommandHandler(CreateAdCommand)
 | 
			
		||||
export class CreateAdService implements ICommandHandler {
 | 
			
		||||
| 
						 | 
				
			
			@ -23,80 +104,9 @@ export class CreateAdService implements ICommandHandler {
 | 
			
		|||
  ) {}
 | 
			
		||||
 | 
			
		||||
  async execute(command: CreateAdCommand): Promise<AggregateID> {
 | 
			
		||||
    const ad = AdEntity.create({
 | 
			
		||||
      userId: command.userId,
 | 
			
		||||
      driver: command.driver,
 | 
			
		||||
      passenger: command.passenger,
 | 
			
		||||
      frequency: command.frequency,
 | 
			
		||||
      fromDate: this.datetimeTransformer.fromDate(
 | 
			
		||||
        {
 | 
			
		||||
          date: command.fromDate,
 | 
			
		||||
          time: command.schedule[0].time,
 | 
			
		||||
          coordinates: {
 | 
			
		||||
            lon: command.waypoints[0].lon,
 | 
			
		||||
            lat: command.waypoints[0].lat,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        command.frequency,
 | 
			
		||||
      ),
 | 
			
		||||
      toDate: this.datetimeTransformer.toDate(
 | 
			
		||||
        command.toDate,
 | 
			
		||||
        {
 | 
			
		||||
          date: command.fromDate,
 | 
			
		||||
          time: command.schedule[0].time,
 | 
			
		||||
          coordinates: {
 | 
			
		||||
            lon: command.waypoints[0].lon,
 | 
			
		||||
            lat: command.waypoints[0].lat,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        command.frequency,
 | 
			
		||||
      ),
 | 
			
		||||
      schedule: command.schedule.map((scheduleItem: ScheduleItem) => ({
 | 
			
		||||
        day: this.datetimeTransformer.day(
 | 
			
		||||
          scheduleItem.day,
 | 
			
		||||
          {
 | 
			
		||||
            date: command.fromDate,
 | 
			
		||||
            time: scheduleItem.time,
 | 
			
		||||
            coordinates: {
 | 
			
		||||
              lon: command.waypoints[0].lon,
 | 
			
		||||
              lat: command.waypoints[0].lat,
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
          command.frequency,
 | 
			
		||||
        ),
 | 
			
		||||
        time: this.datetimeTransformer.time(
 | 
			
		||||
          {
 | 
			
		||||
            date: command.fromDate,
 | 
			
		||||
            time: scheduleItem.time,
 | 
			
		||||
            coordinates: {
 | 
			
		||||
              lon: command.waypoints[0].lon,
 | 
			
		||||
              lat: command.waypoints[0].lat,
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
          command.frequency,
 | 
			
		||||
        ),
 | 
			
		||||
        margin: scheduleItem.margin,
 | 
			
		||||
      })),
 | 
			
		||||
      seatsProposed: command.seatsProposed ?? 0,
 | 
			
		||||
      seatsRequested: command.seatsRequested ?? 0,
 | 
			
		||||
      strict: command.strict,
 | 
			
		||||
      waypoints: command.waypoints.map((waypoint: Waypoint) => ({
 | 
			
		||||
        position: waypoint.position,
 | 
			
		||||
        address: {
 | 
			
		||||
          name: waypoint.name,
 | 
			
		||||
          houseNumber: waypoint.houseNumber,
 | 
			
		||||
          street: waypoint.street,
 | 
			
		||||
          postalCode: waypoint.postalCode,
 | 
			
		||||
          locality: waypoint.locality,
 | 
			
		||||
          country: waypoint.country,
 | 
			
		||||
          coordinates: {
 | 
			
		||||
            lon: waypoint.lon,
 | 
			
		||||
            lat: waypoint.lat,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      })),
 | 
			
		||||
      comment: command.comment,
 | 
			
		||||
    });
 | 
			
		||||
    const ad = AdEntity.create(
 | 
			
		||||
      createPropsFromCommand(command, this.datetimeTransformer),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      await this.repository.insert(ad);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,16 @@
 | 
			
		|||
import { CommandProps } from '@mobicoop/ddd-library';
 | 
			
		||||
import { CreateAdCommand } from '../create-ad/create-ad.command';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Ad updates follow the PUT semantics: they replace the entire object.
 | 
			
		||||
 * Therefore the update command extends the create command to inherit the same properties
 | 
			
		||||
 * and re-use the data transformation logic.
 | 
			
		||||
 */
 | 
			
		||||
export class UpdateAdCommand extends CreateAdCommand {
 | 
			
		||||
  public adId: string;
 | 
			
		||||
 | 
			
		||||
  constructor(props: CommandProps<UpdateAdCommand>) {
 | 
			
		||||
    super(props);
 | 
			
		||||
    this.adId = props.adId;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,36 @@
 | 
			
		|||
import {
 | 
			
		||||
  AD_REPOSITORY,
 | 
			
		||||
  INPUT_DATETIME_TRANSFORMER,
 | 
			
		||||
} from '@modules/ad/ad.di-tokens';
 | 
			
		||||
import { AdUpdatedDomainEvent } from '@modules/ad/core/domain/events/ad.domain-event';
 | 
			
		||||
import { Inject } from '@nestjs/common';
 | 
			
		||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
 | 
			
		||||
import { EventEmitter2 } from '@nestjs/event-emitter';
 | 
			
		||||
import { AdRepositoryPort } from '../../ports/ad.repository.port';
 | 
			
		||||
import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port';
 | 
			
		||||
import { createPropsFromCommand } from '../create-ad/create-ad.service';
 | 
			
		||||
import { UpdateAdCommand } from './update-ad.command';
 | 
			
		||||
 | 
			
		||||
@CommandHandler(UpdateAdCommand)
 | 
			
		||||
export class UpdateAdService implements ICommandHandler {
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(AD_REPOSITORY)
 | 
			
		||||
    private readonly repository: AdRepositoryPort,
 | 
			
		||||
    @Inject(INPUT_DATETIME_TRANSFORMER)
 | 
			
		||||
    private readonly datetimeTransformer: DateTimeTransformerPort,
 | 
			
		||||
    private readonly eventEmitter: EventEmitter2,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  async execute(command: UpdateAdCommand): Promise<void> {
 | 
			
		||||
    const ad = await this.repository.findOneById(command.adId, {
 | 
			
		||||
      waypoints: true,
 | 
			
		||||
      schedule: true,
 | 
			
		||||
    });
 | 
			
		||||
    ad.update(createPropsFromCommand(command, this.datetimeTransformer));
 | 
			
		||||
    await this.repository.update(ad.id, ad);
 | 
			
		||||
    this.eventEmitter.emitAsync(
 | 
			
		||||
      AdUpdatedDomainEvent.name,
 | 
			
		||||
      new AdUpdatedDomainEvent(ad),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,44 @@
 | 
			
		|||
import {
 | 
			
		||||
  IntegrationEvent,
 | 
			
		||||
  IntegrationEventProps,
 | 
			
		||||
  MessagePublisherPort,
 | 
			
		||||
} from '@mobicoop/ddd-library';
 | 
			
		||||
import { AD_MESSAGE_PUBLISHER } from '@modules/ad/ad.di-tokens';
 | 
			
		||||
import { AdMapper } from '@modules/ad/ad.mapper';
 | 
			
		||||
import { AdResponseDto } from '@modules/ad/interface/dtos/ad.response.dto';
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { OnEvent } from '@nestjs/event-emitter';
 | 
			
		||||
import { AD_UPDATED_ROUTING_KEY } from '@src/app.constants';
 | 
			
		||||
import { v4 } from 'uuid';
 | 
			
		||||
import { AdUpdatedDomainEvent } from '../../domain/events/ad.domain-event';
 | 
			
		||||
 | 
			
		||||
class AdIntegrationEvent extends IntegrationEvent {
 | 
			
		||||
  readonly data: AdResponseDto;
 | 
			
		||||
 | 
			
		||||
  constructor(props: IntegrationEventProps<unknown>, data: AdResponseDto) {
 | 
			
		||||
    super(props);
 | 
			
		||||
    this.data = data;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class PublishMessageWhenAdIsUpdatedDomainEventHandler {
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(AD_MESSAGE_PUBLISHER)
 | 
			
		||||
    private readonly messagePublisher: MessagePublisherPort,
 | 
			
		||||
    private readonly mapper: AdMapper,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  @OnEvent(AdUpdatedDomainEvent.name, { async: true, promisify: true })
 | 
			
		||||
  async handle(event: AdUpdatedDomainEvent): Promise<void> {
 | 
			
		||||
    this.messagePublisher.publish(
 | 
			
		||||
      AD_UPDATED_ROUTING_KEY,
 | 
			
		||||
      JSON.stringify(
 | 
			
		||||
        new AdIntegrationEvent(
 | 
			
		||||
          { id: v4(), metadata: event.metadata },
 | 
			
		||||
          this.mapper.toResponse(event.ad),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +0,0 @@
 | 
			
		|||
export type ScheduleItem = {
 | 
			
		||||
  day: number;
 | 
			
		||||
  time: string;
 | 
			
		||||
  margin: number;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
import { Address } from './address';
 | 
			
		||||
 | 
			
		||||
//TODO Why not use the Waypoint value-object from the domain?
 | 
			
		||||
export type Waypoint = {
 | 
			
		||||
  position: number;
 | 
			
		||||
} & Address;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,9 +30,9 @@ export class AdEntity extends AggregateRoot<AdProps> {
 | 
			
		|||
        fromDate: props.fromDate,
 | 
			
		||||
        toDate: props.toDate,
 | 
			
		||||
        schedule: props.schedule.map((day: ScheduleItemProps) => ({
 | 
			
		||||
          day: day.day as number,
 | 
			
		||||
          day: day.day,
 | 
			
		||||
          time: day.time,
 | 
			
		||||
          margin: day.margin as number,
 | 
			
		||||
          margin: day.margin,
 | 
			
		||||
        })),
 | 
			
		||||
        seatsProposed: props.seatsProposed,
 | 
			
		||||
        seatsRequested: props.seatsRequested,
 | 
			
		||||
| 
						 | 
				
			
			@ -96,6 +96,24 @@ export class AdEntity extends AggregateRoot<AdProps> {
 | 
			
		|||
    return this;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  update = (newProps: CreateAdProps): AdEntity => {
 | 
			
		||||
    this.props.driver = newProps.driver;
 | 
			
		||||
    this.props.passenger = newProps.passenger;
 | 
			
		||||
    this.props.frequency = newProps.frequency;
 | 
			
		||||
    this.props.fromDate = newProps.fromDate;
 | 
			
		||||
    this.props.toDate = newProps.toDate;
 | 
			
		||||
    this.props.seatsProposed = newProps.seatsProposed;
 | 
			
		||||
    this.props.seatsRequested = newProps.seatsRequested;
 | 
			
		||||
    this.props.strict = newProps.strict;
 | 
			
		||||
    this.props.comment = newProps.comment;
 | 
			
		||||
    this.props.schedule = newProps.schedule.map((item) => ({ ...item }));
 | 
			
		||||
    this.props.waypoints = newProps.waypoints.map((wp) => ({ ...wp }));
 | 
			
		||||
    //The ad goes back to pending status until it is validated again
 | 
			
		||||
    this.props.status = Status.PENDING;
 | 
			
		||||
    this.validate();
 | 
			
		||||
    return this;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  delete(): void {
 | 
			
		||||
    this.addEvent(
 | 
			
		||||
      new AdDeletedDomainEvent({
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,23 +1,6 @@
 | 
			
		|||
import { ScheduleItemProps } from './value-objects/schedule-item.value-object';
 | 
			
		||||
import { WaypointProps } from './value-objects/waypoint.value-object';
 | 
			
		||||
 | 
			
		||||
// All properties that an Ad has
 | 
			
		||||
export interface AdProps {
 | 
			
		||||
  userId: string;
 | 
			
		||||
  driver: boolean;
 | 
			
		||||
  status: Status;
 | 
			
		||||
  passenger: boolean;
 | 
			
		||||
  frequency: Frequency;
 | 
			
		||||
  fromDate: string;
 | 
			
		||||
  toDate: string;
 | 
			
		||||
  schedule: ScheduleItemProps[];
 | 
			
		||||
  seatsProposed: number;
 | 
			
		||||
  seatsRequested: number;
 | 
			
		||||
  strict: boolean;
 | 
			
		||||
  waypoints: WaypointProps[];
 | 
			
		||||
  comment?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Properties that are needed for an Ad creation
 | 
			
		||||
export interface CreateAdProps {
 | 
			
		||||
  userId: string;
 | 
			
		||||
| 
						 | 
				
			
			@ -34,6 +17,11 @@ export interface CreateAdProps {
 | 
			
		|||
  comment?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// All properties that an Ad has
 | 
			
		||||
export interface AdProps extends CreateAdProps {
 | 
			
		||||
  status: Status;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum Frequency {
 | 
			
		||||
  PUNCTUAL = 'PUNCTUAL',
 | 
			
		||||
  RECURRENT = 'RECURRENT',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,5 @@
 | 
			
		|||
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
 | 
			
		||||
import { ScheduleItemProps } from '../value-objects/schedule-item.value-object';
 | 
			
		||||
 | 
			
		||||
export class AdCreatedDomainEvent extends DomainEvent {
 | 
			
		||||
  readonly userId: string;
 | 
			
		||||
| 
						 | 
				
			
			@ -7,7 +8,7 @@ export class AdCreatedDomainEvent extends DomainEvent {
 | 
			
		|||
  readonly frequency: string;
 | 
			
		||||
  readonly fromDate: string;
 | 
			
		||||
  readonly toDate: string;
 | 
			
		||||
  readonly schedule: ScheduleItem[];
 | 
			
		||||
  readonly schedule: ScheduleItemProps[];
 | 
			
		||||
  readonly seatsProposed: number;
 | 
			
		||||
  readonly seatsRequested: number;
 | 
			
		||||
  readonly strict: boolean;
 | 
			
		||||
| 
						 | 
				
			
			@ -31,12 +32,6 @@ export class AdCreatedDomainEvent extends DomainEvent {
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class ScheduleItem {
 | 
			
		||||
  day: number;
 | 
			
		||||
  time: string;
 | 
			
		||||
  margin: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class Waypoint {
 | 
			
		||||
  position: number;
 | 
			
		||||
  name?: string;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,23 @@
 | 
			
		|||
import { DomainEvent } from '@mobicoop/ddd-library';
 | 
			
		||||
import { AdEntity } from '../ad.entity';
 | 
			
		||||
 | 
			
		||||
export abstract class AdDomainEvent extends DomainEvent {
 | 
			
		||||
  readonly ad: AdEntity;
 | 
			
		||||
 | 
			
		||||
  constructor(ad: AdEntity) {
 | 
			
		||||
    super({
 | 
			
		||||
      metadata: {
 | 
			
		||||
        correlationId: ad.id,
 | 
			
		||||
        timestamp: Date.now(),
 | 
			
		||||
      },
 | 
			
		||||
      aggregateId: ad.id,
 | 
			
		||||
    });
 | 
			
		||||
    this.ad = ad;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class AdUpdatedDomainEvent extends AdDomainEvent {
 | 
			
		||||
  constructor(ad: AdEntity) {
 | 
			
		||||
    super(ad);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -6,13 +6,13 @@ import { ValueObject } from '@mobicoop/ddd-library';
 | 
			
		|||
 * */
 | 
			
		||||
 | 
			
		||||
export interface ScheduleItemProps {
 | 
			
		||||
  day?: number;
 | 
			
		||||
  day: number;
 | 
			
		||||
  time: string;
 | 
			
		||||
  margin?: number;
 | 
			
		||||
  margin: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class ScheduleItem extends ValueObject<ScheduleItemProps> {
 | 
			
		||||
  get day(): number | undefined {
 | 
			
		||||
  get day(): number {
 | 
			
		||||
    return this.props.day;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -20,7 +20,7 @@ export class ScheduleItem extends ValueObject<ScheduleItemProps> {
 | 
			
		|||
    return this.props.time;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get margin(): number | undefined {
 | 
			
		||||
  get margin(): number {
 | 
			
		||||
    return this.props.margin;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,16 +1,16 @@
 | 
			
		|||
import { Inject, Injectable, Logger } from '@nestjs/common';
 | 
			
		||||
import { EventEmitter2 } from '@nestjs/event-emitter';
 | 
			
		||||
import { AdEntity } from '../core/domain/ad.entity';
 | 
			
		||||
import { AdMapper } from '../ad.mapper';
 | 
			
		||||
import { AdRepositoryPort } from '../core/application/ports/ad.repository.port';
 | 
			
		||||
import {
 | 
			
		||||
  LoggerBase,
 | 
			
		||||
  MessagePublisherPort,
 | 
			
		||||
  PrismaRepositoryBase,
 | 
			
		||||
} from '@mobicoop/ddd-library';
 | 
			
		||||
import { PrismaService } from './prisma.service';
 | 
			
		||||
import { AD_MESSAGE_PUBLISHER } from '../ad.di-tokens';
 | 
			
		||||
import { Inject, Injectable, Logger } from '@nestjs/common';
 | 
			
		||||
import { EventEmitter2 } from '@nestjs/event-emitter';
 | 
			
		||||
import { SERVICE_NAME } from '@src/app.constants';
 | 
			
		||||
import { AD_MESSAGE_PUBLISHER } from '../ad.di-tokens';
 | 
			
		||||
import { AdMapper } from '../ad.mapper';
 | 
			
		||||
import { AdRepositoryPort } from '../core/application/ports/ad.repository.port';
 | 
			
		||||
import { AdEntity } from '../core/domain/ad.entity';
 | 
			
		||||
import { PrismaService } from './prisma.service';
 | 
			
		||||
 | 
			
		||||
export type AdBaseModel = {
 | 
			
		||||
  uuid: string;
 | 
			
		||||
| 
						 | 
				
			
			@ -40,13 +40,21 @@ export type AdWriteModel = AdBaseModel & {
 | 
			
		|||
};
 | 
			
		||||
 | 
			
		||||
export type ScheduleWriteModel = {
 | 
			
		||||
  deleteMany?: PastCreatedFilter;
 | 
			
		||||
  create: ScheduleItemModel[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type WaypointWriteModel = {
 | 
			
		||||
  deleteMany?: PastCreatedFilter;
 | 
			
		||||
  create: WaypointModel[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// used to delete records created in the past,
 | 
			
		||||
// because the order of `create` and `deleteMany` is not guaranteed
 | 
			
		||||
export type PastCreatedFilter = {
 | 
			
		||||
  createdAt: { lt: Date };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type ScheduleItemModel = {
 | 
			
		||||
  uuid: string;
 | 
			
		||||
  day: number;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,7 @@ service AdService {
 | 
			
		|||
  rpc FindAllByIds(AdsById) returns (Ads);
 | 
			
		||||
  rpc FindAllByUserId(UserById) returns (Ads);
 | 
			
		||||
  rpc Create(Ad) returns (AdById);
 | 
			
		||||
  rpc Update(Ad) returns (Ad);
 | 
			
		||||
  rpc Update(Ad) returns (Empty);
 | 
			
		||||
  rpc Delete(AdById) returns (Empty);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -58,6 +58,7 @@ message Waypoint {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
enum Frequency {
 | 
			
		||||
  UNSPECIFIED = 0;
 | 
			
		||||
  PUNCTUAL = 1;
 | 
			
		||||
  RECURRENT = 2;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
import { IsUUID } from 'class-validator';
 | 
			
		||||
import { CreateAdRequestDto } from './create-ad.request.dto';
 | 
			
		||||
 | 
			
		||||
export class UpdateAdRequestDto extends CreateAdRequestDto {
 | 
			
		||||
  @IsUUID(4)
 | 
			
		||||
  id: string;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,43 @@
 | 
			
		|||
import {
 | 
			
		||||
  NotFoundException,
 | 
			
		||||
  RpcExceptionCode,
 | 
			
		||||
  RpcValidationPipe,
 | 
			
		||||
} from '@mobicoop/ddd-library';
 | 
			
		||||
import { UpdateAdCommand } from '@modules/ad/core/application/commands/update-ad/update-ad.command';
 | 
			
		||||
import { Controller, UsePipes } from '@nestjs/common';
 | 
			
		||||
import { CommandBus } from '@nestjs/cqrs';
 | 
			
		||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
 | 
			
		||||
import { GRPC_SERVICE_NAME } from '@src/app.constants';
 | 
			
		||||
import { UpdateAdRequestDto } from './dtos/update-ad.request.dto';
 | 
			
		||||
 | 
			
		||||
@UsePipes(
 | 
			
		||||
  new RpcValidationPipe({
 | 
			
		||||
    whitelist: false,
 | 
			
		||||
    forbidUnknownValues: false,
 | 
			
		||||
  }),
 | 
			
		||||
)
 | 
			
		||||
@Controller()
 | 
			
		||||
export class UpdateAdGrpcController {
 | 
			
		||||
  constructor(private readonly commandBus: CommandBus) {}
 | 
			
		||||
 | 
			
		||||
  @GrpcMethod(GRPC_SERVICE_NAME, 'Update')
 | 
			
		||||
  async update(data: UpdateAdRequestDto): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const cmdProps = {
 | 
			
		||||
        adId: data.id,
 | 
			
		||||
        ...data,
 | 
			
		||||
      };
 | 
			
		||||
      delete (cmdProps as { id?: string }).id;
 | 
			
		||||
 | 
			
		||||
      await this.commandBus.execute(new UpdateAdCommand(cmdProps));
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      if (error instanceof NotFoundException) {
 | 
			
		||||
        throw new RpcException({
 | 
			
		||||
          code: RpcExceptionCode.NOT_FOUND,
 | 
			
		||||
          message: error.message,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,10 @@
 | 
			
		|||
import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
 | 
			
		||||
 | 
			
		||||
export function mockInputDateTimeTransformer(): DateTimeTransformerPort {
 | 
			
		||||
  return {
 | 
			
		||||
    fromDate: jest.fn(),
 | 
			
		||||
    toDate: jest.fn(),
 | 
			
		||||
    day: jest.fn(),
 | 
			
		||||
    time: jest.fn(),
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -39,7 +39,9 @@ const punctualCreateAdProps = {
 | 
			
		|||
  toDate: '2023-06-22',
 | 
			
		||||
  schedule: [
 | 
			
		||||
    {
 | 
			
		||||
      day: 4,
 | 
			
		||||
      time: '08:30',
 | 
			
		||||
      margin: 900,
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
  frequency: Frequency.PUNCTUAL,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,18 +1,17 @@
 | 
			
		|||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { AggregateID, ConflictException } from '@mobicoop/ddd-library';
 | 
			
		||||
import {
 | 
			
		||||
  AD_REPOSITORY,
 | 
			
		||||
  INPUT_DATETIME_TRANSFORMER,
 | 
			
		||||
} from '@modules/ad/ad.di-tokens';
 | 
			
		||||
import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
 | 
			
		||||
import { CreateAdRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/create-ad.request.dto';
 | 
			
		||||
import { AggregateID } from '@mobicoop/ddd-library';
 | 
			
		||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
 | 
			
		||||
import { ConflictException } from '@mobicoop/ddd-library';
 | 
			
		||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
 | 
			
		||||
import { CreateAdService } from '@modules/ad/core/application/commands/create-ad/create-ad.service';
 | 
			
		||||
import { CreateAdCommand } from '@modules/ad/core/application/commands/create-ad/create-ad.command';
 | 
			
		||||
import { CreateAdService } from '@modules/ad/core/application/commands/create-ad/create-ad.service';
 | 
			
		||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
 | 
			
		||||
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
 | 
			
		||||
import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
 | 
			
		||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
 | 
			
		||||
import { CreateAdRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/create-ad.request.dto';
 | 
			
		||||
import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
 | 
			
		||||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { mockInputDateTimeTransformer } from '../ad.mocks';
 | 
			
		||||
 | 
			
		||||
const originWaypoint: WaypointDto = {
 | 
			
		||||
  position: 0,
 | 
			
		||||
| 
						 | 
				
			
			@ -64,13 +63,6 @@ const mockAdRepository = {
 | 
			
		|||
    }),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const mockInputDateTimeTransformer: DateTimeTransformerPort = {
 | 
			
		||||
  fromDate: jest.fn(),
 | 
			
		||||
  toDate: jest.fn(),
 | 
			
		||||
  day: jest.fn(),
 | 
			
		||||
  time: jest.fn(),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('create-ad.service', () => {
 | 
			
		||||
  let createAdService: CreateAdService;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -83,7 +75,7 @@ describe('create-ad.service', () => {
 | 
			
		|||
        },
 | 
			
		||||
        {
 | 
			
		||||
          provide: INPUT_DATETIME_TRANSFORMER,
 | 
			
		||||
          useValue: mockInputDateTimeTransformer,
 | 
			
		||||
          useValue: mockInputDateTimeTransformer(),
 | 
			
		||||
        },
 | 
			
		||||
        CreateAdService,
 | 
			
		||||
      ],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,10 +1,10 @@
 | 
			
		|||
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
 | 
			
		||||
import { FindAdsByIdsQuery } from '@modules/ad/core/application/queries/find-ads-by-ids/find-ads-by-ids.query';
 | 
			
		||||
import { FindAdsByIdsQueryHandler } from '@modules/ad/core/application/queries/find-ads-by-ids/find-ads-by-ids.query-handler';
 | 
			
		||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
 | 
			
		||||
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
 | 
			
		||||
import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
 | 
			
		||||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { FindAdsByIdsQueryHandler } from '@modules/ad/core/application/queries/find-ads-by-ids/find-ads-by-ids.query-handler';
 | 
			
		||||
import { FindAdsByIdsQuery } from '@modules/ad/core/application/queries/find-ads-by-ids/find-ads-by-ids.query';
 | 
			
		||||
 | 
			
		||||
const originWaypointProps: WaypointProps = {
 | 
			
		||||
  position: 0,
 | 
			
		||||
| 
						 | 
				
			
			@ -44,7 +44,9 @@ const punctualCreateAdProps = {
 | 
			
		|||
  toDate: '2023-06-22',
 | 
			
		||||
  schedule: [
 | 
			
		||||
    {
 | 
			
		||||
      day: 4,
 | 
			
		||||
      time: '08:30',
 | 
			
		||||
      margin: 900,
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
  frequency: Frequency.PUNCTUAL,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,10 +1,10 @@
 | 
			
		|||
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
 | 
			
		||||
import { FindAdsByUserIdQuery } from '@modules/ad/core/application/queries/find-ads-by-user-id/find-ads-by-user-id.query';
 | 
			
		||||
import { FindAdsByUserIdQueryHandler } from '@modules/ad/core/application/queries/find-ads-by-user-id/find-ads-by-user-id.query-handler';
 | 
			
		||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
 | 
			
		||||
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
 | 
			
		||||
import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
 | 
			
		||||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { FindAdsByUserIdQueryHandler } from '@modules/ad/core/application/queries/find-ads-by-user-id/find-ads-by-user-id.query-handler';
 | 
			
		||||
import { FindAdsByUserIdQuery } from '@modules/ad/core/application/queries/find-ads-by-user-id/find-ads-by-user-id.query';
 | 
			
		||||
 | 
			
		||||
const originWaypointProps: WaypointProps = {
 | 
			
		||||
  position: 0,
 | 
			
		||||
| 
						 | 
				
			
			@ -44,7 +44,9 @@ const punctualCreateAdProps = {
 | 
			
		|||
  toDate: '2023-06-22',
 | 
			
		||||
  schedule: [
 | 
			
		||||
    {
 | 
			
		||||
      day: 4,
 | 
			
		||||
      time: '08:30',
 | 
			
		||||
      margin: 900,
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
  frequency: Frequency.PUNCTUAL,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,11 @@
 | 
			
		|||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
 | 
			
		||||
import { AggregateID } from '@mobicoop/ddd-library';
 | 
			
		||||
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
 | 
			
		||||
import { InvalidateAdCommand } from '@modules/ad/core/application/commands/invalidate-ad/invalidate-ad.command';
 | 
			
		||||
import { InvalidateAdService } from '@modules/ad/core/application/commands/invalidate-ad/invalidate-ad.service';
 | 
			
		||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
 | 
			
		||||
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
 | 
			
		||||
import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
 | 
			
		||||
import { InvalidateAdService } from '@modules/ad/core/application/commands/invalidate-ad/invalidate-ad.service';
 | 
			
		||||
import { InvalidateAdCommand } from '@modules/ad/core/application/commands/invalidate-ad/invalidate-ad.command';
 | 
			
		||||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
 | 
			
		||||
const originWaypointProps: WaypointProps = {
 | 
			
		||||
  position: 0,
 | 
			
		||||
| 
						 | 
				
			
			@ -45,7 +45,9 @@ const punctualCreateAdProps = {
 | 
			
		|||
  toDate: '2023-06-22',
 | 
			
		||||
  schedule: [
 | 
			
		||||
    {
 | 
			
		||||
      day: 4,
 | 
			
		||||
      time: '08:30',
 | 
			
		||||
      margin: 900,
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
  frequency: Frequency.PUNCTUAL,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,61 @@
 | 
			
		|||
import {
 | 
			
		||||
  AD_MESSAGE_PUBLISHER,
 | 
			
		||||
  OUTPUT_DATETIME_TRANSFORMER,
 | 
			
		||||
  TIMEZONE_FINDER,
 | 
			
		||||
  TIME_CONVERTER,
 | 
			
		||||
} from '@modules/ad/ad.di-tokens';
 | 
			
		||||
import { AdMapper } from '@modules/ad/ad.mapper';
 | 
			
		||||
import { PublishMessageWhenAdIsUpdatedDomainEventHandler } from '@modules/ad/core/application/event-handlers/publish-message-when-ad-is-updated.domain-event-handler';
 | 
			
		||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
 | 
			
		||||
import { AdUpdatedDomainEvent } from '@modules/ad/core/domain/events/ad.domain-event';
 | 
			
		||||
import { OutputDateTimeTransformer } from '@modules/ad/infrastructure/output-datetime-transformer';
 | 
			
		||||
import { TimeConverter } from '@modules/ad/infrastructure/time-converter';
 | 
			
		||||
import { TimezoneFinder } from '@modules/ad/infrastructure/timezone-finder';
 | 
			
		||||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { punctualPassengerCreateAdProps } from './ad.fixtures';
 | 
			
		||||
 | 
			
		||||
const mockMessagePublisher = {
 | 
			
		||||
  publish: jest.fn(),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('Publish message when ad is updated domain event handler', () => {
 | 
			
		||||
  let updatedDomainEventHandler: PublishMessageWhenAdIsUpdatedDomainEventHandler;
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
    const module: TestingModule = await Test.createTestingModule({
 | 
			
		||||
      providers: [
 | 
			
		||||
        {
 | 
			
		||||
          provide: AD_MESSAGE_PUBLISHER,
 | 
			
		||||
          useValue: mockMessagePublisher,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          provide: OUTPUT_DATETIME_TRANSFORMER,
 | 
			
		||||
          useClass: OutputDateTimeTransformer,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          provide: TIMEZONE_FINDER,
 | 
			
		||||
          useClass: TimezoneFinder,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          provide: TIME_CONVERTER,
 | 
			
		||||
          useClass: TimeConverter,
 | 
			
		||||
        },
 | 
			
		||||
        AdMapper,
 | 
			
		||||
        PublishMessageWhenAdIsUpdatedDomainEventHandler,
 | 
			
		||||
      ],
 | 
			
		||||
    }).compile();
 | 
			
		||||
 | 
			
		||||
    updatedDomainEventHandler =
 | 
			
		||||
      module.get<PublishMessageWhenAdIsUpdatedDomainEventHandler>(
 | 
			
		||||
        PublishMessageWhenAdIsUpdatedDomainEventHandler,
 | 
			
		||||
      );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should publish a message', () => {
 | 
			
		||||
    expect(updatedDomainEventHandler).toBeDefined();
 | 
			
		||||
    const ad = AdEntity.create(punctualPassengerCreateAdProps());
 | 
			
		||||
    const adUpdatedDomainEvent = new AdUpdatedDomainEvent(ad);
 | 
			
		||||
    updatedDomainEventHandler.handle(adUpdatedDomainEvent);
 | 
			
		||||
    expect(mockMessagePublisher.publish).toHaveBeenCalledTimes(1);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,71 @@
 | 
			
		|||
import {
 | 
			
		||||
  AD_REPOSITORY,
 | 
			
		||||
  INPUT_DATETIME_TRANSFORMER,
 | 
			
		||||
} from '@modules/ad/ad.di-tokens';
 | 
			
		||||
import { UpdateAdCommand } from '@modules/ad/core/application/commands/update-ad/update-ad.command';
 | 
			
		||||
import { UpdateAdService } from '@modules/ad/core/application/commands/update-ad/update-ad.service';
 | 
			
		||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
 | 
			
		||||
import { Status } from '@modules/ad/core/domain/ad.types';
 | 
			
		||||
import { EventEmitter2 } from '@nestjs/event-emitter';
 | 
			
		||||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { mockInputDateTimeTransformer } from '../ad.mocks';
 | 
			
		||||
import { punctualCreateAdRequest } from '../interface/ad.fixtures';
 | 
			
		||||
import { punctualPassengerCreateAdProps } from './ad.fixtures';
 | 
			
		||||
 | 
			
		||||
const mockAdRepository = {
 | 
			
		||||
  findOneById: jest.fn().mockImplementation(
 | 
			
		||||
    async (id) =>
 | 
			
		||||
      new AdEntity({
 | 
			
		||||
        id,
 | 
			
		||||
        props: { ...punctualPassengerCreateAdProps(), status: Status.VALID },
 | 
			
		||||
      }),
 | 
			
		||||
  ),
 | 
			
		||||
  update: jest.fn(),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const mockEventEmitter = {
 | 
			
		||||
  emitAsync: jest.fn(),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('create-ad.service', () => {
 | 
			
		||||
  let updateAdService: UpdateAdService;
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
    const module: TestingModule = await Test.createTestingModule({
 | 
			
		||||
      providers: [
 | 
			
		||||
        {
 | 
			
		||||
          provide: AD_REPOSITORY,
 | 
			
		||||
          useValue: mockAdRepository,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          provide: INPUT_DATETIME_TRANSFORMER,
 | 
			
		||||
          useValue: mockInputDateTimeTransformer(),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          provide: EventEmitter2,
 | 
			
		||||
          useValue: mockEventEmitter,
 | 
			
		||||
        },
 | 
			
		||||
        UpdateAdService,
 | 
			
		||||
      ],
 | 
			
		||||
    }).compile();
 | 
			
		||||
 | 
			
		||||
    updateAdService = module.get<UpdateAdService>(UpdateAdService);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    expect(updateAdService).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('execute', () => {
 | 
			
		||||
    it('should update the ad in the repository and emit an event', async () => {
 | 
			
		||||
      const command = new UpdateAdCommand({
 | 
			
		||||
        adId: '200d61a8-d878-4378-a609-c19ea71633d2',
 | 
			
		||||
        ...punctualCreateAdRequest(),
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      await updateAdService.execute(command);
 | 
			
		||||
      expect(mockAdRepository.update).toHaveBeenCalled();
 | 
			
		||||
      expect(mockEventEmitter.emitAsync).toHaveBeenCalled();
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,11 +1,11 @@
 | 
			
		|||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
 | 
			
		||||
import { AggregateID } from '@mobicoop/ddd-library';
 | 
			
		||||
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
 | 
			
		||||
import { ValidateAdCommand } from '@modules/ad/core/application/commands/validate-ad/validate-ad.command';
 | 
			
		||||
import { ValidateAdService } from '@modules/ad/core/application/commands/validate-ad/validate-ad.service';
 | 
			
		||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
 | 
			
		||||
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
 | 
			
		||||
import { ValidateAdService } from '@modules/ad/core/application/commands/validate-ad/validate-ad.service';
 | 
			
		||||
import { ValidateAdCommand } from '@modules/ad/core/application/commands/validate-ad/validate-ad.command';
 | 
			
		||||
import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
 | 
			
		||||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
 | 
			
		||||
const originWaypointProps: WaypointProps = {
 | 
			
		||||
  position: 0,
 | 
			
		||||
| 
						 | 
				
			
			@ -45,7 +45,9 @@ const punctualCreateAdProps = {
 | 
			
		|||
  toDate: '2023-06-22',
 | 
			
		||||
  schedule: [
 | 
			
		||||
    {
 | 
			
		||||
      day: 4,
 | 
			
		||||
      time: '08:30',
 | 
			
		||||
      margin: 900,
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
  frequency: Frequency.PUNCTUAL,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,43 @@
 | 
			
		|||
import { Frequency } from '@modules/ad/core/domain/ad.types';
 | 
			
		||||
import { CreateAdRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/create-ad.request.dto';
 | 
			
		||||
import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
 | 
			
		||||
 | 
			
		||||
const originWaypoint: WaypointDto = {
 | 
			
		||||
  position: 0,
 | 
			
		||||
  lat: 48.689445,
 | 
			
		||||
  lon: 6.17651,
 | 
			
		||||
  houseNumber: '5',
 | 
			
		||||
  street: 'Avenue Foch',
 | 
			
		||||
  locality: 'Nancy',
 | 
			
		||||
  postalCode: '54000',
 | 
			
		||||
  country: 'France',
 | 
			
		||||
};
 | 
			
		||||
const destinationWaypoint: WaypointDto = {
 | 
			
		||||
  position: 1,
 | 
			
		||||
  lat: 48.8566,
 | 
			
		||||
  lon: 2.3522,
 | 
			
		||||
  locality: 'Paris',
 | 
			
		||||
  postalCode: '75000',
 | 
			
		||||
  country: 'France',
 | 
			
		||||
};
 | 
			
		||||
export function punctualCreateAdRequest(): CreateAdRequestDto {
 | 
			
		||||
  return {
 | 
			
		||||
    userId: '4eb6a6af-ecfd-41c3-9118-473a507014d4',
 | 
			
		||||
    fromDate: '2023-12-21',
 | 
			
		||||
    toDate: '2023-12-21',
 | 
			
		||||
    schedule: [
 | 
			
		||||
      {
 | 
			
		||||
        time: '08:15',
 | 
			
		||||
        day: 4,
 | 
			
		||||
        margin: 600,
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
    driver: false,
 | 
			
		||||
    passenger: true,
 | 
			
		||||
    seatsRequested: 1,
 | 
			
		||||
    seatsProposed: 3,
 | 
			
		||||
    strict: false,
 | 
			
		||||
    frequency: Frequency.PUNCTUAL,
 | 
			
		||||
    waypoints: [originWaypoint, destinationWaypoint],
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,51 +1,10 @@
 | 
			
		|||
import { IdResponse } from '@mobicoop/ddd-library';
 | 
			
		||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
 | 
			
		||||
import { IdResponse, RpcExceptionCode } from '@mobicoop/ddd-library';
 | 
			
		||||
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
 | 
			
		||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
 | 
			
		||||
import { CreateAdGrpcController } from '@modules/ad/interface/grpc-controllers/create-ad.grpc.controller';
 | 
			
		||||
import { CreateAdRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/create-ad.request.dto';
 | 
			
		||||
import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
 | 
			
		||||
import { CommandBus } from '@nestjs/cqrs';
 | 
			
		||||
import { RpcException } from '@nestjs/microservices';
 | 
			
		||||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
 | 
			
		||||
const originWaypoint: WaypointDto = {
 | 
			
		||||
  position: 0,
 | 
			
		||||
  lat: 48.689445,
 | 
			
		||||
  lon: 6.17651,
 | 
			
		||||
  houseNumber: '5',
 | 
			
		||||
  street: 'Avenue Foch',
 | 
			
		||||
  locality: 'Nancy',
 | 
			
		||||
  postalCode: '54000',
 | 
			
		||||
  country: 'France',
 | 
			
		||||
};
 | 
			
		||||
const destinationWaypoint: WaypointDto = {
 | 
			
		||||
  position: 1,
 | 
			
		||||
  lat: 48.8566,
 | 
			
		||||
  lon: 2.3522,
 | 
			
		||||
  locality: 'Paris',
 | 
			
		||||
  postalCode: '75000',
 | 
			
		||||
  country: 'France',
 | 
			
		||||
};
 | 
			
		||||
const punctualCreateAdRequest: CreateAdRequestDto = {
 | 
			
		||||
  userId: '4eb6a6af-ecfd-41c3-9118-473a507014d4',
 | 
			
		||||
  fromDate: '2023-12-21',
 | 
			
		||||
  toDate: '2023-12-21',
 | 
			
		||||
  schedule: [
 | 
			
		||||
    {
 | 
			
		||||
      time: '08:15',
 | 
			
		||||
      day: 4,
 | 
			
		||||
      margin: 600,
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
  driver: false,
 | 
			
		||||
  passenger: true,
 | 
			
		||||
  seatsRequested: 1,
 | 
			
		||||
  seatsProposed: 3,
 | 
			
		||||
  strict: false,
 | 
			
		||||
  frequency: Frequency.PUNCTUAL,
 | 
			
		||||
  waypoints: [originWaypoint, destinationWaypoint],
 | 
			
		||||
};
 | 
			
		||||
import { punctualCreateAdRequest } from './ad.fixtures';
 | 
			
		||||
 | 
			
		||||
const mockCommandBus = {
 | 
			
		||||
  execute: jest
 | 
			
		||||
| 
						 | 
				
			
			@ -89,7 +48,7 @@ describe('Create Ad Grpc Controller', () => {
 | 
			
		|||
  it('should create a new ad', async () => {
 | 
			
		||||
    jest.spyOn(mockCommandBus, 'execute');
 | 
			
		||||
    const result: IdResponse = await createAdGrpcController.create(
 | 
			
		||||
      punctualCreateAdRequest,
 | 
			
		||||
      punctualCreateAdRequest(),
 | 
			
		||||
    );
 | 
			
		||||
    expect(result).toBeInstanceOf(IdResponse);
 | 
			
		||||
    expect(result.id).toBe('200d61a8-d878-4378-a609-c19ea71633d2');
 | 
			
		||||
| 
						 | 
				
			
			@ -100,7 +59,7 @@ describe('Create Ad Grpc Controller', () => {
 | 
			
		|||
    jest.spyOn(mockCommandBus, 'execute');
 | 
			
		||||
    expect.assertions(3);
 | 
			
		||||
    try {
 | 
			
		||||
      await createAdGrpcController.create(punctualCreateAdRequest);
 | 
			
		||||
      await createAdGrpcController.create(punctualCreateAdRequest());
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      expect(e).toBeInstanceOf(RpcException);
 | 
			
		||||
      expect(e.error.code).toBe(RpcExceptionCode.ALREADY_EXISTS);
 | 
			
		||||
| 
						 | 
				
			
			@ -112,7 +71,7 @@ describe('Create Ad Grpc Controller', () => {
 | 
			
		|||
    jest.spyOn(mockCommandBus, 'execute');
 | 
			
		||||
    expect.assertions(3);
 | 
			
		||||
    try {
 | 
			
		||||
      await createAdGrpcController.create(punctualCreateAdRequest);
 | 
			
		||||
      await createAdGrpcController.create(punctualCreateAdRequest());
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      expect(e).toBeInstanceOf(RpcException);
 | 
			
		||||
      expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,76 @@
 | 
			
		|||
import { NotFoundException, RpcExceptionCode } from '@mobicoop/ddd-library';
 | 
			
		||||
import { UpdateAdGrpcController } from '@modules/ad/interface/grpc-controllers/update-ad.grpc.controller';
 | 
			
		||||
import { CommandBus } from '@nestjs/cqrs';
 | 
			
		||||
import { RpcException } from '@nestjs/microservices';
 | 
			
		||||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { punctualCreateAdRequest } from './ad.fixtures';
 | 
			
		||||
 | 
			
		||||
const validAdId = '200d61a8-d878-4378-a609-c19ea71633d2';
 | 
			
		||||
const mockCommandBus = {
 | 
			
		||||
  execute: jest.fn().mockImplementation(async (command) => {
 | 
			
		||||
    if (command.adId === '') throw 'Ad id is empty';
 | 
			
		||||
    if (command.adId != validAdId) throw new NotFoundException();
 | 
			
		||||
  }),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('Update Ad GRPC Controller', () => {
 | 
			
		||||
  let updateAdGrpcController: UpdateAdGrpcController;
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
    const module: TestingModule = await Test.createTestingModule({
 | 
			
		||||
      providers: [
 | 
			
		||||
        {
 | 
			
		||||
          provide: CommandBus,
 | 
			
		||||
          useValue: mockCommandBus,
 | 
			
		||||
        },
 | 
			
		||||
        UpdateAdGrpcController,
 | 
			
		||||
      ],
 | 
			
		||||
    }).compile();
 | 
			
		||||
    updateAdGrpcController = module.get<UpdateAdGrpcController>(
 | 
			
		||||
      UpdateAdGrpcController,
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  afterEach(async () => {
 | 
			
		||||
    jest.clearAllMocks();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    expect(updateAdGrpcController).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should execute the update ad command', async () => {
 | 
			
		||||
    await updateAdGrpcController.update({
 | 
			
		||||
      id: validAdId,
 | 
			
		||||
      ...punctualCreateAdRequest(),
 | 
			
		||||
    });
 | 
			
		||||
    expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should throw a dedicated RpcException if ad is not found', async () => {
 | 
			
		||||
    expect.assertions(3);
 | 
			
		||||
    try {
 | 
			
		||||
      await updateAdGrpcController.update({
 | 
			
		||||
        id: 'ac85f5f4-41cd-4c5d-9aee-0a1acb176fb8',
 | 
			
		||||
        ...punctualCreateAdRequest(),
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      expect(e).toBeInstanceOf(RpcException);
 | 
			
		||||
      expect(e.error.code).toBe(RpcExceptionCode.NOT_FOUND);
 | 
			
		||||
    }
 | 
			
		||||
    expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should rethrow any other exceptions', async () => {
 | 
			
		||||
    expect.assertions(2);
 | 
			
		||||
    try {
 | 
			
		||||
      await updateAdGrpcController.update({
 | 
			
		||||
        id: '',
 | 
			
		||||
        ...punctualCreateAdRequest(),
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      expect(e).toBe('Ad id is empty');
 | 
			
		||||
    }
 | 
			
		||||
    expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
		Loading…
	
		Reference in New Issue