@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.
- package/dist/contracts/routes.d.ts +1 -1
- package/dist/contracts/service-contracts.d.ts +1234 -0
- package/dist/contracts/service-contracts.d.ts.map +1 -0
- package/dist/contracts/service-contracts.js +233 -0
- package/dist/contracts/service-series.d.ts +68 -0
- package/dist/contracts/service-series.d.ts.map +1 -0
- package/dist/contracts/service-series.js +34 -0
- package/dist/contracts/service-shared.d.ts +32 -0
- package/dist/contracts/service-shared.d.ts.map +1 -0
- package/dist/contracts/service-shared.js +142 -0
- package/dist/contracts/service-templates.d.ts +415 -0
- package/dist/contracts/service-templates.d.ts.map +1 -0
- package/dist/contracts/service-templates.js +108 -0
- package/dist/contracts/service.d.ts +790 -836
- package/dist/contracts/service.d.ts.map +1 -1
- package/dist/contracts/service.js +8 -568
- package/dist/policies/routes.d.ts +2 -2
- package/dist/policies/service-core.d.ts +1336 -0
- package/dist/policies/service-core.d.ts.map +1 -0
- package/dist/policies/service-core.js +357 -0
- package/dist/policies/service-shared.d.ts +43 -0
- package/dist/policies/service-shared.d.ts.map +1 -0
- package/dist/policies/service-shared.js +30 -0
- package/dist/policies/service.d.ts +28 -76
- package/dist/policies/service.d.ts.map +1 -1
- package/dist/policies/service.js +4 -436
- package/package.json +6 -6
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-contracts.d.ts","sourceRoot":"","sources":["../../src/contracts/service-contracts.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAQjE,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,6BAA6B,EAClC,KAAK,mBAAmB,EACxB,KAAK,4BAA4B,EAIjC,KAAK,6BAA6B,EAClC,KAAK,mBAAmB,EACzB,MAAM,qBAAqB,CAAA;AAE5B,eAAO,MAAM,sBAAsB;sBACT,kBAAkB,SAAS,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBA2B1C,kBAAkB,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;uBAI/B,kBAAkB,QAAQ,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;uBAO7C,kBAAkB,MAAM,MAAM,QAAQ,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;uBAYzD,kBAAkB,MAAM,MAAM;;;;;;;sBAW/B,kBAAkB,cAAc,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBA2CvC,kBAAkB,cAAc,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAgBtC,kBAAkB,cAAc,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uBAe1C,kBAAkB,cAAc,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAQnD,kBAAkB,cACV,MAAM,QACZ,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAuBV,kBAAkB,cAAc,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAe5C,kBAAkB,cAAc,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yBAQpD,kBAAkB,cACV,MAAM,QACZ,6BAA6B;;;;;;;;;;;;yBAe/B,kBAAkB,gBACR,MAAM,QACd,6BAA6B;;;;;;;;;;;;yBASV,kBAAkB,gBAAgB,MAAM;;;CAOpE,CAAA"}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
|
2
|
+
import { contractAttachments, contractSignatures, contracts, contractTemplateVersions, } from "./schema.js";
|
|
3
|
+
import { allocateContractNumber, paginate, renderTemplate, toTimestamp, } from "./service-shared.js";
|
|
4
|
+
export const contractRecordsService = {
|
|
5
|
+
async listContracts(db, query) {
|
|
6
|
+
const conditions = [];
|
|
7
|
+
if (query.scope)
|
|
8
|
+
conditions.push(eq(contracts.scope, query.scope));
|
|
9
|
+
if (query.status)
|
|
10
|
+
conditions.push(eq(contracts.status, query.status));
|
|
11
|
+
if (query.personId)
|
|
12
|
+
conditions.push(eq(contracts.personId, query.personId));
|
|
13
|
+
if (query.organizationId)
|
|
14
|
+
conditions.push(eq(contracts.organizationId, query.organizationId));
|
|
15
|
+
if (query.supplierId)
|
|
16
|
+
conditions.push(eq(contracts.supplierId, query.supplierId));
|
|
17
|
+
if (query.bookingId)
|
|
18
|
+
conditions.push(eq(contracts.bookingId, query.bookingId));
|
|
19
|
+
if (query.orderId)
|
|
20
|
+
conditions.push(eq(contracts.orderId, query.orderId));
|
|
21
|
+
if (query.search) {
|
|
22
|
+
const term = `%${query.search}%`;
|
|
23
|
+
conditions.push(or(ilike(contracts.title, term), ilike(contracts.contractNumber, term)));
|
|
24
|
+
}
|
|
25
|
+
const where = conditions.length ? and(...conditions) : undefined;
|
|
26
|
+
return paginate(db
|
|
27
|
+
.select()
|
|
28
|
+
.from(contracts)
|
|
29
|
+
.where(where)
|
|
30
|
+
.limit(query.limit)
|
|
31
|
+
.offset(query.offset)
|
|
32
|
+
.orderBy(desc(contracts.createdAt)), db.select({ total: sql `count(*)::int` }).from(contracts).where(where), query.limit, query.offset);
|
|
33
|
+
},
|
|
34
|
+
async getContractById(db, id) {
|
|
35
|
+
const [row] = await db.select().from(contracts).where(eq(contracts.id, id)).limit(1);
|
|
36
|
+
return row ?? null;
|
|
37
|
+
},
|
|
38
|
+
async createContract(db, data) {
|
|
39
|
+
const [row] = await db
|
|
40
|
+
.insert(contracts)
|
|
41
|
+
.values({ ...data, expiresAt: toTimestamp(data.expiresAt) })
|
|
42
|
+
.returning();
|
|
43
|
+
return row ?? null;
|
|
44
|
+
},
|
|
45
|
+
async updateContract(db, id, data) {
|
|
46
|
+
const [row] = await db
|
|
47
|
+
.update(contracts)
|
|
48
|
+
.set({
|
|
49
|
+
...data,
|
|
50
|
+
expiresAt: data.expiresAt === undefined ? undefined : toTimestamp(data.expiresAt),
|
|
51
|
+
updatedAt: new Date(),
|
|
52
|
+
})
|
|
53
|
+
.where(eq(contracts.id, id))
|
|
54
|
+
.returning();
|
|
55
|
+
return row ?? null;
|
|
56
|
+
},
|
|
57
|
+
async deleteContract(db, id) {
|
|
58
|
+
const [existing] = await db
|
|
59
|
+
.select({ id: contracts.id, status: contracts.status })
|
|
60
|
+
.from(contracts)
|
|
61
|
+
.where(eq(contracts.id, id))
|
|
62
|
+
.limit(1);
|
|
63
|
+
if (!existing)
|
|
64
|
+
return { status: "not_found" };
|
|
65
|
+
if (existing.status !== "draft")
|
|
66
|
+
return { status: "not_draft" };
|
|
67
|
+
await db.delete(contracts).where(eq(contracts.id, id));
|
|
68
|
+
return { status: "deleted" };
|
|
69
|
+
},
|
|
70
|
+
async issueContract(db, contractId) {
|
|
71
|
+
return db.transaction(async (tx) => {
|
|
72
|
+
const [contract] = await tx
|
|
73
|
+
.select()
|
|
74
|
+
.from(contracts)
|
|
75
|
+
.where(eq(contracts.id, contractId))
|
|
76
|
+
.limit(1);
|
|
77
|
+
if (!contract)
|
|
78
|
+
return { status: "not_found" };
|
|
79
|
+
if (contract.status !== "draft")
|
|
80
|
+
return { status: "not_draft" };
|
|
81
|
+
let renderedBody = contract.renderedBody;
|
|
82
|
+
let renderedBodyFormat = contract.renderedBodyFormat;
|
|
83
|
+
if (contract.templateVersionId) {
|
|
84
|
+
const [version] = await tx
|
|
85
|
+
.select()
|
|
86
|
+
.from(contractTemplateVersions)
|
|
87
|
+
.where(eq(contractTemplateVersions.id, contract.templateVersionId))
|
|
88
|
+
.limit(1);
|
|
89
|
+
if (version) {
|
|
90
|
+
const vars = contract.variables ?? {};
|
|
91
|
+
renderedBody = renderTemplate(version.body, version.bodyFormat, vars);
|
|
92
|
+
renderedBodyFormat = version.bodyFormat;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
let contractNumber = contract.contractNumber;
|
|
96
|
+
if (!contractNumber && contract.seriesId) {
|
|
97
|
+
const allocated = await allocateContractNumber(tx, contract.seriesId);
|
|
98
|
+
if (allocated)
|
|
99
|
+
contractNumber = allocated.number;
|
|
100
|
+
}
|
|
101
|
+
const [updated] = await tx
|
|
102
|
+
.update(contracts)
|
|
103
|
+
.set({
|
|
104
|
+
status: "issued",
|
|
105
|
+
issuedAt: new Date(),
|
|
106
|
+
renderedBody,
|
|
107
|
+
renderedBodyFormat,
|
|
108
|
+
contractNumber,
|
|
109
|
+
updatedAt: new Date(),
|
|
110
|
+
})
|
|
111
|
+
.where(eq(contracts.id, contractId))
|
|
112
|
+
.returning();
|
|
113
|
+
return { status: "issued", contract: updated ?? null };
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
async sendContract(db, contractId) {
|
|
117
|
+
const [contract] = await db
|
|
118
|
+
.select({ id: contracts.id, status: contracts.status })
|
|
119
|
+
.from(contracts)
|
|
120
|
+
.where(eq(contracts.id, contractId))
|
|
121
|
+
.limit(1);
|
|
122
|
+
if (!contract)
|
|
123
|
+
return { status: "not_found" };
|
|
124
|
+
if (contract.status !== "issued" && contract.status !== "sent")
|
|
125
|
+
return { status: "not_issued" };
|
|
126
|
+
const [updated] = await db
|
|
127
|
+
.update(contracts)
|
|
128
|
+
.set({ status: "sent", sentAt: new Date(), updatedAt: new Date() })
|
|
129
|
+
.where(eq(contracts.id, contractId))
|
|
130
|
+
.returning();
|
|
131
|
+
return { status: "sent", contract: updated ?? null };
|
|
132
|
+
},
|
|
133
|
+
async voidContract(db, contractId) {
|
|
134
|
+
const [contract] = await db
|
|
135
|
+
.select({ id: contracts.id, status: contracts.status })
|
|
136
|
+
.from(contracts)
|
|
137
|
+
.where(eq(contracts.id, contractId))
|
|
138
|
+
.limit(1);
|
|
139
|
+
if (!contract)
|
|
140
|
+
return { status: "not_found" };
|
|
141
|
+
if (contract.status === "void")
|
|
142
|
+
return { status: "already_void" };
|
|
143
|
+
const [updated] = await db
|
|
144
|
+
.update(contracts)
|
|
145
|
+
.set({ status: "void", voidedAt: new Date(), updatedAt: new Date() })
|
|
146
|
+
.where(eq(contracts.id, contractId))
|
|
147
|
+
.returning();
|
|
148
|
+
return { status: "voided", contract: updated ?? null };
|
|
149
|
+
},
|
|
150
|
+
listSignatures(db, contractId) {
|
|
151
|
+
return db
|
|
152
|
+
.select()
|
|
153
|
+
.from(contractSignatures)
|
|
154
|
+
.where(eq(contractSignatures.contractId, contractId))
|
|
155
|
+
.orderBy(desc(contractSignatures.signedAt));
|
|
156
|
+
},
|
|
157
|
+
async signContract(db, contractId, data) {
|
|
158
|
+
return db.transaction(async (tx) => {
|
|
159
|
+
const [contract] = await tx
|
|
160
|
+
.select()
|
|
161
|
+
.from(contracts)
|
|
162
|
+
.where(eq(contracts.id, contractId))
|
|
163
|
+
.limit(1);
|
|
164
|
+
if (!contract)
|
|
165
|
+
return { status: "not_found" };
|
|
166
|
+
if (contract.status !== "issued" && contract.status !== "sent")
|
|
167
|
+
return { status: "not_signable" };
|
|
168
|
+
const [signature] = await tx
|
|
169
|
+
.insert(contractSignatures)
|
|
170
|
+
.values({ ...data, contractId })
|
|
171
|
+
.returning();
|
|
172
|
+
const [updated] = await tx
|
|
173
|
+
.update(contracts)
|
|
174
|
+
.set({ status: "signed", updatedAt: new Date() })
|
|
175
|
+
.where(eq(contracts.id, contractId))
|
|
176
|
+
.returning();
|
|
177
|
+
return { status: "signed", contract: updated ?? null, signature: signature ?? null };
|
|
178
|
+
});
|
|
179
|
+
},
|
|
180
|
+
async executeContract(db, contractId) {
|
|
181
|
+
const [contract] = await db
|
|
182
|
+
.select({ id: contracts.id, status: contracts.status })
|
|
183
|
+
.from(contracts)
|
|
184
|
+
.where(eq(contracts.id, contractId))
|
|
185
|
+
.limit(1);
|
|
186
|
+
if (!contract)
|
|
187
|
+
return { status: "not_found" };
|
|
188
|
+
if (contract.status !== "signed")
|
|
189
|
+
return { status: "not_signed" };
|
|
190
|
+
const [updated] = await db
|
|
191
|
+
.update(contracts)
|
|
192
|
+
.set({ status: "executed", executedAt: new Date(), updatedAt: new Date() })
|
|
193
|
+
.where(eq(contracts.id, contractId))
|
|
194
|
+
.returning();
|
|
195
|
+
return { status: "executed", contract: updated ?? null };
|
|
196
|
+
},
|
|
197
|
+
listAttachments(db, contractId) {
|
|
198
|
+
return db
|
|
199
|
+
.select()
|
|
200
|
+
.from(contractAttachments)
|
|
201
|
+
.where(eq(contractAttachments.contractId, contractId))
|
|
202
|
+
.orderBy(desc(contractAttachments.createdAt));
|
|
203
|
+
},
|
|
204
|
+
async createAttachment(db, contractId, data) {
|
|
205
|
+
const [contract] = await db
|
|
206
|
+
.select({ id: contracts.id })
|
|
207
|
+
.from(contracts)
|
|
208
|
+
.where(eq(contracts.id, contractId))
|
|
209
|
+
.limit(1);
|
|
210
|
+
if (!contract)
|
|
211
|
+
return null;
|
|
212
|
+
const [row] = await db
|
|
213
|
+
.insert(contractAttachments)
|
|
214
|
+
.values({ ...data, contractId })
|
|
215
|
+
.returning();
|
|
216
|
+
return row ?? null;
|
|
217
|
+
},
|
|
218
|
+
async updateAttachment(db, attachmentId, data) {
|
|
219
|
+
const [row] = await db
|
|
220
|
+
.update(contractAttachments)
|
|
221
|
+
.set(data)
|
|
222
|
+
.where(eq(contractAttachments.id, attachmentId))
|
|
223
|
+
.returning();
|
|
224
|
+
return row ?? null;
|
|
225
|
+
},
|
|
226
|
+
async deleteAttachment(db, attachmentId) {
|
|
227
|
+
const [row] = await db
|
|
228
|
+
.delete(contractAttachments)
|
|
229
|
+
.where(eq(contractAttachments.id, attachmentId))
|
|
230
|
+
.returning({ id: contractAttachments.id });
|
|
231
|
+
return row ?? null;
|
|
232
|
+
},
|
|
233
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
2
|
+
import type { CreateContractNumberSeriesInput, UpdateContractNumberSeriesInput } from "./service-shared.js";
|
|
3
|
+
export declare const contractSeriesService: {
|
|
4
|
+
listSeries(db: PostgresJsDatabase): Promise<{
|
|
5
|
+
id: string;
|
|
6
|
+
code: string;
|
|
7
|
+
name: string;
|
|
8
|
+
prefix: string;
|
|
9
|
+
separator: string;
|
|
10
|
+
padLength: number;
|
|
11
|
+
currentSequence: number;
|
|
12
|
+
resetStrategy: "never" | "annual" | "monthly";
|
|
13
|
+
resetAt: Date | null;
|
|
14
|
+
scope: "customer" | "partner" | "supplier" | "other" | "channel";
|
|
15
|
+
active: boolean;
|
|
16
|
+
createdAt: Date;
|
|
17
|
+
updatedAt: Date;
|
|
18
|
+
}[]>;
|
|
19
|
+
getSeriesById(db: PostgresJsDatabase, id: string): Promise<{
|
|
20
|
+
id: string;
|
|
21
|
+
code: string;
|
|
22
|
+
name: string;
|
|
23
|
+
prefix: string;
|
|
24
|
+
separator: string;
|
|
25
|
+
padLength: number;
|
|
26
|
+
currentSequence: number;
|
|
27
|
+
resetStrategy: "never" | "annual" | "monthly";
|
|
28
|
+
resetAt: Date | null;
|
|
29
|
+
scope: "customer" | "partner" | "supplier" | "other" | "channel";
|
|
30
|
+
active: boolean;
|
|
31
|
+
createdAt: Date;
|
|
32
|
+
updatedAt: Date;
|
|
33
|
+
} | null>;
|
|
34
|
+
createSeries(db: PostgresJsDatabase, data: CreateContractNumberSeriesInput): Promise<{
|
|
35
|
+
id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
active: boolean;
|
|
38
|
+
createdAt: Date;
|
|
39
|
+
updatedAt: Date;
|
|
40
|
+
code: string;
|
|
41
|
+
scope: "customer" | "partner" | "supplier" | "other" | "channel";
|
|
42
|
+
prefix: string;
|
|
43
|
+
separator: string;
|
|
44
|
+
padLength: number;
|
|
45
|
+
currentSequence: number;
|
|
46
|
+
resetStrategy: "never" | "annual" | "monthly";
|
|
47
|
+
resetAt: Date | null;
|
|
48
|
+
} | null>;
|
|
49
|
+
updateSeries(db: PostgresJsDatabase, id: string, data: UpdateContractNumberSeriesInput): Promise<{
|
|
50
|
+
id: string;
|
|
51
|
+
code: string;
|
|
52
|
+
name: string;
|
|
53
|
+
prefix: string;
|
|
54
|
+
separator: string;
|
|
55
|
+
padLength: number;
|
|
56
|
+
currentSequence: number;
|
|
57
|
+
resetStrategy: "never" | "annual" | "monthly";
|
|
58
|
+
resetAt: Date | null;
|
|
59
|
+
scope: "customer" | "partner" | "supplier" | "other" | "channel";
|
|
60
|
+
active: boolean;
|
|
61
|
+
createdAt: Date;
|
|
62
|
+
updatedAt: Date;
|
|
63
|
+
} | null>;
|
|
64
|
+
deleteSeries(db: PostgresJsDatabase, id: string): Promise<{
|
|
65
|
+
id: string;
|
|
66
|
+
} | null>;
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=service-series.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-series.d.ts","sourceRoot":"","sources":["../../src/contracts/service-series.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAGjE,OAAO,KAAK,EACV,+BAA+B,EAC/B,+BAA+B,EAChC,MAAM,qBAAqB,CAAA;AAE5B,eAAO,MAAM,qBAAqB;mBACX,kBAAkB;;;;;;;;;;;;;;;sBAGf,kBAAkB,MAAM,MAAM;;;;;;;;;;;;;;;qBAQ/B,kBAAkB,QAAQ,+BAA+B;;;;;;;;;;;;;;;qBAIzD,kBAAkB,MAAM,MAAM,QAAQ,+BAA+B;;;;;;;;;;;;;;;qBAQrE,kBAAkB,MAAM,MAAM;;;CAOtD,CAAA"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { desc, eq } from "drizzle-orm";
|
|
2
|
+
import { contractNumberSeries } from "./schema.js";
|
|
3
|
+
export const contractSeriesService = {
|
|
4
|
+
async listSeries(db) {
|
|
5
|
+
return db.select().from(contractNumberSeries).orderBy(desc(contractNumberSeries.updatedAt));
|
|
6
|
+
},
|
|
7
|
+
async getSeriesById(db, id) {
|
|
8
|
+
const [row] = await db
|
|
9
|
+
.select()
|
|
10
|
+
.from(contractNumberSeries)
|
|
11
|
+
.where(eq(contractNumberSeries.id, id))
|
|
12
|
+
.limit(1);
|
|
13
|
+
return row ?? null;
|
|
14
|
+
},
|
|
15
|
+
async createSeries(db, data) {
|
|
16
|
+
const [row] = await db.insert(contractNumberSeries).values(data).returning();
|
|
17
|
+
return row ?? null;
|
|
18
|
+
},
|
|
19
|
+
async updateSeries(db, id, data) {
|
|
20
|
+
const [row] = await db
|
|
21
|
+
.update(contractNumberSeries)
|
|
22
|
+
.set({ ...data, updatedAt: new Date() })
|
|
23
|
+
.where(eq(contractNumberSeries.id, id))
|
|
24
|
+
.returning();
|
|
25
|
+
return row ?? null;
|
|
26
|
+
},
|
|
27
|
+
async deleteSeries(db, id) {
|
|
28
|
+
const [row] = await db
|
|
29
|
+
.delete(contractNumberSeries)
|
|
30
|
+
.where(eq(contractNumberSeries.id, id))
|
|
31
|
+
.returning({ id: contractNumberSeries.id });
|
|
32
|
+
return row ?? null;
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
2
|
+
import type { z } from "zod";
|
|
3
|
+
import type { contractListQuerySchema, contractTemplateListQuerySchema, insertContractAttachmentSchema, insertContractNumberSeriesSchema, insertContractSchema, insertContractSignatureSchema, insertContractTemplateSchema, insertContractTemplateVersionSchema, renderTemplateInputSchema, updateContractAttachmentSchema, updateContractNumberSeriesSchema, updateContractSchema, updateContractTemplateSchema } from "./validation.js";
|
|
4
|
+
export type ContractListQuery = z.infer<typeof contractListQuerySchema>;
|
|
5
|
+
export type ContractTemplateListQuery = z.infer<typeof contractTemplateListQuerySchema>;
|
|
6
|
+
export type CreateContractTemplateInput = z.infer<typeof insertContractTemplateSchema>;
|
|
7
|
+
export type UpdateContractTemplateInput = z.infer<typeof updateContractTemplateSchema>;
|
|
8
|
+
export type CreateContractTemplateVersionInput = z.infer<typeof insertContractTemplateVersionSchema>;
|
|
9
|
+
export type CreateContractNumberSeriesInput = z.infer<typeof insertContractNumberSeriesSchema>;
|
|
10
|
+
export type UpdateContractNumberSeriesInput = z.infer<typeof updateContractNumberSeriesSchema>;
|
|
11
|
+
export type CreateContractInput = z.infer<typeof insertContractSchema>;
|
|
12
|
+
export type UpdateContractInput = z.infer<typeof updateContractSchema>;
|
|
13
|
+
export type CreateContractSignatureInput = z.infer<typeof insertContractSignatureSchema>;
|
|
14
|
+
export type CreateContractAttachmentInput = z.infer<typeof insertContractAttachmentSchema>;
|
|
15
|
+
export type UpdateContractAttachmentInput = z.infer<typeof updateContractAttachmentSchema>;
|
|
16
|
+
export type RenderTemplateInput = z.infer<typeof renderTemplateInputSchema>;
|
|
17
|
+
export declare function toTimestamp(value?: string | null): Date | null;
|
|
18
|
+
export declare function renderTemplate(body: string, bodyFormat: "markdown" | "html" | "lexical_json", variables: Record<string, unknown>): string;
|
|
19
|
+
export declare function validateTemplateVariables(variableSchema: unknown, values: Record<string, unknown>): string[];
|
|
20
|
+
export declare function allocateContractNumber(db: PostgresJsDatabase, seriesId: string): Promise<{
|
|
21
|
+
number: string;
|
|
22
|
+
sequence: number;
|
|
23
|
+
} | null>;
|
|
24
|
+
export declare function paginate<T extends object>(rowsQuery: Promise<T[]>, countQuery: Promise<Array<{
|
|
25
|
+
total: number;
|
|
26
|
+
}>>, limit: number, offset: number): Promise<{
|
|
27
|
+
data: T[];
|
|
28
|
+
total: number;
|
|
29
|
+
limit: number;
|
|
30
|
+
offset: number;
|
|
31
|
+
}>;
|
|
32
|
+
//# sourceMappingURL=service-shared.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-shared.d.ts","sourceRoot":"","sources":["../../src/contracts/service-shared.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AACjE,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAG5B,OAAO,KAAK,EACV,uBAAuB,EACvB,+BAA+B,EAC/B,8BAA8B,EAC9B,gCAAgC,EAChC,oBAAoB,EACpB,6BAA6B,EAC7B,4BAA4B,EAC5B,mCAAmC,EACnC,yBAAyB,EACzB,8BAA8B,EAC9B,gCAAgC,EAChC,oBAAoB,EACpB,4BAA4B,EAC7B,MAAM,iBAAiB,CAAA;AAExB,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAA;AACvE,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,+BAA+B,CAAC,CAAA;AACvF,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAA;AACtF,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAA;AACtF,MAAM,MAAM,kCAAkC,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mCAAmC,CAAC,CAAA;AACpG,MAAM,MAAM,+BAA+B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gCAAgC,CAAC,CAAA;AAC9F,MAAM,MAAM,+BAA+B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gCAAgC,CAAC,CAAA;AAC9F,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAA;AACtE,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAA;AACtE,MAAM,MAAM,4BAA4B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAA;AACxF,MAAM,MAAM,6BAA6B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,8BAA8B,CAAC,CAAA;AAC1F,MAAM,MAAM,6BAA6B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,8BAA8B,CAAC,CAAA;AAC1F,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAA;AAE3E,wBAAgB,WAAW,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAE9D;AA8DD,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,UAAU,GAAG,MAAM,GAAG,cAAc,EAChD,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjC,MAAM,CAkBR;AAED,wBAAgB,yBAAyB,CACvC,cAAc,EAAE,OAAO,EACvB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,MAAM,EAAE,CAYV;AAkBD,wBAAsB,sBAAsB,CAC1C,EAAE,EAAE,kBAAkB,EACtB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAmCtD;AAED,wBAAsB,QAAQ,CAAC,CAAC,SAAS,MAAM,EAC7C,SAAS,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,EACvB,UAAU,EAAE,OAAO,CAAC,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,EAC7C,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM;;;;;GAIf"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import { contractNumberSeries } from "./schema.js";
|
|
3
|
+
export function toTimestamp(value) {
|
|
4
|
+
return value ? new Date(value) : null;
|
|
5
|
+
}
|
|
6
|
+
function resolvePath(obj, path) {
|
|
7
|
+
if (obj === null || obj === undefined)
|
|
8
|
+
return undefined;
|
|
9
|
+
const segments = [];
|
|
10
|
+
const parts = path.split(".");
|
|
11
|
+
for (const part of parts) {
|
|
12
|
+
if (!part)
|
|
13
|
+
continue;
|
|
14
|
+
const indexMatches = [...part.matchAll(/([^[\]]+)|\[(\d+)\]/g)];
|
|
15
|
+
for (const match of indexMatches) {
|
|
16
|
+
if (match[1] !== undefined)
|
|
17
|
+
segments.push(match[1]);
|
|
18
|
+
else if (match[2] !== undefined)
|
|
19
|
+
segments.push(Number.parseInt(match[2], 10));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
let current = obj;
|
|
23
|
+
for (const seg of segments) {
|
|
24
|
+
if (current === null || current === undefined)
|
|
25
|
+
return undefined;
|
|
26
|
+
if (typeof seg === "number") {
|
|
27
|
+
if (!Array.isArray(current))
|
|
28
|
+
return undefined;
|
|
29
|
+
current = current[seg];
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
if (typeof current !== "object")
|
|
33
|
+
return undefined;
|
|
34
|
+
current = current[seg];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return current;
|
|
38
|
+
}
|
|
39
|
+
function stringifyValue(value) {
|
|
40
|
+
if (value === null || value === undefined)
|
|
41
|
+
return "";
|
|
42
|
+
if (typeof value === "string")
|
|
43
|
+
return value;
|
|
44
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
45
|
+
return String(value);
|
|
46
|
+
return JSON.stringify(value);
|
|
47
|
+
}
|
|
48
|
+
const MUSTACHE_RE = /\{\{\s*([^}]+?)\s*\}\}/g;
|
|
49
|
+
function renderMustache(body, variables) {
|
|
50
|
+
return body.replace(MUSTACHE_RE, (_, path) => {
|
|
51
|
+
const resolved = resolvePath(variables, path.trim());
|
|
52
|
+
return stringifyValue(resolved);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function walkLexical(node, variables) {
|
|
56
|
+
const next = { ...node };
|
|
57
|
+
if (typeof next.text === "string") {
|
|
58
|
+
next.text = renderMustache(next.text, variables);
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(next.children)) {
|
|
61
|
+
next.children = next.children.map((child) => walkLexical(child, variables));
|
|
62
|
+
}
|
|
63
|
+
return next;
|
|
64
|
+
}
|
|
65
|
+
export function renderTemplate(body, bodyFormat, variables) {
|
|
66
|
+
if (bodyFormat === "lexical_json") {
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(body);
|
|
69
|
+
if (parsed && typeof parsed === "object") {
|
|
70
|
+
const obj = parsed;
|
|
71
|
+
if (obj.root && typeof obj.root === "object") {
|
|
72
|
+
const result = { ...obj, root: walkLexical(obj.root, variables) };
|
|
73
|
+
return JSON.stringify(result);
|
|
74
|
+
}
|
|
75
|
+
return JSON.stringify(walkLexical(obj, variables));
|
|
76
|
+
}
|
|
77
|
+
return body;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return renderMustache(body, variables);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return renderMustache(body, variables);
|
|
84
|
+
}
|
|
85
|
+
export function validateTemplateVariables(variableSchema, values) {
|
|
86
|
+
const issues = [];
|
|
87
|
+
if (!variableSchema || typeof variableSchema !== "object")
|
|
88
|
+
return issues;
|
|
89
|
+
const schema = variableSchema;
|
|
90
|
+
const requiredList = Array.isArray(schema.required) ? schema.required : [];
|
|
91
|
+
for (const key of requiredList) {
|
|
92
|
+
const resolved = resolvePath(values, key);
|
|
93
|
+
if (resolved === undefined || resolved === null || resolved === "") {
|
|
94
|
+
issues.push(`missing required variable: ${key}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return issues;
|
|
98
|
+
}
|
|
99
|
+
function currentPeriodBoundary(strategy, now) {
|
|
100
|
+
if (strategy === "never")
|
|
101
|
+
return null;
|
|
102
|
+
if (strategy === "annual")
|
|
103
|
+
return new Date(Date.UTC(now.getUTCFullYear(), 0, 1));
|
|
104
|
+
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
|
|
105
|
+
}
|
|
106
|
+
function formatNumber(prefix, separator, sequence, padLength) {
|
|
107
|
+
const padded = String(sequence).padStart(padLength, "0");
|
|
108
|
+
return `${prefix}${separator}${padded}`;
|
|
109
|
+
}
|
|
110
|
+
export async function allocateContractNumber(db, seriesId) {
|
|
111
|
+
return db.transaction(async (tx) => {
|
|
112
|
+
const rows = await tx.execute(sql `SELECT * FROM ${contractNumberSeries}
|
|
113
|
+
WHERE ${contractNumberSeries.id} = ${seriesId}
|
|
114
|
+
FOR UPDATE`);
|
|
115
|
+
const row = rows[0];
|
|
116
|
+
if (!row)
|
|
117
|
+
return null;
|
|
118
|
+
const strategy = row.reset_strategy;
|
|
119
|
+
const prefix = row.prefix ?? "";
|
|
120
|
+
const separator = row.separator ?? "";
|
|
121
|
+
const padLength = row.pad_length ?? 4;
|
|
122
|
+
const currentSequence = row.current_sequence ?? 0;
|
|
123
|
+
const resetAt = row.reset_at ? new Date(row.reset_at) : null;
|
|
124
|
+
const now = new Date();
|
|
125
|
+
const boundary = currentPeriodBoundary(strategy, now);
|
|
126
|
+
const shouldReset = boundary !== null && (resetAt === null || resetAt.getTime() < boundary.getTime());
|
|
127
|
+
const nextSequence = shouldReset ? 1 : currentSequence + 1;
|
|
128
|
+
const nextResetAt = strategy === "never" ? resetAt : boundary;
|
|
129
|
+
await tx
|
|
130
|
+
.update(contractNumberSeries)
|
|
131
|
+
.set({ currentSequence: nextSequence, resetAt: nextResetAt, updatedAt: new Date() })
|
|
132
|
+
.where(sql `${contractNumberSeries.id} = ${seriesId}`);
|
|
133
|
+
return {
|
|
134
|
+
number: formatNumber(prefix, separator, nextSequence, padLength),
|
|
135
|
+
sequence: nextSequence,
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
export async function paginate(rowsQuery, countQuery, limit, offset) {
|
|
140
|
+
const [data, countResult] = await Promise.all([rowsQuery, countQuery]);
|
|
141
|
+
return { data, total: countResult[0]?.total ?? 0, limit, offset };
|
|
142
|
+
}
|