diff --git a/src/modules/ad/ad.di-tokens.ts b/src/modules/ad/ad.di-tokens.ts index 6ad4746..de012d0 100644 --- a/src/modules/ad/ad.di-tokens.ts +++ b/src/modules/ad/ad.di-tokens.ts @@ -2,5 +2,8 @@ export const AD_MESSAGE_PUBLISHER = Symbol('AD_MESSAGE_PUBLISHER'); export const PARAMS_PROVIDER = Symbol('PARAMS_PROVIDER'); export const TIMEZONE_FINDER = Symbol('TIMEZONE_FINDER'); export const TIME_CONVERTER = Symbol('TIME_CONVERTER'); -export const DATETIME_TRANSFORMER = Symbol('DATETIME_TRANSFORMER'); +export const INPUT_DATETIME_TRANSFORMER = Symbol('INPUT_DATETIME_TRANSFORMER'); +export const OUTPUT_DATETIME_TRANSFORMER = Symbol( + 'OUTPUT_DATETIME_TRANSFORMER', +); export const AD_REPOSITORY = Symbol('AD_REPOSITORY'); diff --git a/src/modules/ad/ad.mapper.ts b/src/modules/ad/ad.mapper.ts index 897c41d..47902bc 100644 --- a/src/modules/ad/ad.mapper.ts +++ b/src/modules/ad/ad.mapper.ts @@ -1,6 +1,6 @@ import { Mapper } from '@mobicoop/ddd-library'; import { AdResponseDto } from './interface/dtos/ad.response.dto'; -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { AdEntity } from './core/domain/ad.entity'; import { AdWriteModel, @@ -12,6 +12,8 @@ import { Frequency } 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'; /** * Mapper constructs objects that are used in different layers: @@ -24,6 +26,11 @@ import { ScheduleItemProps } from './core/domain/value-objects/schedule-item.val export class AdMapper implements Mapper { + constructor( + @Inject(OUTPUT_DATETIME_TRANSFORMER) + private readonly outputDatetimeTransformer: DateTimeTransformerPort, + ) {} + toPersistence = (entity: AdEntity): AdWriteModel => { const copy = entity.getProps(); const now = new Date(); @@ -129,12 +136,42 @@ export class AdMapper response.driver = props.driver; response.passenger = props.passenger; response.frequency = props.frequency; - response.fromDate = props.fromDate; - response.toDate = props.toDate; + response.fromDate = this.outputDatetimeTransformer.fromDate( + { + date: props.fromDate, + time: props.schedule[0].time, + coordinates: props.waypoints[0].address.coordinates, + }, + props.frequency, + ); + response.toDate = this.outputDatetimeTransformer.toDate( + props.toDate, + { + date: props.fromDate, + time: props.schedule[0].time, + coordinates: props.waypoints[0].address.coordinates, + }, + props.frequency, + ); response.schedule = props.schedule.map( (scheduleItem: ScheduleItemProps) => ({ - day: scheduleItem.day, - time: scheduleItem.time, + day: this.outputDatetimeTransformer.day( + scheduleItem.day, + { + date: props.fromDate, + time: scheduleItem.time, + coordinates: props.waypoints[0].address.coordinates, + }, + props.frequency, + ), + time: this.outputDatetimeTransformer.time( + { + date: props.fromDate, + time: scheduleItem.time, + coordinates: props.waypoints[0].address.coordinates, + }, + props.frequency, + ), margin: scheduleItem.margin, }), ); diff --git a/src/modules/ad/ad.module.ts b/src/modules/ad/ad.module.ts index 6ae54a6..915b7db 100644 --- a/src/modules/ad/ad.module.ts +++ b/src/modules/ad/ad.module.ts @@ -4,7 +4,8 @@ import { CqrsModule } from '@nestjs/cqrs'; import { AD_MESSAGE_PUBLISHER, AD_REPOSITORY, - DATETIME_TRANSFORMER, + INPUT_DATETIME_TRANSFORMER, + OUTPUT_DATETIME_TRANSFORMER, PARAMS_PROVIDER, TIMEZONE_FINDER, TIME_CONVERTER, @@ -20,7 +21,8 @@ import { FindAdByIdQueryHandler } from './core/application/queries/find-ad-by-id import { PublishMessageWhenAdIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-ad-is-created.domain-event-handler'; import { PrismaService } from './infrastructure/prisma.service'; import { MessageBrokerPublisher } from '@mobicoop/message-broker-module'; -import { DateTimeTransformer } from './infrastructure/datetime-transformer'; +import { InputDateTimeTransformer } from './infrastructure/input-datetime-transformer'; +import { OutputDateTimeTransformer } from './infrastructure/output-datetime-transformer'; const grpcControllers = [CreateAdGrpcController, FindAdByIdGrpcController]; @@ -63,8 +65,12 @@ const adapters: Provider[] = [ useClass: TimeConverter, }, { - provide: DATETIME_TRANSFORMER, - useClass: DateTimeTransformer, + provide: INPUT_DATETIME_TRANSFORMER, + useClass: InputDateTimeTransformer, + }, + { + provide: OUTPUT_DATETIME_TRANSFORMER, + useClass: OutputDateTimeTransformer, }, ]; diff --git a/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts b/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts index 6d15807..cc9fbf7 100644 --- a/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts +++ b/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts @@ -3,7 +3,7 @@ import { CreateAdCommand } from './create-ad.command'; import { Inject } from '@nestjs/common'; import { AD_REPOSITORY, - DATETIME_TRANSFORMER, + INPUT_DATETIME_TRANSFORMER, PARAMS_PROVIDER, } from '@modules/ad/ad.di-tokens'; import { AdEntity } from '@modules/ad/core/domain/ad.entity'; @@ -25,7 +25,7 @@ export class CreateAdService implements ICommandHandler { private readonly repository: AdRepositoryPort, @Inject(PARAMS_PROVIDER) private readonly defaultParamsProvider: DefaultParamsProviderPort, - @Inject(DATETIME_TRANSFORMER) + @Inject(INPUT_DATETIME_TRANSFORMER) private readonly datetimeTransformer: DateTimeTransformerPort, ) { this._defaultParams = defaultParamsProvider.getParams(); diff --git a/src/modules/ad/core/application/ports/time-converter.port.ts b/src/modules/ad/core/application/ports/time-converter.port.ts index e981717..112340f 100644 --- a/src/modules/ad/core/application/ports/time-converter.port.ts +++ b/src/modules/ad/core/application/ports/time-converter.port.ts @@ -1,10 +1,18 @@ export interface TimeConverterPort { localStringTimeToUtcStringTime(time: string, timezone: string): string; + utcStringTimeToLocalStringTime(time: string, timezone: string): string; localStringDateTimeToUtcDate( date: string, time: string, timezone: string, dst?: boolean, ): Date; + utcStringDateTimeToLocalIsoString( + date: string, + time: string, + timezone: string, + dst?: boolean, + ): string; utcUnixEpochDayFromTime(time: string, timezone: string): number; + localUnixEpochDayFromTime(time: string, timezone: string): number; } diff --git a/src/modules/ad/core/application/queries/find-ad-by-id/find-ad-by-id.query-handler.ts b/src/modules/ad/core/application/queries/find-ad-by-id/find-ad-by-id.query-handler.ts index b036085..a19a67e 100644 --- a/src/modules/ad/core/application/queries/find-ad-by-id/find-ad-by-id.query-handler.ts +++ b/src/modules/ad/core/application/queries/find-ad-by-id/find-ad-by-id.query-handler.ts @@ -12,6 +12,9 @@ export class FindAdByIdQueryHandler implements IQueryHandler { private readonly repository: AdRepositoryPort, ) {} async execute(query: FindAdByIdQuery): Promise { - return await this.repository.findOneById(query.id, { waypoints: true }); + return await this.repository.findOneById(query.id, { + waypoints: true, + schedule: true, + }); } } diff --git a/src/modules/ad/infrastructure/datetime-transformer.ts b/src/modules/ad/infrastructure/input-datetime-transformer.ts similarity index 98% rename from src/modules/ad/infrastructure/datetime-transformer.ts rename to src/modules/ad/infrastructure/input-datetime-transformer.ts index f8aa497..faa4025 100644 --- a/src/modules/ad/infrastructure/datetime-transformer.ts +++ b/src/modules/ad/infrastructure/input-datetime-transformer.ts @@ -14,7 +14,7 @@ import { TimezoneFinderPort } from '../core/application/ports/timezone-finder.po import { DefaultParamsProviderPort } from '../core/application/ports/default-params-provider.port'; @Injectable() -export class DateTimeTransformer implements DateTimeTransformerPort { +export class InputDateTimeTransformer implements DateTimeTransformerPort { private readonly _defaultTimezone: string; constructor( @Inject(PARAMS_PROVIDER) diff --git a/src/modules/ad/infrastructure/output-datetime-transformer.ts b/src/modules/ad/infrastructure/output-datetime-transformer.ts new file mode 100644 index 0000000..d2d44be --- /dev/null +++ b/src/modules/ad/infrastructure/output-datetime-transformer.ts @@ -0,0 +1,116 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { + DateTimeTransformerPort, + Frequency, + GeoDateTime, +} from '../core/application/ports/datetime-transformer.port'; +import { TimeConverterPort } from '../core/application/ports/time-converter.port'; +import { TIMEZONE_FINDER, TIME_CONVERTER } from '../ad.di-tokens'; +import { TimezoneFinderPort } from '../core/application/ports/timezone-finder.port'; + +@Injectable() +export class OutputDateTimeTransformer implements DateTimeTransformerPort { + constructor( + @Inject(TIMEZONE_FINDER) + private readonly timezoneFinder: TimezoneFinderPort, + @Inject(TIME_CONVERTER) private readonly timeConverter: TimeConverterPort, + ) {} + + /** + * Compute the fromDate : if an ad is punctual, the departure date + * is converted from UTC to the local date with the time and timezone + */ + fromDate = (geoFromDate: GeoDateTime, frequency: Frequency): string => { + if (frequency === Frequency.RECURRENT) return geoFromDate.date; + return this.timeConverter + .utcStringDateTimeToLocalIsoString( + geoFromDate.date, + geoFromDate.time, + this.timezoneFinder.timezones( + geoFromDate.coordinates.lon, + geoFromDate.coordinates.lat, + )[0], + ) + .split('T')[0]; + }; + + /** + * Get the toDate depending on frequency, time and timezone : + * if the ad is punctual, the toDate is equal to the fromDate + */ + toDate = ( + toDate: string, + geoFromDate: GeoDateTime, + frequency: Frequency, + ): string => { + if (frequency === Frequency.RECURRENT) return toDate; + return this.fromDate(geoFromDate, frequency); + }; + + /** + * Get the day for a schedule item : + * - if the ad is punctual, the day is infered from fromDate + * - if the ad is recurrent, the day is computed by converting the time from utc to local time + */ + day = ( + day: number, + geoFromDate: GeoDateTime, + frequency: Frequency, + ): number => { + if (frequency === Frequency.RECURRENT) + return this.recurrentDay( + day, + geoFromDate.time, + this.timezoneFinder.timezones( + geoFromDate.coordinates.lon, + geoFromDate.coordinates.lat, + )[0], + ); + return new Date(this.fromDate(geoFromDate, frequency)).getDay(); + }; + + /** + * Get the utc time + */ + time = (geoFromDate: GeoDateTime, frequency: Frequency): string => { + if (frequency === Frequency.RECURRENT) + return this.timeConverter.utcStringTimeToLocalStringTime( + geoFromDate.time, + this.timezoneFinder.timezones( + geoFromDate.coordinates.lon, + geoFromDate.coordinates.lat, + )[0], + ); + return this.timeConverter + .utcStringDateTimeToLocalIsoString( + geoFromDate.date, + geoFromDate.time, + this.timezoneFinder.timezones( + geoFromDate.coordinates.lon, + geoFromDate.coordinates.lat, + )[0], + ) + .split('T')[1] + .split(':', 2) + .join(':'); + }; + + /** + * Get the day for a schedule item for a recurrent ad + * The day may change when transforming from utc to local timezone + */ + private recurrentDay = ( + day: number, + time: string, + timezone: string, + ): number => { + const unixEpochDay = 4; // 1970-01-01 is a thursday ! + const localBaseDay = this.timeConverter.localUnixEpochDayFromTime( + time, + timezone, + ); + if (unixEpochDay == localBaseDay) return day; + if (unixEpochDay > localBaseDay) return day > 0 ? day - 1 : 6; + return day < 6 ? day + 1 : 0; + }; +} diff --git a/src/modules/ad/infrastructure/time-converter.ts b/src/modules/ad/infrastructure/time-converter.ts index 1a54b40..a08ac63 100644 --- a/src/modules/ad/infrastructure/time-converter.ts +++ b/src/modules/ad/infrastructure/time-converter.ts @@ -17,6 +17,17 @@ export class TimeConverter implements TimeConverterPort { } }; + utcStringTimeToLocalStringTime = (time: string, timezone: string): string => { + try { + if (!time || !timezone) throw new Error(); + return new DateTime(`${this.UNIX_EPOCH}T${time}`, TimeZone.zone('UTC')) + .convert(TimeZone.zone(timezone)) + .format('HH:mm'); + } catch (e) { + return undefined; + } + }; + localStringDateTimeToUtcDate = ( date: string, time: string, @@ -24,7 +35,7 @@ export class TimeConverter implements TimeConverterPort { dst = true, ): Date => { try { - if (!time || !timezone) throw new Error(); + if (!date || !time || !timezone) throw new Error(); return new Date( new DateTime( `${date}T${time}`, @@ -36,6 +47,22 @@ export class TimeConverter implements TimeConverterPort { } }; + utcStringDateTimeToLocalIsoString = ( + date: string, + time: string, + timezone: string, + dst?: boolean, + ): string => { + try { + if (!date || !time || !timezone) throw new Error(); + return new DateTime(`${date}T${time}`, TimeZone.zone('UTC')) + .convert(TimeZone.zone(timezone, dst)) + .toIsoString(); + } catch (e) { + return undefined; + } + }; + utcUnixEpochDayFromTime = (time: string, timezone: string): number => { try { if (!time || !timezone) throw new Error(); @@ -52,4 +79,18 @@ export class TimeConverter implements TimeConverterPort { return undefined; } }; + + localUnixEpochDayFromTime = (time: string, timezone: string): number => { + try { + if (!time || !timezone) throw new Error(); + return new Date( + new DateTime(`${this.UNIX_EPOCH}T${time}`, TimeZone.zone('UTC')) + .convert(TimeZone.zone(timezone)) + .toIsoString() + .split('T')[0], + ).getDay(); + } catch (e) { + return undefined; + } + }; } diff --git a/src/modules/ad/interface/grpc-controllers/ad.proto b/src/modules/ad/interface/grpc-controllers/ad.proto index 58f84d2..b241574 100644 --- a/src/modules/ad/interface/grpc-controllers/ad.proto +++ b/src/modules/ad/interface/grpc-controllers/ad.proto @@ -37,8 +37,8 @@ message ScheduleItem { message Waypoint { int32 position = 1; - float lon = 2; - float lat = 3; + double lon = 2; + double lat = 3; string name = 4; string houseNumber = 5; string street = 6; diff --git a/src/modules/ad/interface/grpc-controllers/dtos/coordinates.dto.ts b/src/modules/ad/interface/grpc-controllers/dtos/coordinates.dto.ts index 3e622be..cb636ae 100644 --- a/src/modules/ad/interface/grpc-controllers/dtos/coordinates.dto.ts +++ b/src/modules/ad/interface/grpc-controllers/dtos/coordinates.dto.ts @@ -1,17 +1,9 @@ -import { Transform } from 'class-transformer'; import { IsLatitude, IsLongitude } from 'class-validator'; -import { toPrecision } from './transformers/to-precision'; export class CoordinatesDto { - @Transform(({ value }) => toPrecision(value, 6), { - toClassOnly: true, - }) @IsLongitude() lon: number; - @Transform(({ value }) => toPrecision(value, 6), { - toClassOnly: true, - }) @IsLatitude() lat: number; } diff --git a/src/modules/ad/interface/grpc-controllers/dtos/transformers/int-to-frequency.ts b/src/modules/ad/interface/grpc-controllers/dtos/transformers/int-to-frequency.ts deleted file mode 100644 index bd707b7..0000000 --- a/src/modules/ad/interface/grpc-controllers/dtos/transformers/int-to-frequency.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Frequency } from '@modules/ad/core/domain/ad.types'; - -export const intToFrequency = (frequencyAsInt: number): Frequency => { - if (frequencyAsInt == 1) return Frequency.PUNCTUAL; - if (frequencyAsInt == 2) return Frequency.RECURRENT; - throw new Error('Unknown frequency value'); -}; diff --git a/src/modules/ad/interface/grpc-controllers/dtos/transformers/to-precision.ts b/src/modules/ad/interface/grpc-controllers/dtos/transformers/to-precision.ts deleted file mode 100644 index 997e89c..0000000 --- a/src/modules/ad/interface/grpc-controllers/dtos/transformers/to-precision.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const toPrecision = (input: number, precision: number): number => { - const multiplier = 10 ** precision; - return Math.round((input + Number.EPSILON) * multiplier) / multiplier; -}; diff --git a/src/modules/ad/interface/grpc-controllers/find-ad-by-id.grpc.controller.ts b/src/modules/ad/interface/grpc-controllers/find-ad-by-id.grpc.controller.ts index eb31d19..66d896f 100644 --- a/src/modules/ad/interface/grpc-controllers/find-ad-by-id.grpc.controller.ts +++ b/src/modules/ad/interface/grpc-controllers/find-ad-by-id.grpc.controller.ts @@ -23,7 +23,7 @@ export class FindAdByIdGrpcController { private readonly queryBus: QueryBus, ) {} - @GrpcMethod('AdsService', 'FindOneById') + @GrpcMethod('AdService', 'FindOneById') async findOnebyId(data: FindAdByIdRequestDto): Promise { try { const ad: AdEntity = await this.queryBus.execute( diff --git a/src/modules/ad/tests/unit/ad.mapper.spec.ts b/src/modules/ad/tests/unit/ad.mapper.spec.ts index b8cd8a3..e47333a 100644 --- a/src/modules/ad/tests/unit/ad.mapper.spec.ts +++ b/src/modules/ad/tests/unit/ad.mapper.spec.ts @@ -1,4 +1,6 @@ +import { OUTPUT_DATETIME_TRANSFORMER } from '@modules/ad/ad.di-tokens'; import { AdMapper } from '@modules/ad/ad.mapper'; +import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port'; import { AdEntity } from '@modules/ad/core/domain/ad.entity'; import { Frequency } from '@modules/ad/core/domain/ad.types'; import { @@ -111,12 +113,25 @@ const adReadModel: AdReadModel = { updatedAt: now, }; +const mockOutputDatetimeTransformer: DateTimeTransformerPort = { + fromDate: jest.fn(), + toDate: jest.fn(), + day: jest.fn(), + time: jest.fn(), +}; + describe('Ad Mapper', () => { let adMapper: AdMapper; beforeAll(async () => { const module = await Test.createTestingModule({ - providers: [AdMapper], + providers: [ + AdMapper, + { + provide: OUTPUT_DATETIME_TRANSFORMER, + useValue: mockOutputDatetimeTransformer, + }, + ], }).compile(); adMapper = module.get(AdMapper); }); diff --git a/src/modules/ad/tests/unit/core/create-ad.service.spec.ts b/src/modules/ad/tests/unit/core/create-ad.service.spec.ts index 0f99875..9e4982a 100644 --- a/src/modules/ad/tests/unit/core/create-ad.service.spec.ts +++ b/src/modules/ad/tests/unit/core/create-ad.service.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AD_REPOSITORY, - DATETIME_TRANSFORMER, + INPUT_DATETIME_TRANSFORMER, PARAMS_PROVIDER, } from '@modules/ad/ad.di-tokens'; import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto'; @@ -76,7 +76,7 @@ const mockDefaultParamsProvider: DefaultParamsProviderPort = { }, }; -const mockDateTimeTransformer: DateTimeTransformerPort = { +const mockInputDateTimeTransformer: DateTimeTransformerPort = { fromDate: jest.fn(), toDate: jest.fn(), day: jest.fn(), @@ -98,8 +98,8 @@ describe('create-ad.service', () => { useValue: mockDefaultParamsProvider, }, { - provide: DATETIME_TRANSFORMER, - useValue: mockDateTimeTransformer, + provide: INPUT_DATETIME_TRANSFORMER, + useValue: mockInputDateTimeTransformer, }, CreateAdService, ], diff --git a/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts b/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts index 43ed4ac..7c5f45a 100644 --- a/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts @@ -1,4 +1,6 @@ +import { OUTPUT_DATETIME_TRANSFORMER } from '@modules/ad/ad.di-tokens'; import { AdMapper } from '@modules/ad/ad.mapper'; +import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port'; import { AdRepository } from '@modules/ad/infrastructure/ad.repository'; import { PrismaService } from '@modules/ad/infrastructure/prisma.service'; import { EventEmitter2, EventEmitterModule } from '@nestjs/event-emitter'; @@ -8,6 +10,13 @@ const mockMessagePublisher = { publish: jest.fn().mockImplementation(), }; +const mockOutputDatetimeTransformer: DateTimeTransformerPort = { + fromDate: jest.fn(), + toDate: jest.fn(), + day: jest.fn(), + time: jest.fn(), +}; + describe('Ad repository', () => { let prismaService: PrismaService; let adMapper: AdMapper; @@ -16,7 +25,14 @@ describe('Ad repository', () => { beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [EventEmitterModule.forRoot()], - providers: [PrismaService, AdMapper], + providers: [ + PrismaService, + AdMapper, + { + provide: OUTPUT_DATETIME_TRANSFORMER, + useValue: mockOutputDatetimeTransformer, + }, + ], }).compile(); prismaService = module.get(PrismaService); diff --git a/src/modules/ad/tests/unit/infrastructure/datetime-transformer.spec.ts b/src/modules/ad/tests/unit/infrastructure/input-datetime-transformer.spec.ts similarity index 83% rename from src/modules/ad/tests/unit/infrastructure/datetime-transformer.spec.ts rename to src/modules/ad/tests/unit/infrastructure/input-datetime-transformer.spec.ts index c2192f4..11733a0 100644 --- a/src/modules/ad/tests/unit/infrastructure/datetime-transformer.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/input-datetime-transformer.spec.ts @@ -7,7 +7,7 @@ import { Frequency } from '@modules/ad/core/application/ports/datetime-transform import { DefaultParamsProviderPort } from '@modules/ad/core/application/ports/default-params-provider.port'; import { TimeConverterPort } from '@modules/ad/core/application/ports/time-converter.port'; import { TimezoneFinderPort } from '@modules/ad/core/application/ports/timezone-finder.port'; -import { DateTimeTransformer } from '@modules/ad/infrastructure/datetime-transformer'; +import { InputDateTimeTransformer } from '@modules/ad/infrastructure/input-datetime-transformer'; import { Test, TestingModule } from '@nestjs/testing'; const mockDefaultParamsProvider: DefaultParamsProviderPort = { @@ -32,12 +32,14 @@ const mockTimeConverter: TimeConverterPort = { localStringTimeToUtcStringTime: jest .fn() .mockImplementationOnce(() => '00:15'), + utcStringTimeToLocalStringTime: jest.fn(), localStringDateTimeToUtcDate: jest .fn() .mockImplementationOnce(() => new Date('2023-07-30T06:15:00.000Z')) .mockImplementationOnce(() => new Date('2023-07-20T08:15:00.000Z')) .mockImplementationOnce(() => new Date('2023-07-19T23:15:00.000Z')) .mockImplementationOnce(() => new Date('2023-07-19T23:15:00.000Z')), + utcStringDateTimeToLocalIsoString: jest.fn(), utcUnixEpochDayFromTime: jest .fn() .mockImplementationOnce(() => 4) @@ -45,10 +47,11 @@ const mockTimeConverter: TimeConverterPort = { .mockImplementationOnce(() => 3) .mockImplementationOnce(() => 5) .mockImplementationOnce(() => 5), + localUnixEpochDayFromTime: jest.fn(), }; -describe('Datetime Transformer', () => { - let datetimeTransformer: DateTimeTransformer; +describe('Input Datetime Transformer', () => { + let inputDatetimeTransformer: InputDateTimeTransformer; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -65,20 +68,22 @@ describe('Datetime Transformer', () => { provide: TIME_CONVERTER, useValue: mockTimeConverter, }, - DateTimeTransformer, + InputDateTimeTransformer, ], }).compile(); - datetimeTransformer = module.get(DateTimeTransformer); + inputDatetimeTransformer = module.get( + InputDateTimeTransformer, + ); }); it('should be defined', () => { - expect(datetimeTransformer).toBeDefined(); + expect(inputDatetimeTransformer).toBeDefined(); }); describe('fromDate', () => { it('should return fromDate as is if frequency is recurrent', () => { - const transformedFromDate: string = datetimeTransformer.fromDate( + const transformedFromDate: string = inputDatetimeTransformer.fromDate( { date: '2023-07-30', time: '07:15', @@ -92,7 +97,7 @@ describe('Datetime Transformer', () => { expect(transformedFromDate).toBe('2023-07-30'); }); it('should return transformed fromDate if frequency is punctual and coordinates are those of Nancy', () => { - const transformedFromDate: string = datetimeTransformer.fromDate( + const transformedFromDate: string = inputDatetimeTransformer.fromDate( { date: '2023-07-30', time: '07:15', @@ -109,7 +114,7 @@ describe('Datetime Transformer', () => { describe('toDate', () => { it('should return toDate as is if frequency is recurrent', () => { - const transformedToDate: string = datetimeTransformer.toDate( + const transformedToDate: string = inputDatetimeTransformer.toDate( '2024-07-29', { date: '2023-07-20', @@ -124,7 +129,7 @@ describe('Datetime Transformer', () => { expect(transformedToDate).toBe('2024-07-29'); }); it('should return transformed fromDate if frequency is punctual', () => { - const transformedToDate: string = datetimeTransformer.toDate( + const transformedToDate: string = inputDatetimeTransformer.toDate( '2024-07-30', { date: '2023-07-20', @@ -142,7 +147,7 @@ describe('Datetime Transformer', () => { describe('day', () => { it('should not change day if frequency is recurrent and converted UTC time is on the same day', () => { - const day: number = datetimeTransformer.day( + const day: number = inputDatetimeTransformer.day( 1, { date: '2023-07-24', @@ -157,7 +162,7 @@ describe('Datetime Transformer', () => { expect(day).toBe(1); }); it('should change day if frequency is recurrent and converted UTC time is on the previous day', () => { - const day: number = datetimeTransformer.day( + const day: number = inputDatetimeTransformer.day( 1, { date: '2023-07-24', @@ -172,7 +177,7 @@ describe('Datetime Transformer', () => { expect(day).toBe(0); }); it('should change day if frequency is recurrent and converted UTC time is on the previous day and given day is sunday', () => { - const day: number = datetimeTransformer.day( + const day: number = inputDatetimeTransformer.day( 0, { date: '2023-07-23', @@ -187,7 +192,7 @@ describe('Datetime Transformer', () => { expect(day).toBe(6); }); it('should change day if frequency is recurrent and converted UTC time is on the next day', () => { - const day: number = datetimeTransformer.day( + const day: number = inputDatetimeTransformer.day( 1, { date: '2023-07-24', @@ -202,7 +207,7 @@ describe('Datetime Transformer', () => { expect(day).toBe(2); }); it('should change day if frequency is recurrent and converted UTC time is on the next day and given day is saturday(6)', () => { - const day: number = datetimeTransformer.day( + const day: number = inputDatetimeTransformer.day( 6, { date: '2023-07-29', @@ -217,7 +222,7 @@ describe('Datetime Transformer', () => { expect(day).toBe(0); }); it('should return utc fromDate day if frequency is punctual', () => { - const day: number = datetimeTransformer.day( + const day: number = inputDatetimeTransformer.day( 1, { date: '2023-07-20', @@ -235,7 +240,7 @@ describe('Datetime Transformer', () => { describe('time', () => { it('should transform given time to utc time if frequency is recurrent', () => { - const time: string = datetimeTransformer.time( + const time: string = inputDatetimeTransformer.time( { date: '2023-07-24', time: '01:15', @@ -249,7 +254,7 @@ describe('Datetime Transformer', () => { expect(time).toBe('00:15'); }); it('should return given time to utc time if frequency is punctual', () => { - const time: string = datetimeTransformer.time( + const time: string = inputDatetimeTransformer.time( { date: '2023-07-24', time: '01:15', diff --git a/src/modules/ad/tests/unit/infrastructure/time-converter.spec.ts b/src/modules/ad/tests/unit/infrastructure/time-converter.spec.ts index eaf6361..bdfee03 100644 --- a/src/modules/ad/tests/unit/infrastructure/time-converter.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/time-converter.spec.ts @@ -54,6 +54,54 @@ describe('Time Converter', () => { }); }); + describe('utcStringTimeToLocalStringTime', () => { + it('should convert a utc time to a paris time', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcTime = '07:00'; + const parisTime = timeConverter.utcStringTimeToLocalStringTime( + utcTime, + 'Europe/Paris', + ); + expect(parisTime).toBe('08:00'); + }); + it('should return undefined if time is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcTime = '27:00'; + const parisTime = timeConverter.utcStringTimeToLocalStringTime( + utcTime, + 'Europe/Paris', + ); + expect(parisTime).toBeUndefined(); + }); + it('should return undefined if time is undefined', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcTime = undefined; + const parisTime = timeConverter.utcStringTimeToLocalStringTime( + utcTime, + 'Europe/Paris', + ); + expect(parisTime).toBeUndefined(); + }); + it('should return undefined if timezone is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcTime = '07:00'; + const parisTime = timeConverter.utcStringTimeToLocalStringTime( + utcTime, + 'Foo/Bar', + ); + expect(parisTime).toBeUndefined(); + }); + it('should return undefined if timezone is undefined', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcTime = '07:00'; + const parisTime = timeConverter.utcStringTimeToLocalStringTime( + utcTime, + undefined, + ); + expect(parisTime).toBeUndefined(); + }); + }); + describe('localStringDateTimeToUtcDate', () => { it('should convert a summer paris date and time to a utc date', () => { const timeConverter: TimeConverter = new TimeConverter(); @@ -111,6 +159,28 @@ describe('Time Converter', () => { ); expect(utcDate.toISOString()).toBe('2023-02-03T01:00:00.000Z'); }); + it('should return undefined if date is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const parisDate = '2023-06-32'; + const parisTime = '08:00'; + const utcDate = timeConverter.localStringDateTimeToUtcDate( + parisDate, + parisTime, + 'Europe/Paris', + ); + expect(utcDate).toBeUndefined(); + }); + it('should return undefined if date is undefined', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const parisDate = undefined; + const parisTime = '08:00'; + const utcDate = timeConverter.localStringDateTimeToUtcDate( + parisDate, + parisTime, + 'Europe/Paris', + ); + expect(utcDate).toBeUndefined(); + }); it('should return undefined if time is invalid', () => { const timeConverter: TimeConverter = new TimeConverter(); const parisDate = '2023-06-22'; @@ -157,7 +227,132 @@ describe('Time Converter', () => { }); }); - describe('utcBaseDayFromTime', () => { + describe('utcStringDateTimeToLocalIsoString', () => { + it('should convert a utc string date and time to a summer paris date isostring', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcDate = '2023-06-22'; + const utcTime = '10:00'; + const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString( + utcDate, + utcTime, + 'Europe/Paris', + ); + expect(localIsoString).toBe('2023-06-22T12:00:00.000+02:00'); + }); + it('should convert a utc string date and time to a winter paris date isostring', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcDate = '2023-02-02'; + const utcTime = '10:00'; + const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString( + utcDate, + utcTime, + 'Europe/Paris', + ); + expect(localIsoString).toBe('2023-02-02T11:00:00.000+01:00'); + }); + it('should convert a utc string date and time to a summer paris date isostring without dst', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcDate = '2023-06-22'; + const utcTime = '10:00'; + const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString( + utcDate, + utcTime, + 'Europe/Paris', + false, + ); + expect(localIsoString).toBe('2023-06-22T11:00:00.000+01:00'); + }); + it('should convert a utc date to a tonga date isostring', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcDate = '2023-02-01'; + const utcTime = '23:00'; + const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString( + utcDate, + utcTime, + 'Pacific/Tongatapu', + ); + expect(localIsoString).toBe('2023-02-02T12:00:00.000+13:00'); + }); + it('should convert a utc date to a papeete date isostring', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcDate = '2023-02-03'; + const utcTime = '01:00'; + const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString( + utcDate, + utcTime, + 'Pacific/Tahiti', + ); + expect(localIsoString).toBe('2023-02-02T15:00:00.000-10:00'); + }); + it('should return undefined if date is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcDate = '2023-06-32'; + const utcTime = '07:00'; + const parisTime = timeConverter.utcStringDateTimeToLocalIsoString( + utcDate, + utcTime, + 'Europe/Paris', + ); + expect(parisTime).toBeUndefined(); + }); + it('should return undefined if date is undefined', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcDate = undefined; + const utcTime = '07:00'; + const parisTime = timeConverter.utcStringDateTimeToLocalIsoString( + utcDate, + utcTime, + 'Europe/Paris', + ); + expect(parisTime).toBeUndefined(); + }); + it('should return undefined if time is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcDate = '2023-06-22'; + const utcTime = '27:00'; + const parisTime = timeConverter.utcStringDateTimeToLocalIsoString( + utcDate, + utcTime, + 'Europe/Paris', + ); + expect(parisTime).toBeUndefined(); + }); + it('should return undefined if time is undefined', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcDate = '2023-06-22'; + const utcTime = undefined; + const parisTime = timeConverter.utcStringDateTimeToLocalIsoString( + utcDate, + utcTime, + 'Europe/Paris', + ); + expect(parisTime).toBeUndefined(); + }); + it('should return undefined if timezone is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcDate = '2023-06-22'; + const utcTime = '07:00'; + const parisTime = timeConverter.utcStringDateTimeToLocalIsoString( + utcDate, + utcTime, + 'Foo/Bar', + ); + expect(parisTime).toBeUndefined(); + }); + it('should return undefined if timezone is undefined', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcDate = '2023-06-22'; + const utcTime = '07:00'; + const parisTime = timeConverter.utcStringDateTimeToLocalIsoString( + utcDate, + utcTime, + undefined, + ); + expect(parisTime).toBeUndefined(); + }); + }); + + describe('utcUnixEpochDayFromTime', () => { it('should get the utc day of paris at 12:00', () => { const timeConverter: TimeConverter = new TimeConverter(); expect( @@ -201,4 +396,49 @@ describe('Time Converter', () => { ).toBeUndefined(); }); }); + + describe('localUnixEpochDayFromTime', () => { + it('should get the day of paris at 12:00 utc', () => { + const timeConverter: TimeConverter = new TimeConverter(); + expect( + timeConverter.localUnixEpochDayFromTime('12:00', 'Europe/Paris'), + ).toBe(4); + }); + it('should get the day of paris at 23:00 utc', () => { + const timeConverter: TimeConverter = new TimeConverter(); + expect( + timeConverter.localUnixEpochDayFromTime('23:00', 'Europe/Paris'), + ).toBe(5); + }); + it('should get the day of papeete at 05:00 utc', () => { + const timeConverter: TimeConverter = new TimeConverter(); + expect( + timeConverter.localUnixEpochDayFromTime('05:00', 'Pacific/Tahiti'), + ).toBe(3); + }); + it('should return undefined if time is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + expect( + timeConverter.localUnixEpochDayFromTime('28:00', 'Europe/Paris'), + ).toBeUndefined(); + }); + it('should return undefined if time is undefined', () => { + const timeConverter: TimeConverter = new TimeConverter(); + expect( + timeConverter.localUnixEpochDayFromTime(undefined, 'Europe/Paris'), + ).toBeUndefined(); + }); + it('should return undefined if timezone is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + expect( + timeConverter.localUnixEpochDayFromTime('12:00', 'Foo/Bar'), + ).toBeUndefined(); + }); + it('should return undefined if timezone is undefined', () => { + const timeConverter: TimeConverter = new TimeConverter(); + expect( + timeConverter.localUnixEpochDayFromTime('12:00', undefined), + ).toBeUndefined(); + }); + }); }); diff --git a/src/modules/ad/tests/unit/interface/int-to-frequency.spec.ts b/src/modules/ad/tests/unit/interface/int-to-frequency.spec.ts deleted file mode 100644 index 1f4a0d7..0000000 --- a/src/modules/ad/tests/unit/interface/int-to-frequency.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Frequency } from '@modules/ad/core/domain/ad.types'; -import { intToFrequency } from '@modules/ad/interface/grpc-controllers/dtos/transformers/int-to-frequency'; - -describe('frequency mapping', () => { - it('should return punctual if frequency is 1', () => { - expect(intToFrequency(1)).toBe(Frequency.PUNCTUAL); - }); - it('should return recurrent if frequency is 2', () => { - expect(intToFrequency(2)).toBe(Frequency.RECURRENT); - }); - it('should throw an error if frequency is unknown', () => { - expect(() => intToFrequency(0)).toThrow(); - expect(() => intToFrequency(3)).toThrow(); - }); -}); diff --git a/src/modules/ad/tests/unit/interface/to-precision.spec.ts b/src/modules/ad/tests/unit/interface/to-precision.spec.ts deleted file mode 100644 index 2da0933..0000000 --- a/src/modules/ad/tests/unit/interface/to-precision.spec.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { toPrecision } from '@modules/ad/interface/grpc-controllers/dtos/transformers/to-precision'; - -describe('precision handler', () => { - it('should return a 6 digits float number for a 10 digits float input number and 6 as precision', () => { - const precised = toPrecision(1.1234567891, 6); - const stringPrecised = precised.toString().split('.')[1]; - expect(stringPrecised.length).toBe(6); - }); - it('should return a 2 digits float number for a 2 digits float input number and 4 as precision', () => { - const precised = toPrecision(1.12, 4); - const stringPrecised = precised.toString().split('.')[1]; - expect(stringPrecised.length).toBe(2); - }); -});