@critical-path/core 0.36.0 → 0.36.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -1
- package/CHANGELOG.md +0 -593
- package/dist/deliverables.test.d.ts +0 -2
- package/dist/deliverables.test.d.ts.map +0 -1
- package/dist/deliverables.test.js +0 -232
- package/dist/deliverables.test.js.map +0 -1
- package/dist/domain/calendar.test.d.ts +0 -2
- package/dist/domain/calendar.test.d.ts.map +0 -1
- package/dist/domain/calendar.test.js +0 -181
- package/dist/domain/calendar.test.js.map +0 -1
- package/dist/domain/cpm.test.d.ts +0 -2
- package/dist/domain/cpm.test.d.ts.map +0 -1
- package/dist/domain/cpm.test.js +0 -166
- package/dist/domain/cpm.test.js.map +0 -1
- package/dist/domain/ddd.test.d.ts +0 -2
- package/dist/domain/ddd.test.d.ts.map +0 -1
- package/dist/domain/ddd.test.js +0 -349
- package/dist/domain/ddd.test.js.map +0 -1
- package/dist/domain/ladder.test.d.ts +0 -2
- package/dist/domain/ladder.test.d.ts.map +0 -1
- package/dist/domain/ladder.test.js +0 -213
- package/dist/domain/ladder.test.js.map +0 -1
- package/dist/domain/metrics.test.d.ts +0 -2
- package/dist/domain/metrics.test.d.ts.map +0 -1
- package/dist/domain/metrics.test.js +0 -252
- package/dist/domain/metrics.test.js.map +0 -1
- package/dist/domain/task-duration.test.d.ts +0 -2
- package/dist/domain/task-duration.test.d.ts.map +0 -1
- package/dist/domain/task-duration.test.js +0 -307
- package/dist/domain/task-duration.test.js.map +0 -1
- package/dist/domain/workload.test.d.ts +0 -2
- package/dist/domain/workload.test.d.ts.map +0 -1
- package/dist/domain/workload.test.js +0 -261
- package/dist/domain/workload.test.js.map +0 -1
- package/dist/engine/actor.test.d.ts +0 -2
- package/dist/engine/actor.test.d.ts.map +0 -1
- package/dist/engine/actor.test.js +0 -54
- package/dist/engine/actor.test.js.map +0 -1
- package/dist/engine/authorization.test.d.ts +0 -2
- package/dist/engine/authorization.test.d.ts.map +0 -1
- package/dist/engine/authorization.test.js +0 -154
- package/dist/engine/authorization.test.js.map +0 -1
- package/dist/engine/cascade.test.d.ts +0 -2
- package/dist/engine/cascade.test.d.ts.map +0 -1
- package/dist/engine/cascade.test.js +0 -99
- package/dist/engine/cascade.test.js.map +0 -1
- package/dist/engine/config.test.d.ts +0 -2
- package/dist/engine/config.test.d.ts.map +0 -1
- package/dist/engine/config.test.js +0 -46
- package/dist/engine/config.test.js.map +0 -1
- package/dist/engine/entity-events.test.d.ts +0 -2
- package/dist/engine/entity-events.test.d.ts.map +0 -1
- package/dist/engine/entity-events.test.js +0 -61
- package/dist/engine/entity-events.test.js.map +0 -1
- package/dist/index.test.d.ts +0 -2
- package/dist/index.test.d.ts.map +0 -1
- package/dist/index.test.js +0 -429
- package/dist/index.test.js.map +0 -1
- package/dist/plugins/plugins.test.d.ts +0 -2
- package/dist/plugins/plugins.test.d.ts.map +0 -1
- package/dist/plugins/plugins.test.js +0 -137
- package/dist/plugins/plugins.test.js.map +0 -1
- package/dist/schemas/schemas.test.d.ts +0 -2
- package/dist/schemas/schemas.test.d.ts.map +0 -1
- package/dist/schemas/schemas.test.js +0 -58
- package/dist/schemas/schemas.test.js.map +0 -1
- package/dist/status.test.d.ts +0 -2
- package/dist/status.test.d.ts.map +0 -1
- package/dist/status.test.js +0 -295
- package/dist/status.test.js.map +0 -1
- package/dist/storage/storage.test.d.ts +0 -2
- package/dist/storage/storage.test.d.ts.map +0 -1
- package/dist/storage/storage.test.js +0 -236
- package/dist/storage/storage.test.js.map +0 -1
- package/dist/storage/upload-limits.test.d.ts +0 -2
- package/dist/storage/upload-limits.test.d.ts.map +0 -1
- package/dist/storage/upload-limits.test.js +0 -31
- package/dist/storage/upload-limits.test.js.map +0 -1
- package/dist/store/conformance.test.d.ts +0 -2
- package/dist/store/conformance.test.d.ts.map +0 -1
- package/dist/store/conformance.test.js +0 -15
- package/dist/store/conformance.test.js.map +0 -1
- package/dist/store/firebase.test.d.ts +0 -2
- package/dist/store/firebase.test.d.ts.map +0 -1
- package/dist/store/firebase.test.js +0 -103
- package/dist/store/firebase.test.js.map +0 -1
- package/dist/store/query.test.d.ts +0 -2
- package/dist/store/query.test.d.ts.map +0 -1
- package/dist/store/query.test.js +0 -89
- package/dist/store/query.test.js.map +0 -1
- package/dist/store/sqlite.test.d.ts +0 -2
- package/dist/store/sqlite.test.d.ts.map +0 -1
- package/dist/store/sqlite.test.js +0 -177
- package/dist/store/sqlite.test.js.map +0 -1
- package/dist/task-state.test.d.ts +0 -2
- package/dist/task-state.test.d.ts.map +0 -1
- package/dist/task-state.test.js +0 -423
- package/dist/task-state.test.js.map +0 -1
- package/dist/use-case-validation.test.d.ts +0 -2
- package/dist/use-case-validation.test.d.ts.map +0 -1
- package/dist/use-case-validation.test.js +0 -281
- package/dist/use-case-validation.test.js.map +0 -1
- package/dist/utils/fractional-index.test.d.ts +0 -2
- package/dist/utils/fractional-index.test.d.ts.map +0 -1
- package/dist/utils/fractional-index.test.js +0 -182
- package/dist/utils/fractional-index.test.js.map +0 -1
- package/dist/utils/key.test.d.ts +0 -2
- package/dist/utils/key.test.d.ts.map +0 -1
- package/dist/utils/key.test.js +0 -65
- package/dist/utils/key.test.js.map +0 -1
- package/dist/utils/mentions.test.d.ts +0 -2
- package/dist/utils/mentions.test.d.ts.map +0 -1
- package/dist/utils/mentions.test.js +0 -114
- package/dist/utils/mentions.test.js.map +0 -1
- package/dist/webhooks/webhooks.test.d.ts +0 -2
- package/dist/webhooks/webhooks.test.d.ts.map +0 -1
- package/dist/webhooks/webhooks.test.js +0 -154
- package/dist/webhooks/webhooks.test.js.map +0 -1
- package/src/deliverables.test.ts +0 -284
- package/src/domain/authorization.ts +0 -82
- package/src/domain/calendar.test.ts +0 -216
- package/src/domain/calendar.ts +0 -502
- package/src/domain/cpm.test.ts +0 -189
- package/src/domain/cpm.ts +0 -258
- package/src/domain/custom-fields.ts +0 -172
- package/src/domain/ddd.test.ts +0 -442
- package/src/domain/entities.ts +0 -798
- package/src/domain/errors.ts +0 -38
- package/src/domain/events.ts +0 -369
- package/src/domain/graph.ts +0 -70
- package/src/domain/index.ts +0 -12
- package/src/domain/ladder.test.ts +0 -251
- package/src/domain/ladder.ts +0 -474
- package/src/domain/metrics.test.ts +0 -288
- package/src/domain/metrics.ts +0 -497
- package/src/domain/task-duration.test.ts +0 -354
- package/src/domain/workload.test.ts +0 -311
- package/src/domain/workload.ts +0 -457
- package/src/engine/actor.test.ts +0 -63
- package/src/engine/authorization.test.ts +0 -199
- package/src/engine/cascade.test.ts +0 -121
- package/src/engine/config.test.ts +0 -51
- package/src/engine/entity-events.test.ts +0 -67
- package/src/engine/index.ts +0 -2495
- package/src/index.test.ts +0 -514
- package/src/index.ts +0 -9
- package/src/plugins/index.ts +0 -103
- package/src/plugins/plugins.test.ts +0 -161
- package/src/schemas/index.ts +0 -402
- package/src/schemas/schemas.test.ts +0 -116
- package/src/status.test.ts +0 -354
- package/src/storage/file-storage.ts +0 -200
- package/src/storage/firebase-storage.ts +0 -209
- package/src/storage/index.ts +0 -3
- package/src/storage/s3.ts +0 -151
- package/src/storage/storage.test.ts +0 -274
- package/src/storage/upload-limits.test.ts +0 -41
- package/src/store/conformance.test.ts +0 -15
- package/src/store/firebase.test.ts +0 -131
- package/src/store/firebase.ts +0 -658
- package/src/store/index.ts +0 -623
- package/src/store/query.test.ts +0 -110
- package/src/store/query.ts +0 -113
- package/src/store/sqlite.test.ts +0 -218
- package/src/store/sqlite.ts +0 -1480
- package/src/task-state.test.ts +0 -506
- package/src/testing/conformance.ts +0 -319
- package/src/testing/index.ts +0 -1
- package/src/types/index.ts +0 -871
- package/src/use-case-validation.test.ts +0 -324
- package/src/utils/fractional-index.test.ts +0 -228
- package/src/utils/fractional-index.ts +0 -254
- package/src/utils/index.ts +0 -7
- package/src/utils/key.test.ts +0 -76
- package/src/utils/key.ts +0 -62
- package/src/utils/mentions.test.ts +0 -132
- package/src/utils/mentions.ts +0 -131
- package/src/utils/status.ts +0 -419
- package/src/utils/workflow.ts +0 -306
- package/src/webhooks/dispatcher.ts +0 -190
- package/src/webhooks/index.ts +0 -2
- package/src/webhooks/signature.ts +0 -57
- package/src/webhooks/webhooks.test.ts +0 -201
- package/tsconfig.json +0 -8
- package/tsconfig.tsbuildinfo +0 -1
package/src/domain/entities.ts
DELETED
|
@@ -1,798 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ProjectMember,
|
|
3
|
-
Task,
|
|
4
|
-
Project,
|
|
5
|
-
Workflow,
|
|
6
|
-
TimeEntry,
|
|
7
|
-
Priority,
|
|
8
|
-
TaskStatus,
|
|
9
|
-
TaskTodoItem,
|
|
10
|
-
CustomFieldDefinition,
|
|
11
|
-
CreateTaskInput,
|
|
12
|
-
CreateProjectInput,
|
|
13
|
-
Deliverable,
|
|
14
|
-
DeliverableStatus,
|
|
15
|
-
CreateDeliverableInput,
|
|
16
|
-
SemanticStatus
|
|
17
|
-
} from '../types/index.js';
|
|
18
|
-
import type {
|
|
19
|
-
DomainEvent,
|
|
20
|
-
TaskCreatedEvent,
|
|
21
|
-
TaskStatusChangedEvent,
|
|
22
|
-
TaskUpdatedEvent,
|
|
23
|
-
TimeLoggedEvent,
|
|
24
|
-
ProjectCreatedEvent,
|
|
25
|
-
DeliverableCreatedEvent,
|
|
26
|
-
DeliverableUpdatedEvent,
|
|
27
|
-
DeliverableStatusChangedEvent
|
|
28
|
-
} from './events.js';
|
|
29
|
-
import { validateTransition, WorkflowValidationError } from '../utils/workflow.js';
|
|
30
|
-
import { resolveStatusDefinition } from '../utils/status.js';
|
|
31
|
-
import { validateCustomFieldValues } from './custom-fields.js';
|
|
32
|
-
import { generateProjectKey } from '../utils/key.js';
|
|
33
|
-
import { ValidationError } from './errors.js';
|
|
34
|
-
|
|
35
|
-
export abstract class BaseEntity {
|
|
36
|
-
public readonly id: string;
|
|
37
|
-
public createdAt: string;
|
|
38
|
-
public updatedAt: string;
|
|
39
|
-
private uncommittedEvents: DomainEvent[] = [];
|
|
40
|
-
|
|
41
|
-
constructor(id: string, createdAt?: string, updatedAt?: string) {
|
|
42
|
-
const now = new Date().toISOString();
|
|
43
|
-
this.id = id;
|
|
44
|
-
this.createdAt = createdAt || now;
|
|
45
|
-
this.updatedAt = updatedAt || now;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
protected raiseEvent(event: DomainEvent): void {
|
|
49
|
-
this.uncommittedEvents.push(event);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
public getUncommittedEvents(): DomainEvent[] {
|
|
53
|
-
return [...this.uncommittedEvents];
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
public clearEvents(): void {
|
|
57
|
-
this.uncommittedEvents = [];
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
protected markUpdated(): void {
|
|
61
|
-
this.updatedAt = new Date().toISOString();
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export class TaskEntity extends BaseEntity {
|
|
66
|
-
public projectId: string;
|
|
67
|
-
public key?: string;
|
|
68
|
-
public title: string;
|
|
69
|
-
public description?: string;
|
|
70
|
-
public status: TaskStatus;
|
|
71
|
-
public semanticStatus?: SemanticStatus;
|
|
72
|
-
public priority: Priority;
|
|
73
|
-
public taskType?: string;
|
|
74
|
-
public assigneeId?: string;
|
|
75
|
-
public reporterId?: string;
|
|
76
|
-
public reviewerId?: string;
|
|
77
|
-
public iterationId?: string;
|
|
78
|
-
public teamId?: string;
|
|
79
|
-
public containerId?: string;
|
|
80
|
-
public deliverableId?: string;
|
|
81
|
-
public plannedStartDate?: string;
|
|
82
|
-
public actualStartDate?: string;
|
|
83
|
-
public actualEndDate?: string;
|
|
84
|
-
public dueDate?: string;
|
|
85
|
-
public estimatedHours?: number;
|
|
86
|
-
public loggedHours?: number;
|
|
87
|
-
public actualHours?: number;
|
|
88
|
-
public billableHours?: number;
|
|
89
|
-
public estimatedDurationMinutes?: number;
|
|
90
|
-
public actualDurationMinutes?: number;
|
|
91
|
-
public billableDurationMinutes?: number;
|
|
92
|
-
public actualDurationSeconds?: number;
|
|
93
|
-
public inProgressSince?: string | null;
|
|
94
|
-
public blockedDurationSeconds?: number;
|
|
95
|
-
public blockedSince?: string | null;
|
|
96
|
-
public progress?: number;
|
|
97
|
-
public isBlocked?: boolean;
|
|
98
|
-
public blockedReason?: string | null;
|
|
99
|
-
public tags?: string[];
|
|
100
|
-
public todos?: TaskTodoItem[];
|
|
101
|
-
public customFields?: Record<string, unknown>;
|
|
102
|
-
public parentId?: string;
|
|
103
|
-
|
|
104
|
-
constructor(data: Task) {
|
|
105
|
-
super(data.id, data.createdAt, data.updatedAt);
|
|
106
|
-
this.projectId = data.projectId;
|
|
107
|
-
this.key = data.key;
|
|
108
|
-
this.title = data.title;
|
|
109
|
-
this.description = data.description;
|
|
110
|
-
this.status = data.status;
|
|
111
|
-
this.semanticStatus = data.semanticStatus;
|
|
112
|
-
this.priority = data.priority;
|
|
113
|
-
this.taskType = data.taskType;
|
|
114
|
-
this.assigneeId = data.assigneeId;
|
|
115
|
-
this.reporterId = data.reporterId;
|
|
116
|
-
this.reviewerId = data.reviewerId;
|
|
117
|
-
this.iterationId = data.iterationId;
|
|
118
|
-
this.teamId = data.teamId;
|
|
119
|
-
this.containerId = data.containerId;
|
|
120
|
-
this.deliverableId = data.deliverableId;
|
|
121
|
-
this.plannedStartDate = data.plannedStartDate;
|
|
122
|
-
this.actualStartDate = data.actualStartDate;
|
|
123
|
-
this.actualEndDate = data.actualEndDate;
|
|
124
|
-
this.dueDate = data.dueDate;
|
|
125
|
-
this.estimatedHours = data.estimatedHours;
|
|
126
|
-
this.loggedHours = data.loggedHours ?? 0;
|
|
127
|
-
this.actualHours = data.actualHours;
|
|
128
|
-
this.billableHours = data.billableHours;
|
|
129
|
-
this.estimatedDurationMinutes = data.estimatedDurationMinutes;
|
|
130
|
-
this.actualDurationMinutes = data.actualDurationMinutes;
|
|
131
|
-
this.billableDurationMinutes = data.billableDurationMinutes;
|
|
132
|
-
this.actualDurationSeconds = data.actualDurationSeconds;
|
|
133
|
-
this.inProgressSince = data.inProgressSince;
|
|
134
|
-
this.blockedDurationSeconds = data.blockedDurationSeconds;
|
|
135
|
-
this.blockedSince = data.blockedSince;
|
|
136
|
-
this.progress = data.progress ?? 0;
|
|
137
|
-
this.isBlocked = data.isBlocked;
|
|
138
|
-
this.blockedReason = data.blockedReason;
|
|
139
|
-
this.tags = data.tags ? [...data.tags] : [];
|
|
140
|
-
this.todos = data.todos ? data.todos.map((t) => ({ ...t })) : [];
|
|
141
|
-
this.customFields = data.customFields ? { ...data.customFields } : {};
|
|
142
|
-
this.parentId = data.parentId;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
public static create(
|
|
146
|
-
input: CreateTaskInput,
|
|
147
|
-
options?: { id?: string; workflow?: Workflow; customFieldDefs?: CustomFieldDefinition[] }
|
|
148
|
-
): TaskEntity {
|
|
149
|
-
const id = options?.id || `task_${crypto.randomUUID()}`;
|
|
150
|
-
const now = new Date().toISOString();
|
|
151
|
-
|
|
152
|
-
if (options?.customFieldDefs) {
|
|
153
|
-
validateCustomFieldValues(options.customFieldDefs, input.customFields);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const status = input.status || options?.workflow?.defaultStatusKey || 'todo';
|
|
157
|
-
const statusDef = resolveStatusDefinition(status, options?.workflow?.statuses);
|
|
158
|
-
const semanticStatus = input.semanticStatus || statusDef.category;
|
|
159
|
-
const actualStartDate = input.actualStartDate ?? (semanticStatus === 'in_progress' ? now : undefined);
|
|
160
|
-
const inProgressSince = input.inProgressSince ?? (semanticStatus === 'in_progress' ? now : undefined);
|
|
161
|
-
const actualEndDate = input.actualEndDate ?? ((semanticStatus === 'completed' || semanticStatus === 'canceled') ? now : undefined);
|
|
162
|
-
const isBlocked = input.isBlocked ?? false;
|
|
163
|
-
const blockedSince = input.blockedSince ?? (isBlocked && semanticStatus === 'in_progress' ? now : undefined);
|
|
164
|
-
|
|
165
|
-
const task = new TaskEntity({
|
|
166
|
-
...input,
|
|
167
|
-
id,
|
|
168
|
-
createdAt: now,
|
|
169
|
-
updatedAt: now,
|
|
170
|
-
status,
|
|
171
|
-
semanticStatus,
|
|
172
|
-
actualStartDate,
|
|
173
|
-
inProgressSince,
|
|
174
|
-
actualEndDate,
|
|
175
|
-
actualDurationSeconds: input.actualDurationSeconds,
|
|
176
|
-
blockedDurationSeconds: input.blockedDurationSeconds,
|
|
177
|
-
blockedSince,
|
|
178
|
-
isBlocked,
|
|
179
|
-
blockedReason: input.blockedReason ?? null,
|
|
180
|
-
priority: input.priority || 'medium',
|
|
181
|
-
loggedHours: input.loggedHours ?? 0,
|
|
182
|
-
progress: input.progress ?? 0,
|
|
183
|
-
tags: input.tags ?? [],
|
|
184
|
-
todos: input.todos ?? [],
|
|
185
|
-
customFields: input.customFields ?? {}
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
const createdEvent: TaskCreatedEvent = {
|
|
189
|
-
id: `evt_${crypto.randomUUID()}`,
|
|
190
|
-
name: 'task.created',
|
|
191
|
-
aggregateId: task.id,
|
|
192
|
-
aggregateType: 'Task',
|
|
193
|
-
occurredAt: now,
|
|
194
|
-
payload: { task: task.toPlain() }
|
|
195
|
-
};
|
|
196
|
-
task.raiseEvent(createdEvent);
|
|
197
|
-
|
|
198
|
-
return task;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
public transitionTo(
|
|
202
|
-
newStatus: string,
|
|
203
|
-
workflow?: Workflow,
|
|
204
|
-
options?: { statusDefs?: Workflow['statuses']; actorId?: string }
|
|
205
|
-
): this {
|
|
206
|
-
if (this.status === newStatus) return this;
|
|
207
|
-
|
|
208
|
-
const isValid = validateTransition(workflow, this.status, newStatus);
|
|
209
|
-
if (!isValid) {
|
|
210
|
-
throw new WorkflowValidationError(this.status, newStatus, workflow?.id);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const previousStatus = this.status;
|
|
214
|
-
this.status = newStatus;
|
|
215
|
-
const now = new Date().toISOString();
|
|
216
|
-
|
|
217
|
-
const statusDef = resolveStatusDefinition(newStatus, options?.statusDefs || workflow?.statuses);
|
|
218
|
-
this.semanticStatus = statusDef.category;
|
|
219
|
-
|
|
220
|
-
const prevStatusDef = resolveStatusDefinition(previousStatus, options?.statusDefs || workflow?.statuses);
|
|
221
|
-
|
|
222
|
-
// 1. Leaving in_progress: finalize current session duration
|
|
223
|
-
if (prevStatusDef.category === 'in_progress' && statusDef.category !== 'in_progress') {
|
|
224
|
-
if (this.inProgressSince) {
|
|
225
|
-
const startMs = new Date(this.inProgressSince).getTime();
|
|
226
|
-
const endMs = new Date(now).getTime();
|
|
227
|
-
if (!isNaN(startMs) && !isNaN(endMs) && endMs >= startMs) {
|
|
228
|
-
const sessionSeconds = Math.max(0, (endMs - startMs) / 1000);
|
|
229
|
-
this.actualDurationSeconds = (this.actualDurationSeconds || 0) + sessionSeconds;
|
|
230
|
-
this.actualHours = Math.round(((this.actualDurationSeconds || 0) / 3600) * 100) / 100;
|
|
231
|
-
this.actualDurationMinutes = Math.round((this.actualDurationSeconds || 0) / 60);
|
|
232
|
-
}
|
|
233
|
-
this.inProgressSince = null;
|
|
234
|
-
}
|
|
235
|
-
if (this.blockedSince) {
|
|
236
|
-
const startMs = new Date(this.blockedSince).getTime();
|
|
237
|
-
const endMs = new Date(now).getTime();
|
|
238
|
-
if (!isNaN(startMs) && !isNaN(endMs) && endMs >= startMs) {
|
|
239
|
-
const blockedSeconds = Math.max(0, (endMs - startMs) / 1000);
|
|
240
|
-
this.blockedDurationSeconds = (this.blockedDurationSeconds || 0) + blockedSeconds;
|
|
241
|
-
}
|
|
242
|
-
this.blockedSince = null;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// 2. Entering in_progress: retain earliest start date & start session timer
|
|
247
|
-
if (statusDef.category === 'in_progress') {
|
|
248
|
-
if (!this.actualStartDate) {
|
|
249
|
-
this.actualStartDate = now;
|
|
250
|
-
}
|
|
251
|
-
if (!this.inProgressSince) {
|
|
252
|
-
this.inProgressSince = now;
|
|
253
|
-
}
|
|
254
|
-
if (this.isBlocked && !this.blockedSince) {
|
|
255
|
-
this.blockedSince = now;
|
|
256
|
-
}
|
|
257
|
-
if (prevStatusDef.category === 'completed' || prevStatusDef.category === 'canceled') {
|
|
258
|
-
this.actualEndDate = undefined;
|
|
259
|
-
}
|
|
260
|
-
} else if (
|
|
261
|
-
(prevStatusDef.category === 'completed' || prevStatusDef.category === 'canceled') &&
|
|
262
|
-
statusDef.category === 'not_started'
|
|
263
|
-
) {
|
|
264
|
-
this.actualEndDate = undefined;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// 3. Entering completed or canceled
|
|
268
|
-
if (statusDef.category === 'completed' || statusDef.category === 'canceled') {
|
|
269
|
-
if (!this.actualEndDate) {
|
|
270
|
-
this.actualEndDate = now;
|
|
271
|
-
}
|
|
272
|
-
if (statusDef.category === 'completed' && this.progress !== undefined && this.progress < 100) {
|
|
273
|
-
this.progress = 100;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
this.markUpdated();
|
|
278
|
-
|
|
279
|
-
const event: TaskStatusChangedEvent = {
|
|
280
|
-
id: `evt_${crypto.randomUUID()}`,
|
|
281
|
-
name: 'task.status_changed',
|
|
282
|
-
aggregateId: this.id,
|
|
283
|
-
aggregateType: 'Task',
|
|
284
|
-
occurredAt: now,
|
|
285
|
-
payload: {
|
|
286
|
-
task: this.toPlain(),
|
|
287
|
-
previousStatus,
|
|
288
|
-
newStatus
|
|
289
|
-
}
|
|
290
|
-
};
|
|
291
|
-
this.raiseEvent(event);
|
|
292
|
-
|
|
293
|
-
return this;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
public logTime(
|
|
297
|
-
entry: { hours: number; isBillable?: boolean; userId?: string; description?: string }
|
|
298
|
-
): TimeEntry {
|
|
299
|
-
if (!Number.isFinite(entry.hours) || entry.hours <= 0) {
|
|
300
|
-
throw new ValidationError('Logged hours must be a positive number.');
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
this.loggedHours = (this.loggedHours || 0) + entry.hours;
|
|
304
|
-
this.actualHours = (this.actualHours || 0) + entry.hours;
|
|
305
|
-
if (entry.isBillable !== false) {
|
|
306
|
-
this.billableHours = (this.billableHours || 0) + entry.hours;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const now = new Date().toISOString();
|
|
310
|
-
this.markUpdated();
|
|
311
|
-
|
|
312
|
-
const timeEntry: TimeEntry = {
|
|
313
|
-
id: `time_${crypto.randomUUID()}`,
|
|
314
|
-
taskId: this.id,
|
|
315
|
-
userId: entry.userId || this.assigneeId || 'system',
|
|
316
|
-
hours: entry.hours,
|
|
317
|
-
isBillable: entry.isBillable ?? true,
|
|
318
|
-
description: entry.description,
|
|
319
|
-
loggedAt: now
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
const event: TimeLoggedEvent = {
|
|
323
|
-
id: `evt_${crypto.randomUUID()}`,
|
|
324
|
-
name: 'time.logged',
|
|
325
|
-
aggregateId: this.id,
|
|
326
|
-
aggregateType: 'Task',
|
|
327
|
-
occurredAt: now,
|
|
328
|
-
payload: {
|
|
329
|
-
taskId: this.id,
|
|
330
|
-
timeEntry
|
|
331
|
-
}
|
|
332
|
-
};
|
|
333
|
-
this.raiseEvent(event);
|
|
334
|
-
|
|
335
|
-
return timeEntry;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
public addTodo(title: string, id?: string): TaskTodoItem {
|
|
339
|
-
const previous = this.toPlain();
|
|
340
|
-
const item: TaskTodoItem = {
|
|
341
|
-
id: id || `todo_${crypto.randomUUID()}`,
|
|
342
|
-
title,
|
|
343
|
-
completed: false,
|
|
344
|
-
createdAt: new Date().toISOString()
|
|
345
|
-
};
|
|
346
|
-
if (!this.todos) {
|
|
347
|
-
this.todos = [];
|
|
348
|
-
}
|
|
349
|
-
this.todos.push(item);
|
|
350
|
-
this.markUpdated();
|
|
351
|
-
|
|
352
|
-
const event: TaskUpdatedEvent = {
|
|
353
|
-
id: `evt_${crypto.randomUUID()}`,
|
|
354
|
-
name: 'task.updated',
|
|
355
|
-
aggregateId: this.id,
|
|
356
|
-
aggregateType: 'Task',
|
|
357
|
-
occurredAt: new Date().toISOString(),
|
|
358
|
-
payload: {
|
|
359
|
-
task: this.toPlain(),
|
|
360
|
-
previous
|
|
361
|
-
}
|
|
362
|
-
};
|
|
363
|
-
this.raiseEvent(event);
|
|
364
|
-
|
|
365
|
-
return item;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
public toggleTodo(todoId: string, completed?: boolean): this {
|
|
369
|
-
if (!this.todos) return this;
|
|
370
|
-
const item = this.todos.find((t) => t.id === todoId);
|
|
371
|
-
if (item) {
|
|
372
|
-
const previous = this.toPlain();
|
|
373
|
-
item.completed = completed !== undefined ? completed : !item.completed;
|
|
374
|
-
item.completedAt = item.completed ? new Date().toISOString() : undefined;
|
|
375
|
-
this.markUpdated();
|
|
376
|
-
|
|
377
|
-
const event: TaskUpdatedEvent = {
|
|
378
|
-
id: `evt_${crypto.randomUUID()}`,
|
|
379
|
-
name: 'task.updated',
|
|
380
|
-
aggregateId: this.id,
|
|
381
|
-
aggregateType: 'Task',
|
|
382
|
-
occurredAt: new Date().toISOString(),
|
|
383
|
-
payload: {
|
|
384
|
-
task: this.toPlain(),
|
|
385
|
-
previous
|
|
386
|
-
}
|
|
387
|
-
};
|
|
388
|
-
this.raiseEvent(event);
|
|
389
|
-
}
|
|
390
|
-
return this;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
public removeTodo(todoId: string): this {
|
|
394
|
-
if (!this.todos) return this;
|
|
395
|
-
const previous = this.toPlain();
|
|
396
|
-
this.todos = this.todos.filter((t) => t.id !== todoId);
|
|
397
|
-
this.markUpdated();
|
|
398
|
-
|
|
399
|
-
const event: TaskUpdatedEvent = {
|
|
400
|
-
id: `evt_${crypto.randomUUID()}`,
|
|
401
|
-
name: 'task.updated',
|
|
402
|
-
aggregateId: this.id,
|
|
403
|
-
aggregateType: 'Task',
|
|
404
|
-
occurredAt: new Date().toISOString(),
|
|
405
|
-
payload: {
|
|
406
|
-
task: this.toPlain(),
|
|
407
|
-
previous
|
|
408
|
-
}
|
|
409
|
-
};
|
|
410
|
-
this.raiseEvent(event);
|
|
411
|
-
|
|
412
|
-
return this;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
public update(updates: Partial<Task>, customFieldDefs?: CustomFieldDefinition[]): this {
|
|
416
|
-
const previous = this.toPlain();
|
|
417
|
-
|
|
418
|
-
if (updates.customFields && customFieldDefs) {
|
|
419
|
-
validateCustomFieldValues(customFieldDefs, {
|
|
420
|
-
...this.customFields,
|
|
421
|
-
...updates.customFields
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const wasBlocked = Boolean(this.isBlocked);
|
|
426
|
-
const isNowBlocked = updates.isBlocked !== undefined ? Boolean(updates.isBlocked) : wasBlocked;
|
|
427
|
-
const now = new Date().toISOString();
|
|
428
|
-
const effectiveSemantic = updates.semanticStatus ?? this.semanticStatus;
|
|
429
|
-
|
|
430
|
-
if (effectiveSemantic === 'in_progress') {
|
|
431
|
-
if (!wasBlocked && isNowBlocked) {
|
|
432
|
-
if (!updates.blockedSince) {
|
|
433
|
-
updates.blockedSince = now;
|
|
434
|
-
}
|
|
435
|
-
} else if (wasBlocked && !isNowBlocked) {
|
|
436
|
-
if (this.blockedSince) {
|
|
437
|
-
const startMs = new Date(this.blockedSince).getTime();
|
|
438
|
-
const endMs = new Date(now).getTime();
|
|
439
|
-
if (!isNaN(startMs) && !isNaN(endMs) && endMs >= startMs) {
|
|
440
|
-
const sessionSeconds = Math.max(0, (endMs - startMs) / 1000);
|
|
441
|
-
updates.blockedDurationSeconds =
|
|
442
|
-
(updates.blockedDurationSeconds ?? this.blockedDurationSeconds ?? 0) + sessionSeconds;
|
|
443
|
-
}
|
|
444
|
-
updates.blockedSince = null;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
} else if (!isNowBlocked && this.blockedSince) {
|
|
448
|
-
updates.blockedSince = null;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
Object.assign(this, updates);
|
|
452
|
-
if (updates.actualDurationSeconds !== undefined && updates.actualHours === undefined) {
|
|
453
|
-
this.actualHours = Math.round((updates.actualDurationSeconds / 3600) * 100) / 100;
|
|
454
|
-
this.actualDurationMinutes = updates.actualDurationMinutes ?? Math.round(updates.actualDurationSeconds / 60);
|
|
455
|
-
} else if (updates.actualHours !== undefined && updates.actualDurationSeconds === undefined) {
|
|
456
|
-
this.actualDurationSeconds = Math.round(updates.actualHours * 3600);
|
|
457
|
-
this.actualDurationMinutes = updates.actualDurationMinutes ?? Math.round(updates.actualHours * 60);
|
|
458
|
-
}
|
|
459
|
-
this.markUpdated();
|
|
460
|
-
|
|
461
|
-
const event: TaskUpdatedEvent = {
|
|
462
|
-
id: `evt_${crypto.randomUUID()}`,
|
|
463
|
-
name: 'task.updated',
|
|
464
|
-
aggregateId: this.id,
|
|
465
|
-
aggregateType: 'Task',
|
|
466
|
-
occurredAt: new Date().toISOString(),
|
|
467
|
-
payload: {
|
|
468
|
-
task: this.toPlain(),
|
|
469
|
-
previous
|
|
470
|
-
}
|
|
471
|
-
};
|
|
472
|
-
this.raiseEvent(event);
|
|
473
|
-
|
|
474
|
-
return this;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
public toPlain(): Task {
|
|
478
|
-
return {
|
|
479
|
-
id: this.id,
|
|
480
|
-
projectId: this.projectId,
|
|
481
|
-
key: this.key,
|
|
482
|
-
title: this.title,
|
|
483
|
-
description: this.description,
|
|
484
|
-
status: this.status,
|
|
485
|
-
semanticStatus: this.semanticStatus,
|
|
486
|
-
priority: this.priority,
|
|
487
|
-
taskType: this.taskType,
|
|
488
|
-
assigneeId: this.assigneeId,
|
|
489
|
-
reporterId: this.reporterId,
|
|
490
|
-
reviewerId: this.reviewerId,
|
|
491
|
-
iterationId: this.iterationId,
|
|
492
|
-
teamId: this.teamId,
|
|
493
|
-
containerId: this.containerId,
|
|
494
|
-
deliverableId: this.deliverableId,
|
|
495
|
-
plannedStartDate: this.plannedStartDate,
|
|
496
|
-
actualStartDate: this.actualStartDate,
|
|
497
|
-
actualEndDate: this.actualEndDate,
|
|
498
|
-
dueDate: this.dueDate,
|
|
499
|
-
estimatedHours: this.estimatedHours,
|
|
500
|
-
loggedHours: this.loggedHours,
|
|
501
|
-
actualHours: this.actualHours,
|
|
502
|
-
billableHours: this.billableHours,
|
|
503
|
-
estimatedDurationMinutes: this.estimatedDurationMinutes,
|
|
504
|
-
actualDurationMinutes: this.actualDurationMinutes,
|
|
505
|
-
billableDurationMinutes: this.billableDurationMinutes,
|
|
506
|
-
actualDurationSeconds: this.actualDurationSeconds,
|
|
507
|
-
inProgressSince: this.inProgressSince,
|
|
508
|
-
blockedDurationSeconds: this.blockedDurationSeconds,
|
|
509
|
-
blockedSince: this.blockedSince,
|
|
510
|
-
progress: this.progress,
|
|
511
|
-
isBlocked: this.isBlocked,
|
|
512
|
-
blockedReason: this.blockedReason,
|
|
513
|
-
tags: this.tags ? [...this.tags] : [],
|
|
514
|
-
todos: this.todos ? this.todos.map((t) => ({ ...t })) : [],
|
|
515
|
-
customFields: this.customFields ? { ...this.customFields } : {},
|
|
516
|
-
parentId: this.parentId,
|
|
517
|
-
createdAt: this.createdAt,
|
|
518
|
-
updatedAt: this.updatedAt
|
|
519
|
-
};
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
export class ProjectEntity extends BaseEntity {
|
|
524
|
-
public key: string;
|
|
525
|
-
public name: string;
|
|
526
|
-
public description?: string;
|
|
527
|
-
public ownerId?: string;
|
|
528
|
-
public members?: ProjectMember[];
|
|
529
|
-
public teamIds?: string[];
|
|
530
|
-
public workflowId?: string;
|
|
531
|
-
public workflow?: Workflow;
|
|
532
|
-
public taskTypes?: Project['taskTypes'];
|
|
533
|
-
public statusDefinitions?: Project['statusDefinitions'];
|
|
534
|
-
public priorityDefinitions?: Project['priorityDefinitions'];
|
|
535
|
-
public customFieldDefinitions?: CustomFieldDefinition[];
|
|
536
|
-
|
|
537
|
-
constructor(data: Project) {
|
|
538
|
-
super(data.id, data.createdAt, data.updatedAt);
|
|
539
|
-
this.key = data.key || generateProjectKey(data.name);
|
|
540
|
-
this.name = data.name;
|
|
541
|
-
this.description = data.description;
|
|
542
|
-
this.ownerId = data.ownerId;
|
|
543
|
-
this.members = data.members ? [...data.members] : [];
|
|
544
|
-
this.teamIds = data.teamIds ? [...data.teamIds] : [];
|
|
545
|
-
this.workflowId = data.workflowId;
|
|
546
|
-
this.workflow = data.workflow;
|
|
547
|
-
this.taskTypes = data.taskTypes;
|
|
548
|
-
this.statusDefinitions = data.statusDefinitions;
|
|
549
|
-
this.priorityDefinitions = data.priorityDefinitions;
|
|
550
|
-
this.customFieldDefinitions = data.customFieldDefinitions ? [...data.customFieldDefinitions] : [];
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
public static create(
|
|
554
|
-
input: CreateProjectInput,
|
|
555
|
-
options?: { id?: string }
|
|
556
|
-
): ProjectEntity {
|
|
557
|
-
const id = options?.id || `proj_${crypto.randomUUID()}`;
|
|
558
|
-
const now = new Date().toISOString();
|
|
559
|
-
const key = input.key || generateProjectKey(input.name);
|
|
560
|
-
|
|
561
|
-
const project = new ProjectEntity({
|
|
562
|
-
...input,
|
|
563
|
-
id,
|
|
564
|
-
key,
|
|
565
|
-
createdAt: now,
|
|
566
|
-
updatedAt: now
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
const event: ProjectCreatedEvent = {
|
|
570
|
-
id: `evt_${crypto.randomUUID()}`,
|
|
571
|
-
name: 'project.created',
|
|
572
|
-
aggregateId: project.id,
|
|
573
|
-
aggregateType: 'Project',
|
|
574
|
-
occurredAt: now,
|
|
575
|
-
payload: { project: project.toPlain() }
|
|
576
|
-
};
|
|
577
|
-
project.raiseEvent(event);
|
|
578
|
-
|
|
579
|
-
return project;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
public validateCustomFields(values?: Record<string, unknown>): void {
|
|
583
|
-
validateCustomFieldValues(this.customFieldDefinitions, values);
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
public toPlain(): Project {
|
|
587
|
-
return {
|
|
588
|
-
id: this.id,
|
|
589
|
-
key: this.key,
|
|
590
|
-
name: this.name,
|
|
591
|
-
description: this.description,
|
|
592
|
-
ownerId: this.ownerId,
|
|
593
|
-
members: this.members ? [...this.members] : [],
|
|
594
|
-
teamIds: this.teamIds ? [...this.teamIds] : [],
|
|
595
|
-
workflowId: this.workflowId,
|
|
596
|
-
workflow: this.workflow,
|
|
597
|
-
taskTypes: this.taskTypes,
|
|
598
|
-
statusDefinitions: this.statusDefinitions,
|
|
599
|
-
priorityDefinitions: this.priorityDefinitions,
|
|
600
|
-
customFieldDefinitions: this.customFieldDefinitions ? [...this.customFieldDefinitions] : [],
|
|
601
|
-
createdAt: this.createdAt,
|
|
602
|
-
updatedAt: this.updatedAt
|
|
603
|
-
};
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
export class DeliverableEntity extends BaseEntity {
|
|
608
|
-
public projectId: string;
|
|
609
|
-
public title: string;
|
|
610
|
-
public description?: string;
|
|
611
|
-
public status: DeliverableStatus;
|
|
612
|
-
public format?: string;
|
|
613
|
-
public specs?: Record<string, unknown>;
|
|
614
|
-
public leadId?: string;
|
|
615
|
-
public reviewerId?: string;
|
|
616
|
-
public dueDate?: string;
|
|
617
|
-
public deliveredAt?: string;
|
|
618
|
-
public outputUrls?: string[];
|
|
619
|
-
public customFields?: Record<string, unknown>;
|
|
620
|
-
|
|
621
|
-
constructor(data: Deliverable) {
|
|
622
|
-
super(data.id, data.createdAt, data.updatedAt);
|
|
623
|
-
this.projectId = data.projectId;
|
|
624
|
-
this.title = data.title;
|
|
625
|
-
this.description = data.description;
|
|
626
|
-
this.status = data.status;
|
|
627
|
-
this.format = data.format;
|
|
628
|
-
this.specs = data.specs ? { ...data.specs } : undefined;
|
|
629
|
-
this.leadId = data.leadId;
|
|
630
|
-
this.reviewerId = data.reviewerId;
|
|
631
|
-
this.dueDate = data.dueDate;
|
|
632
|
-
this.deliveredAt = data.deliveredAt;
|
|
633
|
-
this.outputUrls = data.outputUrls ? [...data.outputUrls] : [];
|
|
634
|
-
this.customFields = data.customFields ? { ...data.customFields } : {};
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
public static create(
|
|
638
|
-
input: CreateDeliverableInput,
|
|
639
|
-
options?: { id?: string; customFieldDefs?: CustomFieldDefinition[] }
|
|
640
|
-
): DeliverableEntity {
|
|
641
|
-
const id = options?.id || `deliv_${crypto.randomUUID()}`;
|
|
642
|
-
const now = new Date().toISOString();
|
|
643
|
-
|
|
644
|
-
if (options?.customFieldDefs) {
|
|
645
|
-
validateCustomFieldValues(options.customFieldDefs, input.customFields);
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
const deliverable = new DeliverableEntity({
|
|
649
|
-
...input,
|
|
650
|
-
id,
|
|
651
|
-
createdAt: now,
|
|
652
|
-
updatedAt: now,
|
|
653
|
-
status: input.status || 'planned',
|
|
654
|
-
outputUrls: input.outputUrls ?? [],
|
|
655
|
-
customFields: input.customFields ?? {}
|
|
656
|
-
});
|
|
657
|
-
|
|
658
|
-
const createdEvent: DeliverableCreatedEvent = {
|
|
659
|
-
id: `evt_${crypto.randomUUID()}`,
|
|
660
|
-
name: 'deliverable.created',
|
|
661
|
-
aggregateId: deliverable.id,
|
|
662
|
-
aggregateType: 'Deliverable',
|
|
663
|
-
occurredAt: now,
|
|
664
|
-
payload: { deliverable: deliverable.toPlain() }
|
|
665
|
-
};
|
|
666
|
-
deliverable.raiseEvent(createdEvent);
|
|
667
|
-
|
|
668
|
-
return deliverable;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
public transitionTo(newStatus: DeliverableStatus): this {
|
|
672
|
-
if (this.status === newStatus) return this;
|
|
673
|
-
|
|
674
|
-
const previousStatus = this.status;
|
|
675
|
-
this.status = newStatus;
|
|
676
|
-
const now = new Date().toISOString();
|
|
677
|
-
|
|
678
|
-
if (newStatus === 'delivered' && !this.deliveredAt) {
|
|
679
|
-
this.deliveredAt = now;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
this.markUpdated();
|
|
683
|
-
|
|
684
|
-
const event: DeliverableStatusChangedEvent = {
|
|
685
|
-
id: `evt_${crypto.randomUUID()}`,
|
|
686
|
-
name: 'deliverable.status_changed',
|
|
687
|
-
aggregateId: this.id,
|
|
688
|
-
aggregateType: 'Deliverable',
|
|
689
|
-
occurredAt: now,
|
|
690
|
-
payload: {
|
|
691
|
-
deliverable: this.toPlain(),
|
|
692
|
-
previousStatus,
|
|
693
|
-
newStatus
|
|
694
|
-
}
|
|
695
|
-
};
|
|
696
|
-
this.raiseEvent(event);
|
|
697
|
-
|
|
698
|
-
return this;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
public addOutputUrl(url: string): this {
|
|
702
|
-
if (!this.outputUrls) {
|
|
703
|
-
this.outputUrls = [];
|
|
704
|
-
}
|
|
705
|
-
if (!this.outputUrls.includes(url)) {
|
|
706
|
-
const previous = this.toPlain();
|
|
707
|
-
this.outputUrls.push(url);
|
|
708
|
-
this.markUpdated();
|
|
709
|
-
|
|
710
|
-
const event: DeliverableUpdatedEvent = {
|
|
711
|
-
id: `evt_${crypto.randomUUID()}`,
|
|
712
|
-
name: 'deliverable.updated',
|
|
713
|
-
aggregateId: this.id,
|
|
714
|
-
aggregateType: 'Deliverable',
|
|
715
|
-
occurredAt: new Date().toISOString(),
|
|
716
|
-
payload: {
|
|
717
|
-
deliverable: this.toPlain(),
|
|
718
|
-
previous
|
|
719
|
-
}
|
|
720
|
-
};
|
|
721
|
-
this.raiseEvent(event);
|
|
722
|
-
}
|
|
723
|
-
return this;
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
public update(updates: Partial<Deliverable>, customFieldDefs?: CustomFieldDefinition[]): this {
|
|
727
|
-
const previous = this.toPlain();
|
|
728
|
-
|
|
729
|
-
if (updates.customFields && customFieldDefs) {
|
|
730
|
-
validateCustomFieldValues(customFieldDefs, {
|
|
731
|
-
...this.customFields,
|
|
732
|
-
...updates.customFields
|
|
733
|
-
});
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
if (updates.status && updates.status !== this.status) {
|
|
737
|
-
this.transitionTo(updates.status);
|
|
738
|
-
delete updates.status;
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
Object.assign(this, updates);
|
|
742
|
-
this.markUpdated();
|
|
743
|
-
|
|
744
|
-
const event: DeliverableUpdatedEvent = {
|
|
745
|
-
id: `evt_${crypto.randomUUID()}`,
|
|
746
|
-
name: 'deliverable.updated',
|
|
747
|
-
aggregateId: this.id,
|
|
748
|
-
aggregateType: 'Deliverable',
|
|
749
|
-
occurredAt: new Date().toISOString(),
|
|
750
|
-
payload: {
|
|
751
|
-
deliverable: this.toPlain(),
|
|
752
|
-
previous
|
|
753
|
-
}
|
|
754
|
-
};
|
|
755
|
-
this.raiseEvent(event);
|
|
756
|
-
|
|
757
|
-
return this;
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
public toPlain(): Deliverable {
|
|
761
|
-
return {
|
|
762
|
-
id: this.id,
|
|
763
|
-
projectId: this.projectId,
|
|
764
|
-
title: this.title,
|
|
765
|
-
description: this.description,
|
|
766
|
-
status: this.status,
|
|
767
|
-
format: this.format,
|
|
768
|
-
specs: this.specs ? { ...this.specs } : undefined,
|
|
769
|
-
leadId: this.leadId,
|
|
770
|
-
reviewerId: this.reviewerId,
|
|
771
|
-
dueDate: this.dueDate,
|
|
772
|
-
deliveredAt: this.deliveredAt,
|
|
773
|
-
outputUrls: this.outputUrls ? [...this.outputUrls] : [],
|
|
774
|
-
customFields: this.customFields ? { ...this.customFields } : {},
|
|
775
|
-
createdAt: this.createdAt,
|
|
776
|
-
updatedAt: this.updatedAt
|
|
777
|
-
};
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
export class AttachmentValidationError extends Error {
|
|
782
|
-
constructor(message: string) {
|
|
783
|
-
super(message);
|
|
784
|
-
this.name = 'AttachmentValidationError';
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
export function validateAttachmentUrl(url?: string): void {
|
|
789
|
-
if (!url || typeof url !== 'string' || url.trim() === '') {
|
|
790
|
-
throw new AttachmentValidationError('Attachment URL must be a non-empty string.');
|
|
791
|
-
}
|
|
792
|
-
if (url.startsWith('data:') && url.length > 2048) {
|
|
793
|
-
throw new AttachmentValidationError(
|
|
794
|
-
'Attachment URL cannot be a large data URI. Please upload the file using a FileStorageAdapter or Cloud Storage and provide the resulting URL.'
|
|
795
|
-
);
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
|