@voyant-travel/finance 0.119.5
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 +201 -0
- package/README.md +192 -0
- package/dist/action-ledger-drift.d.ts +29 -0
- package/dist/action-ledger-drift.d.ts.map +1 -0
- package/dist/action-ledger-drift.js +166 -0
- package/dist/booking-tax.d.ts +124 -0
- package/dist/booking-tax.d.ts.map +1 -0
- package/dist/booking-tax.js +264 -0
- package/dist/checkout-routes.d.ts +1154 -0
- package/dist/checkout-routes.d.ts.map +1 -0
- package/dist/checkout-routes.js +116 -0
- package/dist/checkout-service-plan.d.ts +137 -0
- package/dist/checkout-service-plan.d.ts.map +1 -0
- package/dist/checkout-service-plan.js +119 -0
- package/dist/checkout-service.d.ts +9 -0
- package/dist/checkout-service.d.ts.map +1 -0
- package/dist/checkout-service.js +324 -0
- package/dist/checkout-validation.d.ts +1682 -0
- package/dist/checkout-validation.d.ts.map +1 -0
- package/dist/checkout-validation.js +228 -0
- package/dist/document-download.d.ts +3 -0
- package/dist/document-download.d.ts.map +1 -0
- package/dist/document-download.js +1 -0
- package/dist/fx-money.d.ts +17 -0
- package/dist/fx-money.d.ts.map +1 -0
- package/dist/fx-money.js +194 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +108 -0
- package/dist/invoice-fx.d.ts +134 -0
- package/dist/invoice-fx.d.ts.map +1 -0
- package/dist/invoice-fx.js +240 -0
- package/dist/invoice-number-errors.d.ts +2 -0
- package/dist/invoice-number-errors.d.ts.map +1 -0
- package/dist/invoice-number-errors.js +58 -0
- package/dist/markets-ref.d.ts +149 -0
- package/dist/markets-ref.d.ts.map +1 -0
- package/dist/markets-ref.js +17 -0
- package/dist/payment-link.d.ts +23 -0
- package/dist/payment-link.d.ts.map +1 -0
- package/dist/payment-link.js +30 -0
- package/dist/payment-policy.d.ts +113 -0
- package/dist/payment-policy.d.ts.map +1 -0
- package/dist/payment-policy.js +193 -0
- package/dist/route-runtime.d.ts +22 -0
- package/dist/route-runtime.d.ts.map +1 -0
- package/dist/route-runtime.js +18 -0
- package/dist/routes-action-ledger.d.ts +181 -0
- package/dist/routes-action-ledger.d.ts.map +1 -0
- package/dist/routes-action-ledger.js +142 -0
- package/dist/routes-booking-billing.d.ts +852 -0
- package/dist/routes-booking-billing.d.ts.map +1 -0
- package/dist/routes-booking-billing.js +223 -0
- package/dist/routes-booking-create.d.ts +3 -0
- package/dist/routes-booking-create.d.ts.map +1 -0
- package/dist/routes-booking-create.js +194 -0
- package/dist/routes-booking-reads.d.ts +46 -0
- package/dist/routes-booking-reads.d.ts.map +1 -0
- package/dist/routes-booking-reads.js +20 -0
- package/dist/routes-documents.d.ts +195 -0
- package/dist/routes-documents.d.ts.map +1 -0
- package/dist/routes-documents.js +93 -0
- package/dist/routes-invoice-core.d.ts +794 -0
- package/dist/routes-invoice-core.d.ts.map +1 -0
- package/dist/routes-invoice-core.js +238 -0
- package/dist/routes-invoice-documents.d.ts +401 -0
- package/dist/routes-invoice-documents.d.ts.map +1 -0
- package/dist/routes-invoice-documents.js +91 -0
- package/dist/routes-invoice-issue.d.ts +384 -0
- package/dist/routes-invoice-issue.d.ts.map +1 -0
- package/dist/routes-invoice-issue.js +208 -0
- package/dist/routes-payment-processing.d.ts +1193 -0
- package/dist/routes-payment-processing.d.ts.map +1 -0
- package/dist/routes-payment-processing.js +238 -0
- package/dist/routes-payments.d.ts +309 -0
- package/dist/routes-payments.d.ts.map +1 -0
- package/dist/routes-payments.js +94 -0
- package/dist/routes-public.d.ts +1948 -0
- package/dist/routes-public.d.ts.map +1 -0
- package/dist/routes-public.js +275 -0
- package/dist/routes-reference-data.d.ts +977 -0
- package/dist/routes-reference-data.d.ts.map +1 -0
- package/dist/routes-reference-data.js +191 -0
- package/dist/routes-reports.d.ts +344 -0
- package/dist/routes-reports.d.ts.map +1 -0
- package/dist/routes-reports.js +93 -0
- package/dist/routes-runtime.d.ts +71 -0
- package/dist/routes-runtime.d.ts.map +1 -0
- package/dist/routes-runtime.js +59 -0
- package/dist/routes-settlement.d.ts +67 -0
- package/dist/routes-settlement.d.ts.map +1 -0
- package/dist/routes-settlement.js +23 -0
- package/dist/routes-shared.d.ts +35 -0
- package/dist/routes-shared.d.ts.map +1 -0
- package/dist/routes-shared.js +10 -0
- package/dist/routes-supplier-invoices.d.ts +778 -0
- package/dist/routes-supplier-invoices.d.ts.map +1 -0
- package/dist/routes-supplier-invoices.js +159 -0
- package/dist/routes-vouchers.d.ts +228 -0
- package/dist/routes-vouchers.d.ts.map +1 -0
- package/dist/routes-vouchers.js +54 -0
- package/dist/routes.d.ts +5577 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +44 -0
- package/dist/schema/booking-billing.d.ts +1006 -0
- package/dist/schema/booking-billing.d.ts.map +1 -0
- package/dist/schema/booking-billing.js +106 -0
- package/dist/schema/enums.d.ts +48 -0
- package/dist/schema/enums.d.ts.map +1 -0
- package/dist/schema/enums.js +237 -0
- package/dist/schema/invoice-documents.d.ts +1245 -0
- package/dist/schema/invoice-documents.d.ts.map +1 -0
- package/dist/schema/invoice-documents.js +140 -0
- package/dist/schema/payment-instruments.d.ts +418 -0
- package/dist/schema/payment-instruments.d.ts.map +1 -0
- package/dist/schema/payment-instruments.js +45 -0
- package/dist/schema/payment-processing.d.ts +563 -0
- package/dist/schema/payment-processing.d.ts.map +1 -0
- package/dist/schema/payment-processing.js +65 -0
- package/dist/schema/payment-sessions.d.ts +728 -0
- package/dist/schema/payment-sessions.d.ts.map +1 -0
- package/dist/schema/payment-sessions.js +79 -0
- package/dist/schema/receivables.d.ts +1474 -0
- package/dist/schema/receivables.d.ts.map +1 -0
- package/dist/schema/receivables.js +179 -0
- package/dist/schema/relations.d.ts +82 -0
- package/dist/schema/relations.d.ts.map +1 -0
- package/dist/schema/relations.js +144 -0
- package/dist/schema/supplier-invoices.d.ts +1619 -0
- package/dist/schema/supplier-invoices.d.ts.map +1 -0
- package/dist/schema/supplier-invoices.js +228 -0
- package/dist/schema/tax.d.ts +712 -0
- package/dist/schema/tax.d.ts.map +1 -0
- package/dist/schema/tax.js +98 -0
- package/dist/schema/vouchers.d.ts +444 -0
- package/dist/schema/vouchers.d.ts.map +1 -0
- package/dist/schema/vouchers.js +64 -0
- package/dist/schema.d.ts +12 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +11 -0
- package/dist/service-accountant-shares.d.ts +106 -0
- package/dist/service-accountant-shares.d.ts.map +1 -0
- package/dist/service-accountant-shares.js +331 -0
- package/dist/service-action-ledger-accounting.d.ts +104 -0
- package/dist/service-action-ledger-accounting.d.ts.map +1 -0
- package/dist/service-action-ledger-accounting.js +386 -0
- package/dist/service-action-ledger-booking-payments.d.ts +48 -0
- package/dist/service-action-ledger-booking-payments.d.ts.map +1 -0
- package/dist/service-action-ledger-booking-payments.js +178 -0
- package/dist/service-action-ledger-bookings.d.ts +44 -0
- package/dist/service-action-ledger-bookings.d.ts.map +1 -0
- package/dist/service-action-ledger-bookings.js +81 -0
- package/dist/service-action-ledger-payment-authorizations.d.ts +48 -0
- package/dist/service-action-ledger-payment-authorizations.d.ts.map +1 -0
- package/dist/service-action-ledger-payment-authorizations.js +209 -0
- package/dist/service-action-ledger-payment-sessions.d.ts +83 -0
- package/dist/service-action-ledger-payment-sessions.d.ts.map +1 -0
- package/dist/service-action-ledger-payment-sessions.js +294 -0
- package/dist/service-action-ledger-supplier-invoices.d.ts +27 -0
- package/dist/service-action-ledger-supplier-invoices.d.ts.map +1 -0
- package/dist/service-action-ledger-supplier-invoices.js +111 -0
- package/dist/service-action-ledger-supplier-payments.d.ts +21 -0
- package/dist/service-action-ledger-supplier-payments.d.ts.map +1 -0
- package/dist/service-action-ledger-supplier-payments.js +97 -0
- package/dist/service-action-ledger.d.ts +7 -0
- package/dist/service-action-ledger.d.ts.map +1 -0
- package/dist/service-action-ledger.js +6 -0
- package/dist/service-aggregates.d.ts +96 -0
- package/dist/service-aggregates.d.ts.map +1 -0
- package/dist/service-aggregates.js +294 -0
- package/dist/service-booking-billing.d.ts +2322 -0
- package/dist/service-booking-billing.d.ts.map +1 -0
- package/dist/service-booking-billing.js +8 -0
- package/dist/service-booking-create.d.ts +410 -0
- package/dist/service-booking-create.d.ts.map +1 -0
- package/dist/service-booking-create.js +1256 -0
- package/dist/service-booking-guarantees.d.ts +725 -0
- package/dist/service-booking-guarantees.d.ts.map +1 -0
- package/dist/service-booking-guarantees.js +153 -0
- package/dist/service-booking-item-billing.d.ts +1062 -0
- package/dist/service-booking-item-billing.d.ts.map +1 -0
- package/dist/service-booking-item-billing.js +77 -0
- package/dist/service-booking-payment-schedules.d.ts +557 -0
- package/dist/service-booking-payment-schedules.d.ts.map +1 -0
- package/dist/service-booking-payment-schedules.js +372 -0
- package/dist/service-bookings-dual-create.d.ts +308 -0
- package/dist/service-bookings-dual-create.d.ts.map +1 -0
- package/dist/service-bookings-dual-create.js +131 -0
- package/dist/service-boundary-sql.d.ts +6 -0
- package/dist/service-boundary-sql.d.ts.map +1 -0
- package/dist/service-boundary-sql.js +15 -0
- package/dist/service-cost-categories.d.ts +26 -0
- package/dist/service-cost-categories.d.ts.map +1 -0
- package/dist/service-cost-categories.js +76 -0
- package/dist/service-documents.d.ts +80 -0
- package/dist/service-documents.d.ts.map +1 -0
- package/dist/service-documents.js +228 -0
- package/dist/service-invoice-artifacts.d.ts +246 -0
- package/dist/service-invoice-artifacts.d.ts.map +1 -0
- package/dist/service-invoice-artifacts.js +277 -0
- package/dist/service-invoice-core.d.ts +405 -0
- package/dist/service-invoice-core.d.ts.map +1 -0
- package/dist/service-invoice-core.js +290 -0
- package/dist/service-invoice-credit-notes.d.ts +973 -0
- package/dist/service-invoice-credit-notes.d.ts.map +1 -0
- package/dist/service-invoice-credit-notes.js +142 -0
- package/dist/service-invoice-from-booking.d.ts +41 -0
- package/dist/service-invoice-from-booking.d.ts.map +1 -0
- package/dist/service-invoice-from-booking.js +267 -0
- package/dist/service-invoice-line-items.d.ts +432 -0
- package/dist/service-invoice-line-items.d.ts.map +1 -0
- package/dist/service-invoice-line-items.js +102 -0
- package/dist/service-invoice-numbering.d.ts +227 -0
- package/dist/service-invoice-numbering.d.ts.map +1 -0
- package/dist/service-invoice-numbering.js +260 -0
- package/dist/service-invoice-payments.d.ts +673 -0
- package/dist/service-invoice-payments.d.ts.map +1 -0
- package/dist/service-invoice-payments.js +398 -0
- package/dist/service-invoices.d.ts +2501 -0
- package/dist/service-invoices.d.ts.map +1 -0
- package/dist/service-invoices.js +12 -0
- package/dist/service-issue.d.ts +207 -0
- package/dist/service-issue.d.ts.map +1 -0
- package/dist/service-issue.js +431 -0
- package/dist/service-payment-authorizations.d.ts +164 -0
- package/dist/service-payment-authorizations.d.ts.map +1 -0
- package/dist/service-payment-authorizations.js +227 -0
- package/dist/service-payment-instruments.d.ts +116 -0
- package/dist/service-payment-instruments.d.ts.map +1 -0
- package/dist/service-payment-instruments.js +99 -0
- package/dist/service-payment-processing.d.ts +676 -0
- package/dist/service-payment-processing.d.ts.map +1 -0
- package/dist/service-payment-processing.js +10 -0
- package/dist/service-payment-session-completion.d.ts +48 -0
- package/dist/service-payment-session-completion.d.ts.map +1 -0
- package/dist/service-payment-session-completion.js +238 -0
- package/dist/service-payment-sessions.d.ts +361 -0
- package/dist/service-payment-sessions.d.ts.map +1 -0
- package/dist/service-payment-sessions.js +280 -0
- package/dist/service-profitability.d.ts +114 -0
- package/dist/service-profitability.d.ts.map +1 -0
- package/dist/service-profitability.js +794 -0
- package/dist/service-public.d.ts +553 -0
- package/dist/service-public.d.ts.map +1 -0
- package/dist/service-public.js +583 -0
- package/dist/service-reference-data.d.ts +272 -0
- package/dist/service-reference-data.d.ts.map +1 -0
- package/dist/service-reference-data.js +280 -0
- package/dist/service-rendition-wait.d.ts +38 -0
- package/dist/service-rendition-wait.d.ts.map +1 -0
- package/dist/service-rendition-wait.js +67 -0
- package/dist/service-reports.d.ts +37 -0
- package/dist/service-reports.d.ts.map +1 -0
- package/dist/service-reports.js +62 -0
- package/dist/service-settlement.d.ts +46 -0
- package/dist/service-settlement.d.ts.map +1 -0
- package/dist/service-settlement.js +185 -0
- package/dist/service-shared.d.ts +541 -0
- package/dist/service-shared.d.ts.map +1 -0
- package/dist/service-shared.js +764 -0
- package/dist/service-supplier-invoices.d.ts +871 -0
- package/dist/service-supplier-invoices.d.ts.map +1 -0
- package/dist/service-supplier-invoices.js +744 -0
- package/dist/service-supplier-payments.d.ts +69 -0
- package/dist/service-supplier-payments.d.ts.map +1 -0
- package/dist/service-supplier-payments.js +136 -0
- package/dist/service-vouchers-migration.d.ts +44 -0
- package/dist/service-vouchers-migration.d.ts.map +1 -0
- package/dist/service-vouchers-migration.js +148 -0
- package/dist/service-vouchers.d.ts +157 -0
- package/dist/service-vouchers.d.ts.map +1 -0
- package/dist/service-vouchers.js +191 -0
- package/dist/service.d.ts +6490 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +29 -0
- package/dist/validation-billing.d.ts +2 -0
- package/dist/validation-billing.d.ts.map +1 -0
- package/dist/validation-billing.js +1 -0
- package/dist/validation-payments.d.ts +2 -0
- package/dist/validation-payments.d.ts.map +1 -0
- package/dist/validation-payments.js +1 -0
- package/dist/validation-public.d.ts +2 -0
- package/dist/validation-public.d.ts.map +1 -0
- package/dist/validation-public.js +1 -0
- package/dist/validation-shared.d.ts +2 -0
- package/dist/validation-shared.d.ts.map +1 -0
- package/dist/validation-shared.js +1 -0
- package/dist/validation-vouchers.d.ts +2 -0
- package/dist/validation-vouchers.d.ts.map +1 -0
- package/dist/validation-vouchers.js +1 -0
- package/dist/validation.d.ts +2 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +1 -0
- package/package.json +121 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-bookings-dual-create.d.ts","sourceRoot":"","sources":["../src/service-bookings-dual-create.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAA;AACtF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AACjE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACzD,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EAGzB,MAAM,6BAA6B,CAAA;AAkBpC,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAIlC,CAAA;AAEF,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAA;AAI5E,MAAM,WAAW,wBAAyB,SAAQ,qBAAqB;CAAG;AAE1E,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAA;IACf,gBAAgB,EAAE,MAAM,CAAA;IACxB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,SAAS,EAAE,MAAM,CAAA;IACjB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,UAAU,EAAE,IAAI,CAAA;CACjB;AAID,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,mBAAmB,CAAA;IAC5B,SAAS,EAAE,mBAAmB,CAAA;IAC9B,KAAK,EAAE,YAAY,CAAA;IACnB,aAAa,EAAE,kBAAkB,CAAA;IACjC,eAAe,EAAE,kBAAkB,CAAA;CACpC;AAED,MAAM,MAAM,wBAAwB,GAChC;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,uBAAuB,CAAA;CAAE,GACjD;IACE,MAAM,EAAE,gBAAgB,GAAG,kBAAkB,CAAA;IAC7C,MAAM,EAAE,OAAO,CAAC,oBAAoB,EAAE;QAAE,MAAM,EAAE,IAAI,CAAA;KAAE,CAAC,CAAA;CACxD,CAAA;AAqBL;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,kBAAkB,EACtB,QAAQ,EAAE,sBAAsB,EAChC,OAAO,GAAE;IACP,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,wBAAwB,CAAA;CAC9B,GACL,OAAO,CAAC,wBAAwB,CAAC,CAkFnC"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { bookingGroupsService } from "@voyant-travel/bookings";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { bookingCreateSubSchema, createBooking, } from "./service-booking-create.js";
|
|
4
|
+
// ---------- validation ----------
|
|
5
|
+
/**
|
|
6
|
+
* Sub-booking input. Takes the full create payload minus `group
|
|
7
|
+
* Membership` — dual-create owns the group lifecycle (one new group, both
|
|
8
|
+
* bookings linked as members) so accepting a nested group override would
|
|
9
|
+
* just be an opportunity for the caller to desync.
|
|
10
|
+
*/
|
|
11
|
+
const dualSubBookingSchema = bookingCreateSubSchema;
|
|
12
|
+
const dualCreateGroupSchema = z.object({
|
|
13
|
+
kind: z.enum(["shared_room", "other"]).default("shared_room"),
|
|
14
|
+
label: z.string().max(255).optional().nullable(),
|
|
15
|
+
optionUnitId: z.string().optional().nullable(),
|
|
16
|
+
});
|
|
17
|
+
export const dualCreateBookingSchema = z.object({
|
|
18
|
+
primary: dualSubBookingSchema,
|
|
19
|
+
secondary: dualSubBookingSchema,
|
|
20
|
+
group: dualCreateGroupSchema.default({ kind: "shared_room" }),
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* Thrown inside the outer tx to force drizzle to roll back both bookings +
|
|
24
|
+
* the group when one of the inner create calls returns non-ok.
|
|
25
|
+
* Drizzle doesn't abort on a non-throwing tx callback, so we convert the
|
|
26
|
+
* discriminated outcome into a throw here.
|
|
27
|
+
*/
|
|
28
|
+
class DualCreateAbort extends Error {
|
|
29
|
+
outcome;
|
|
30
|
+
constructor(outcome) {
|
|
31
|
+
super(outcome.status === "ok"
|
|
32
|
+
? "dual-create aborted: ok (unexpected)"
|
|
33
|
+
: `dual-create aborted: ${outcome.status}:${outcome.reason.status}`);
|
|
34
|
+
this.outcome = outcome;
|
|
35
|
+
this.name = "DualCreateAbort";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// ---------- service ----------
|
|
39
|
+
/**
|
|
40
|
+
* Create two bookings linked via a new `booking_group`, atomically. The
|
|
41
|
+
* canonical operator flow: two travelers book a shared room ("partaj"), each
|
|
42
|
+
* gets their own booking, and both are attached to a new shared_room group
|
|
43
|
+
* so subsequent payment / cancellation decisions can fan out across the
|
|
44
|
+
* pair.
|
|
45
|
+
*
|
|
46
|
+
* Transaction shape:
|
|
47
|
+
* - Outer tx opens via `db.transaction`.
|
|
48
|
+
* - Inner: two savepoint-scoped `createBooking(tx, ...)` calls — the
|
|
49
|
+
* nested transactions drizzle opens use SAVEPOINTs, so partial failures
|
|
50
|
+
* surface up to this layer as non-ok outcomes.
|
|
51
|
+
* - If either fails, the outer tx throws `DualCreateAbort` so the whole
|
|
52
|
+
* thing rolls back (no orphan booking, no orphan group).
|
|
53
|
+
* - Group creation + both memberships run last, inside the same outer tx.
|
|
54
|
+
*
|
|
55
|
+
* Event emission (`booking.dual-created`) is post-commit — subscribers only
|
|
56
|
+
* hear about successful pairs.
|
|
57
|
+
*/
|
|
58
|
+
export async function dualCreateBooking(db, rawInput, options = {}) {
|
|
59
|
+
const { userId, runtime } = options;
|
|
60
|
+
const input = dualCreateBookingSchema.parse(rawInput);
|
|
61
|
+
let result;
|
|
62
|
+
try {
|
|
63
|
+
result = await db.transaction(async (tx) => {
|
|
64
|
+
const primaryOutcome = await createBooking(tx, input.primary, { userId, runtime });
|
|
65
|
+
if (primaryOutcome.status !== "ok") {
|
|
66
|
+
throw new DualCreateAbort({ status: "primary_failed", reason: primaryOutcome });
|
|
67
|
+
}
|
|
68
|
+
const secondaryOutcome = await createBooking(tx, input.secondary, { userId, runtime });
|
|
69
|
+
if (secondaryOutcome.status !== "ok") {
|
|
70
|
+
throw new DualCreateAbort({ status: "secondary_failed", reason: secondaryOutcome });
|
|
71
|
+
}
|
|
72
|
+
const primaryBooking = primaryOutcome.result.booking;
|
|
73
|
+
const secondaryBooking = secondaryOutcome.result.booking;
|
|
74
|
+
const group = await bookingGroupsService.createBookingGroup(tx, {
|
|
75
|
+
kind: input.group.kind,
|
|
76
|
+
label: input.group.label ??
|
|
77
|
+
`Shared — ${primaryBooking.bookingNumber} + ${secondaryBooking.bookingNumber}`,
|
|
78
|
+
productId: input.primary.productId,
|
|
79
|
+
optionUnitId: input.group.optionUnitId ?? null,
|
|
80
|
+
primaryBookingId: primaryBooking.id,
|
|
81
|
+
});
|
|
82
|
+
const primaryMemberResult = await bookingGroupsService.addGroupMember(tx, group.id, {
|
|
83
|
+
bookingId: primaryBooking.id,
|
|
84
|
+
role: "primary",
|
|
85
|
+
});
|
|
86
|
+
if (primaryMemberResult.status !== "ok") {
|
|
87
|
+
// Shouldn't happen — we just created both rows in this tx — but bail
|
|
88
|
+
// through the same abort path to unwind cleanly.
|
|
89
|
+
throw new DualCreateAbort({
|
|
90
|
+
status: "primary_failed",
|
|
91
|
+
reason: { status: "group_not_found" },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const secondaryMemberResult = await bookingGroupsService.addGroupMember(tx, group.id, {
|
|
95
|
+
bookingId: secondaryBooking.id,
|
|
96
|
+
role: "shared",
|
|
97
|
+
});
|
|
98
|
+
if (secondaryMemberResult.status !== "ok") {
|
|
99
|
+
throw new DualCreateAbort({
|
|
100
|
+
status: "secondary_failed",
|
|
101
|
+
reason: { status: "group_not_found" },
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
primary: primaryOutcome.result,
|
|
106
|
+
secondary: secondaryOutcome.result,
|
|
107
|
+
group,
|
|
108
|
+
primaryMember: primaryMemberResult.member,
|
|
109
|
+
secondaryMember: secondaryMemberResult.member,
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (error instanceof DualCreateAbort) {
|
|
115
|
+
return error.outcome;
|
|
116
|
+
}
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
if (runtime?.eventBus) {
|
|
120
|
+
const event = {
|
|
121
|
+
groupId: result.group.id,
|
|
122
|
+
primaryBookingId: result.primary.booking.id,
|
|
123
|
+
secondaryBookingId: result.secondary.booking.id,
|
|
124
|
+
productId: input.primary.productId,
|
|
125
|
+
createdByUserId: userId ?? null,
|
|
126
|
+
occurredAt: new Date(),
|
|
127
|
+
};
|
|
128
|
+
await runtime.eventBus.emit("booking.dual-created", event);
|
|
129
|
+
}
|
|
130
|
+
return { status: "ok", result };
|
|
131
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type SQL } from "drizzle-orm";
|
|
2
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
3
|
+
export declare function executeBoundaryRows<T extends object>(db: PostgresJsDatabase, query: SQL): Promise<T[]>;
|
|
4
|
+
export declare function sqlList(values: readonly string[]): SQL;
|
|
5
|
+
export declare function normalizeDateOnly(value: Date | string | null | undefined): string | null;
|
|
6
|
+
//# sourceMappingURL=service-boundary-sql.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-boundary-sql.d.ts","sourceRoot":"","sources":["../src/service-boundary-sql.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,GAAG,EAAO,MAAM,aAAa,CAAA;AAC3C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAEjE,wBAAsB,mBAAmB,CAAC,CAAC,SAAS,MAAM,EACxD,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,GAAG,GACT,OAAO,CAAC,CAAC,EAAE,CAAC,CAId;AAED,wBAAgB,OAAO,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,GAAG,CAMtD;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAGxF"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
export async function executeBoundaryRows(db, query) {
|
|
3
|
+
// biome-ignore lint/suspicious/noExplicitAny: #1141 keeps cross-package SQL boundary reads driver-agnostic.
|
|
4
|
+
const result = await db.execute(query);
|
|
5
|
+
return (Array.isArray(result) ? result : (result?.rows ?? []));
|
|
6
|
+
}
|
|
7
|
+
export function sqlList(values) {
|
|
8
|
+
// agent-quality: raw-sql reviewed -- owner: finance; callers pass only parameter-bound scalar ids into the joined SQL fragment.
|
|
9
|
+
return sql.join(values.map((value) => sql `${value}`), sql `, `);
|
|
10
|
+
}
|
|
11
|
+
export function normalizeDateOnly(value) {
|
|
12
|
+
if (!value)
|
|
13
|
+
return null;
|
|
14
|
+
return value instanceof Date ? value.toISOString().slice(0, 10) : value.slice(0, 10);
|
|
15
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
2
|
+
export interface CostCategoryRecord {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
sortOrder: number;
|
|
6
|
+
archived: boolean;
|
|
7
|
+
createdAt: string;
|
|
8
|
+
updatedAt: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const costCategoriesService: {
|
|
11
|
+
list(db: PostgresJsDatabase, options?: {
|
|
12
|
+
includeArchived?: boolean;
|
|
13
|
+
}): Promise<CostCategoryRecord[]>;
|
|
14
|
+
create(db: PostgresJsDatabase, input: {
|
|
15
|
+
name: string;
|
|
16
|
+
sortOrder?: number;
|
|
17
|
+
}): Promise<CostCategoryRecord>;
|
|
18
|
+
update(db: PostgresJsDatabase, id: string, input: {
|
|
19
|
+
name?: string;
|
|
20
|
+
sortOrder?: number;
|
|
21
|
+
archived?: boolean;
|
|
22
|
+
}): Promise<CostCategoryRecord | null>;
|
|
23
|
+
/** Resolve id → name for a set of category ids (for breakdown labelling). */
|
|
24
|
+
nameMap(db: PostgresJsDatabase, ids: string[]): Promise<Map<string, string>>;
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=service-cost-categories.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-cost-categories.d.ts","sourceRoot":"","sources":["../src/service-cost-categories.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAkBjE,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,OAAO,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB;AAaD,eAAO,MAAM,qBAAqB;aAE1B,kBAAkB,YACb;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GACrC,OAAO,CAAC,kBAAkB,EAAE,CAAC;eAiB1B,kBAAkB,SACf;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAC1C,OAAO,CAAC,kBAAkB,CAAC;eAUxB,kBAAkB,MAClB,MAAM,SACH;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAC/D,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IAarC,6EAA6E;gBAC3D,kBAAkB,OAAO,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CASnF,CAAA"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { asc, eq, isNull, sql } from "drizzle-orm";
|
|
2
|
+
import { costCategories } from "./schema.js";
|
|
3
|
+
/**
|
|
4
|
+
* Operator-configurable cost categories. Used to classify supplier-invoice
|
|
5
|
+
* lines and drive the per-category cost breakdown. Seeded lazily with sensible
|
|
6
|
+
* defaults the first time the list is read, so a fresh operator already has
|
|
7
|
+
* transportation / accommodation / guides / other to pick from.
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_CATEGORIES = [
|
|
10
|
+
"Transportation",
|
|
11
|
+
"Accommodation",
|
|
12
|
+
"Guides / touristic services",
|
|
13
|
+
"Other",
|
|
14
|
+
];
|
|
15
|
+
function toRecord(row) {
|
|
16
|
+
return {
|
|
17
|
+
id: row.id,
|
|
18
|
+
name: row.name,
|
|
19
|
+
sortOrder: row.sortOrder,
|
|
20
|
+
archived: row.archivedAt != null,
|
|
21
|
+
createdAt: row.createdAt.toISOString(),
|
|
22
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export const costCategoriesService = {
|
|
26
|
+
async list(db, options = {}) {
|
|
27
|
+
const existing = await db.select().from(costCategories);
|
|
28
|
+
if (existing.length === 0) {
|
|
29
|
+
await db
|
|
30
|
+
.insert(costCategories)
|
|
31
|
+
.values(DEFAULT_CATEGORIES.map((name, index) => ({ name, sortOrder: index })))
|
|
32
|
+
.onConflictDoNothing();
|
|
33
|
+
}
|
|
34
|
+
const rows = await db
|
|
35
|
+
.select()
|
|
36
|
+
.from(costCategories)
|
|
37
|
+
.where(options.includeArchived ? undefined : isNull(costCategories.archivedAt))
|
|
38
|
+
.orderBy(asc(costCategories.sortOrder), asc(costCategories.name));
|
|
39
|
+
return rows.map(toRecord);
|
|
40
|
+
},
|
|
41
|
+
async create(db, input) {
|
|
42
|
+
const [row] = await db
|
|
43
|
+
.insert(costCategories)
|
|
44
|
+
.values({ name: input.name.trim(), sortOrder: input.sortOrder ?? 0 })
|
|
45
|
+
.returning();
|
|
46
|
+
if (!row)
|
|
47
|
+
throw new Error("Failed to create cost category");
|
|
48
|
+
return toRecord(row);
|
|
49
|
+
},
|
|
50
|
+
async update(db, id, input) {
|
|
51
|
+
const patch = { updatedAt: new Date() };
|
|
52
|
+
if (input.name !== undefined)
|
|
53
|
+
patch.name = input.name.trim();
|
|
54
|
+
if (input.sortOrder !== undefined)
|
|
55
|
+
patch.sortOrder = input.sortOrder;
|
|
56
|
+
if (input.archived !== undefined)
|
|
57
|
+
patch.archivedAt = input.archived ? new Date() : null;
|
|
58
|
+
const [row] = await db
|
|
59
|
+
.update(costCategories)
|
|
60
|
+
.set(patch)
|
|
61
|
+
.where(eq(costCategories.id, id))
|
|
62
|
+
.returning();
|
|
63
|
+
return row ? toRecord(row) : null;
|
|
64
|
+
},
|
|
65
|
+
/** Resolve id → name for a set of category ids (for breakdown labelling). */
|
|
66
|
+
async nameMap(db, ids) {
|
|
67
|
+
if (ids.length === 0)
|
|
68
|
+
return new Map();
|
|
69
|
+
const rows = await db
|
|
70
|
+
.select({ id: costCategories.id, name: costCategories.name })
|
|
71
|
+
.from(costCategories)
|
|
72
|
+
// agent-quality: raw-sql reviewed -- owner: finance; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
|
|
73
|
+
.where(sql `${costCategories.id} = any(${ids})`);
|
|
74
|
+
return new Map(rows.map((r) => [r.id, r.name]));
|
|
75
|
+
},
|
|
76
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { EventBus } from "@voyant-travel/core";
|
|
2
|
+
import type { StorageProvider, StorageUploadBody } from "@voyant-travel/storage";
|
|
3
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
4
|
+
import { type invoiceLineItems, type invoiceRenditions, type invoices, invoiceTemplates, type payments } from "./schema.js";
|
|
5
|
+
import type { GenerateInvoiceDocumentInput } from "./validation.js";
|
|
6
|
+
export interface GeneratedInvoiceRenditionArtifact {
|
|
7
|
+
format?: "html" | "pdf" | "xml" | "json";
|
|
8
|
+
storageKey?: string | null;
|
|
9
|
+
contentType?: string | null;
|
|
10
|
+
fileSize?: number | null;
|
|
11
|
+
checksum?: string | null;
|
|
12
|
+
language?: string | null;
|
|
13
|
+
metadata?: Record<string, unknown> | null;
|
|
14
|
+
}
|
|
15
|
+
export interface InvoiceDocumentGeneratorContext {
|
|
16
|
+
db: PostgresJsDatabase;
|
|
17
|
+
invoice: typeof invoices.$inferSelect;
|
|
18
|
+
template: typeof invoiceTemplates.$inferSelect | null;
|
|
19
|
+
lineItems: Array<typeof invoiceLineItems.$inferSelect>;
|
|
20
|
+
payments: Array<typeof payments.$inferSelect>;
|
|
21
|
+
renderedBody: string;
|
|
22
|
+
renderedBodyFormat: "html" | "markdown" | "lexical_json";
|
|
23
|
+
variables: Record<string, unknown>;
|
|
24
|
+
bindings: Record<string, unknown>;
|
|
25
|
+
targetFormat: "html" | "pdf" | "xml" | "json";
|
|
26
|
+
language: string | null;
|
|
27
|
+
}
|
|
28
|
+
export type InvoiceDocumentGenerator = (context: InvoiceDocumentGeneratorContext) => Promise<GeneratedInvoiceRenditionArtifact>;
|
|
29
|
+
export interface InvoiceDocumentRuntimeOptions {
|
|
30
|
+
bindings?: Record<string, unknown>;
|
|
31
|
+
generator: InvoiceDocumentGenerator;
|
|
32
|
+
eventBus?: EventBus;
|
|
33
|
+
}
|
|
34
|
+
export interface StorageBackedInvoiceDocumentUpload {
|
|
35
|
+
body: StorageUploadBody;
|
|
36
|
+
format?: "html" | "pdf" | "xml" | "json";
|
|
37
|
+
key?: string | null;
|
|
38
|
+
metadata?: Record<string, unknown> | null;
|
|
39
|
+
language?: string | null;
|
|
40
|
+
}
|
|
41
|
+
export type StorageBackedInvoiceDocumentSerializer = (context: InvoiceDocumentGeneratorContext) => Promise<StorageBackedInvoiceDocumentUpload> | StorageBackedInvoiceDocumentUpload;
|
|
42
|
+
export interface StorageBackedInvoiceDocumentGeneratorOptions {
|
|
43
|
+
storage: StorageProvider;
|
|
44
|
+
keyPrefix?: string | ((context: InvoiceDocumentGeneratorContext) => Promise<string> | string);
|
|
45
|
+
serializer?: StorageBackedInvoiceDocumentSerializer;
|
|
46
|
+
}
|
|
47
|
+
export interface GeneratedInvoiceDocumentRecord {
|
|
48
|
+
invoiceId: string;
|
|
49
|
+
renderedBodyFormat: "html" | "markdown" | "lexical_json";
|
|
50
|
+
renderedBody: string;
|
|
51
|
+
rendition: typeof invoiceRenditions.$inferSelect;
|
|
52
|
+
}
|
|
53
|
+
export interface InvoiceDocumentGeneratedEvent {
|
|
54
|
+
invoiceId: string;
|
|
55
|
+
invoiceStatus: (typeof invoices.$inferSelect)["status"];
|
|
56
|
+
invoiceType: (typeof invoices.$inferSelect)["invoiceType"];
|
|
57
|
+
renditionId: string;
|
|
58
|
+
format: (typeof invoiceRenditions.$inferSelect)["format"];
|
|
59
|
+
renderedBodyFormat: "html" | "markdown" | "lexical_json";
|
|
60
|
+
regenerated: boolean;
|
|
61
|
+
}
|
|
62
|
+
export declare function defaultStorageBackedInvoiceDocumentSerializer(context: InvoiceDocumentGeneratorContext): Promise<StorageBackedInvoiceDocumentUpload> | StorageBackedInvoiceDocumentUpload;
|
|
63
|
+
export declare function defaultPdfInvoiceDocumentSerializer(context: InvoiceDocumentGeneratorContext): Promise<StorageBackedInvoiceDocumentUpload>;
|
|
64
|
+
export declare function createStorageBackedInvoiceDocumentGenerator(options: StorageBackedInvoiceDocumentGeneratorOptions): InvoiceDocumentGenerator;
|
|
65
|
+
export declare function createPdfInvoiceDocumentGenerator(options: Omit<StorageBackedInvoiceDocumentGeneratorOptions, "serializer">): InvoiceDocumentGenerator;
|
|
66
|
+
export declare const financeDocumentsService: {
|
|
67
|
+
generateInvoiceDocument(db: PostgresJsDatabase, invoiceId: string, input: GenerateInvoiceDocumentInput, runtime: InvoiceDocumentRuntimeOptions, options?: {
|
|
68
|
+
regenerated?: boolean;
|
|
69
|
+
}): Promise<{
|
|
70
|
+
status: "not_found" | "generator_failed";
|
|
71
|
+
} | ({
|
|
72
|
+
status: "generated";
|
|
73
|
+
} & GeneratedInvoiceDocumentRecord)>;
|
|
74
|
+
regenerateInvoiceDocument(db: PostgresJsDatabase, invoiceId: string, input: GenerateInvoiceDocumentInput, runtime: InvoiceDocumentRuntimeOptions): Promise<{
|
|
75
|
+
status: "not_found" | "generator_failed";
|
|
76
|
+
} | ({
|
|
77
|
+
status: "generated";
|
|
78
|
+
} & GeneratedInvoiceDocumentRecord)>;
|
|
79
|
+
};
|
|
80
|
+
//# sourceMappingURL=service-documents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-documents.d.ts","sourceRoot":"","sources":["../src/service-documents.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { renderPdfDocument } from "@voyant-travel/utils/pdf-renderer";
|
|
2
|
+
import { and, desc, eq } from "drizzle-orm";
|
|
3
|
+
import { invoiceTemplates, } from "./schema.js";
|
|
4
|
+
import { financeService, renderInvoiceBody } from "./service.js";
|
|
5
|
+
function defaultInvoiceDocumentMimeType(format) {
|
|
6
|
+
switch (format) {
|
|
7
|
+
case "html":
|
|
8
|
+
return "text/html; charset=utf-8";
|
|
9
|
+
case "json":
|
|
10
|
+
return "application/json; charset=utf-8";
|
|
11
|
+
case "xml":
|
|
12
|
+
return "application/xml; charset=utf-8";
|
|
13
|
+
default:
|
|
14
|
+
return "application/pdf";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function encodeStringBody(value) {
|
|
18
|
+
return new TextEncoder().encode(value);
|
|
19
|
+
}
|
|
20
|
+
function getBodySize(body) {
|
|
21
|
+
if (body instanceof Uint8Array)
|
|
22
|
+
return body.byteLength;
|
|
23
|
+
if (body instanceof ArrayBuffer)
|
|
24
|
+
return body.byteLength;
|
|
25
|
+
return body.size;
|
|
26
|
+
}
|
|
27
|
+
function toUploadMetadata(metadata) {
|
|
28
|
+
const entries = Object.entries(metadata ?? {}).filter(([, value]) => ["string", "number", "boolean"].includes(typeof value));
|
|
29
|
+
return entries.length > 0
|
|
30
|
+
? Object.fromEntries(entries.map(([key, value]) => [key, String(value)]))
|
|
31
|
+
: undefined;
|
|
32
|
+
}
|
|
33
|
+
export function defaultStorageBackedInvoiceDocumentSerializer(context) {
|
|
34
|
+
switch (context.targetFormat) {
|
|
35
|
+
case "html":
|
|
36
|
+
return {
|
|
37
|
+
body: encodeStringBody(context.renderedBody),
|
|
38
|
+
format: "html",
|
|
39
|
+
language: context.language,
|
|
40
|
+
metadata: { renderedBodyFormat: context.renderedBodyFormat },
|
|
41
|
+
};
|
|
42
|
+
case "json":
|
|
43
|
+
return {
|
|
44
|
+
body: encodeStringBody(JSON.stringify(context.variables, null, 2)),
|
|
45
|
+
format: "json",
|
|
46
|
+
language: context.language,
|
|
47
|
+
metadata: { renderedBodyFormat: context.renderedBodyFormat },
|
|
48
|
+
};
|
|
49
|
+
case "xml":
|
|
50
|
+
return {
|
|
51
|
+
body: encodeStringBody(context.renderedBody),
|
|
52
|
+
format: "xml",
|
|
53
|
+
language: context.language,
|
|
54
|
+
metadata: { renderedBodyFormat: context.renderedBodyFormat },
|
|
55
|
+
};
|
|
56
|
+
default:
|
|
57
|
+
return defaultPdfInvoiceDocumentSerializer(context);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export async function defaultPdfInvoiceDocumentSerializer(context) {
|
|
61
|
+
const body = await renderPdfDocument({
|
|
62
|
+
title: `Invoice ${context.invoice.id}`,
|
|
63
|
+
content: context.renderedBody,
|
|
64
|
+
format: context.renderedBodyFormat === "lexical_json"
|
|
65
|
+
? "lexical_json"
|
|
66
|
+
: context.renderedBodyFormat === "html"
|
|
67
|
+
? "html"
|
|
68
|
+
: "markdown",
|
|
69
|
+
metadataLines: [
|
|
70
|
+
`Invoice ID: ${context.invoice.id}`,
|
|
71
|
+
...(context.language ? [`Language: ${context.language}`] : []),
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
body,
|
|
76
|
+
format: "pdf",
|
|
77
|
+
language: context.language,
|
|
78
|
+
metadata: {
|
|
79
|
+
renderedBodyFormat: context.renderedBodyFormat,
|
|
80
|
+
renderer: "voyant-basic-pdf",
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export function createStorageBackedInvoiceDocumentGenerator(options) {
|
|
85
|
+
const serializer = options.serializer ?? defaultStorageBackedInvoiceDocumentSerializer;
|
|
86
|
+
return async (context) => {
|
|
87
|
+
const upload = await serializer(context);
|
|
88
|
+
const format = upload.format ?? context.targetFormat;
|
|
89
|
+
const keyPrefix = typeof options.keyPrefix === "function"
|
|
90
|
+
? await options.keyPrefix(context)
|
|
91
|
+
: (options.keyPrefix ?? `invoices/${context.invoice.id}`);
|
|
92
|
+
const key = upload.key?.trim() || `${keyPrefix.replace(/\/$/, "")}/rendition.${format}`;
|
|
93
|
+
const uploaded = await options.storage.upload(upload.body, {
|
|
94
|
+
key,
|
|
95
|
+
contentType: defaultInvoiceDocumentMimeType(format),
|
|
96
|
+
metadata: toUploadMetadata(upload.metadata),
|
|
97
|
+
});
|
|
98
|
+
return {
|
|
99
|
+
format,
|
|
100
|
+
storageKey: uploaded.key,
|
|
101
|
+
contentType: defaultInvoiceDocumentMimeType(format),
|
|
102
|
+
fileSize: getBodySize(upload.body),
|
|
103
|
+
language: upload.language ?? context.language,
|
|
104
|
+
metadata: {
|
|
105
|
+
...(upload.metadata ?? {}),
|
|
106
|
+
storageProvider: options.storage.name,
|
|
107
|
+
...(uploaded.url ? { url: uploaded.url } : {}),
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export function createPdfInvoiceDocumentGenerator(options) {
|
|
113
|
+
return createStorageBackedInvoiceDocumentGenerator({
|
|
114
|
+
...options,
|
|
115
|
+
serializer: defaultPdfInvoiceDocumentSerializer,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
async function prepareInvoiceDocument(db, invoiceId, input) {
|
|
119
|
+
const invoice = await financeService.getInvoiceById(db, invoiceId);
|
|
120
|
+
if (!invoice) {
|
|
121
|
+
return { status: "not_found" };
|
|
122
|
+
}
|
|
123
|
+
let templateId = input.templateId ?? invoice.templateId ?? null;
|
|
124
|
+
if (!templateId) {
|
|
125
|
+
const [defaultTemplate] = await db
|
|
126
|
+
.select()
|
|
127
|
+
.from(invoiceTemplates)
|
|
128
|
+
.where(and(eq(invoiceTemplates.isDefault, true), eq(invoiceTemplates.active, true)))
|
|
129
|
+
.orderBy(desc(invoiceTemplates.updatedAt))
|
|
130
|
+
.limit(1);
|
|
131
|
+
templateId = defaultTemplate?.id ?? null;
|
|
132
|
+
}
|
|
133
|
+
const [template, lineItems, paymentRows] = await Promise.all([
|
|
134
|
+
templateId ? financeService.getInvoiceTemplateById(db, templateId) : Promise.resolve(null),
|
|
135
|
+
financeService.listInvoiceLineItems(db, invoiceId),
|
|
136
|
+
financeService.listPayments(db, invoiceId),
|
|
137
|
+
]);
|
|
138
|
+
const renderedBodyFormat = template?.bodyFormat ?? "html";
|
|
139
|
+
const variables = {
|
|
140
|
+
invoice,
|
|
141
|
+
lineItems,
|
|
142
|
+
payments: paymentRows,
|
|
143
|
+
};
|
|
144
|
+
const renderedBody = template
|
|
145
|
+
? renderInvoiceBody(template.body, template.bodyFormat, variables)
|
|
146
|
+
: JSON.stringify(variables);
|
|
147
|
+
return {
|
|
148
|
+
status: "ready",
|
|
149
|
+
invoice,
|
|
150
|
+
template,
|
|
151
|
+
lineItems,
|
|
152
|
+
payments: paymentRows,
|
|
153
|
+
renderedBody,
|
|
154
|
+
renderedBodyFormat,
|
|
155
|
+
variables,
|
|
156
|
+
targetFormat: input.format,
|
|
157
|
+
language: input.language ?? invoice.language ?? template?.language ?? null,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
export const financeDocumentsService = {
|
|
161
|
+
async generateInvoiceDocument(db, invoiceId, input, runtime, options = {}) {
|
|
162
|
+
const prepared = await prepareInvoiceDocument(db, invoiceId, input);
|
|
163
|
+
if (prepared.status === "not_found") {
|
|
164
|
+
return { status: "not_found" };
|
|
165
|
+
}
|
|
166
|
+
let artifact;
|
|
167
|
+
try {
|
|
168
|
+
artifact = await runtime.generator({
|
|
169
|
+
db,
|
|
170
|
+
invoice: prepared.invoice,
|
|
171
|
+
template: prepared.template,
|
|
172
|
+
lineItems: prepared.lineItems,
|
|
173
|
+
payments: prepared.payments,
|
|
174
|
+
renderedBody: prepared.renderedBody,
|
|
175
|
+
renderedBodyFormat: prepared.renderedBodyFormat,
|
|
176
|
+
variables: prepared.variables,
|
|
177
|
+
bindings: runtime.bindings ?? {},
|
|
178
|
+
targetFormat: prepared.targetFormat,
|
|
179
|
+
language: prepared.language,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return { status: "generator_failed" };
|
|
184
|
+
}
|
|
185
|
+
const format = artifact.format ?? prepared.targetFormat;
|
|
186
|
+
const bindResult = await financeService.bindInvoiceRendition(db, invoiceId, {
|
|
187
|
+
templateId: prepared.template?.id ?? null,
|
|
188
|
+
format,
|
|
189
|
+
storageKey: artifact.storageKey?.trim() || null,
|
|
190
|
+
contentType: artifact.contentType ?? defaultInvoiceDocumentMimeType(format),
|
|
191
|
+
fileSize: artifact.fileSize ?? null,
|
|
192
|
+
checksum: artifact.checksum ?? null,
|
|
193
|
+
language: artifact.language ?? prepared.language ?? null,
|
|
194
|
+
generatedAt: new Date().toISOString(),
|
|
195
|
+
metadata: {
|
|
196
|
+
...(artifact.metadata ?? {}),
|
|
197
|
+
renderedBodyFormat: prepared.renderedBodyFormat,
|
|
198
|
+
},
|
|
199
|
+
replaceExisting: input.replaceExisting,
|
|
200
|
+
}, { eventBus: runtime.eventBus });
|
|
201
|
+
if (bindResult.status !== "bound") {
|
|
202
|
+
return { status: "not_found" };
|
|
203
|
+
}
|
|
204
|
+
const { rendition } = bindResult;
|
|
205
|
+
await runtime.eventBus?.emit("invoice.document.generated", {
|
|
206
|
+
invoiceId: prepared.invoice.id,
|
|
207
|
+
invoiceStatus: prepared.invoice.status,
|
|
208
|
+
invoiceType: prepared.invoice.invoiceType,
|
|
209
|
+
renditionId: rendition.id,
|
|
210
|
+
format: rendition.format,
|
|
211
|
+
renderedBodyFormat: prepared.renderedBodyFormat,
|
|
212
|
+
regenerated: options.regenerated ?? false,
|
|
213
|
+
}, {
|
|
214
|
+
category: "internal",
|
|
215
|
+
source: "service",
|
|
216
|
+
});
|
|
217
|
+
return {
|
|
218
|
+
status: "generated",
|
|
219
|
+
invoiceId: prepared.invoice.id,
|
|
220
|
+
renderedBodyFormat: prepared.renderedBodyFormat,
|
|
221
|
+
renderedBody: prepared.renderedBody,
|
|
222
|
+
rendition,
|
|
223
|
+
};
|
|
224
|
+
},
|
|
225
|
+
async regenerateInvoiceDocument(db, invoiceId, input, runtime) {
|
|
226
|
+
return this.generateInvoiceDocument(db, invoiceId, input, runtime, { regenerated: true });
|
|
227
|
+
},
|
|
228
|
+
};
|