@voyantjs/finance 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +8 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -4
- 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/routes-documents.d.ts +2 -2
- package/dist/routes-public.d.ts +12 -12
- package/dist/routes.d.ts +530 -31
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +120 -5
- package/dist/schema.d.ts +538 -6
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +84 -0
- package/dist/service-issue.d.ts +108 -0
- package/dist/service-issue.d.ts.map +1 -0
- package/dist/service-issue.js +57 -0
- package/dist/service-public.d.ts +5 -5
- package/dist/service.d.ts +257 -37
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +278 -20
- package/dist/validation-billing.d.ts +109 -0
- package/dist/validation-billing.d.ts.map +1 -1
- package/dist/validation-billing.js +58 -0
- package/dist/validation-payments.d.ts +16 -16
- package/dist/validation-public.d.ts +3 -3
- package/dist/validation-shared.d.ts +5 -5
- package/package.json +7 -7
package/dist/schema.js
CHANGED
|
@@ -528,6 +528,15 @@ export const invoices = pgTable("invoices", {
|
|
|
528
528
|
id: typeId("invoices"),
|
|
529
529
|
invoiceNumber: text("invoice_number").notNull().unique(),
|
|
530
530
|
invoiceType: invoiceTypeEnum("invoice_type").notNull().default("invoice"),
|
|
531
|
+
/**
|
|
532
|
+
* Source proforma when this row is the final invoice that
|
|
533
|
+
* superseded one. Lets the bank-transfer flow link the proforma
|
|
534
|
+
* issued at checkout to the invoice issued after payment lands —
|
|
535
|
+
* SmartBill's "convert proforma" call returns the same number
|
|
536
|
+
* series, but the local rows stay distinct so the audit trail
|
|
537
|
+
* shows both documents.
|
|
538
|
+
*/
|
|
539
|
+
convertedFromInvoiceId: text("converted_from_invoice_id"),
|
|
531
540
|
seriesId: typeIdRef("series_id"),
|
|
532
541
|
sequence: integer("sequence"),
|
|
533
542
|
templateId: typeIdRef("template_id"),
|
|
@@ -567,6 +576,7 @@ export const invoices = pgTable("invoices", {
|
|
|
567
576
|
index("idx_invoices_fx_rate_set").on(table.fxRateSetId),
|
|
568
577
|
index("idx_invoices_number").on(table.invoiceNumber),
|
|
569
578
|
index("idx_invoices_due_date").on(table.dueDate),
|
|
579
|
+
index("idx_invoices_converted_from").on(table.convertedFromInvoiceId),
|
|
570
580
|
// base_currency covers every base_*_cents column. If any base amount is
|
|
571
581
|
// present, base_currency must be set so reporting can interpret it.
|
|
572
582
|
check("ck_invoices_base_currency_amounts", sql `(
|
|
@@ -810,6 +820,80 @@ export const taxRegimes = pgTable("tax_regimes", {
|
|
|
810
820
|
index("idx_tax_regimes_active").on(table.active),
|
|
811
821
|
index("idx_tax_regimes_active_updated").on(table.active, table.updatedAt),
|
|
812
822
|
]);
|
|
823
|
+
// ---------- tax_classes ----------
|
|
824
|
+
//
|
|
825
|
+
// Per-product tax-treatment decision. Stacks on top of `tax_regimes`
|
|
826
|
+
// (the jurisdictional rate catalog) — a class points at a default
|
|
827
|
+
// regime, plus optional regime-per-applies_to overrides for products
|
|
828
|
+
// that mix base / addon / accommodation treatments.
|
|
829
|
+
//
|
|
830
|
+
// Per booking-journey-architecture §9.
|
|
831
|
+
export const taxClassAppliesToEnum = pgEnum("tax_class_applies_to", [
|
|
832
|
+
"base",
|
|
833
|
+
"addon",
|
|
834
|
+
"accommodation",
|
|
835
|
+
"all",
|
|
836
|
+
]);
|
|
837
|
+
export const taxPolicySideEnum = pgEnum("tax_policy_side", ["sell", "buy"]);
|
|
838
|
+
export const taxClasses = pgTable("tax_classes", {
|
|
839
|
+
id: typeId("tax_classes"),
|
|
840
|
+
/** Stable code for idempotent seeding (e.g. "vat-standard-ro",
|
|
841
|
+
* "exempt-art311", "reduced-de"). */
|
|
842
|
+
code: text("code").notNull(),
|
|
843
|
+
label: text("label").notNull(),
|
|
844
|
+
description: text("description"),
|
|
845
|
+
/** Default regime resolved at quote time when no per-line rule
|
|
846
|
+
* matches. Plain text — cross-domain refs go through link service
|
|
847
|
+
* per schema-discipline. */
|
|
848
|
+
defaultRegimeId: text("default_regime_id"),
|
|
849
|
+
/**
|
|
850
|
+
* Regime-per-applies_to overrides. Empty / null falls through to
|
|
851
|
+
* `default_regime_id`. Parsed at quote time by the engine.
|
|
852
|
+
*/
|
|
853
|
+
lines: jsonb("lines").$type(),
|
|
854
|
+
active: boolean("active").notNull().default(true),
|
|
855
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
856
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
857
|
+
}, (table) => [
|
|
858
|
+
index("idx_tax_classes_code").on(table.code),
|
|
859
|
+
index("idx_tax_classes_active").on(table.active),
|
|
860
|
+
]);
|
|
861
|
+
// ---------- tax_policy_profiles ----------
|
|
862
|
+
//
|
|
863
|
+
// Operator/jurisdiction-specific tax decision profiles. Profiles are
|
|
864
|
+
// implementation presets such as "Romanian travel operator"; rules under
|
|
865
|
+
// the profile map product/order facts to tax regimes for sell-side and
|
|
866
|
+
// buy-side tax decisions.
|
|
867
|
+
export const taxPolicyProfiles = pgTable("tax_policy_profiles", {
|
|
868
|
+
id: typeId("tax_policy_profiles"),
|
|
869
|
+
code: text("code").notNull(),
|
|
870
|
+
name: text("name").notNull(),
|
|
871
|
+
jurisdiction: text("jurisdiction"),
|
|
872
|
+
description: text("description"),
|
|
873
|
+
active: boolean("active").notNull().default(true),
|
|
874
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
875
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
876
|
+
}, (table) => [
|
|
877
|
+
index("idx_tax_policy_profiles_code").on(table.code),
|
|
878
|
+
index("idx_tax_policy_profiles_active").on(table.active),
|
|
879
|
+
]);
|
|
880
|
+
export const taxPolicyRules = pgTable("tax_policy_rules", {
|
|
881
|
+
id: typeId("tax_policy_rules"),
|
|
882
|
+
profileId: text("profile_id").notNull(),
|
|
883
|
+
side: taxPolicySideEnum("side").notNull().default("sell"),
|
|
884
|
+
priority: integer("priority").notNull().default(100),
|
|
885
|
+
name: text("name").notNull(),
|
|
886
|
+
appliesTo: taxClassAppliesToEnum("applies_to").notNull().default("all"),
|
|
887
|
+
condition: jsonb("condition").$type(),
|
|
888
|
+
taxRegimeId: text("tax_regime_id").notNull(),
|
|
889
|
+
active: boolean("active").notNull().default(true),
|
|
890
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
891
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
892
|
+
}, (table) => [
|
|
893
|
+
index("idx_tax_policy_rules_profile").on(table.profileId),
|
|
894
|
+
index("idx_tax_policy_rules_profile_side_priority").on(table.profileId, table.side, table.priority),
|
|
895
|
+
index("idx_tax_policy_rules_active").on(table.active),
|
|
896
|
+
]);
|
|
813
897
|
// ---------- invoice_external_refs ----------
|
|
814
898
|
export const invoiceExternalRefs = pgTable("invoice_external_refs", {
|
|
815
899
|
id: typeId("invoice_external_refs"),
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { EventBus } from "@voyantjs/core";
|
|
2
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
3
|
+
import { type CreateInvoiceFromBookingInput, type InvoiceFromBookingData } from "./service.js";
|
|
4
|
+
/**
|
|
5
|
+
* Issue / proforma orchestration helpers that wrap the bare invoice
|
|
6
|
+
* creators with EventBus emissions so subscribers (SmartBill plugin,
|
|
7
|
+
* checkout-finalize workflow) can react.
|
|
8
|
+
*
|
|
9
|
+
* `createInvoice*` services in `service.ts` stay pure DB writers —
|
|
10
|
+
* the workflow / route layer chooses when to mark them as "issued"
|
|
11
|
+
* (a status change that's also a system signal).
|
|
12
|
+
*/
|
|
13
|
+
export interface InvoiceIssueRuntime {
|
|
14
|
+
eventBus?: EventBus;
|
|
15
|
+
}
|
|
16
|
+
export interface InvoiceIssuedEvent {
|
|
17
|
+
invoiceId: string;
|
|
18
|
+
invoiceNumber: string;
|
|
19
|
+
invoiceType: "invoice" | "proforma" | "credit_note";
|
|
20
|
+
bookingId: string | null;
|
|
21
|
+
totalCents: number;
|
|
22
|
+
currency: string;
|
|
23
|
+
/** Linkage when this invoice replaced a proforma. */
|
|
24
|
+
convertedFromInvoiceId?: string | null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Create + emit an invoice from a booking. Returns the persisted row
|
|
28
|
+
* after flipping the status from `draft` → `sent`. The status flip is
|
|
29
|
+
* what consumers treat as "issued" — drafts shouldn't trigger
|
|
30
|
+
* SmartBill sync.
|
|
31
|
+
*/
|
|
32
|
+
export declare function issueInvoiceFromBooking(db: PostgresJsDatabase, input: CreateInvoiceFromBookingInput, bookingData: InvoiceFromBookingData, runtime?: InvoiceIssueRuntime): Promise<{
|
|
33
|
+
id: string;
|
|
34
|
+
createdAt: Date;
|
|
35
|
+
updatedAt: Date;
|
|
36
|
+
organizationId: string | null;
|
|
37
|
+
status: "void" | "draft" | "sent" | "partially_paid" | "paid" | "overdue";
|
|
38
|
+
issueDate: string;
|
|
39
|
+
currency: string;
|
|
40
|
+
notes: string | null;
|
|
41
|
+
bookingId: string;
|
|
42
|
+
personId: string | null;
|
|
43
|
+
baseCurrency: string | null;
|
|
44
|
+
fxRateSetId: string | null;
|
|
45
|
+
invoiceNumber: string;
|
|
46
|
+
invoiceType: "invoice" | "proforma" | "credit_note";
|
|
47
|
+
convertedFromInvoiceId: string | null;
|
|
48
|
+
seriesId: string | null;
|
|
49
|
+
sequence: number | null;
|
|
50
|
+
templateId: string | null;
|
|
51
|
+
taxRegimeId: string | null;
|
|
52
|
+
language: string | null;
|
|
53
|
+
subtotalCents: number;
|
|
54
|
+
baseSubtotalCents: number | null;
|
|
55
|
+
taxCents: number;
|
|
56
|
+
baseTaxCents: number | null;
|
|
57
|
+
totalCents: number;
|
|
58
|
+
baseTotalCents: number | null;
|
|
59
|
+
paidCents: number;
|
|
60
|
+
basePaidCents: number | null;
|
|
61
|
+
balanceDueCents: number;
|
|
62
|
+
baseBalanceDueCents: number | null;
|
|
63
|
+
commissionPercent: number | null;
|
|
64
|
+
commissionAmountCents: number | null;
|
|
65
|
+
dueDate: string;
|
|
66
|
+
} | null>;
|
|
67
|
+
/**
|
|
68
|
+
* Create + emit a proforma from a booking. Same shape as
|
|
69
|
+
* `issueInvoiceFromBooking` but marks the row as `invoiceType:
|
|
70
|
+
* 'proforma'` and emits the proforma-specific event so the
|
|
71
|
+
* SmartBill plugin can route to its proforma endpoint.
|
|
72
|
+
*/
|
|
73
|
+
export declare function issueProformaFromBooking(db: PostgresJsDatabase, input: CreateInvoiceFromBookingInput, bookingData: InvoiceFromBookingData, runtime?: InvoiceIssueRuntime): Promise<{
|
|
74
|
+
id: string;
|
|
75
|
+
createdAt: Date;
|
|
76
|
+
updatedAt: Date;
|
|
77
|
+
organizationId: string | null;
|
|
78
|
+
status: "void" | "draft" | "sent" | "partially_paid" | "paid" | "overdue";
|
|
79
|
+
issueDate: string;
|
|
80
|
+
currency: string;
|
|
81
|
+
notes: string | null;
|
|
82
|
+
bookingId: string;
|
|
83
|
+
personId: string | null;
|
|
84
|
+
baseCurrency: string | null;
|
|
85
|
+
fxRateSetId: string | null;
|
|
86
|
+
invoiceNumber: string;
|
|
87
|
+
invoiceType: "invoice" | "proforma" | "credit_note";
|
|
88
|
+
convertedFromInvoiceId: string | null;
|
|
89
|
+
seriesId: string | null;
|
|
90
|
+
sequence: number | null;
|
|
91
|
+
templateId: string | null;
|
|
92
|
+
taxRegimeId: string | null;
|
|
93
|
+
language: string | null;
|
|
94
|
+
subtotalCents: number;
|
|
95
|
+
baseSubtotalCents: number | null;
|
|
96
|
+
taxCents: number;
|
|
97
|
+
baseTaxCents: number | null;
|
|
98
|
+
totalCents: number;
|
|
99
|
+
baseTotalCents: number | null;
|
|
100
|
+
paidCents: number;
|
|
101
|
+
basePaidCents: number | null;
|
|
102
|
+
balanceDueCents: number;
|
|
103
|
+
baseBalanceDueCents: number | null;
|
|
104
|
+
commissionPercent: number | null;
|
|
105
|
+
commissionAmountCents: number | null;
|
|
106
|
+
dueDate: string;
|
|
107
|
+
} | null>;
|
|
108
|
+
//# sourceMappingURL=service-issue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-issue.d.ts","sourceRoot":"","sources":["../src/service-issue.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAE9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAGjE,OAAO,EACL,KAAK,6BAA6B,EAElC,KAAK,sBAAsB,EAC5B,MAAM,cAAc,CAAA;AAErB;;;;;;;;GAQG;AAEH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,QAAQ,CAAA;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,WAAW,EAAE,SAAS,GAAG,UAAU,GAAG,aAAa,CAAA;IACnD,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,qDAAqD;IACrD,sBAAsB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACvC;AAKD;;;;;GAKG;AACH,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,6BAA6B,EACpC,WAAW,EAAE,sBAAsB,EACnC,OAAO,GAAE,mBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAclC;AAED;;;;;GAKG;AACH,wBAAsB,wBAAwB,CAC5C,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,6BAA6B,EACpC,WAAW,EAAE,sBAAsB,EACnC,OAAO,GAAE,mBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAclC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { invoices } from "./schema.js";
|
|
3
|
+
import { financeService, } from "./service.js";
|
|
4
|
+
const ISSUED_EVENT = "invoice.issued";
|
|
5
|
+
const PROFORMA_ISSUED_EVENT = "invoice.proforma.issued";
|
|
6
|
+
/**
|
|
7
|
+
* Create + emit an invoice from a booking. Returns the persisted row
|
|
8
|
+
* after flipping the status from `draft` → `sent`. The status flip is
|
|
9
|
+
* what consumers treat as "issued" — drafts shouldn't trigger
|
|
10
|
+
* SmartBill sync.
|
|
11
|
+
*/
|
|
12
|
+
export async function issueInvoiceFromBooking(db, input, bookingData, runtime = {}) {
|
|
13
|
+
const draft = await financeService.createInvoiceFromBooking(db, input, bookingData);
|
|
14
|
+
if (!draft)
|
|
15
|
+
return null;
|
|
16
|
+
const [issued] = await db
|
|
17
|
+
.update(invoices)
|
|
18
|
+
.set({ status: "sent", updatedAt: new Date() })
|
|
19
|
+
.where(eq(invoices.id, draft.id))
|
|
20
|
+
.returning();
|
|
21
|
+
const row = issued ?? draft;
|
|
22
|
+
await emitIssued(runtime, ISSUED_EVENT, row);
|
|
23
|
+
return row;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Create + emit a proforma from a booking. Same shape as
|
|
27
|
+
* `issueInvoiceFromBooking` but marks the row as `invoiceType:
|
|
28
|
+
* 'proforma'` and emits the proforma-specific event so the
|
|
29
|
+
* SmartBill plugin can route to its proforma endpoint.
|
|
30
|
+
*/
|
|
31
|
+
export async function issueProformaFromBooking(db, input, bookingData, runtime = {}) {
|
|
32
|
+
const draft = await financeService.createInvoiceFromBooking(db, input, bookingData);
|
|
33
|
+
if (!draft)
|
|
34
|
+
return null;
|
|
35
|
+
const [issued] = await db
|
|
36
|
+
.update(invoices)
|
|
37
|
+
.set({ invoiceType: "proforma", status: "sent", updatedAt: new Date() })
|
|
38
|
+
.where(eq(invoices.id, draft.id))
|
|
39
|
+
.returning();
|
|
40
|
+
const row = issued ?? draft;
|
|
41
|
+
await emitIssued(runtime, PROFORMA_ISSUED_EVENT, row);
|
|
42
|
+
return row;
|
|
43
|
+
}
|
|
44
|
+
async function emitIssued(runtime, eventName, invoice) {
|
|
45
|
+
if (!runtime.eventBus)
|
|
46
|
+
return;
|
|
47
|
+
const payload = {
|
|
48
|
+
invoiceId: invoice.id,
|
|
49
|
+
invoiceNumber: invoice.invoiceNumber,
|
|
50
|
+
invoiceType: invoice.invoiceType,
|
|
51
|
+
bookingId: invoice.bookingId,
|
|
52
|
+
totalCents: invoice.totalCents,
|
|
53
|
+
currency: invoice.currency,
|
|
54
|
+
convertedFromInvoiceId: invoice.convertedFromInvoiceId,
|
|
55
|
+
};
|
|
56
|
+
await runtime.eventBus.emit(eventName, payload);
|
|
57
|
+
}
|
package/dist/service-public.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export declare const publicFinanceService: {
|
|
|
33
33
|
id: string;
|
|
34
34
|
bookingPaymentScheduleId: string | null;
|
|
35
35
|
guaranteeType: "other" | "voucher" | "bank_transfer" | "credit_card" | "deposit" | "preauth" | "card_on_file" | "agency_letter";
|
|
36
|
-
status: "pending" | "active" | "
|
|
36
|
+
status: "pending" | "active" | "failed" | "expired" | "cancelled" | "released";
|
|
37
37
|
currency: string | null;
|
|
38
38
|
amountCents: number | null;
|
|
39
39
|
provider: string | null;
|
|
@@ -55,7 +55,7 @@ export declare const publicFinanceService: {
|
|
|
55
55
|
invoiceId: string | null;
|
|
56
56
|
bookingPaymentScheduleId: string | null;
|
|
57
57
|
bookingGuaranteeId: string | null;
|
|
58
|
-
status: "pending" | "
|
|
58
|
+
status: "pending" | "failed" | "expired" | "cancelled" | "paid" | "requires_redirect" | "processing" | "authorized";
|
|
59
59
|
provider: string | null;
|
|
60
60
|
providerSessionId: string | null;
|
|
61
61
|
providerPaymentId: string | null;
|
|
@@ -83,7 +83,7 @@ export declare const publicFinanceService: {
|
|
|
83
83
|
invoiceId: string | null;
|
|
84
84
|
bookingPaymentScheduleId: string | null;
|
|
85
85
|
bookingGuaranteeId: string | null;
|
|
86
|
-
status: "pending" | "
|
|
86
|
+
status: "pending" | "failed" | "expired" | "cancelled" | "paid" | "requires_redirect" | "processing" | "authorized";
|
|
87
87
|
provider: string | null;
|
|
88
88
|
providerSessionId: string | null;
|
|
89
89
|
providerPaymentId: string | null;
|
|
@@ -111,7 +111,7 @@ export declare const publicFinanceService: {
|
|
|
111
111
|
invoiceId: string | null;
|
|
112
112
|
bookingPaymentScheduleId: string | null;
|
|
113
113
|
bookingGuaranteeId: string | null;
|
|
114
|
-
status: "pending" | "
|
|
114
|
+
status: "pending" | "failed" | "expired" | "cancelled" | "paid" | "requires_redirect" | "processing" | "authorized";
|
|
115
115
|
provider: string | null;
|
|
116
116
|
providerSessionId: string | null;
|
|
117
117
|
providerPaymentId: string | null;
|
|
@@ -139,7 +139,7 @@ export declare const publicFinanceService: {
|
|
|
139
139
|
invoiceId: string | null;
|
|
140
140
|
bookingPaymentScheduleId: string | null;
|
|
141
141
|
bookingGuaranteeId: string | null;
|
|
142
|
-
status: "pending" | "
|
|
142
|
+
status: "pending" | "failed" | "expired" | "cancelled" | "paid" | "requires_redirect" | "processing" | "authorized";
|
|
143
143
|
provider: string | null;
|
|
144
144
|
providerSessionId: string | null;
|
|
145
145
|
providerPaymentId: string | null;
|