@voyantjs/legal 0.1.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/LICENSE +109 -0
- package/README.md +60 -0
- package/dist/contracts/index.d.ts +13 -0
- package/dist/contracts/index.d.ts.map +1 -0
- package/dist/contracts/index.js +19 -0
- package/dist/contracts/routes.d.ts +1297 -0
- package/dist/contracts/routes.d.ts.map +1 -0
- package/dist/contracts/routes.js +224 -0
- package/dist/contracts/schema.d.ts +1531 -0
- package/dist/contracts/schema.d.ts.map +1 -0
- package/dist/contracts/schema.js +227 -0
- package/dist/contracts/service.d.ts +1753 -0
- package/dist/contracts/service.d.ts.map +1 -0
- package/dist/contracts/service.js +570 -0
- package/dist/contracts/validation.d.ts +274 -0
- package/dist/contracts/validation.d.ts.map +1 -0
- package/dist/contracts/validation.js +125 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/policies/index.d.ts +16 -0
- package/dist/policies/index.d.ts.map +1 -0
- package/dist/policies/index.js +26 -0
- package/dist/policies/routes.d.ts +916 -0
- package/dist/policies/routes.d.ts.map +1 -0
- package/dist/policies/routes.js +162 -0
- package/dist/policies/schema.d.ts +1176 -0
- package/dist/policies/schema.d.ts.map +1 -0
- package/dist/policies/schema.js +189 -0
- package/dist/policies/service.d.ts +1384 -0
- package/dist/policies/service.d.ts.map +1 -0
- package/dist/policies/service.js +438 -0
- package/dist/policies/validation.d.ts +273 -0
- package/dist/policies/validation.d.ts.map +1 -0
- package/dist/policies/validation.js +140 -0
- package/package.json +83 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/policies/service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import { and, desc, eq, gte, ilike, inArray, lte, or, sql } from "drizzle-orm";
|
|
2
|
+
import { policies, policyAcceptances, policyAssignments, policyRules, policyVersions, } from "./schema.js";
|
|
3
|
+
async function paginate(rowsQuery, countQuery, limit, offset) {
|
|
4
|
+
const [data, countResult] = await Promise.all([rowsQuery, countQuery]);
|
|
5
|
+
return { data, total: countResult[0]?.total ?? 0, limit, offset };
|
|
6
|
+
}
|
|
7
|
+
function toDateString(value) {
|
|
8
|
+
return value ?? null;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Evaluate a cancellation policy against a context. Rules are sorted descending
|
|
12
|
+
* by `daysBeforeDeparture`; the first rule whose threshold is satisfied by the
|
|
13
|
+
* input's `daysBeforeDeparture` is applied. `refundPercent` is expressed in
|
|
14
|
+
* basis points (0-10000 where 10000 = 100%).
|
|
15
|
+
*/
|
|
16
|
+
export function evaluateCancellationPolicy(rules, input) {
|
|
17
|
+
if (rules.length === 0) {
|
|
18
|
+
return { refundPercent: 0, refundCents: 0, refundType: "none", appliedRule: null };
|
|
19
|
+
}
|
|
20
|
+
// Sort by daysBeforeDeparture DESC; nulls float to the bottom
|
|
21
|
+
const sorted = [...rules].sort((a, b) => {
|
|
22
|
+
const ad = a.daysBeforeDeparture ?? Number.NEGATIVE_INFINITY;
|
|
23
|
+
const bd = b.daysBeforeDeparture ?? Number.NEGATIVE_INFINITY;
|
|
24
|
+
return bd - ad;
|
|
25
|
+
});
|
|
26
|
+
const applied = sorted.find((rule) => rule.daysBeforeDeparture !== null && input.daysBeforeDeparture >= rule.daysBeforeDeparture) ??
|
|
27
|
+
// fallback: last rule (lowest threshold) applies when caller is inside
|
|
28
|
+
// the tightest window
|
|
29
|
+
sorted[sorted.length - 1] ??
|
|
30
|
+
null;
|
|
31
|
+
if (!applied) {
|
|
32
|
+
return { refundPercent: 0, refundCents: 0, refundType: "none", appliedRule: null };
|
|
33
|
+
}
|
|
34
|
+
const refundPercent = applied.refundPercent ?? 0;
|
|
35
|
+
const refundType = applied.refundType ?? "none";
|
|
36
|
+
// basis points → amount
|
|
37
|
+
const percentageRefundCents = Math.floor((input.totalCents * refundPercent) / 10000);
|
|
38
|
+
const refundCents = applied.flatAmountCents ?? percentageRefundCents;
|
|
39
|
+
return { refundPercent, refundCents, refundType, appliedRule: applied };
|
|
40
|
+
}
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Service
|
|
43
|
+
// ============================================================================
|
|
44
|
+
export const policiesService = {
|
|
45
|
+
// ---------- policies ----------
|
|
46
|
+
async listPolicies(db, query) {
|
|
47
|
+
const conditions = [];
|
|
48
|
+
if (query.kind)
|
|
49
|
+
conditions.push(eq(policies.kind, query.kind));
|
|
50
|
+
if (query.language)
|
|
51
|
+
conditions.push(eq(policies.language, query.language));
|
|
52
|
+
if (query.search) {
|
|
53
|
+
const term = `%${query.search}%`;
|
|
54
|
+
conditions.push(or(ilike(policies.name, term), ilike(policies.slug, term), ilike(policies.description, term)));
|
|
55
|
+
}
|
|
56
|
+
const where = conditions.length ? and(...conditions) : undefined;
|
|
57
|
+
return paginate(db
|
|
58
|
+
.select()
|
|
59
|
+
.from(policies)
|
|
60
|
+
.where(where)
|
|
61
|
+
.limit(query.limit)
|
|
62
|
+
.offset(query.offset)
|
|
63
|
+
.orderBy(desc(policies.updatedAt)), db.select({ total: sql `count(*)::int` }).from(policies).where(where), query.limit, query.offset);
|
|
64
|
+
},
|
|
65
|
+
async getPolicyById(db, id) {
|
|
66
|
+
const [row] = await db.select().from(policies).where(eq(policies.id, id)).limit(1);
|
|
67
|
+
return row ?? null;
|
|
68
|
+
},
|
|
69
|
+
async getPolicyBySlug(db, slug) {
|
|
70
|
+
const [row] = await db.select().from(policies).where(eq(policies.slug, slug)).limit(1);
|
|
71
|
+
return row ?? null;
|
|
72
|
+
},
|
|
73
|
+
async createPolicy(db, data) {
|
|
74
|
+
const [row] = await db.insert(policies).values(data).returning();
|
|
75
|
+
return row ?? null;
|
|
76
|
+
},
|
|
77
|
+
async updatePolicy(db, id, data) {
|
|
78
|
+
const [row] = await db
|
|
79
|
+
.update(policies)
|
|
80
|
+
.set({ ...data, updatedAt: new Date() })
|
|
81
|
+
.where(eq(policies.id, id))
|
|
82
|
+
.returning();
|
|
83
|
+
return row ?? null;
|
|
84
|
+
},
|
|
85
|
+
async deletePolicy(db, id) {
|
|
86
|
+
const [row] = await db
|
|
87
|
+
.delete(policies)
|
|
88
|
+
.where(eq(policies.id, id))
|
|
89
|
+
.returning({ id: policies.id });
|
|
90
|
+
return row ?? null;
|
|
91
|
+
},
|
|
92
|
+
// ---------- versions ----------
|
|
93
|
+
listPolicyVersions(db, policyId) {
|
|
94
|
+
return db
|
|
95
|
+
.select()
|
|
96
|
+
.from(policyVersions)
|
|
97
|
+
.where(eq(policyVersions.policyId, policyId))
|
|
98
|
+
.orderBy(desc(policyVersions.version));
|
|
99
|
+
},
|
|
100
|
+
async getPolicyVersionById(db, id) {
|
|
101
|
+
const [row] = await db.select().from(policyVersions).where(eq(policyVersions.id, id)).limit(1);
|
|
102
|
+
return row ?? null;
|
|
103
|
+
},
|
|
104
|
+
async createPolicyVersion(db, policyId, data) {
|
|
105
|
+
return db.transaction(async (tx) => {
|
|
106
|
+
const [policy] = await tx
|
|
107
|
+
.select({ id: policies.id })
|
|
108
|
+
.from(policies)
|
|
109
|
+
.where(eq(policies.id, policyId))
|
|
110
|
+
.limit(1);
|
|
111
|
+
if (!policy)
|
|
112
|
+
return null;
|
|
113
|
+
const [maxRow] = await tx
|
|
114
|
+
.select({ max: sql `coalesce(max(${policyVersions.version}), 0)::int` })
|
|
115
|
+
.from(policyVersions)
|
|
116
|
+
.where(eq(policyVersions.policyId, policyId));
|
|
117
|
+
const nextVersion = (maxRow?.max ?? 0) + 1;
|
|
118
|
+
const [row] = await tx
|
|
119
|
+
.insert(policyVersions)
|
|
120
|
+
.values({
|
|
121
|
+
policyId,
|
|
122
|
+
version: nextVersion,
|
|
123
|
+
status: "draft",
|
|
124
|
+
title: data.title,
|
|
125
|
+
bodyFormat: data.bodyFormat,
|
|
126
|
+
body: data.body ?? null,
|
|
127
|
+
publishedBy: data.publishedBy ?? null,
|
|
128
|
+
metadata: data.metadata ?? null,
|
|
129
|
+
})
|
|
130
|
+
.returning();
|
|
131
|
+
return row ?? null;
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
async updatePolicyVersion(db, versionId, data) {
|
|
135
|
+
// Only draft versions are editable
|
|
136
|
+
const [row] = await db
|
|
137
|
+
.update(policyVersions)
|
|
138
|
+
.set({ ...data, updatedAt: new Date() })
|
|
139
|
+
.where(and(eq(policyVersions.id, versionId), eq(policyVersions.status, "draft")))
|
|
140
|
+
.returning();
|
|
141
|
+
return row ?? null;
|
|
142
|
+
},
|
|
143
|
+
/**
|
|
144
|
+
* Publish a draft version: retires any currently-published version of the
|
|
145
|
+
* same policy, marks the target as published, updates `policies.currentVersionId`.
|
|
146
|
+
*/
|
|
147
|
+
async publishPolicyVersion(db, versionId) {
|
|
148
|
+
return db.transaction(async (tx) => {
|
|
149
|
+
const [version] = await tx
|
|
150
|
+
.select()
|
|
151
|
+
.from(policyVersions)
|
|
152
|
+
.where(eq(policyVersions.id, versionId))
|
|
153
|
+
.limit(1);
|
|
154
|
+
if (!version)
|
|
155
|
+
return { status: "not_found" };
|
|
156
|
+
if (version.status !== "draft")
|
|
157
|
+
return { status: "not_draft" };
|
|
158
|
+
const now = new Date();
|
|
159
|
+
// Retire existing published version(s) for the same policy
|
|
160
|
+
await tx
|
|
161
|
+
.update(policyVersions)
|
|
162
|
+
.set({ status: "retired", retiredAt: now, updatedAt: now })
|
|
163
|
+
.where(and(eq(policyVersions.policyId, version.policyId), eq(policyVersions.status, "published")));
|
|
164
|
+
const [published] = await tx
|
|
165
|
+
.update(policyVersions)
|
|
166
|
+
.set({ status: "published", publishedAt: now, updatedAt: now })
|
|
167
|
+
.where(eq(policyVersions.id, versionId))
|
|
168
|
+
.returning();
|
|
169
|
+
await tx
|
|
170
|
+
.update(policies)
|
|
171
|
+
.set({ currentVersionId: versionId, updatedAt: now })
|
|
172
|
+
.where(eq(policies.id, version.policyId));
|
|
173
|
+
return { status: "published", version: published ?? null };
|
|
174
|
+
});
|
|
175
|
+
},
|
|
176
|
+
async retirePolicyVersion(db, versionId) {
|
|
177
|
+
const [row] = await db
|
|
178
|
+
.update(policyVersions)
|
|
179
|
+
.set({ status: "retired", retiredAt: new Date(), updatedAt: new Date() })
|
|
180
|
+
.where(eq(policyVersions.id, versionId))
|
|
181
|
+
.returning();
|
|
182
|
+
return row ?? null;
|
|
183
|
+
},
|
|
184
|
+
// ---------- rules ----------
|
|
185
|
+
listPolicyRules(db, versionId) {
|
|
186
|
+
return db
|
|
187
|
+
.select()
|
|
188
|
+
.from(policyRules)
|
|
189
|
+
.where(eq(policyRules.policyVersionId, versionId))
|
|
190
|
+
.orderBy(policyRules.sortOrder, policyRules.createdAt);
|
|
191
|
+
},
|
|
192
|
+
async createPolicyRule(db, versionId, data) {
|
|
193
|
+
const [version] = await db
|
|
194
|
+
.select({ id: policyVersions.id })
|
|
195
|
+
.from(policyVersions)
|
|
196
|
+
.where(eq(policyVersions.id, versionId))
|
|
197
|
+
.limit(1);
|
|
198
|
+
if (!version)
|
|
199
|
+
return null;
|
|
200
|
+
const [row] = await db
|
|
201
|
+
.insert(policyRules)
|
|
202
|
+
.values({
|
|
203
|
+
policyVersionId: versionId,
|
|
204
|
+
ruleType: data.ruleType,
|
|
205
|
+
label: data.label ?? null,
|
|
206
|
+
daysBeforeDeparture: data.daysBeforeDeparture ?? null,
|
|
207
|
+
refundPercent: data.refundPercent ?? null,
|
|
208
|
+
refundType: data.refundType ?? null,
|
|
209
|
+
flatAmountCents: data.flatAmountCents ?? null,
|
|
210
|
+
currency: data.currency ?? null,
|
|
211
|
+
validFrom: toDateString(data.validFrom),
|
|
212
|
+
validTo: toDateString(data.validTo),
|
|
213
|
+
conditions: data.conditions ?? null,
|
|
214
|
+
sortOrder: data.sortOrder,
|
|
215
|
+
})
|
|
216
|
+
.returning();
|
|
217
|
+
return row ?? null;
|
|
218
|
+
},
|
|
219
|
+
async updatePolicyRule(db, ruleId, data) {
|
|
220
|
+
const [row] = await db
|
|
221
|
+
.update(policyRules)
|
|
222
|
+
.set({ ...data, updatedAt: new Date() })
|
|
223
|
+
.where(eq(policyRules.id, ruleId))
|
|
224
|
+
.returning();
|
|
225
|
+
return row ?? null;
|
|
226
|
+
},
|
|
227
|
+
async deletePolicyRule(db, ruleId) {
|
|
228
|
+
const [row] = await db
|
|
229
|
+
.delete(policyRules)
|
|
230
|
+
.where(eq(policyRules.id, ruleId))
|
|
231
|
+
.returning({ id: policyRules.id });
|
|
232
|
+
return row ?? null;
|
|
233
|
+
},
|
|
234
|
+
// ---------- assignments ----------
|
|
235
|
+
async listPolicyAssignments(db, query) {
|
|
236
|
+
const conditions = [];
|
|
237
|
+
if (query.policyId)
|
|
238
|
+
conditions.push(eq(policyAssignments.policyId, query.policyId));
|
|
239
|
+
if (query.scope)
|
|
240
|
+
conditions.push(eq(policyAssignments.scope, query.scope));
|
|
241
|
+
if (query.productId)
|
|
242
|
+
conditions.push(eq(policyAssignments.productId, query.productId));
|
|
243
|
+
if (query.channelId)
|
|
244
|
+
conditions.push(eq(policyAssignments.channelId, query.channelId));
|
|
245
|
+
if (query.supplierId)
|
|
246
|
+
conditions.push(eq(policyAssignments.supplierId, query.supplierId));
|
|
247
|
+
if (query.marketId)
|
|
248
|
+
conditions.push(eq(policyAssignments.marketId, query.marketId));
|
|
249
|
+
if (query.organizationId)
|
|
250
|
+
conditions.push(eq(policyAssignments.organizationId, query.organizationId));
|
|
251
|
+
const where = conditions.length ? and(...conditions) : undefined;
|
|
252
|
+
return paginate(db
|
|
253
|
+
.select()
|
|
254
|
+
.from(policyAssignments)
|
|
255
|
+
.where(where)
|
|
256
|
+
.limit(query.limit)
|
|
257
|
+
.offset(query.offset)
|
|
258
|
+
.orderBy(desc(policyAssignments.priority), desc(policyAssignments.createdAt)), db.select({ total: sql `count(*)::int` }).from(policyAssignments).where(where), query.limit, query.offset);
|
|
259
|
+
},
|
|
260
|
+
async createPolicyAssignment(db, data) {
|
|
261
|
+
const [row] = await db
|
|
262
|
+
.insert(policyAssignments)
|
|
263
|
+
.values({
|
|
264
|
+
policyId: data.policyId,
|
|
265
|
+
scope: data.scope,
|
|
266
|
+
productId: data.productId ?? null,
|
|
267
|
+
channelId: data.channelId ?? null,
|
|
268
|
+
supplierId: data.supplierId ?? null,
|
|
269
|
+
marketId: data.marketId ?? null,
|
|
270
|
+
organizationId: data.organizationId ?? null,
|
|
271
|
+
validFrom: toDateString(data.validFrom),
|
|
272
|
+
validTo: toDateString(data.validTo),
|
|
273
|
+
priority: data.priority,
|
|
274
|
+
metadata: data.metadata ?? null,
|
|
275
|
+
})
|
|
276
|
+
.returning();
|
|
277
|
+
return row ?? null;
|
|
278
|
+
},
|
|
279
|
+
async updatePolicyAssignment(db, id, data) {
|
|
280
|
+
const [row] = await db
|
|
281
|
+
.update(policyAssignments)
|
|
282
|
+
.set({ ...data, updatedAt: new Date() })
|
|
283
|
+
.where(eq(policyAssignments.id, id))
|
|
284
|
+
.returning();
|
|
285
|
+
return row ?? null;
|
|
286
|
+
},
|
|
287
|
+
async deletePolicyAssignment(db, id) {
|
|
288
|
+
const [row] = await db
|
|
289
|
+
.delete(policyAssignments)
|
|
290
|
+
.where(eq(policyAssignments.id, id))
|
|
291
|
+
.returning({ id: policyAssignments.id });
|
|
292
|
+
return row ?? null;
|
|
293
|
+
},
|
|
294
|
+
// ---------- resolution ----------
|
|
295
|
+
/**
|
|
296
|
+
* Resolve the best-matching policy for a context. Matches candidate
|
|
297
|
+
* assignments on scope column values and (optional) validity window, then
|
|
298
|
+
* picks the highest-priority one for the requested policy kind.
|
|
299
|
+
*/
|
|
300
|
+
async resolvePolicy(db, input) {
|
|
301
|
+
const conditions = [];
|
|
302
|
+
const atDate = input.at ?? new Date().toISOString().slice(0, 10);
|
|
303
|
+
// Scope conditions: collect all provided target IDs; an assignment
|
|
304
|
+
// matches if its column equals the provided ID OR the column is null
|
|
305
|
+
// (global within that dimension).
|
|
306
|
+
const scopeConditions = [];
|
|
307
|
+
if (input.productId) {
|
|
308
|
+
scopeConditions.push(or(eq(policyAssignments.productId, input.productId), sql `${policyAssignments.productId} IS NULL`));
|
|
309
|
+
}
|
|
310
|
+
if (input.channelId) {
|
|
311
|
+
scopeConditions.push(or(eq(policyAssignments.channelId, input.channelId), sql `${policyAssignments.channelId} IS NULL`));
|
|
312
|
+
}
|
|
313
|
+
if (input.supplierId) {
|
|
314
|
+
scopeConditions.push(or(eq(policyAssignments.supplierId, input.supplierId), sql `${policyAssignments.supplierId} IS NULL`));
|
|
315
|
+
}
|
|
316
|
+
if (input.marketId) {
|
|
317
|
+
scopeConditions.push(or(eq(policyAssignments.marketId, input.marketId), sql `${policyAssignments.marketId} IS NULL`));
|
|
318
|
+
}
|
|
319
|
+
if (input.organizationId) {
|
|
320
|
+
scopeConditions.push(or(eq(policyAssignments.organizationId, input.organizationId), sql `${policyAssignments.organizationId} IS NULL`));
|
|
321
|
+
}
|
|
322
|
+
// Validity filter
|
|
323
|
+
const validity = and(or(sql `${policyAssignments.validFrom} IS NULL`, lte(policyAssignments.validFrom, atDate)), or(sql `${policyAssignments.validTo} IS NULL`, gte(policyAssignments.validTo, atDate)));
|
|
324
|
+
const candidates = await db
|
|
325
|
+
.select({
|
|
326
|
+
assignment: policyAssignments,
|
|
327
|
+
policy: policies,
|
|
328
|
+
})
|
|
329
|
+
.from(policyAssignments)
|
|
330
|
+
.innerJoin(policies, eq(policyAssignments.policyId, policies.id))
|
|
331
|
+
.where(and(eq(policies.kind, input.kind), validity, ...conditions, ...(scopeConditions.length ? [and(...scopeConditions)] : [])))
|
|
332
|
+
.orderBy(desc(policyAssignments.priority), desc(policyAssignments.createdAt));
|
|
333
|
+
if (candidates.length === 0)
|
|
334
|
+
return null;
|
|
335
|
+
// Specificity ranking: count non-null scope columns (more specific = higher score)
|
|
336
|
+
const scored = candidates.map((c) => {
|
|
337
|
+
const a = c.assignment;
|
|
338
|
+
const specificity = (a.productId ? 1 : 0) +
|
|
339
|
+
(a.channelId ? 1 : 0) +
|
|
340
|
+
(a.supplierId ? 1 : 0) +
|
|
341
|
+
(a.marketId ? 1 : 0) +
|
|
342
|
+
(a.organizationId ? 1 : 0);
|
|
343
|
+
return { ...c, specificity };
|
|
344
|
+
});
|
|
345
|
+
scored.sort((a, b) => {
|
|
346
|
+
if (b.assignment.priority !== a.assignment.priority) {
|
|
347
|
+
return b.assignment.priority - a.assignment.priority;
|
|
348
|
+
}
|
|
349
|
+
if (b.specificity !== a.specificity)
|
|
350
|
+
return b.specificity - a.specificity;
|
|
351
|
+
return b.assignment.createdAt.getTime() - a.assignment.createdAt.getTime();
|
|
352
|
+
});
|
|
353
|
+
const winner = scored[0];
|
|
354
|
+
if (!winner)
|
|
355
|
+
return null;
|
|
356
|
+
// Load current published version + rules
|
|
357
|
+
if (!winner.policy.currentVersionId) {
|
|
358
|
+
return { policy: winner.policy, assignment: winner.assignment, version: null, rules: [] };
|
|
359
|
+
}
|
|
360
|
+
const [version] = await db
|
|
361
|
+
.select()
|
|
362
|
+
.from(policyVersions)
|
|
363
|
+
.where(eq(policyVersions.id, winner.policy.currentVersionId))
|
|
364
|
+
.limit(1);
|
|
365
|
+
const rules = version
|
|
366
|
+
? await db
|
|
367
|
+
.select()
|
|
368
|
+
.from(policyRules)
|
|
369
|
+
.where(eq(policyRules.policyVersionId, version.id))
|
|
370
|
+
.orderBy(policyRules.sortOrder)
|
|
371
|
+
: [];
|
|
372
|
+
return {
|
|
373
|
+
policy: winner.policy,
|
|
374
|
+
assignment: winner.assignment,
|
|
375
|
+
version: version ?? null,
|
|
376
|
+
rules,
|
|
377
|
+
};
|
|
378
|
+
},
|
|
379
|
+
async evaluateCancellation(db, policyId, input) {
|
|
380
|
+
const [policy] = await db.select().from(policies).where(eq(policies.id, policyId)).limit(1);
|
|
381
|
+
if (!policy?.currentVersionId)
|
|
382
|
+
return null;
|
|
383
|
+
const rules = await db
|
|
384
|
+
.select()
|
|
385
|
+
.from(policyRules)
|
|
386
|
+
.where(eq(policyRules.policyVersionId, policy.currentVersionId));
|
|
387
|
+
const mapped = rules.map((r) => ({
|
|
388
|
+
id: r.id,
|
|
389
|
+
daysBeforeDeparture: r.daysBeforeDeparture,
|
|
390
|
+
refundPercent: r.refundPercent,
|
|
391
|
+
refundType: r.refundType,
|
|
392
|
+
flatAmountCents: r.flatAmountCents,
|
|
393
|
+
label: r.label,
|
|
394
|
+
}));
|
|
395
|
+
return evaluateCancellationPolicy(mapped, input);
|
|
396
|
+
},
|
|
397
|
+
// ---------- acceptances ----------
|
|
398
|
+
async listPolicyAcceptances(db, query) {
|
|
399
|
+
const conditions = [];
|
|
400
|
+
if (query.policyVersionId)
|
|
401
|
+
conditions.push(eq(policyAcceptances.policyVersionId, query.policyVersionId));
|
|
402
|
+
if (query.personId)
|
|
403
|
+
conditions.push(eq(policyAcceptances.personId, query.personId));
|
|
404
|
+
if (query.bookingId)
|
|
405
|
+
conditions.push(eq(policyAcceptances.bookingId, query.bookingId));
|
|
406
|
+
if (query.orderId)
|
|
407
|
+
conditions.push(eq(policyAcceptances.orderId, query.orderId));
|
|
408
|
+
const where = conditions.length ? and(...conditions) : undefined;
|
|
409
|
+
return paginate(db
|
|
410
|
+
.select()
|
|
411
|
+
.from(policyAcceptances)
|
|
412
|
+
.where(where)
|
|
413
|
+
.limit(query.limit)
|
|
414
|
+
.offset(query.offset)
|
|
415
|
+
.orderBy(desc(policyAcceptances.acceptedAt)), db.select({ total: sql `count(*)::int` }).from(policyAcceptances).where(where), query.limit, query.offset);
|
|
416
|
+
},
|
|
417
|
+
async recordPolicyAcceptance(db, data) {
|
|
418
|
+
const [row] = await db
|
|
419
|
+
.insert(policyAcceptances)
|
|
420
|
+
.values({
|
|
421
|
+
policyVersionId: data.policyVersionId,
|
|
422
|
+
personId: data.personId ?? null,
|
|
423
|
+
bookingId: data.bookingId ?? null,
|
|
424
|
+
orderId: data.orderId ?? null,
|
|
425
|
+
offerId: data.offerId ?? null,
|
|
426
|
+
acceptedBy: data.acceptedBy ?? null,
|
|
427
|
+
method: data.method,
|
|
428
|
+
ipAddress: data.ipAddress ?? null,
|
|
429
|
+
userAgent: data.userAgent ?? null,
|
|
430
|
+
metadata: data.metadata ?? null,
|
|
431
|
+
})
|
|
432
|
+
.returning();
|
|
433
|
+
return row ?? null;
|
|
434
|
+
},
|
|
435
|
+
};
|
|
436
|
+
// Remove unused imports placeholder — `inArray` is referenced to allow callers
|
|
437
|
+
// to use it via re-export if needed but not used internally. Kept minimal.
|
|
438
|
+
export const _unused = inArray;
|