@voyantjs/finance 0.3.0 → 0.3.1
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/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/routes-public.d.ts +476 -0
- package/dist/routes-public.d.ts.map +1 -0
- package/dist/routes-public.js +56 -0
- package/dist/routes-shared.d.ts +12 -0
- package/dist/routes-shared.d.ts.map +1 -0
- package/dist/routes-shared.js +3 -0
- package/dist/routes.d.ts +207 -161
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +40 -1
- package/dist/schema.d.ts +17 -17
- package/dist/service-public.d.ts +250 -0
- package/dist/service-public.d.ts.map +1 -0
- package/dist/service-public.js +366 -0
- package/dist/service.d.ts +175 -181
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +52 -10
- package/dist/validation-billing.d.ts +10 -10
- package/dist/validation-payments.d.ts +83 -83
- package/dist/validation-public.d.ts +374 -0
- package/dist/validation-public.d.ts.map +1 -0
- package/dist/validation-public.js +166 -0
- package/dist/validation-shared.d.ts +24 -24
- package/dist/validation.d.ts +1 -0
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +1 -0
- package/package.json +13 -5
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { bookings } from "@voyantjs/bookings/schema";
|
|
2
|
+
import { and, asc, desc, eq, or, sql } from "drizzle-orm";
|
|
3
|
+
import { bookingGuarantees, bookingPaymentSchedules, invoiceRenditions, invoices, paymentInstruments, } from "./schema.js";
|
|
4
|
+
import { financeService } from "./service.js";
|
|
5
|
+
function normalizeDateTime(value) {
|
|
6
|
+
if (!value) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
10
|
+
}
|
|
11
|
+
function isDefaultInstrument(metadata) {
|
|
12
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
const record = metadata;
|
|
16
|
+
return record.default === true || record.isDefault === true;
|
|
17
|
+
}
|
|
18
|
+
function getMetadataRecord(metadata) {
|
|
19
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return metadata;
|
|
23
|
+
}
|
|
24
|
+
function getMetadataString(record, key) {
|
|
25
|
+
const value = record?.[key];
|
|
26
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
27
|
+
}
|
|
28
|
+
function getMetadataNumber(record, key) {
|
|
29
|
+
const value = record?.[key];
|
|
30
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
31
|
+
}
|
|
32
|
+
function getMetadataStringArray(record, key) {
|
|
33
|
+
const value = record?.[key];
|
|
34
|
+
if (!Array.isArray(value)) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
return value.filter((entry) => typeof entry === "string" && entry.length > 0);
|
|
38
|
+
}
|
|
39
|
+
function maybeUrl(value) {
|
|
40
|
+
if (!value) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return /^https?:\/\//i.test(value) ? value : null;
|
|
44
|
+
}
|
|
45
|
+
function getMetadataDownloadUrl(record) {
|
|
46
|
+
const directKeys = [
|
|
47
|
+
"downloadUrl",
|
|
48
|
+
"download_url",
|
|
49
|
+
"signedUrl",
|
|
50
|
+
"signed_url",
|
|
51
|
+
"publicUrl",
|
|
52
|
+
"public_url",
|
|
53
|
+
"fileUrl",
|
|
54
|
+
"file_url",
|
|
55
|
+
"url",
|
|
56
|
+
];
|
|
57
|
+
for (const key of directKeys) {
|
|
58
|
+
const value = getMetadataString(record, key);
|
|
59
|
+
const url = maybeUrl(value);
|
|
60
|
+
if (url) {
|
|
61
|
+
return url;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const artifact = record?.artifact;
|
|
65
|
+
if (artifact && typeof artifact === "object" && !Array.isArray(artifact)) {
|
|
66
|
+
const nested = artifact;
|
|
67
|
+
for (const key of ["downloadUrl", "download_url", "signedUrl", "publicUrl", "url"]) {
|
|
68
|
+
const value = nested[key];
|
|
69
|
+
if (typeof value === "string") {
|
|
70
|
+
const url = maybeUrl(value);
|
|
71
|
+
if (url) {
|
|
72
|
+
return url;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
function toPublicPaymentSession(session) {
|
|
80
|
+
return {
|
|
81
|
+
id: session.id,
|
|
82
|
+
targetType: session.targetType,
|
|
83
|
+
targetId: session.targetId ?? null,
|
|
84
|
+
bookingId: session.bookingId ?? null,
|
|
85
|
+
invoiceId: session.invoiceId ?? null,
|
|
86
|
+
bookingPaymentScheduleId: session.bookingPaymentScheduleId ?? null,
|
|
87
|
+
bookingGuaranteeId: session.bookingGuaranteeId ?? null,
|
|
88
|
+
status: session.status,
|
|
89
|
+
provider: session.provider ?? null,
|
|
90
|
+
providerSessionId: session.providerSessionId ?? null,
|
|
91
|
+
providerPaymentId: session.providerPaymentId ?? null,
|
|
92
|
+
externalReference: session.externalReference ?? null,
|
|
93
|
+
clientReference: session.clientReference ?? null,
|
|
94
|
+
currency: session.currency,
|
|
95
|
+
amountCents: session.amountCents,
|
|
96
|
+
paymentMethod: session.paymentMethod ?? null,
|
|
97
|
+
payerEmail: session.payerEmail ?? null,
|
|
98
|
+
payerName: session.payerName ?? null,
|
|
99
|
+
redirectUrl: session.redirectUrl ?? null,
|
|
100
|
+
returnUrl: session.returnUrl ?? null,
|
|
101
|
+
cancelUrl: session.cancelUrl ?? null,
|
|
102
|
+
expiresAt: normalizeDateTime(session.expiresAt),
|
|
103
|
+
completedAt: normalizeDateTime(session.completedAt),
|
|
104
|
+
failureCode: session.failureCode ?? null,
|
|
105
|
+
failureMessage: session.failureMessage ?? null,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export const publicFinanceService = {
|
|
109
|
+
async getBookingDocuments(db, bookingId) {
|
|
110
|
+
const [booking] = await db
|
|
111
|
+
.select({ id: bookings.id })
|
|
112
|
+
.from(bookings)
|
|
113
|
+
.where(eq(bookings.id, bookingId))
|
|
114
|
+
.limit(1);
|
|
115
|
+
if (!booking) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
const invoiceRows = await db
|
|
119
|
+
.select()
|
|
120
|
+
.from(invoices)
|
|
121
|
+
.where(eq(invoices.bookingId, bookingId))
|
|
122
|
+
.orderBy(desc(invoices.createdAt));
|
|
123
|
+
if (invoiceRows.length === 0) {
|
|
124
|
+
return { bookingId, documents: [] };
|
|
125
|
+
}
|
|
126
|
+
const renditionRows = await db
|
|
127
|
+
.select()
|
|
128
|
+
.from(invoiceRenditions)
|
|
129
|
+
.where(or(...invoiceRows.map((invoice) => eq(invoiceRenditions.invoiceId, invoice.id))))
|
|
130
|
+
.orderBy(desc(invoiceRenditions.createdAt));
|
|
131
|
+
const renditionByInvoiceId = new Map();
|
|
132
|
+
for (const rendition of renditionRows) {
|
|
133
|
+
const existing = renditionByInvoiceId.get(rendition.invoiceId) ?? [];
|
|
134
|
+
existing.push(rendition);
|
|
135
|
+
renditionByInvoiceId.set(rendition.invoiceId, existing);
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
bookingId,
|
|
139
|
+
documents: invoiceRows.map((invoice) => {
|
|
140
|
+
const renditions = renditionByInvoiceId.get(invoice.id) ?? [];
|
|
141
|
+
const selectedRendition = renditions.find((rendition) => rendition.status === "ready") ?? renditions[0] ?? null;
|
|
142
|
+
const metadata = getMetadataRecord(selectedRendition?.metadata ?? null);
|
|
143
|
+
const downloadUrl = getMetadataDownloadUrl(metadata) ?? maybeUrl(selectedRendition?.storageKey ?? null);
|
|
144
|
+
return {
|
|
145
|
+
invoiceId: invoice.id,
|
|
146
|
+
invoiceNumber: invoice.invoiceNumber,
|
|
147
|
+
invoiceType: invoice.invoiceType,
|
|
148
|
+
invoiceStatus: invoice.status,
|
|
149
|
+
currency: invoice.currency,
|
|
150
|
+
totalCents: invoice.totalCents,
|
|
151
|
+
paidCents: invoice.paidCents,
|
|
152
|
+
balanceDueCents: invoice.balanceDueCents,
|
|
153
|
+
issueDate: invoice.issueDate,
|
|
154
|
+
dueDate: invoice.dueDate,
|
|
155
|
+
renditionId: selectedRendition?.id ?? null,
|
|
156
|
+
documentStatus: selectedRendition?.status ?? "missing",
|
|
157
|
+
format: selectedRendition?.format ?? null,
|
|
158
|
+
language: selectedRendition?.language ?? null,
|
|
159
|
+
generatedAt: normalizeDateTime(selectedRendition?.generatedAt),
|
|
160
|
+
fileSize: selectedRendition?.fileSize ?? null,
|
|
161
|
+
checksum: selectedRendition?.checksum ?? null,
|
|
162
|
+
downloadUrl,
|
|
163
|
+
};
|
|
164
|
+
}),
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
async getBookingPaymentOptions(db, bookingId, query) {
|
|
168
|
+
const [booking] = await db
|
|
169
|
+
.select({ id: bookings.id })
|
|
170
|
+
.from(bookings)
|
|
171
|
+
.where(eq(bookings.id, bookingId))
|
|
172
|
+
.limit(1);
|
|
173
|
+
if (!booking) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const instrumentConditions = [eq(paymentInstruments.ownerType, "client")];
|
|
177
|
+
if (!query.includeInactive) {
|
|
178
|
+
instrumentConditions.push(eq(paymentInstruments.status, "active"));
|
|
179
|
+
}
|
|
180
|
+
if (query.personId) {
|
|
181
|
+
instrumentConditions.push(eq(paymentInstruments.personId, query.personId));
|
|
182
|
+
}
|
|
183
|
+
if (query.organizationId) {
|
|
184
|
+
instrumentConditions.push(eq(paymentInstruments.organizationId, query.organizationId));
|
|
185
|
+
}
|
|
186
|
+
if (query.provider) {
|
|
187
|
+
instrumentConditions.push(eq(paymentInstruments.provider, query.provider));
|
|
188
|
+
}
|
|
189
|
+
if (query.instrumentType) {
|
|
190
|
+
instrumentConditions.push(eq(paymentInstruments.instrumentType, query.instrumentType));
|
|
191
|
+
}
|
|
192
|
+
const [accounts, schedules, guarantees] = await Promise.all([
|
|
193
|
+
db
|
|
194
|
+
.select()
|
|
195
|
+
.from(paymentInstruments)
|
|
196
|
+
.where(and(...instrumentConditions))
|
|
197
|
+
.orderBy(desc(paymentInstruments.updatedAt))
|
|
198
|
+
.limit(50),
|
|
199
|
+
db
|
|
200
|
+
.select()
|
|
201
|
+
.from(bookingPaymentSchedules)
|
|
202
|
+
.where(and(eq(bookingPaymentSchedules.bookingId, bookingId), or(eq(bookingPaymentSchedules.status, "pending"), eq(bookingPaymentSchedules.status, "due"))))
|
|
203
|
+
.orderBy(asc(bookingPaymentSchedules.dueDate), asc(bookingPaymentSchedules.createdAt)),
|
|
204
|
+
db
|
|
205
|
+
.select()
|
|
206
|
+
.from(bookingGuarantees)
|
|
207
|
+
.where(and(eq(bookingGuarantees.bookingId, bookingId), or(eq(bookingGuarantees.status, "pending"), eq(bookingGuarantees.status, "failed"), eq(bookingGuarantees.status, "expired"))))
|
|
208
|
+
.orderBy(desc(bookingGuarantees.createdAt)),
|
|
209
|
+
]);
|
|
210
|
+
const recommendedSchedule = schedules[0] ?? null;
|
|
211
|
+
const recommendedGuarantee = recommendedSchedule === null ? (guarantees[0] ?? null) : null;
|
|
212
|
+
return {
|
|
213
|
+
bookingId,
|
|
214
|
+
accounts: accounts.map((account) => ({
|
|
215
|
+
id: account.id,
|
|
216
|
+
label: account.label,
|
|
217
|
+
provider: account.provider ?? null,
|
|
218
|
+
instrumentType: account.instrumentType,
|
|
219
|
+
status: account.status,
|
|
220
|
+
brand: account.brand ?? null,
|
|
221
|
+
last4: account.last4 ?? null,
|
|
222
|
+
expiryMonth: account.expiryMonth ?? null,
|
|
223
|
+
expiryYear: account.expiryYear ?? null,
|
|
224
|
+
isDefault: isDefaultInstrument(account.metadata),
|
|
225
|
+
})),
|
|
226
|
+
schedules: schedules.map((schedule) => ({
|
|
227
|
+
id: schedule.id,
|
|
228
|
+
scheduleType: schedule.scheduleType,
|
|
229
|
+
status: schedule.status,
|
|
230
|
+
dueDate: schedule.dueDate,
|
|
231
|
+
currency: schedule.currency,
|
|
232
|
+
amountCents: schedule.amountCents,
|
|
233
|
+
notes: schedule.notes ?? null,
|
|
234
|
+
})),
|
|
235
|
+
guarantees: guarantees.map((guarantee) => ({
|
|
236
|
+
id: guarantee.id,
|
|
237
|
+
bookingPaymentScheduleId: guarantee.bookingPaymentScheduleId ?? null,
|
|
238
|
+
guaranteeType: guarantee.guaranteeType,
|
|
239
|
+
status: guarantee.status,
|
|
240
|
+
currency: guarantee.currency ?? null,
|
|
241
|
+
amountCents: guarantee.amountCents ?? null,
|
|
242
|
+
provider: guarantee.provider ?? null,
|
|
243
|
+
referenceNumber: guarantee.referenceNumber ?? null,
|
|
244
|
+
expiresAt: normalizeDateTime(guarantee.expiresAt),
|
|
245
|
+
notes: guarantee.notes ?? null,
|
|
246
|
+
})),
|
|
247
|
+
recommendedTarget: recommendedSchedule || recommendedGuarantee
|
|
248
|
+
? {
|
|
249
|
+
targetType: recommendedSchedule
|
|
250
|
+
? "booking_payment_schedule"
|
|
251
|
+
: "booking_guarantee",
|
|
252
|
+
targetId: recommendedSchedule?.id ?? recommendedGuarantee?.id ?? null,
|
|
253
|
+
}
|
|
254
|
+
: null,
|
|
255
|
+
};
|
|
256
|
+
},
|
|
257
|
+
async getPaymentSession(db, sessionId) {
|
|
258
|
+
const session = await financeService.getPaymentSessionById(db, sessionId);
|
|
259
|
+
return session ? toPublicPaymentSession(session) : null;
|
|
260
|
+
},
|
|
261
|
+
async startBookingSchedulePaymentSession(db, bookingId, scheduleId, input) {
|
|
262
|
+
const [schedule] = await db
|
|
263
|
+
.select({
|
|
264
|
+
id: bookingPaymentSchedules.id,
|
|
265
|
+
})
|
|
266
|
+
.from(bookingPaymentSchedules)
|
|
267
|
+
.where(and(eq(bookingPaymentSchedules.id, scheduleId), eq(bookingPaymentSchedules.bookingId, bookingId)))
|
|
268
|
+
.limit(1);
|
|
269
|
+
if (!schedule) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
const session = await financeService.createPaymentSessionFromBookingSchedule(db, scheduleId, input);
|
|
273
|
+
return session ? toPublicPaymentSession(session) : null;
|
|
274
|
+
},
|
|
275
|
+
async startBookingGuaranteePaymentSession(db, bookingId, guaranteeId, input) {
|
|
276
|
+
const [guarantee] = await db
|
|
277
|
+
.select({
|
|
278
|
+
id: bookingGuarantees.id,
|
|
279
|
+
})
|
|
280
|
+
.from(bookingGuarantees)
|
|
281
|
+
.where(and(eq(bookingGuarantees.id, guaranteeId), eq(bookingGuarantees.bookingId, bookingId)))
|
|
282
|
+
.limit(1);
|
|
283
|
+
if (!guarantee) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
const session = await financeService.createPaymentSessionFromBookingGuarantee(db, guaranteeId, input);
|
|
287
|
+
return session ? toPublicPaymentSession(session) : null;
|
|
288
|
+
},
|
|
289
|
+
async startInvoicePaymentSession(db, invoiceId, input) {
|
|
290
|
+
const session = await financeService.createPaymentSessionFromInvoice(db, invoiceId, input);
|
|
291
|
+
return session ? toPublicPaymentSession(session) : null;
|
|
292
|
+
},
|
|
293
|
+
async validateVoucher(db, input) {
|
|
294
|
+
const normalizedCode = input.code.trim().toLowerCase();
|
|
295
|
+
const voucherConditions = [
|
|
296
|
+
eq(paymentInstruments.instrumentType, "voucher"),
|
|
297
|
+
or(sql `lower(coalesce(${paymentInstruments.externalToken}, '')) = ${normalizedCode}`, sql `lower(coalesce(${paymentInstruments.directBillReference}, '')) = ${normalizedCode}`, sql `lower(coalesce(${paymentInstruments.metadata} ->> 'code', '')) = ${normalizedCode}`),
|
|
298
|
+
];
|
|
299
|
+
if (input.provider) {
|
|
300
|
+
voucherConditions.push(eq(paymentInstruments.provider, input.provider));
|
|
301
|
+
}
|
|
302
|
+
const [voucher] = await db
|
|
303
|
+
.select()
|
|
304
|
+
.from(paymentInstruments)
|
|
305
|
+
.where(and(...voucherConditions))
|
|
306
|
+
.orderBy(desc(paymentInstruments.updatedAt))
|
|
307
|
+
.limit(1);
|
|
308
|
+
if (!voucher) {
|
|
309
|
+
return { valid: false, reason: "not_found", voucher: null };
|
|
310
|
+
}
|
|
311
|
+
const metadata = getMetadataRecord(voucher.metadata);
|
|
312
|
+
const voucherCode = getMetadataString(metadata, "code") ??
|
|
313
|
+
voucher.externalToken ??
|
|
314
|
+
voucher.directBillReference ??
|
|
315
|
+
input.code;
|
|
316
|
+
const currency = getMetadataString(metadata, "currency");
|
|
317
|
+
const amountCents = getMetadataNumber(metadata, "amountCents");
|
|
318
|
+
const remainingAmountCents = getMetadataNumber(metadata, "remainingAmountCents") ?? amountCents;
|
|
319
|
+
const validFrom = getMetadataString(metadata, "validFrom");
|
|
320
|
+
const expiresAt = getMetadataString(metadata, "expiresAt");
|
|
321
|
+
const bookingIds = getMetadataStringArray(metadata, "bookingIds");
|
|
322
|
+
const bookingId = getMetadataString(metadata, "bookingId");
|
|
323
|
+
const appliesToBookingIds = bookingId ? [bookingId, ...bookingIds] : bookingIds;
|
|
324
|
+
const publicVoucher = {
|
|
325
|
+
id: voucher.id,
|
|
326
|
+
code: voucherCode,
|
|
327
|
+
label: voucher.label,
|
|
328
|
+
provider: voucher.provider ?? null,
|
|
329
|
+
currency,
|
|
330
|
+
amountCents,
|
|
331
|
+
remainingAmountCents,
|
|
332
|
+
expiresAt,
|
|
333
|
+
};
|
|
334
|
+
if (voucher.status !== "active") {
|
|
335
|
+
return { valid: false, reason: "inactive", voucher: publicVoucher };
|
|
336
|
+
}
|
|
337
|
+
if (validFrom && new Date(validFrom) > new Date()) {
|
|
338
|
+
return { valid: false, reason: "not_started", voucher: publicVoucher };
|
|
339
|
+
}
|
|
340
|
+
if (expiresAt && new Date(expiresAt) < new Date()) {
|
|
341
|
+
return { valid: false, reason: "expired", voucher: publicVoucher };
|
|
342
|
+
}
|
|
343
|
+
if (input.bookingId &&
|
|
344
|
+
appliesToBookingIds.length > 0 &&
|
|
345
|
+
!appliesToBookingIds.includes(input.bookingId)) {
|
|
346
|
+
return { valid: false, reason: "booking_mismatch", voucher: publicVoucher };
|
|
347
|
+
}
|
|
348
|
+
if (input.currency && currency && input.currency !== currency) {
|
|
349
|
+
return {
|
|
350
|
+
valid: false,
|
|
351
|
+
reason: "currency_mismatch",
|
|
352
|
+
voucher: publicVoucher,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
if (input.amountCents &&
|
|
356
|
+
remainingAmountCents !== null &&
|
|
357
|
+
input.amountCents > remainingAmountCents) {
|
|
358
|
+
return {
|
|
359
|
+
valid: false,
|
|
360
|
+
reason: "insufficient_balance",
|
|
361
|
+
voucher: publicVoucher,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
return { valid: true, reason: null, voucher: publicVoucher };
|
|
365
|
+
},
|
|
366
|
+
};
|