@voyantjs/finance 0.3.0 → 0.4.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.
Files changed (43) hide show
  1. package/dist/index.d.ts +18 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +25 -5
  4. package/dist/routes-documents.d.ts +159 -0
  5. package/dist/routes-documents.d.ts.map +1 -0
  6. package/dist/routes-documents.js +35 -0
  7. package/dist/routes-public.d.ts +517 -0
  8. package/dist/routes-public.d.ts.map +1 -0
  9. package/dist/routes-public.js +60 -0
  10. package/dist/routes-settlement.d.ts +63 -0
  11. package/dist/routes-settlement.d.ts.map +1 -0
  12. package/dist/routes-settlement.js +18 -0
  13. package/dist/routes-shared.d.ts +12 -0
  14. package/dist/routes-shared.d.ts.map +1 -0
  15. package/dist/routes-shared.js +3 -0
  16. package/dist/routes.d.ts +207 -161
  17. package/dist/routes.d.ts.map +1 -1
  18. package/dist/routes.js +40 -1
  19. package/dist/schema.d.ts +17 -17
  20. package/dist/service-documents.d.ts +67 -0
  21. package/dist/service-documents.d.ts.map +1 -0
  22. package/dist/service-documents.js +226 -0
  23. package/dist/service-public.d.ts +251 -0
  24. package/dist/service-public.d.ts.map +1 -0
  25. package/dist/service-public.js +418 -0
  26. package/dist/service-settlement.d.ts +36 -0
  27. package/dist/service-settlement.d.ts.map +1 -0
  28. package/dist/service-settlement.js +172 -0
  29. package/dist/service.d.ts +175 -181
  30. package/dist/service.d.ts.map +1 -1
  31. package/dist/service.js +54 -67
  32. package/dist/validation-billing.d.ts +119 -9
  33. package/dist/validation-billing.d.ts.map +1 -1
  34. package/dist/validation-billing.js +54 -1
  35. package/dist/validation-payments.d.ts +83 -83
  36. package/dist/validation-public.d.ts +443 -0
  37. package/dist/validation-public.d.ts.map +1 -0
  38. package/dist/validation-public.js +183 -0
  39. package/dist/validation-shared.d.ts +24 -24
  40. package/dist/validation.d.ts +1 -0
  41. package/dist/validation.d.ts.map +1 -1
  42. package/dist/validation.js +1 -0
  43. package/package.json +15 -5
