@voyantjs/finance 0.20.0 → 0.21.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/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
+ }
@@ -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" | "expired" | "cancelled" | "released" | "failed";
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" | "expired" | "cancelled" | "failed" | "paid" | "requires_redirect" | "processing" | "authorized";
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" | "expired" | "cancelled" | "failed" | "paid" | "requires_redirect" | "processing" | "authorized";
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" | "expired" | "cancelled" | "failed" | "paid" | "requires_redirect" | "processing" | "authorized";
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" | "expired" | "cancelled" | "failed" | "paid" | "requires_redirect" | "processing" | "authorized";
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;