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