@@ -0,0 +1,251 @@
1
+ import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
2
+ import type { PublicBookingFinanceDocuments, PublicBookingFinancePayments, PublicPaymentOptionsQuery, PublicStartPaymentSessionInput, PublicValidateVoucherInput } from "./validation-public.js";
3
+ export declare const publicFinanceService: {
4
+ getBookingDocuments(db: PostgresJsDatabase, bookingId: string): Promise<PublicBookingFinanceDocuments | null>;
5
+ getBookingPaymentOptions(db: PostgresJsDatabase, bookingId: string, query: PublicPaymentOptionsQuery): Promise<{
6
+ bookingId: string;
7
+ accounts: {
8
+ id: string;
9
+ label: string;
10
+ provider: string | null;
11
+ instrumentType: "other" | "voucher" | "wallet" | "credit_card" | "debit_card" | "cash" | "direct_bill" | "bank_account";
12
+ status: "expired" | "revoked" | "active" | "inactive" | "failed_verification";
13
+ brand: string | null;
14
+ last4: string | null;
15
+ expiryMonth: number | null;
16
+ expiryYear: number | null;
17
+ isDefault: boolean;
18
+ }[];
19
+ schedules: {
20
+ id: string;
21
+ scheduleType: "other" | "deposit" | "installment" | "balance" | "hold";
22
+ status: "expired" | "cancelled" | "pending" | "paid" | "due" | "waived";
23
+ dueDate: string;
24
+ currency: string;
25
+ amountCents: number;
26
+ notes: string | null;
27
+ }[];
28
+ guarantees: {
29
+ id: string;
30
+ bookingPaymentScheduleId: string | null;
31
+ guaranteeType: "other" | "voucher" | "bank_transfer" | "credit_card" | "deposit" | "preauth" | "card_on_file" | "agency_letter";
32
+ status: "expired" | "cancelled" | "pending" | "released" | "failed" | "active";
33
+ currency: string | null;
34
+ amountCents: number | null;
35
+ provider: string | null;
36
+ referenceNumber: string | null;
37
+ expiresAt: string | null;
38
+ notes: string | null;
39
+ }[];
40
+ recommendedTarget: {
41
+ targetType: "booking_payment_schedule" | "booking_guarantee";
42
+ targetId: string | null;
43
+ } | null;
44
+ } | null>;
45
+ getBookingPayments(db: PostgresJsDatabase, bookingId: string): Promise<PublicBookingFinancePayments | null>;
46
+ getPaymentSession(db: PostgresJsDatabase, sessionId: string): Promise<{
47
+ id: string;
48
+ targetType: "other" | "booking" | "order" | "invoice" | "booking_payment_schedule" | "booking_guarantee";
49
+ targetId: string | null;
50
+ bookingId: string | null;
51
+ invoiceId: string | null;
52
+ bookingPaymentScheduleId: string | null;
53
+ bookingGuaranteeId: string | null;
54
+ status: "expired" | "cancelled" | "pending" | "failed" | "paid" | "requires_redirect" | "processing" | "authorized";
55
+ provider: string | null;
56
+ providerSessionId: string | null;
57
+ providerPaymentId: string | null;
58
+ externalReference: string | null;
59
+ clientReference: string | null;
60
+ currency: string;
61
+ amountCents: number;
62
+ paymentMethod: "other" | "voucher" | "wallet" | "bank_transfer" | "credit_card" | "debit_card" | "cash" | "cheque" | "direct_bill" | null;
63
+ payerEmail: string | null;
64
+ payerName: string | null;
65
+ redirectUrl: string | null;
66
+ returnUrl: string | null;
67
+ cancelUrl: string | null;
68
+ expiresAt: string | null;
69
+ completedAt: string | null;
70
+ failureCode: string | null;
71
+ failureMessage: string | null;
72
+ } | null>;
73
+ startBookingSchedulePaymentSession(db: PostgresJsDatabase, bookingId: string, scheduleId: string, input: PublicStartPaymentSessionInput): Promise<{
74
+ id: string;
75
+ targetType: "other" | "booking" | "order" | "invoice" | "booking_payment_schedule" | "booking_guarantee";
76
+ targetId: string | null;
77
+ bookingId: string | null;
78
+ invoiceId: string | null;
79
+ bookingPaymentScheduleId: string | null;
80
+ bookingGuaranteeId: string | null;
81
+ status: "expired" | "cancelled" | "pending" | "failed" | "paid" | "requires_redirect" | "processing" | "authorized";
82
+ provider: string | null;
83
+ providerSessionId: string | null;
84
+ providerPaymentId: string | null;
85
+ externalReference: string | null;
86
+ clientReference: string | null;
87
+ currency: string;
88
+ amountCents: number;
89
+ paymentMethod: "other" | "voucher" | "wallet" | "bank_transfer" | "credit_card" | "debit_card" | "cash" | "cheque" | "direct_bill" | null;
90
+ payerEmail: string | null;
91
+ payerName: string | null;
92
+ redirectUrl: string | null;
93
+ returnUrl: string | null;
94
+ cancelUrl: string | null;
95
+ expiresAt: string | null;
96
+ completedAt: string | null;
97
+ failureCode: string | null;
98
+ failureMessage: string | null;
99
+ } | null>;
100
+ startBookingGuaranteePaymentSession(db: PostgresJsDatabase, bookingId: string, guaranteeId: string, input: PublicStartPaymentSessionInput): Promise<{
101
+ id: string;
102
+ targetType: "other" | "booking" | "order" | "invoice" | "booking_payment_schedule" | "booking_guarantee";
103
+ targetId: string | null;
104
+ bookingId: string | null;
105
+ invoiceId: string | null;
106
+ bookingPaymentScheduleId: string | null;
107
+ bookingGuaranteeId: string | null;
108
+ status: "expired" | "cancelled" | "pending" | "failed" | "paid" | "requires_redirect" | "processing" | "authorized";
109
+ provider: string | null;
110
+ providerSessionId: string | null;
111
+ providerPaymentId: string | null;
112
+ externalReference: string | null;
113
+ clientReference: string | null;
114
+ currency: string;
115
+ amountCents: number;
116
+ paymentMethod: "other" | "voucher" | "wallet" | "bank_transfer" | "credit_card" | "debit_card" | "cash" | "cheque" | "direct_bill" | null;
117
+ payerEmail: string | null;
118
+ payerName: string | null;
119
+ redirectUrl: string | null;
120
+ returnUrl: string | null;
121
+ cancelUrl: string | null;
122
+ expiresAt: string | null;
123
+ completedAt: string | null;
124
+ failureCode: string | null;
125
+ failureMessage: string | null;
126
+ } | null>;
127
+ startInvoicePaymentSession(db: PostgresJsDatabase, invoiceId: string, input: PublicStartPaymentSessionInput): Promise<{
128
+ id: string;
129
+ targetType: "other" | "booking" | "order" | "invoice" | "booking_payment_schedule" | "booking_guarantee";
130
+ targetId: string | null;
131
+ bookingId: string | null;
132
+ invoiceId: string | null;
133
+ bookingPaymentScheduleId: string | null;
134
+ bookingGuaranteeId: string | null;
135
+ status: "expired" | "cancelled" | "pending" | "failed" | "paid" | "requires_redirect" | "processing" | "authorized";
136
+ provider: string | null;
137
+ providerSessionId: string | null;
138
+ providerPaymentId: string | null;
139
+ externalReference: string | null;
140
+ clientReference: string | null;
141
+ currency: string;
142
+ amountCents: number;
143
+ paymentMethod: "other" | "voucher" | "wallet" | "bank_transfer" | "credit_card" | "debit_card" | "cash" | "cheque" | "direct_bill" | null;
144
+ payerEmail: string | null;
145
+ payerName: string | null;
146
+ redirectUrl: string | null;
147
+ returnUrl: string | null;
148
+ cancelUrl: string | null;
149
+ expiresAt: string | null;
150
+ completedAt: string | null;
151
+ failureCode: string | null;
152
+ failureMessage: string | null;
153
+ } | null>;
154
+ validateVoucher(db: PostgresJsDatabase, input: PublicValidateVoucherInput): Promise<{
155
+ valid: false;
156
+ reason: "not_found";
157
+ voucher: null;
158
+ } | {
159
+ valid: false;
160
+ reason: "inactive";
161
+ voucher: {
162
+ id: string;
163
+ code: string;
164
+ label: string;
165
+ provider: string | null;
166
+ currency: string | null;
167
+ amountCents: number | null;
168
+ remainingAmountCents: number | null;
169
+ expiresAt: string | null;
170
+ };
171
+ } | {
172
+ valid: false;
173
+ reason: "not_started";
174
+ voucher: {
175
+ id: string;
176
+ code: string;
177
+ label: string;
178
+ provider: string | null;
179
+ currency: string | null;
180
+ amountCents: number | null;
181
+ remainingAmountCents: number | null;
182
+ expiresAt: string | null;
183
+ };
184
+ } | {
185
+ valid: false;
186
+ reason: "expired";
187
+ voucher: {
188
+ id: string;
189
+ code: string;
190
+ label: string;
191
+ provider: string | null;
192
+ currency: string | null;
193
+ amountCents: number | null;
194
+ remainingAmountCents: number | null;
195
+ expiresAt: string | null;
196
+ };
197
+ } | {
198
+ valid: false;
199
+ reason: "booking_mismatch";
200
+ voucher: {
201
+ id: string;
202
+ code: string;
203
+ label: string;
204
+ provider: string | null;
205
+ currency: string | null;
206
+ amountCents: number | null;
207
+ remainingAmountCents: number | null;
208
+ expiresAt: string | null;
209
+ };
210
+ } | {
211
+ valid: false;
212
+ reason: "currency_mismatch";
213
+ voucher: {
214
+ id: string;
215
+ code: string;
216
+ label: string;
217
+ provider: string | null;
218
+ currency: string | null;
219
+ amountCents: number | null;
220
+ remainingAmountCents: number | null;
221
+ expiresAt: string | null;
222
+ };
223
+ } | {
224
+ valid: false;
225
+ reason: "insufficient_balance";
226
+ voucher: {
227
+ id: string;
228
+ code: string;
229
+ label: string;
230
+ provider: string | null;
231
+ currency: string | null;
232
+ amountCents: number | null;
233
+ remainingAmountCents: number | null;
234
+ expiresAt: string | null;
235
+ };
236
+ } | {
237
+ valid: true;
238
+ reason: null;
239
+ voucher: {
240
+ id: string;
241
+ code: string;
242
+ label: string;
243
+ provider: string | null;
244
+ currency: string | null;
245
+ amountCents: number | null;
246
+ remainingAmountCents: number | null;
247
+ expiresAt: string | null;
248
+ };
249
+ }>;
250
+ };
251
+ //# sourceMappingURL=service-public.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-public.d.ts","sourceRoot":"","sources":["../src/service-public.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAWjE,OAAO,KAAK,EACV,6BAA6B,EAC7B,4BAA4B,EAC5B,yBAAyB,EACzB,8BAA8B,EAC9B,0BAA0B,EAC3B,MAAM,wBAAwB,CAAA;AA4H/B,eAAO,MAAM,oBAAoB;4BAEzB,kBAAkB,aACX,MAAM,GAChB,OAAO,CAAC,6BAA6B,GAAG,IAAI,CAAC;iCAqE1C,kBAAkB,aACX,MAAM,SACV,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2BAoH5B,kBAAkB,aACX,MAAM,GAChB,OAAO,CAAC,4BAA4B,GAAG,IAAI,CAAC;0BA2DnB,kBAAkB,aAAa,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;2CAM3D,kBAAkB,aACX,MAAM,cACL,MAAM,SACX,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;4CA4BjC,kBAAkB,aACX,MAAM,eACJ,MAAM,SACZ,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;mCAuBjC,kBAAkB,aACX,MAAM,SACV,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAMb,kBAAkB,SAAS,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8FhF,CAAA"}
@@ -0,0 +1,418 @@
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, payments, } 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 getBookingPayments(db, bookingId) {
258
+ const [booking] = await db
259
+ .select({ id: bookings.id })
260
+ .from(bookings)
261
+ .where(eq(bookings.id, bookingId))
262
+ .limit(1);
263
+ if (!booking) {
264
+ return null;
265
+ }
266
+ const invoiceRows = await db
267
+ .select({
268
+ id: invoices.id,
269
+ invoiceNumber: invoices.invoiceNumber,
270
+ invoiceType: invoices.invoiceType,
271
+ })
272
+ .from(invoices)
273
+ .where(eq(invoices.bookingId, bookingId))
274
+ .orderBy(desc(invoices.createdAt));
275
+ if (invoiceRows.length === 0) {
276
+ return { bookingId, payments: [] };
277
+ }
278
+ const invoiceById = new Map(invoiceRows.map((invoice) => [invoice.id, invoice]));
279
+ const paymentRows = await db
280
+ .select()
281
+ .from(payments)
282
+ .where(or(...invoiceRows.map((invoice) => eq(payments.invoiceId, invoice.id))))
283
+ .orderBy(desc(payments.paymentDate), desc(payments.createdAt));
284
+ return {
285
+ bookingId,
286
+ payments: paymentRows.flatMap((payment) => {
287
+ const invoice = invoiceById.get(payment.invoiceId);
288
+ if (!invoice) {
289
+ return [];
290
+ }
291
+ return [
292
+ {
293
+ id: payment.id,
294
+ invoiceId: invoice.id,
295
+ invoiceNumber: invoice.invoiceNumber,
296
+ invoiceType: invoice.invoiceType,
297
+ status: payment.status,
298
+ paymentMethod: payment.paymentMethod,
299
+ amountCents: payment.amountCents,
300
+ currency: payment.currency,
301
+ paymentDate: payment.paymentDate,
302
+ referenceNumber: payment.referenceNumber ?? null,
303
+ notes: payment.notes ?? null,
304
+ },
305
+ ];
306
+ }),
307
+ };
308
+ },
309
+ async getPaymentSession(db, sessionId) {
310
+ const session = await financeService.getPaymentSessionById(db, sessionId);
311
+ return session ? toPublicPaymentSession(session) : null;
312
+ },
313
+ async startBookingSchedulePaymentSession(db, bookingId, scheduleId, input) {
314
+ const [schedule] = await db
315
+ .select({
316
+ id: bookingPaymentSchedules.id,
317
+ })
318
+ .from(bookingPaymentSchedules)
319
+ .where(and(eq(bookingPaymentSchedules.id, scheduleId), eq(bookingPaymentSchedules.bookingId, bookingId)))
320
+ .limit(1);
321
+ if (!schedule) {
322
+ return null;
323
+ }
324
+ const session = await financeService.createPaymentSessionFromBookingSchedule(db, scheduleId, input);
325
+ return session ? toPublicPaymentSession(session) : null;
326
+ },
327
+ async startBookingGuaranteePaymentSession(db, bookingId, guaranteeId, input) {
328
+ const [guarantee] = await db
329
+ .select({
330
+ id: bookingGuarantees.id,
331
+ })
332
+ .from(bookingGuarantees)
333
+ .where(and(eq(bookingGuarantees.id, guaranteeId), eq(bookingGuarantees.bookingId, bookingId)))
334
+ .limit(1);
335
+ if (!guarantee) {
336
+ return null;
337
+ }
338
+ const session = await financeService.createPaymentSessionFromBookingGuarantee(db, guaranteeId, input);
339
+ return session ? toPublicPaymentSession(session) : null;
340
+ },
341
+ async startInvoicePaymentSession(db, invoiceId, input) {
342
+ const session = await financeService.createPaymentSessionFromInvoice(db, invoiceId, input);
343
+ return session ? toPublicPaymentSession(session) : null;
344
+ },
345
+ async validateVoucher(db, input) {
346
+ const normalizedCode = input.code.trim().toLowerCase();
347
+ const voucherConditions = [
348
+ eq(paymentInstruments.instrumentType, "voucher"),
349
+ or(sql `lower(coalesce(${paymentInstruments.externalToken}, '')) = ${normalizedCode}`, sql `lower(coalesce(${paymentInstruments.directBillReference}, '')) = ${normalizedCode}`, sql `lower(coalesce(${paymentInstruments.metadata} ->> 'code', '')) = ${normalizedCode}`),
350
+ ];
351
+ if (input.provider) {
352
+ voucherConditions.push(eq(paymentInstruments.provider, input.provider));
353
+ }
354
+ const [voucher] = await db
355
+ .select()
356
+ .from(paymentInstruments)
357
+ .where(and(...voucherConditions))
358
+ .orderBy(desc(paymentInstruments.updatedAt))
359
+ .limit(1);
360
+ if (!voucher) {
361
+ return { valid: false, reason: "not_found", voucher: null };
362
+ }
363
+ const metadata = getMetadataRecord(voucher.metadata);
364
+ const voucherCode = getMetadataString(metadata, "code") ??
365
+ voucher.externalToken ??
366
+ voucher.directBillReference ??
367
+ input.code;
368
+ const currency = getMetadataString(metadata, "currency");
369
+ const amountCents = getMetadataNumber(metadata, "amountCents");
370
+ const remainingAmountCents = getMetadataNumber(metadata, "remainingAmountCents") ?? amountCents;
371
+ const validFrom = getMetadataString(metadata, "validFrom");
372
+ const expiresAt = getMetadataString(metadata, "expiresAt");
373
+ const bookingIds = getMetadataStringArray(metadata, "bookingIds");
374
+ const bookingId = getMetadataString(metadata, "bookingId");
375
+ const appliesToBookingIds = bookingId ? [bookingId, ...bookingIds] : bookingIds;
376
+ const publicVoucher = {
377
+ id: voucher.id,
378
+ code: voucherCode,
379
+ label: voucher.label,
380
+ provider: voucher.provider ?? null,
381
+ currency,
382
+ amountCents,
383
+ remainingAmountCents,
384
+ expiresAt,
385
+ };
386
+ if (voucher.status !== "active") {
387
+ return { valid: false, reason: "inactive", voucher: publicVoucher };
388
+ }
389
+ if (validFrom && new Date(validFrom) > new Date()) {
390
+ return { valid: false, reason: "not_started", voucher: publicVoucher };
391
+ }
392
+ if (expiresAt && new Date(expiresAt) < new Date()) {
393
+ return { valid: false, reason: "expired", voucher: publicVoucher };
394
+ }
395
+ if (input.bookingId &&
396
+ appliesToBookingIds.length > 0 &&
397
+ !appliesToBookingIds.includes(input.bookingId)) {
398
+ return { valid: false, reason: "booking_mismatch", voucher: publicVoucher };
399
+ }
400
+ if (input.currency && currency && input.currency !== currency) {
401
+ return {
402
+ valid: false,
403
+ reason: "currency_mismatch",
404
+ voucher: publicVoucher,
405
+ };
406
+ }
407
+ if (input.amountCents &&
408
+ remainingAmountCents !== null &&
409
+ input.amountCents > remainingAmountCents) {
410
+ return {
411
+ valid: false,
412
+ reason: "insufficient_balance",
413
+ voucher: publicVoucher,
414
+ };
415
+ }
416
+ return { valid: true, reason: null, voucher: publicVoucher };
417
+ },
418
+ };