@voyant-travel/quotes 0.119.2

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.
Files changed (51) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +39 -0
  3. package/dist/booking-extension.d.ts +123 -0
  4. package/dist/booking-extension.d.ts.map +1 -0
  5. package/dist/booking-extension.js +87 -0
  6. package/dist/index.d.ts +16 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +32 -0
  9. package/dist/routes/index.d.ts +1410 -0
  10. package/dist/routes/index.d.ts.map +1 -0
  11. package/dist/routes/index.js +8 -0
  12. package/dist/routes/pipelines.d.ts +292 -0
  13. package/dist/routes/pipelines.d.ts.map +1 -0
  14. package/dist/routes/pipelines.js +59 -0
  15. package/dist/routes/quote-versions.d.ts +746 -0
  16. package/dist/routes/quote-versions.d.ts.map +1 -0
  17. package/dist/routes/quote-versions.js +175 -0
  18. package/dist/routes/quotes.d.ts +391 -0
  19. package/dist/routes/quotes.d.ts.map +1 -0
  20. package/dist/routes/quotes.js +70 -0
  21. package/dist/schema-relations.d.ts +30 -0
  22. package/dist/schema-relations.d.ts.map +1 -0
  23. package/dist/schema-relations.js +49 -0
  24. package/dist/schema-sales.d.ts +1511 -0
  25. package/dist/schema-sales.d.ts.map +1 -0
  26. package/dist/schema-sales.js +164 -0
  27. package/dist/schema-shared.d.ts +5 -0
  28. package/dist/schema-shared.d.ts.map +1 -0
  29. package/dist/schema-shared.js +18 -0
  30. package/dist/schema.d.ts +5 -0
  31. package/dist/schema.d.ts.map +1 -0
  32. package/dist/schema.js +4 -0
  33. package/dist/service/helpers.d.ts +22 -0
  34. package/dist/service/helpers.d.ts.map +1 -0
  35. package/dist/service/helpers.js +39 -0
  36. package/dist/service/index.d.ts +1734 -0
  37. package/dist/service/index.d.ts.map +1 -0
  38. package/dist/service/index.js +11 -0
  39. package/dist/service/pipelines.d.ts +113 -0
  40. package/dist/service/pipelines.d.ts.map +1 -0
  41. package/dist/service/pipelines.js +68 -0
  42. package/dist/service/quote-versions.d.ts +674 -0
  43. package/dist/service/quote-versions.d.ts.map +1 -0
  44. package/dist/service/quote-versions.js +400 -0
  45. package/dist/service/quotes.d.ts +826 -0
  46. package/dist/service/quotes.d.ts.map +1 -0
  47. package/dist/service/quotes.js +110 -0
  48. package/dist/validation.d.ts +2 -0
  49. package/dist/validation.d.ts.map +1 -0
  50. package/dist/validation.js +1 -0
  51. package/package.json +72 -0
