Skip to content

Commit 3aee239

Browse files
committed
Refactor DateOverrideConflict to be more de-decoupled
1 parent 7e32841 commit 3aee239

File tree

4 files changed

+106
-76
lines changed

4 files changed

+106
-76
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { isNotFalsy, type NonEmptyArray } from '@seedcompany/common';
2+
import type { ID } from '../id-field';
3+
import { CalendarDate } from '../temporal';
4+
import type { Range } from '../types';
5+
import { RangeException } from './range.exception';
6+
7+
type Conflict = Readonly<{
8+
__typename: string;
9+
id: ID;
10+
label: string;
11+
point: 'start' | 'end';
12+
date: CalendarDate;
13+
}>;
14+
15+
interface IdentifiableResource {
16+
__typename: string;
17+
id: ID;
18+
[key: string]: unknown;
19+
}
20+
21+
export class DateOverrideConflictException extends RangeException {
22+
constructor(
23+
readonly object: IdentifiableResource,
24+
readonly canonical: Range<CalendarDate | null>,
25+
label: [singular: string, plural: string],
26+
readonly conflicts: NonEmptyArray<Conflict>,
27+
) {
28+
const message = [
29+
conflicts.length === 1
30+
? `${label[0]} has a date outside the new range`
31+
: `${label[1]} have dates outside the new range`,
32+
...conflicts.map(({ date, point, label }) => {
33+
const pointStr = point === 'start' ? 'Start' : 'End';
34+
const dateStr = date.toISO();
35+
return ` - ${pointStr} date of ${label} is ${dateStr}`;
36+
}),
37+
].join('\n');
38+
super({ message });
39+
}
40+
41+
static findConflicts(
42+
canonical: Range<CalendarDate | null>,
43+
items: ReadonlyArray<{
44+
__typename: string;
45+
id: ID;
46+
label: string;
47+
start: CalendarDate | null;
48+
end: CalendarDate | null;
49+
}>,
50+
): NonEmptyArray<Conflict> | undefined {
51+
const maybeConflicts = items.flatMap<Conflict | null>(
52+
({ start, end, ...item }) => [
53+
canonical.start && start && canonical.start > start
54+
? {
55+
...item,
56+
point: 'start' as const,
57+
date: start,
58+
}
59+
: null,
60+
canonical.end && end && canonical.end < end
61+
? {
62+
...item,
63+
point: 'end' as const,
64+
date: end,
65+
}
66+
: null,
67+
],
68+
);
69+
return asNonEmpty(maybeConflicts.filter(isNotFalsy));
70+
}
71+
}
72+
73+
const asNonEmpty = <T>(items: readonly T[]) =>
74+
items.length === 0 ? undefined : (items as NonEmptyArray<T>);

src/common/exceptions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ export * from './unauthorized.exception';
99
export * from './service-unavailable.exception';
1010
export * from './invalid-id-for-type.exception';
1111
export * from './creation-failed.exception';
12+
export * from './date-override-conflict.exception';
Lines changed: 24 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import { isNotFalsy, NonEmptyArray } from '@seedcompany/common';
2-
import { CalendarDate, ID, RangeException, type UnsecuredDto } from '~/common';
1+
import { DateOverrideConflictException, EnhancedResource } from '~/common';
32
import { EventsHandler, IEventHandler } from '~/core';
4-
import type { Project } from '../../project/dto';
53
import { ProjectUpdatedEvent } from '../../project/events';
64
import { EngagementService } from '../engagement.service';
75

@@ -12,84 +10,37 @@ export class ValidateEngDateOverridesOnProjectChangeHandler
1210
constructor(private readonly engagements: EngagementService) {}
1311

