@voyantjs/legal 0.2.0 → 0.3.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.
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/contracts/service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/contracts/service.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,sBAAsB,EACtB,cAAc,EACd,yBAAyB,EAC1B,MAAM,qBAAqB,CAAA;AAG5B,OAAO,EAAE,sBAAsB,EAAE,cAAc,EAAE,yBAAyB,EAAE,CAAA;AAE5E,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAI5B,CAAA"}
@@ -1,570 +1,10 @@
1
- import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
2
- import { contractAttachments, contractNumberSeries, contractSignatures, contracts, contractTemplates, contractTemplateVersions, } from "./schema.js";
3
- function toTimestamp(value) {
4
- return value ? new Date(value) : null;
5
- }
6
- // ============================================================================
7
- // Template rendering
8
- // ============================================================================
9
- /**
10
- * Resolve a dot-path against an object, supporting array index accessors:
11
- * "a.b.c" → obj.a.b.c
12
- * "arr[0].name" → obj.arr[0].name
13
- * "a.b[2].c" → obj.a.b[2].c
14
- */
15
- function resolvePath(obj, path) {
16
- if (obj === null || obj === undefined)
17
- return undefined;
18
- const segments = [];
19
- const parts = path.split(".");
20
- for (const part of parts) {
21
- if (!part)
22
- continue;
23
- // Match "name[0]" → ["name", 0]
24
- const indexMatches = [...part.matchAll(/([^[\]]+)|\[(\d+)\]/g)];
25
- for (const match of indexMatches) {
26
- if (match[1] !== undefined)
27
- segments.push(match[1]);
28
- else if (match[2] !== undefined)
29
- segments.push(Number.parseInt(match[2], 10));
30
- }
31
- }
32
- let current = obj;
33
- for (const seg of segments) {
34
- if (current === null || current === undefined)
35
- return undefined;
36
- if (typeof seg === "number") {
37
- if (!Array.isArray(current))
38
- return undefined;
39
- current = current[seg];
40
- }
41
- else {
42
- if (typeof current !== "object")
43
- return undefined;
44
- current = current[seg];
45
- }
46
- }
47
- return current;
48
- }
49
- function stringifyValue(value) {
50
- if (value === null || value === undefined)
51
- return "";
52
- if (typeof value === "string")
53
- return value;
54
- if (typeof value === "number" || typeof value === "boolean")
55
- return String(value);
56
- return JSON.stringify(value);
57
- }
58
- const MUSTACHE_RE = /\{\{\s*([^}]+?)\s*\}\}/g;
59
- function renderMustache(body, variables) {
60
- return body.replace(MUSTACHE_RE, (_, path) => {
61
- const resolved = resolvePath(variables, path.trim());
62
- return stringifyValue(resolved);
63
- });
64
- }
65
- function walkLexical(node, variables) {
66
- const next = { ...node };
67
- if (typeof next.text === "string") {
68
- next.text = renderMustache(next.text, variables);
69
- }
70
- if (Array.isArray(next.children)) {
71
- next.children = next.children.map((child) => walkLexical(child, variables));
72
- }
73
- return next;
74
- }
75
- /**
76
- * Substitute variables in a template body. Supports:
77
- * - markdown / html / plain text: mustache replacement `{{path.to.value}}`
78
- * - lexical_json: walks the AST, rewriting text nodes only
79
- */
80
- export function renderTemplate(body, bodyFormat, variables) {
81
- if (bodyFormat === "lexical_json") {
82
- try {
83
- const parsed = JSON.parse(body);
84
- if (parsed && typeof parsed === "object") {
85
- const obj = parsed;
86
- if (obj.root && typeof obj.root === "object") {
87
- const result = { ...obj, root: walkLexical(obj.root, variables) };
88
- return JSON.stringify(result);
89
- }
90
- return JSON.stringify(walkLexical(obj, variables));
91
- }
92
- return body;
93
- }
94
- catch {
95
- // Fall through to mustache on parse error (treat as string template)
96
- return renderMustache(body, variables);
97
- }
98
- }
99
- return renderMustache(body, variables);
100
- }
101
- /**
102
- * Validate variable values against a template's variableSchema (JSON object
103
- * shape describing required fields). Returns a list of issues; empty array
104
- * means valid.
105
- */
106
- export function validateTemplateVariables(variableSchema, values) {
107
- const issues = [];
108
- if (!variableSchema || typeof variableSchema !== "object")
109
- return issues;
110
- const schema = variableSchema;
111
- const requiredList = Array.isArray(schema.required) ? schema.required : [];
112
- for (const key of requiredList) {
113
- const resolved = resolvePath(values, key);
114
- if (resolved === undefined || resolved === null || resolved === "") {
115
- issues.push(`missing required variable: ${key}`);
116
- }
117
- }
118
- return issues;
119
- }
120
- // ============================================================================
121
- // Number allocation
122
- // ============================================================================
123
- function currentPeriodBoundary(strategy, now) {
124
- if (strategy === "never")
125
- return null;
126
- if (strategy === "annual") {
127
- return new Date(Date.UTC(now.getUTCFullYear(), 0, 1));
128
- }
129
- return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
130
- }
131
- function formatNumber(prefix, separator, sequence, padLength) {
132
- const padded = String(sequence).padStart(padLength, "0");
133
- return `${prefix}${separator}${padded}`;
134
- }
135
- /**
136
- * Transactionally allocate the next sequence number for a series. Honors the
137
- * reset strategy (never/annual/monthly). Uses SELECT ... FOR UPDATE to avoid
138
- * concurrent-increment races.
139
- */
140
- export async function allocateContractNumber(db, seriesId) {
141
- return db.transaction(async (tx) => {
142
- const rows = await tx.execute(sql `SELECT * FROM ${contractNumberSeries}
143
- WHERE ${contractNumberSeries.id} = ${seriesId}
144
- FOR UPDATE`);
145
- const row = rows[0];
146
- if (!row)
147
- return null;
148
- const strategy = row.reset_strategy;
149
- const prefix = row.prefix ?? "";
150
- const separator = row.separator ?? "";
151
- const padLength = row.pad_length ?? 4;
152
- const currentSequence = row.current_sequence ?? 0;
153
- const resetAt = row.reset_at ? new Date(row.reset_at) : null;
154
- const now = new Date();
155
- const boundary = currentPeriodBoundary(strategy, now);
156
- const shouldReset = boundary !== null && (resetAt === null || resetAt.getTime() < boundary.getTime());
157
- const nextSequence = shouldReset ? 1 : currentSequence + 1;
158
- const nextResetAt = strategy === "never" ? resetAt : boundary;
159
- await tx
160
- .update(contractNumberSeries)
161
- .set({
162
- currentSequence: nextSequence,
163
- resetAt: nextResetAt,
164
- updatedAt: new Date(),
165
- })
166
- .where(eq(contractNumberSeries.id, seriesId));
167
- return {
168
- number: formatNumber(prefix, separator, nextSequence, padLength),
169
- sequence: nextSequence,
170
- };
171
- });
172
- }
173
- // ============================================================================
174
- // Service
175
- // ============================================================================
176
- async function paginate(rowsQuery, countQuery, limit, offset) {
177
- const [data, countResult] = await Promise.all([rowsQuery, countQuery]);
178
- return { data, total: countResult[0]?.total ?? 0, limit, offset };
179
- }
1
+ import { contractRecordsService } from "./service-contracts.js";
2
+ import { contractSeriesService } from "./service-series.js";
3
+ import { allocateContractNumber, renderTemplate, validateTemplateVariables, } from "./service-shared.js";
4
+ import { contractTemplatesService } from "./service-templates.js";
5
+ export { allocateContractNumber, renderTemplate, validateTemplateVariables };
180
6
  export const contractsService = {
181
- // ---------- contract templates ----------
182
- async listTemplates(db, query) {
183
- const conditions = [];
184
- if (query.scope)
185
- conditions.push(eq(contractTemplates.scope, query.scope));
186
- if (query.language)
187
- conditions.push(eq(contractTemplates.language, query.language));
188
- if (query.active !== undefined)
189
- conditions.push(eq(contractTemplates.active, query.active));
190
- if (query.search) {
191
- const term = `%${query.search}%`;
192
- conditions.push(or(ilike(contractTemplates.name, term), ilike(contractTemplates.slug, term), ilike(contractTemplates.description, term)));
193
- }
194
- const where = conditions.length ? and(...conditions) : undefined;
195
- return paginate(db
196
- .select()
197
- .from(contractTemplates)
198
- .where(where)
199
- .limit(query.limit)
200
- .offset(query.offset)
201
- .orderBy(desc(contractTemplates.updatedAt)), db.select({ total: sql `count(*)::int` }).from(contractTemplates).where(where), query.limit, query.offset);
202
- },
203
- async getTemplateById(db, id) {
204
- const [row] = await db
205
- .select()
206
- .from(contractTemplates)
207
- .where(eq(contractTemplates.id, id))
208
- .limit(1);
209
- return row ?? null;
210
- },
211
- async createTemplate(db, data) {
212
- const [row] = await db.insert(contractTemplates).values(data).returning();
213
- return row ?? null;
214
- },
215
- async updateTemplate(db, id, data) {
216
- const [row] = await db
217
- .update(contractTemplates)
218
- .set({ ...data, updatedAt: new Date() })
219
- .where(eq(contractTemplates.id, id))
220
- .returning();
221
- return row ?? null;
222
- },
223
- async deleteTemplate(db, id) {
224
- const [row] = await db
225
- .delete(contractTemplates)
226
- .where(eq(contractTemplates.id, id))
227
- .returning({ id: contractTemplates.id });
228
- return row ?? null;
229
- },
230
- // ---------- contract template versions ----------
231
- listTemplateVersions(db, templateId) {
232
- return db
233
- .select()
234
- .from(contractTemplateVersions)
235
- .where(eq(contractTemplateVersions.templateId, templateId))
236
- .orderBy(desc(contractTemplateVersions.version));
237
- },
238
- async getTemplateVersionById(db, id) {
239
- const [row] = await db
240
- .select()
241
- .from(contractTemplateVersions)
242
- .where(eq(contractTemplateVersions.id, id))
243
- .limit(1);
244
- return row ?? null;
245
- },
246
- async createTemplateVersion(db, templateId, data) {
247
- return db.transaction(async (tx) => {
248
- const [template] = await tx
249
- .select({ id: contractTemplates.id })
250
- .from(contractTemplates)
251
- .where(eq(contractTemplates.id, templateId))
252
- .limit(1);
253
- if (!template)
254
- return null;
255
- const [maxRow] = await tx
256
- .select({ max: sql `coalesce(max(${contractTemplateVersions.version}), 0)::int` })
257
- .from(contractTemplateVersions)
258
- .where(eq(contractTemplateVersions.templateId, templateId));
259
- const nextVersion = (maxRow?.max ?? 0) + 1;
260
- const [version] = await tx
261
- .insert(contractTemplateVersions)
262
- .values({
263
- templateId,
264
- version: nextVersion,
265
- bodyFormat: data.bodyFormat,
266
- body: data.body,
267
- variableSchema: data.variableSchema ?? null,
268
- changelog: data.changelog ?? null,
269
- createdBy: data.createdBy ?? null,
270
- })
271
- .returning();
272
- if (version) {
273
- await tx
274
- .update(contractTemplates)
275
- .set({ currentVersionId: version.id, updatedAt: new Date() })
276
- .where(eq(contractTemplates.id, templateId));
277
- }
278
- return version ?? null;
279
- });
280
- },
281
- // ---------- contract number series ----------
282
- async listSeries(db) {
283
- return db.select().from(contractNumberSeries).orderBy(desc(contractNumberSeries.updatedAt));
284
- },
285
- async getSeriesById(db, id) {
286
- const [row] = await db
287
- .select()
288
- .from(contractNumberSeries)
289
- .where(eq(contractNumberSeries.id, id))
290
- .limit(1);
291
- return row ?? null;
292
- },
293
- async createSeries(db, data) {
294
- const [row] = await db.insert(contractNumberSeries).values(data).returning();
295
- return row ?? null;
296
- },
297
- async updateSeries(db, id, data) {
298
- const [row] = await db
299
- .update(contractNumberSeries)
300
- .set({ ...data, updatedAt: new Date() })
301
- .where(eq(contractNumberSeries.id, id))
302
- .returning();
303
- return row ?? null;
304
- },
305
- async deleteSeries(db, id) {
306
- const [row] = await db
307
- .delete(contractNumberSeries)
308
- .where(eq(contractNumberSeries.id, id))
309
- .returning({ id: contractNumberSeries.id });
310
- return row ?? null;
311
- },
312
- // ---------- contracts ----------
313
- async listContracts(db, query) {
314
- const conditions = [];
315
- if (query.scope)
316
- conditions.push(eq(contracts.scope, query.scope));
317
- if (query.status)
318
- conditions.push(eq(contracts.status, query.status));
319
- if (query.personId)
320
- conditions.push(eq(contracts.personId, query.personId));
321
- if (query.organizationId)
322
- conditions.push(eq(contracts.organizationId, query.organizationId));
323
- if (query.supplierId)
324
- conditions.push(eq(contracts.supplierId, query.supplierId));
325
- if (query.bookingId)
326
- conditions.push(eq(contracts.bookingId, query.bookingId));
327
- if (query.orderId)
328
- conditions.push(eq(contracts.orderId, query.orderId));
329
- if (query.search) {
330
- const term = `%${query.search}%`;
331
- conditions.push(or(ilike(contracts.title, term), ilike(contracts.contractNumber, term)));
332
- }
333
- const where = conditions.length ? and(...conditions) : undefined;
334
- return paginate(db
335
- .select()
336
- .from(contracts)
337
- .where(where)
338
- .limit(query.limit)
339
- .offset(query.offset)
340
- .orderBy(desc(contracts.createdAt)), db.select({ total: sql `count(*)::int` }).from(contracts).where(where), query.limit, query.offset);
341
- },
342
- async getContractById(db, id) {
343
- const [row] = await db.select().from(contracts).where(eq(contracts.id, id)).limit(1);
344
- return row ?? null;
345
- },
346
- async createContract(db, data) {
347
- const [row] = await db
348
- .insert(contracts)
349
- .values({
350
- ...data,
351
- expiresAt: toTimestamp(data.expiresAt),
352
- })
353
- .returning();
354
- return row ?? null;
355
- },
356
- async updateContract(db, id, data) {
357
- const [row] = await db
358
- .update(contracts)
359
- .set({
360
- ...data,
361
- expiresAt: data.expiresAt === undefined ? undefined : toTimestamp(data.expiresAt),
362
- updatedAt: new Date(),
363
- })
364
- .where(eq(contracts.id, id))
365
- .returning();
366
- return row ?? null;
367
- },
368
- async deleteContract(db, id) {
369
- const [existing] = await db
370
- .select({ id: contracts.id, status: contracts.status })
371
- .from(contracts)
372
- .where(eq(contracts.id, id))
373
- .limit(1);
374
- if (!existing)
375
- return { status: "not_found" };
376
- if (existing.status !== "draft")
377
- return { status: "not_draft" };
378
- await db.delete(contracts).where(eq(contracts.id, id));
379
- return { status: "deleted" };
380
- },
381
- // ---------- lifecycle actions ----------
382
- /**
383
- * Transition a draft contract to `issued`: renders body from the template
384
- * version, allocates a contract number if a series is set, timestamps
385
- * `issuedAt`. Returns null if contract is missing or not in `draft`.
386
- */
387
- async issueContract(db, contractId) {
388
- return db.transaction(async (tx) => {
389
- const [contract] = await tx
390
- .select()
391
- .from(contracts)
392
- .where(eq(contracts.id, contractId))
393
- .limit(1);
394
- if (!contract)
395
- return { status: "not_found" };
396
- if (contract.status !== "draft")
397
- return { status: "not_draft" };
398
- // Render body from template version if referenced
399
- let renderedBody = contract.renderedBody;
400
- let renderedBodyFormat = contract.renderedBodyFormat;
401
- if (contract.templateVersionId) {
402
- const [version] = await tx
403
- .select()
404
- .from(contractTemplateVersions)
405
- .where(eq(contractTemplateVersions.id, contract.templateVersionId))
406
- .limit(1);
407
- if (version) {
408
- const vars = contract.variables ?? {};
409
- renderedBody = renderTemplate(version.body, version.bodyFormat, vars);
410
- renderedBodyFormat = version.bodyFormat;
411
- }
412
- }
413
- // Allocate contract number if not already set and a series is linked
414
- let contractNumber = contract.contractNumber;
415
- if (!contractNumber && contract.seriesId) {
416
- const allocated = await allocateContractNumber(tx, contract.seriesId);
417
- if (allocated)
418
- contractNumber = allocated.number;
419
- }
420
- const [updated] = await tx
421
- .update(contracts)
422
- .set({
423
- status: "issued",
424
- issuedAt: new Date(),
425
- renderedBody,
426
- renderedBodyFormat,
427
- contractNumber,
428
- updatedAt: new Date(),
429
- })
430
- .where(eq(contracts.id, contractId))
431
- .returning();
432
- return { status: "issued", contract: updated ?? null };
433
- });
434
- },
435
- async sendContract(db, contractId) {
436
- const [contract] = await db
437
- .select({ id: contracts.id, status: contracts.status })
438
- .from(contracts)
439
- .where(eq(contracts.id, contractId))
440
- .limit(1);
441
- if (!contract)
442
- return { status: "not_found" };
443
- if (contract.status !== "issued" && contract.status !== "sent") {
444
- return { status: "not_issued" };
445
- }
446
- const [updated] = await db
447
- .update(contracts)
448
- .set({ status: "sent", sentAt: new Date(), updatedAt: new Date() })
449
- .where(eq(contracts.id, contractId))
450
- .returning();
451
- return { status: "sent", contract: updated ?? null };
452
- },
453
- async voidContract(db, contractId) {
454
- const [contract] = await db
455
- .select({ id: contracts.id, status: contracts.status })
456
- .from(contracts)
457
- .where(eq(contracts.id, contractId))
458
- .limit(1);
459
- if (!contract)
460
- return { status: "not_found" };
461
- if (contract.status === "void")
462
- return { status: "already_void" };
463
- const [updated] = await db
464
- .update(contracts)
465
- .set({ status: "void", voidedAt: new Date(), updatedAt: new Date() })
466
- .where(eq(contracts.id, contractId))
467
- .returning();
468
- return { status: "voided", contract: updated ?? null };
469
- },
470
- // ---------- signatures ----------
471
- listSignatures(db, contractId) {
472
- return db
473
- .select()
474
- .from(contractSignatures)
475
- .where(eq(contractSignatures.contractId, contractId))
476
- .orderBy(desc(contractSignatures.signedAt));
477
- },
478
- async signContract(db, contractId, data) {
479
- return db.transaction(async (tx) => {
480
- const [contract] = await tx
481
- .select()
482
- .from(contracts)
483
- .where(eq(contracts.id, contractId))
484
- .limit(1);
485
- if (!contract)
486
- return { status: "not_found" };
487
- if (contract.status !== "issued" && contract.status !== "sent") {
488
- return { status: "not_signable" };
489
- }
490
- const [signature] = await tx
491
- .insert(contractSignatures)
492
- .values({ ...data, contractId })
493
- .returning();
494
- // Transition: first signature → signed; two-party → executed if both sides signed.
495
- // MVP: single-signer flow → mark as "signed". Callers that need multi-party
496
- // workflows should issue additional signatures and explicitly call execute.
497
- const [updated] = await tx
498
- .update(contracts)
499
- .set({ status: "signed", updatedAt: new Date() })
500
- .where(eq(contracts.id, contractId))
501
- .returning();
502
- return { status: "signed", contract: updated ?? null, signature: signature ?? null };
503
- });
504
- },
505
- async executeContract(db, contractId) {
506
- const [contract] = await db
507
- .select({ id: contracts.id, status: contracts.status })
508
- .from(contracts)
509
- .where(eq(contracts.id, contractId))
510
- .limit(1);
511
- if (!contract)
512
- return { status: "not_found" };
513
- if (contract.status !== "signed")
514
- return { status: "not_signed" };
515
- const [updated] = await db
516
- .update(contracts)
517
- .set({ status: "executed", executedAt: new Date(), updatedAt: new Date() })
518
- .where(eq(contracts.id, contractId))
519
- .returning();
520
- return { status: "executed", contract: updated ?? null };
521
- },
522
- // ---------- attachments ----------
523
- listAttachments(db, contractId) {
524
- return db
525
- .select()
526
- .from(contractAttachments)
527
- .where(eq(contractAttachments.contractId, contractId))
528
- .orderBy(desc(contractAttachments.createdAt));
529
- },
530
- async createAttachment(db, contractId, data) {
531
- const [contract] = await db
532
- .select({ id: contracts.id })
533
- .from(contracts)
534
- .where(eq(contracts.id, contractId))
535
- .limit(1);
536
- if (!contract)
537
- return null;
538
- const [row] = await db
539
- .insert(contractAttachments)
540
- .values({ ...data, contractId })
541
- .returning();
542
- return row ?? null;
543
- },
544
- async updateAttachment(db, attachmentId, data) {
545
- const [row] = await db
546
- .update(contractAttachments)
547
- .set(data)
548
- .where(eq(contractAttachments.id, attachmentId))
549
- .returning();
550
- return row ?? null;
551
- },
552
- async deleteAttachment(db, attachmentId) {
553
- const [row] = await db
554
- .delete(contractAttachments)
555
- .where(eq(contractAttachments.id, attachmentId))
556
- .returning({ id: contractAttachments.id });
557
- return row ?? null;
558
- },
559
- // ---------- preview ----------
560
- /**
561
- * Preview render: substitute variables against an arbitrary body without
562
- * touching the database. Used by `/v1/admin/contracts/:id/render` and
563
- * `/v1/admin/contracts/templates/:id/preview`.
564
- */
565
- renderPreview(input) {
566
- const body = input.body ?? "";
567
- const format = input.bodyFormat ?? "markdown";
568
- return renderTemplate(body, format, input.variables);
569
- },
7
+ ...contractTemplatesService,
8
+ ...contractSeriesService,
9
+ ...contractRecordsService,
570
10
  };
@@ -564,10 +564,10 @@ export declare const policiesAdminRoutes: import("hono/hono-base").HonoBase<Env,
564
564
  id: string;
565
565
  createdAt: string;
566
566
  updatedAt: string;
567
+ conditions: import("hono/utils/types").JSONValue;
568
+ label: string | null;
567
569
  sortOrder: number;
568
570
  currency: string | null;
569
- label: string | null;
570
- conditions: import("hono/utils/types").JSONValue;
571
571
  validFrom: string | null;
572
572
  validTo: string | null;
573
573
  policyVersionId: string;