@@ -0,0 +1,400 @@
1
+ // agent-quality: file-size exception -- owner: crm; existing service module stays co-located until a dedicated split preserves behavior and tests.
2
+ import { and, desc, eq, isNotNull, isNull, lt, ne, or, sql } from "drizzle-orm";
3
+ import { quotes, quoteVersionLines, quoteVersions, } from "../schema.js";
4
+ import { paginate } from "./helpers.js";
5
+ export class QuoteVersionConflictError extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = "QuoteVersionConflictError";
9
+ }
10
+ }
11
+ function normalizeTimestamp(value) {
12
+ return value == null ? value : new Date(value);
13
+ }
14
+ function normalizeNow(value) {
15
+ return value instanceof Date ? value : value ? new Date(value) : new Date();
16
+ }
17
+ function toDateString(value) {
18
+ return value.toISOString().slice(0, 10);
19
+ }
20
+ export const quoteVersionsService = {
21
+ async listQuoteVersions(db, query) {
22
+ const conditions = [];
23
+ if (query.quoteId)
24
+ conditions.push(eq(quoteVersions.quoteId, query.quoteId));
25
+ if (query.status)
26
+ conditions.push(eq(quoteVersions.status, query.status));
27
+ const where = conditions.length ? and(...conditions) : undefined;
28
+ return paginate(db
29
+ .select()
30
+ .from(quoteVersions)
31
+ .where(where)
32
+ .limit(query.limit)
33
+ .offset(query.offset)
34
+ .orderBy(desc(quoteVersions.updatedAt)), db.select({ count: sql `count(*)::int` }).from(quoteVersions).where(where), query.limit, query.offset);
35
+ },
36
+ async getQuoteVersionById(db, id) {
37
+ const [row] = await db.select().from(quoteVersions).where(eq(quoteVersions.id, id)).limit(1);
38
+ return row ?? null;
39
+ },
40
+ async getQuoteVersionProposal(db, id) {
41
+ const [row] = await db
42
+ .select({
43
+ quoteVersion: quoteVersions,
44
+ quote: quotes,
45
+ })
46
+ .from(quoteVersions)
47
+ .innerJoin(quotes, eq(quoteVersions.quoteId, quotes.id))
48
+ .where(eq(quoteVersions.id, id))
49
+ .limit(1);
50
+ if (!row)
51
+ return null;
52
+ return {
53
+ quote: row.quote,
54
+ quoteVersion: row.quoteVersion,
55
+ lines: await db
56
+ .select()
57
+ .from(quoteVersionLines)
58
+ .where(eq(quoteVersionLines.quoteVersionId, id))
59
+ .orderBy(quoteVersionLines.createdAt),
60
+ };
61
+ },
62
+ async createQuoteVersion(db, data) {
63
+ if (data.status && data.status !== "draft") {
64
+ throw new QuoteVersionConflictError("Quote Versions must be created as draft; use lifecycle routes for status changes");
65
+ }
66
+ const values = {
67
+ ...data,
68
+ sentAt: normalizeTimestamp(data.sentAt),
69
+ viewedAt: normalizeTimestamp(data.viewedAt),
70
+ decidedAt: normalizeTimestamp(data.decidedAt),
71
+ };
72
+ const [row] = await db.insert(quoteVersions).values(values).returning();
73
+ return row;
74
+ },
75
+ async updateQuoteVersion(db, id, data) {
76
+ if (data.status !== undefined) {
77
+ throw new QuoteVersionConflictError("Quote Version status changes must use lifecycle routes");
78
+ }
79
+ const values = {
80
+ ...data,
81
+ sentAt: normalizeTimestamp(data.sentAt),
82
+ viewedAt: normalizeTimestamp(data.viewedAt),
83
+ decidedAt: normalizeTimestamp(data.decidedAt),
84
+ updatedAt: new Date(),
85
+ };
86
+ const [row] = await db
87
+ .update(quoteVersions)
88
+ .set(values)
89
+ .where(and(eq(quoteVersions.id, id), eq(quoteVersions.status, "draft")))
90
+ .returning();
91
+ if (row)
92
+ return row;
93
+ const [existing] = await db
94
+ .select({ status: quoteVersions.status })
95
+ .from(quoteVersions)
96
+ .where(eq(quoteVersions.id, id))
97
+ .limit(1);
98
+ if (!existing)
99
+ return null;
100
+ throw new QuoteVersionConflictError("Quote Versions can only be edited while draft");
101
+ },
102
+ async deleteQuoteVersion(db, id) {
103
+ const [row] = await db
104
+ .delete(quoteVersions)
105
+ .where(and(eq(quoteVersions.id, id), eq(quoteVersions.status, "draft")))
106
+ .returning({ id: quoteVersions.id });
107
+ if (row)
108
+ return row;
109
+ const [existing] = await db
110
+ .select({ status: quoteVersions.status })
111
+ .from(quoteVersions)
112
+ .where(eq(quoteVersions.id, id))
113
+ .limit(1);
114
+ if (!existing)
115
+ return null;
116
+ throw new QuoteVersionConflictError("Quote Versions can only be deleted while draft");
117
+ },
118
+ async applyTripSnapshotToQuoteVersion(db, id, data) {
119
+ return db.transaction(async (tx) => {
120
+ const [quoteVersion] = await tx
121
+ .update(quoteVersions)
122
+ .set({
123
+ tripSnapshotId: data.tripSnapshotId,
124
+ currency: data.currency,
125
+ subtotalAmountCents: data.subtotalAmountCents,
126
+ taxAmountCents: data.taxAmountCents,
127
+ totalAmountCents: data.totalAmountCents,
128
+ updatedAt: new Date(),
129
+ })
130
+ .where(and(eq(quoteVersions.id, id), eq(quoteVersions.status, "draft")))
131
+ .returning();
132
+ if (!quoteVersion) {
133
+ const [existing] = await tx
134
+ .select({ status: quoteVersions.status })
135
+ .from(quoteVersions)
136
+ .where(eq(quoteVersions.id, id))
137
+ .limit(1);
138
+ if (!existing)
139
+ return null;
140
+ throw new QuoteVersionConflictError("Trip snapshots can only be applied to draft Quote Versions");
141
+ }
142
+ await tx.delete(quoteVersionLines).where(eq(quoteVersionLines.quoteVersionId, id));
143
+ const lineValues = data.lines.map(({ componentId: _componentId, ...line }) => ({
144
+ ...line,
145
+ quoteVersionId: id,
146
+ }));
147
+ const lines = lineValues.length > 0
148
+ ? await tx.insert(quoteVersionLines).values(lineValues).returning()
149
+ : [];
150
+ return { quoteVersion, lines };
151
+ });
152
+ },
153
+ async sendQuoteVersion(db, id, data = {}) {
154
+ return db.transaction(async (tx) => {
155
+ const [existing] = await tx
156
+ .select()
157
+ .from(quoteVersions)
158
+ .where(eq(quoteVersions.id, id))
159
+ .limit(1);
160
+ if (!existing)
161
+ return null;
162
+ if (existing.status !== "draft") {
163
+ throw new QuoteVersionConflictError("Quote Versions can only be sent from draft");
164
+ }
165
+ if (!existing.tripSnapshotId) {
166
+ throw new QuoteVersionConflictError("Quote Versions must have a Trip snapshot before they can be sent");
167
+ }
168
+ const now = new Date();
169
+ const validUntil = data.validUntil === undefined ? existing.validUntil : data.validUntil;
170
+ if (validUntil && validUntil < toDateString(now)) {
171
+ throw new QuoteVersionConflictError("Quote Version validUntil must be today or later");
172
+ }
173
+ const [row] = await tx
174
+ .update(quoteVersions)
175
+ .set({
176
+ status: "sent",
177
+ validUntil,
178
+ sentAt: now,
179
+ viewedAt: null,
180
+ decidedAt: null,
181
+ updatedAt: now,
182
+ })
183
+ .where(eq(quoteVersions.id, id))
184
+ .returning();
185
+ return row ?? null;
186
+ });
187
+ },
188
+ async markQuoteVersionViewed(db, id) {
189
+ const now = new Date();
190
+ const [row] = await db
191
+ .update(quoteVersions)
192
+ .set({
193
+ viewedAt: now,
194
+ updatedAt: now,
195
+ })
196
+ .where(and(eq(quoteVersions.id, id), eq(quoteVersions.status, "sent"), isNull(quoteVersions.viewedAt)))
197
+ .returning();
198
+ if (row)
199
+ return row;
200
+ const [existing] = await db
201
+ .select()
202
+ .from(quoteVersions)
203
+ .where(eq(quoteVersions.id, id))
204
+ .limit(1);
205
+ return existing ?? null;
206
+ },
207
+ async acceptQuoteVersion(db, id, _data = {}) {
208
+ return db.transaction(async (tx) => {
209
+ const [current] = await tx
210
+ .select({
211
+ quoteVersion: quoteVersions,
212
+ quote: quotes,
213
+ })
214
+ .from(quoteVersions)
215
+ .innerJoin(quotes, eq(quoteVersions.quoteId, quotes.id))
216
+ .where(eq(quoteVersions.id, id))
217
+ .limit(1);
218
+ if (!current)
219
+ return null;
220
+ if (current.quote.acceptedVersionId &&
221
+ current.quote.acceptedVersionId !== current.quoteVersion.id) {
222
+ throw new QuoteVersionConflictError("Quote already has an accepted Quote Version");
223
+ }
224
+ if (current.quoteVersion.status === "accepted") {
225
+ return {
226
+ quote: current.quote,
227
+ quoteVersion: current.quoteVersion,
228
+ closedQuoteVersions: [],
229
+ };
230
+ }
231
+ if (current.quoteVersion.status !== "sent") {
232
+ throw new QuoteVersionConflictError("Quote Versions can only be accepted after they are sent");
233
+ }
234
+ const now = new Date();
235
+ const [quoteVersion] = await tx
236
+ .update(quoteVersions)
237
+ .set({
238
+ status: "accepted",
239
+ decidedAt: now,
240
+ updatedAt: now,
241
+ })
242
+ .where(and(eq(quoteVersions.id, id), eq(quoteVersions.status, "sent")))
243
+ .returning();
244
+ if (!quoteVersion) {
245
+ throw new QuoteVersionConflictError("Quote Versions can only be accepted after they are sent");
246
+ }
247
+ const declinedVersions = await tx
248
+ .update(quoteVersions)
249
+ .set({
250
+ status: "declined",
251
+ decidedAt: now,
252
+ updatedAt: now,
253
+ })
254
+ .where(and(eq(quoteVersions.quoteId, quoteVersion.quoteId), ne(quoteVersions.id, quoteVersion.id), eq(quoteVersions.status, "sent")))
255
+ .returning();
256
+ const supersededVersions = await tx
257
+ .update(quoteVersions)
258
+ .set({
259
+ status: "superseded",
260
+ decidedAt: now,
261
+ updatedAt: now,
262
+ })
263
+ .where(and(eq(quoteVersions.quoteId, quoteVersion.quoteId), ne(quoteVersions.id, quoteVersion.id), eq(quoteVersions.status, "draft")))
264
+ .returning();
265
+ const [quote] = await tx
266
+ .update(quotes)
267
+ .set({
268
+ status: "won",
269
+ acceptedVersionId: quoteVersion.id,
270
+ valueAmountCents: quoteVersion.totalAmountCents,
271
+ valueCurrency: quoteVersion.currency,
272
+ closedAt: now,
273
+ updatedAt: now,
274
+ })
275
+ .where(and(eq(quotes.id, quoteVersion.quoteId), or(isNull(quotes.acceptedVersionId), eq(quotes.acceptedVersionId, quoteVersion.id))))
276
+ .returning();
277
+ if (!quote) {
278
+ throw new QuoteVersionConflictError("Quote already has an accepted Quote Version");
279
+ }
280
+ return {
281
+ quote,
282
+ quoteVersion,
283
+ closedQuoteVersions: [...declinedVersions, ...supersededVersions],
284
+ };
285
+ });
286
+ },
287
+ async declineQuoteVersion(db, id, _data = {}) {
288
+ const now = new Date();
289
+ const [row] = await db
290
+ .update(quoteVersions)
291
+ .set({
292
+ status: "declined",
293
+ decidedAt: now,
294
+ updatedAt: now,
295
+ })
296
+ .where(and(eq(quoteVersions.id, id), eq(quoteVersions.status, "sent")))
297
+ .returning();
298
+ if (row)
299
+ return row;
300
+ const [existing] = await db
301
+ .select({ status: quoteVersions.status })
302
+ .from(quoteVersions)
303
+ .where(eq(quoteVersions.id, id))
304
+ .limit(1);
305
+ if (!existing)
306
+ return null;
307
+ throw new QuoteVersionConflictError("Quote Versions can only be declined after they are sent");
308
+ },
309
+ async expireQuoteVersionIfPastValidUntil(db, id, nowValue) {
310
+ const now = normalizeNow(nowValue);
311
+ const [row] = await db
312
+ .update(quoteVersions)
313
+ .set({
314
+ status: "expired",
315
+ decidedAt: now,
316
+ updatedAt: now,
317
+ })
318
+ .where(and(eq(quoteVersions.id, id), eq(quoteVersions.status, "sent"), isNotNull(quoteVersions.validUntil), lt(quoteVersions.validUntil, toDateString(now))))
319
+ .returning();
320
+ return row ?? null;
321
+ },
322
+ async expireQuoteVersions(db, data = {}) {
323
+ const now = normalizeNow(data.now);
324
+ const rows = await db
325
+ .update(quoteVersions)
326
+ .set({
327
+ status: "expired",
328
+ decidedAt: now,
329
+ updatedAt: now,
330
+ })
331
+ .where(and(eq(quoteVersions.status, "sent"), isNotNull(quoteVersions.validUntil), lt(quoteVersions.validUntil, toDateString(now))))
332
+ .returning();
333
+ return rows;
334
+ },
335
+ listQuoteVersionLines(db, quoteVersionId) {
336
+ return db
337
+ .select()
338
+ .from(quoteVersionLines)
339
+ .where(eq(quoteVersionLines.quoteVersionId, quoteVersionId))
340
+ .orderBy(quoteVersionLines.createdAt);
341
+ },
342
+ async createQuoteVersionLine(db, quoteVersionId, data) {
343
+ const [quoteVersion] = await db
344
+ .select({ status: quoteVersions.status })
345
+ .from(quoteVersions)
346
+ .where(eq(quoteVersions.id, quoteVersionId))
347
+ .limit(1);
348
+ if (!quoteVersion)
349
+ return null;
350
+ if (quoteVersion.status !== "draft") {
351
+ throw new QuoteVersionConflictError("Quote Version lines can only be edited while draft");
352
+ }
353
+ const [row] = await db
354
+ .insert(quoteVersionLines)
355
+ .values({ ...data, quoteVersionId })
356
+ .returning();
357
+ return row;
358
+ },
359
+ async updateQuoteVersionLine(db, id, data) {
360
+ const [existing] = await db
361
+ .select({
362
+ status: quoteVersions.status,
363
+ })
364
+ .from(quoteVersionLines)
365
+ .innerJoin(quoteVersions, eq(quoteVersionLines.quoteVersionId, quoteVersions.id))
366
+ .where(eq(quoteVersionLines.id, id))
367
+ .limit(1);
368
+ if (!existing)
369
+ return null;
370
+ if (existing.status !== "draft") {
371
+ throw new QuoteVersionConflictError("Quote Version lines can only be edited while draft");
372
+ }
373
+ const [row] = await db
374
+ .update(quoteVersionLines)
375
+ .set({ ...data, updatedAt: new Date() })
376
+ .where(eq(quoteVersionLines.id, id))
377
+ .returning();
378
+ return row ?? null;
379
+ },
380
+ async deleteQuoteVersionLine(db, id) {
381
+ const [existing] = await db
382
+ .select({
383
+ status: quoteVersions.status,
384
+ })
385
+ .from(quoteVersionLines)
386
+ .innerJoin(quoteVersions, eq(quoteVersionLines.quoteVersionId, quoteVersions.id))
387
+ .where(eq(quoteVersionLines.id, id))
388
+ .limit(1);
389
+ if (!existing)
390
+ return null;
391
+ if (existing.status !== "draft") {
392
+ throw new QuoteVersionConflictError("Quote Version lines can only be edited while draft");
393
+ }
394
+ const [row] = await db
395
+ .delete(quoteVersionLines)
396
+ .where(eq(quoteVersionLines.id, id))
397
+ .returning({ id: quoteVersionLines.id });
398
+ return row ?? null;
399
+ },
400
+ };