1412
async handle(event: ProjectUpdatedEvent) {
15-
const { changes, updated, session } = event;
13+
const { updated: project, changes, session } = event;
1614

1715
if (changes.mouStart === undefined && changes.mouEnd === undefined) {
1816
return;
1917
}
2018

21-
const project = {
22-
id: updated.id,
23-
name: updated.name,
24-
mouStart: updated.mouStart,
25-
mouEnd: updated.mouEnd,
26-
};
2719
const engagements = await this.engagements.listAllByProjectId(
2820
project.id,
2921
session,
3022
);
31-
const errors = engagements
32-
.flatMap<
33-
EngagementDateOverrideConflictException['engagements'][0] | null
34-
>((eng) => {
35-
const common = {
36-
id: eng.id,
37-
label: (eng.label.language ?? eng.label.intern)!,
38-
} as const;
39-
const { startDateOverride: start, endDateOverride: end } = eng;
40-
return [
41-
project.mouStart && start && project.mouStart > start
42-
? {
43-
...common,
44-
point: 'start' as const,
45-
date: start,
46-
}
47-
: null,
48-
project.mouEnd && end && project.mouEnd < end
49-
? {
50-
...common,
51-
point: 'end' as const,
52-
date: end,
53-
}
54-
: null,
55-
];
56-
})
57-
.filter(isNotFalsy);
58-
if (errors.length === 0) {
59-
return;
60-
}
61-
throw new EngagementDateOverrideConflictException(project, [
62-
errors[0]!,
63-
...errors.slice(1),
64-
]);
65-
}
66-
}
67-
68-
class EngagementDateOverrideConflictException extends RangeException {
69-
constructor(
70-
readonly project: Pick<
71-
UnsecuredDto<Project>,
72-
'id' | 'name' | 'mouStart' | 'mouEnd'
73-
>,
74-
readonly engagements: NonEmptyArray<
75-
Readonly<{
76-
id: ID<'Engagement'>;
77-
label: string;
78-
point: 'start' | 'end';
79-
date: CalendarDate;
80-
}>
81-
>,
82-
) {
83-
const message = [
84-
engagements.length === 1
85-
? 'An engagement has a date outside the new range'
86-
: 'Some engagements have dates outside the new range',
87-
...engagements.map((eng) => {
88-
const pointStr = eng.point === 'start' ? 'Start' : 'End';
89-
const dateStr = eng.date.toISO();
90-
return ` - ${pointStr} date of ${eng.label} is ${dateStr}`;
91-
}),
92-
].join('\n');
93-
super({ message });
23+
const canonical = { start: project.mouStart, end: project.mouEnd };
24+
const conflicts = DateOverrideConflictException.findConflicts(
25+
canonical,
26+
engagements.map((eng) => ({
27+
__typename: EnhancedResource.resolve(eng.__typename).name,
28+
id: eng.id,
29+
label: (eng.label.language ?? eng.label.intern)!,
30+
start: eng.startDateOverride,
31+
end: eng.endDateOverride,
32+
})),
33+
);
34+
if (!conflicts) return;
35+
throw new DateOverrideConflictException(
36+
{
37+
__typename: event.resource.name,
38+
id: project.id,
39+
name: project.name,
40+
},
41+
canonical,
42+
['An engagement', 'Some engagements'],
43+
conflicts,
44+
);
9445
}
9546
}
Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
import { Session, UnsecuredDto } from '~/common';
2-
import { Project, UpdateProject } from '../dto';
1+
import { EnhancedResource, Session, UnsecuredDto } from '~/common';
2+
import { Project, resolveProjectType, UpdateProject } from '../dto';
33

44
export class ProjectUpdatedEvent {
5+
readonly resource: EnhancedResource<ReturnType<typeof resolveProjectType>>;
6+
57
constructor(
68
public updated: UnsecuredDto<Project>,
79
readonly previous: UnsecuredDto<Project>,
810
readonly changes: UpdateProject,
911
readonly session: Session,
10-
) {}
12+
) {
13+
this.resource = EnhancedResource.of(resolveProjectType(this.updated));
14+
}
1115
}

0 commit comments

Comments
 (0)