fixed bad ad mapping

This commit is contained in:
sbriat 2023-09-12 14:40:16 +02:00
parent 2058bfce4c
commit 1939f62049
13 changed files with 238 additions and 144 deletions

View File

@ -7,10 +7,14 @@ import {
AdWriteExtraModel,
} from './infrastructure/ad.repository';
import { v4 } from 'uuid';
import { ScheduleItemProps } from './core/domain/value-objects/schedule-item.value-object';
import {
ScheduleItem,
ScheduleItemProps,
} from './core/domain/value-objects/schedule-item.value-object';
import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port';
import { AD_DIRECTION_ENCODER } from './ad.di-tokens';
import { ExtendedMapper } from '@mobicoop/ddd-library';
import { Waypoint } from './core/domain/value-objects/waypoint.value-object';
/**
* Mapper constructs objects that are used in different layers:
@ -76,28 +80,12 @@ export class AdMapper
return record;
};
toDomain = (record: AdReadModel): AdEntity => {
const entity = new AdEntity({
toDomain = (record: AdReadModel): AdEntity =>
new AdEntity({
id: record.uuid,
createdAt: new Date(record.createdAt),
updatedAt: new Date(record.updatedAt),
props: {
driver: record.driver,
passenger: record.passenger,
frequency: record.frequency,
fromDate: record.fromDate.toISOString().split('T')[0],
toDate: record.toDate.toISOString().split('T')[0],
schedule: record.schedule.map((scheduleItem: ScheduleItemModel) => ({
day: scheduleItem.day,
time: `${scheduleItem.time
.getUTCHours()
.toString()
.padStart(2, '0')}:${scheduleItem.time
.getUTCMinutes()
.toString()
.padStart(2, '0')}`,
margin: scheduleItem.margin,
})),
seatsProposed: record.seatsProposed,
seatsRequested: record.seatsRequested,
strict: record.strict,
@ -105,19 +93,37 @@ export class AdMapper
driverDistance: record.driverDistance,
passengerDuration: record.passengerDuration,
passengerDistance: record.passengerDistance,
waypoints: this.directionEncoder
.decode(record.waypoints)
.map((coordinates, index) => ({
position: index,
...coordinates,
})),
driver: record.driver,
passenger: record.passenger,
frequency: record.frequency,
fromDate: record.fromDate.toISOString().split('T')[0],
toDate: record.toDate.toISOString().split('T')[0],
schedule: record.schedule.map(
(scheduleItem: ScheduleItemModel) =>
new ScheduleItem({
day: scheduleItem.day,
time: `${scheduleItem.time
.getUTCHours()
.toString()
.padStart(2, '0')}:${scheduleItem.time
.getUTCMinutes()
.toString()
.padStart(2, '0')}`,
margin: scheduleItem.margin,
}),
),
waypoints: this.directionEncoder.decode(record.waypoints).map(
(coordinates, index) =>
new Waypoint({
position: index,
...coordinates,
}),
),
fwdAzimuth: record.fwdAzimuth,
backAzimuth: record.backAzimuth,
points: [],
},
});
return entity;
};
toPersistenceExtra = (entity: AdEntity): AdWriteExtraModel => ({
waypoints: this.directionEncoder.encode(entity.getProps().waypoints),

View File

@ -1,7 +1,6 @@
import { ExtendedRepositoryPort } from '@mobicoop/ddd-library';
import { AdEntity } from '../../domain/ad.entity';
import { AdReadModel } from '@modules/ad/infrastructure/ad.repository';
export type AdRepositoryPort = ExtendedRepositoryPort<AdEntity> & {
getCandidates(queryString: string): Promise<AdReadModel[]>;
getCandidateAds(queryString: string): Promise<AdEntity[]>;
};

View File

@ -1,10 +1,10 @@
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
import { Selector } from '../algorithm.abstract';
import { AdReadModel } from '@modules/ad/infrastructure/ad.repository';
import { ScheduleItem } from '../match.query';
import { Waypoint } from '../../../types/waypoint.type';
import { Point } from '../../../types/point.type';
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
export class PassengerOrientedSelector extends Selector {
select = async (): Promise<CandidateEntity[]> => {
@ -25,17 +25,18 @@ export class PassengerOrientedSelector extends Selector {
queryStringRoles.map<Promise<AdsRole>>(
async (queryStringRole: QueryStringRole) =>
<AdsRole>{
ads: await this.repository.getCandidates(queryStringRole.query),
ads: await this.repository.getCandidateAds(queryStringRole.query),
role: queryStringRole.role,
},
),
)
)
.map((adsRole: AdsRole) =>
adsRole.ads.map((adReadModel: AdReadModel) =>
adsRole.ads.map((adEntity: AdEntity) =>
CandidateEntity.create({
id: adReadModel.uuid,
id: adEntity.id,
role: adsRole.role,
waypoints: adEntity.getProps().waypoints,
}),
),
)
@ -60,7 +61,8 @@ export class PassengerOrientedSelector extends Selector {
"seatsProposed","seatsRequested",\
strict,\
"fwdAzimuth","backAzimuth",\
si.day,si.time,si.margin`,
ad."createdAt",ad."updatedAt",\
si.uuid as "scheduleItemUuid",si.day,si.time,si.margin,si."createdAt" as "scheduleItemCreatedAt",si."updatedAt" as "scheduleItemUpdatedAt"`,
role == Role.DRIVER ? this._selectAsDriver() : this._selectAsPassenger(),
].join();
@ -295,6 +297,6 @@ export type QueryStringRole = {
};
type AdsRole = {
ads: AdReadModel[];
ads: AdEntity[];
role: Role;
};

View File

@ -3,12 +3,14 @@ import { Role } from './ad.types';
// All properties that a Candidate has
export interface CandidateProps {
role: Role;
waypoints: Waypoint[];
}
// Properties that are needed for a Candidate creation
export interface CreateCandidateProps {
id: string;
role: Role;
waypoints: Waypoint[];
}
export type Waypoint = {

View File

@ -29,11 +29,17 @@ export type AdModel = {
updatedAt: Date;
};
/**
* The record as returned by the peristence system
*/
export type AdReadModel = AdModel & {
waypoints: string;
schedule: ScheduleItemModel[];
};
/**
* The record ready to be sent to the peristence system
*/
export type AdWriteModel = AdModel & {
schedule: {
create: ScheduleItemModel[];
@ -59,11 +65,14 @@ export type ScheduleItemModel = ScheduleItem & {
export type UngroupedAdModel = AdModel &
ScheduleItem & {
scheduleItemUuid: string;
scheduleItemCreatedAt: Date;
scheduleItemUpdatedAt: Date;
waypoints: string;
};
export type GroupedAdModel = AdModel & {
schedule: ScheduleItem[];
schedule: ScheduleItemModel[];
waypoints: string;
};
@ -100,14 +109,18 @@ export class AdRepository
);
}
getCandidates = async (queryString: string): Promise<AdReadModel[]> => {
// console.log(queryString);
return this.toAdReadModels(
getCandidateAds = async (queryString: string): Promise<AdEntity[]> =>
this._toAdReadModels(
(await this.prismaRaw.$queryRawUnsafe(queryString)) as UngroupedAdModel[],
);
};
)
.map((adReadModel: AdReadModel) => {
if (this.mapper.toDomain) return this.mapper.toDomain(adReadModel);
})
.filter(
(adEntity: AdEntity | undefined) => adEntity !== undefined,
) as AdEntity[];
private toAdReadModels = (
private _toAdReadModels = (
ungroupedAds: UngroupedAdModel[],
): AdReadModel[] => {
const groupedAdModels: GroupedAdModel[] = ungroupedAds.map(
@ -120,9 +133,12 @@ export class AdRepository
toDate: ungroupedAd.toDate,
schedule: [
{
uuid: ungroupedAd.scheduleItemUuid,
day: ungroupedAd.day,
time: ungroupedAd.time,
margin: ungroupedAd.margin,
createdAt: ungroupedAd.scheduleItemCreatedAt,
updatedAt: ungroupedAd.scheduleItemUpdatedAt,
},
],
seatsProposed: ungroupedAd.seatsProposed,
@ -140,14 +156,14 @@ export class AdRepository
}),
);
const adReadModels: AdReadModel[] = [];
groupedAdModels.forEach((adReadModel: AdReadModel) => {
const ad: AdReadModel | undefined = adReadModels.find(
(arm: AdReadModel) => arm.uuid == adReadModel.uuid,
groupedAdModels.forEach((groupdeAdModel: GroupedAdModel) => {
const adReadModel: AdReadModel | undefined = adReadModels.find(
(arm: AdReadModel) => arm.uuid == groupdeAdModel.uuid,
);
if (ad) {
ad.schedule.push(...adReadModel.schedule);
if (adReadModel) {
adReadModel.schedule.push(...groupdeAdModel.schedule);
} else {
adReadModels.push(adReadModel);
adReadModels.push(groupdeAdModel);
}
});
return adReadModels;

View File

@ -35,12 +35,12 @@ const destinationWaypoint: Waypoint = {
};
const mockAdRepository = {
getCandidates: jest.fn().mockImplementation(() => [
getCandidateAds: jest.fn().mockImplementation(() => [
{
ad: {
id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
},
role: Role.DRIVER,
id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
getProps: jest.fn().mockImplementation(() => ({
role: Role.DRIVER,
})),
},
]),
};

View File

@ -3,7 +3,7 @@ import { MatchQuery } from '@modules/ad/core/application/queries/match/match.que
import { PassengerOrientedAlgorithm } from '@modules/ad/core/application/queries/match/passenger-oriented-algorithm';
import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
import { Frequency } from '@modules/ad/core/domain/ad.types';
import { MatchEntity } from '@modules/ad/core/domain/match.entity';
const originWaypoint: Waypoint = {
@ -51,12 +51,12 @@ const mockMatcherRepository: AdRepositoryPort = {
delete: jest.fn(),
count: jest.fn(),
healthCheck: jest.fn(),
getCandidates: jest.fn().mockImplementation(() => [
getCandidateAds: jest.fn().mockImplementation(() => [
{
ad: {
id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
},
role: Role.DRIVER,
id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
getProps: jest.fn().mockImplementation(() => ({
waypoints: [],
})),
},
]),
};

View File

@ -44,10 +44,34 @@ const candidates: CandidateEntity[] = [
CandidateEntity.create({
id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
role: Role.DRIVER,
waypoints: [
{
position: 0,
lat: 48.678454,
lon: 6.189745,
},
{
position: 1,
lat: 48.84877,
lon: 2.398457,
},
],
}),
CandidateEntity.create({
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
role: Role.PASSENGER,
waypoints: [
{
position: 0,
lat: 48.668487,
lon: 6.178457,
},
{
position: 1,
lat: 48.897457,
lon: 2.3688487,
},
],
}),
];

View File

@ -99,33 +99,35 @@ const mockMatcherRepository: AdRepositoryPort = {
delete: jest.fn(),
count: jest.fn(),
healthCheck: jest.fn(),
getCandidates: jest.fn().mockImplementation(() => [
getCandidateAds: jest.fn().mockImplementation(() => [
{
uuid: 'cc260669-1c6d-441f-80a5-19cd59afb777',
driver: true,
passenger: true,
frequency: Frequency.PUNCTUAL,
fromDate: new Date('2023-06-21'),
toDate: new Date('2023-06-21'),
seatsProposed: 3,
seatsRequested: 1,
strict: false,
ddriverDistance: 350000,
driverDuration: 14400,
passengerDistance: 350000,
passengerDuration: 14400,
fwdAzimuth: 273,
backAzimuth: 93,
createdAt: new Date('2023-06-20T17:05:00Z'),
updatedAt: new Date('2023-06-20T17:05:00Z'),
waypoints: 'LINESTRING(6.1765102 48.689445,2.3522 48.8566)',
schedule: [
{
day: 3,
time: new Date('2023-06-21T07:05:00Z'),
margin: 900,
},
],
id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
getProps: jest.fn().mockImplementation(() => ({
driver: true,
passenger: true,
frequency: Frequency.PUNCTUAL,
fromDate: new Date('2023-06-21'),
toDate: new Date('2023-06-21'),
seatsProposed: 3,
seatsRequested: 1,
strict: false,
ddriverDistance: 350000,
driverDuration: 14400,
passengerDistance: 350000,
passengerDuration: 14400,
fwdAzimuth: 273,
backAzimuth: 93,
createdAt: new Date('2023-06-20T17:05:00Z'),
updatedAt: new Date('2023-06-20T17:05:00Z'),
waypoints: 'LINESTRING(6.1765102 48.689445,2.3522 48.8566)',
schedule: [
{
day: 3,
time: new Date('2023-06-21T07:05:00Z'),
margin: 900,
},
],
})),
},
]),
};

View File

@ -44,10 +44,34 @@ const candidates: CandidateEntity[] = [
CandidateEntity.create({
id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
role: Role.DRIVER,
waypoints: [
{
position: 0,
lat: 48.678454,
lon: 6.189745,
},
{
position: 1,
lat: 48.84877,
lon: 2.398457,
},
],
}),
CandidateEntity.create({
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
role: Role.PASSENGER,
waypoints: [
{
position: 0,
lat: 48.668487,
lon: 6.178457,
},
{
position: 1,
lat: 48.897457,
lon: 2.3688487,
},
],
}),
];

View File

@ -5,11 +5,9 @@ import {
} from '@modules/ad/ad.di-tokens';
import { AdMapper } from '@modules/ad/ad.mapper';
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { Frequency } from '@modules/ad/core/domain/ad.types';
import {
AdReadModel,
AdRepository,
} from '@modules/ad/infrastructure/ad.repository';
import { AdRepository } from '@modules/ad/infrastructure/ad.repository';
import { PrismaService } from '@modules/ad/infrastructure/prisma.service';
import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port';
import { EventEmitterModule } from '@nestjs/event-emitter';
@ -21,7 +19,58 @@ const mockMessagePublisher = {
const mockDirectionEncoder: DirectionEncoderPort = {
encode: jest.fn(),
decode: jest.fn(),
decode: jest
.fn()
.mockImplementationOnce(() => [
{
lon: 6.1765102,
lat: 48.689445,
},
{
lon: 2.3522,
lat: 48.8566,
},
])
.mockImplementationOnce(() => [
{
lon: 6.1765109,
lat: 48.689455,
},
{
lon: 2.3598,
lat: 48.8589,
},
])
.mockImplementationOnce(() => [
{
lon: 6.1765102,
lat: 48.689445,
},
{
lon: 2.3522,
lat: 48.8566,
},
])
.mockImplementationOnce(() => [
{
lon: 6.1765102,
lat: 48.689445,
},
{
lon: 2.3522,
lat: 48.8566,
},
])
.mockImplementationOnce(() => [
{
lon: 6.1765102,
lat: 48.689445,
},
{
lon: 2.3522,
lat: 48.8566,
},
]),
};
const mockRouteProvider: RouteProviderPort = {
@ -43,7 +92,7 @@ const mockPrismaService = {
seatsProposed: 3,
seatsRequested: 1,
strict: false,
ddriverDistance: 350000,
driverDistance: 350000,
driverDuration: 14400,
passengerDistance: 350000,
passengerDuration: 14400,
@ -52,9 +101,12 @@ const mockPrismaService = {
createdAt: new Date('2023-06-20T17:05:00Z'),
updatedAt: new Date('2023-06-20T17:05:00Z'),
waypoints: 'LINESTRING(6.1765102 48.689445,2.3522 48.8566)',
scheduleItemUuid: 'b6bfac1f-e62e-4622-9641-a3475e15fc00',
day: 3,
time: new Date('2023-06-21T07:05:00Z'),
margin: 900,
scheduleItemCreatedAt: new Date('2023-06-20T17:05:00Z'),
scheduleItemUpdatedAt: new Date('2023-06-20T17:05:00Z'),
},
{
uuid: '84af18ff-8779-4cac-9651-1ed5ab0713c4',
@ -66,7 +118,7 @@ const mockPrismaService = {
seatsProposed: 3,
seatsRequested: 1,
strict: false,
ddriverDistance: 349000,
driverDistance: 349000,
driverDuration: 14300,
passengerDistance: 350000,
passengerDuration: 14400,
@ -75,9 +127,12 @@ const mockPrismaService = {
createdAt: new Date('2023-06-18T14:16:10Z'),
updatedAt: new Date('2023-06-18T14:16:10Z'),
waypoints: 'LINESTRING(6.1765109 48.689455,2.3598 48.8589)',
scheduleItemUuid: '01524541-2044-49dc-8be6-1a3ccdc653b0',
day: 3,
time: new Date('2023-06-21T07:14:00Z'),
margin: 900,
scheduleItemCreatedAt: new Date('2023-06-18T14:16:10Z'),
scheduleItemUpdatedAt: new Date('2023-06-18T14:16:10Z'),
},
];
})
@ -93,7 +148,7 @@ const mockPrismaService = {
seatsProposed: 3,
seatsRequested: 1,
strict: false,
ddriverDistance: 350000,
driverDistance: 350000,
driverDuration: 14400,
passengerDistance: 350000,
passengerDuration: 14400,
@ -102,9 +157,12 @@ const mockPrismaService = {
createdAt: new Date('2023-06-20T17:05:00Z'),
updatedAt: new Date('2023-06-20T17:05:00Z'),
waypoints: 'LINESTRING(6.1765102 48.689445,2.3522 48.8566)',
scheduleItemUuid: '1387b34f-8ab1-46e0-8d0f-803af0f40f28',
day: 3,
time: new Date('2023-06-21T07:05:00Z'),
margin: 900,
scheduleItemCreatedAt: new Date('2023-06-20T17:05:00Z'),
scheduleItemUpdatedAt: new Date('2023-06-20T17:05:00Z'),
},
{
uuid: 'cc260669-1c6d-441f-80a5-19cd59afb777',
@ -116,7 +174,7 @@ const mockPrismaService = {
seatsProposed: 3,
seatsRequested: 1,
strict: false,
ddriverDistance: 350000,
driverDistance: 350000,
driverDuration: 14400,
passengerDistance: 350000,
passengerDuration: 14400,
@ -125,9 +183,12 @@ const mockPrismaService = {
createdAt: new Date('2023-06-20T17:05:00Z'),
updatedAt: new Date('2023-06-20T17:05:00Z'),
waypoints: 'LINESTRING(6.1765102 48.689445,2.3522 48.8566)',
scheduleItemUuid: '1fa88104-c50b-4f10-b8ce-389df765f3a6',
day: 4,
time: new Date('2023-06-21T07:15:00Z'),
margin: 900,
scheduleItemCreatedAt: new Date('2023-06-20T17:05:00Z'),
scheduleItemUpdatedAt: new Date('2023-06-20T17:05:00Z'),
},
{
uuid: 'cc260669-1c6d-441f-80a5-19cd59afb777',
@ -139,7 +200,7 @@ const mockPrismaService = {
seatsProposed: 3,
seatsRequested: 1,
strict: false,
ddriverDistance: 350000,
driverDistance: 350000,
driverDuration: 14400,
passengerDistance: 350000,
passengerDuration: 14400,
@ -148,9 +209,12 @@ const mockPrismaService = {
createdAt: new Date('2023-06-20T17:05:00Z'),
updatedAt: new Date('2023-06-20T17:05:00Z'),
waypoints: 'LINESTRING(6.1765102 48.689445,2.3522 48.8566)',
scheduleItemUuid: '760bb1bb-256b-4e79-9d82-6d13011118f1',
day: 5,
time: new Date('2023-06-21T07:16:00Z'),
margin: 900,
scheduleItemCreatedAt: new Date('2023-06-20T17:05:00Z'),
scheduleItemUpdatedAt: new Date('2023-06-20T17:05:00Z'),
},
];
})
@ -194,22 +258,22 @@ describe('Ad repository', () => {
});
it('should get candidates if query returns punctual Ads', async () => {
const candidates: AdReadModel[] = await adRepository.getCandidates(
const candidates: AdEntity[] = await adRepository.getCandidateAds(
'somePunctualQueryString',
);
expect(candidates.length).toBe(2);
});
it('should get candidates if query returns recurrent Ads', async () => {
const candidates: AdReadModel[] = await adRepository.getCandidates(
const candidates: AdEntity[] = await adRepository.getCandidateAds(
'someRecurrentQueryString',
);
expect(candidates.length).toBe(1);
expect(candidates[0].schedule.length).toBe(3);
expect(candidates[0].getProps().schedule.length).toBe(3);
});
it('should return an empty array of candidates if query does not return Ads', async () => {
const candidates: AdReadModel[] = await adRepository.getCandidates(
const candidates: AdEntity[] = await adRepository.getCandidateAds(
'someQueryString',
);
expect(candidates.length).toBe(0);

View File

@ -29,47 +29,4 @@ export class RouteEntity extends AggregateRoot<RouteProps> {
validate(): void {
// entity business rules validation to protect it's invariant before saving entity to a database
}
// private static getPaths = (
// roles: Role[],
// waypoints: WaypointProps[],
// ): Path[] => {
// const paths: Path[] = [];
// if (roles.includes(Role.DRIVER) && roles.includes(Role.PASSENGER)) {
// if (waypoints.length == 2) {
// // 2 points => same route for driver and passenger
// paths.push(this.createGenericPath(waypoints));
// } else {
// paths.push(
// this.createDriverPath(waypoints),
// this.createPassengerPath(waypoints),
// );
// }
// } else if (roles.includes(Role.DRIVER)) {
// paths.push(this.createDriverPath(waypoints));
// } else if (roles.includes(Role.PASSENGER)) {
// paths.push(this.createPassengerPath(waypoints));
// }
// return paths;
// };
// private static createGenericPath = (waypoints: WaypointProps[]): Path =>
// this.createPath(waypoints, PathType.GENERIC);
// private static createDriverPath = (waypoints: WaypointProps[]): Path =>
// this.createPath(waypoints, PathType.DRIVER);
// private static createPassengerPath = (waypoints: WaypointProps[]): Path =>
// this.createPath(
// [waypoints[0], waypoints[waypoints.length - 1]],
// PathType.PASSENGER,
// );
// private static createPath = (
// points: WaypointProps[],
// type: PathType,
// ): Path => ({
// type,
// points,
// });
}

View File

@ -45,5 +45,3 @@ export type Spacetime = {
};
export type Step = Point & Spacetime;
export type Waystep = Waypoint & Spacetime;