@voyantjs/finance 0.77.4 → 0.77.8

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.
@@ -62,8 +62,11 @@ export interface InvoiceIssuedLineItem {
62
62
  unitPrice: number;
63
63
  currency: string;
64
64
  taxPercentage?: number;
65
+ taxName?: string | null;
66
+ taxRegimeCode?: TaxRegimeCode | null;
65
67
  isService?: boolean;
66
68
  }
69
+ export type TaxRegimeCode = "standard" | "reduced" | "exempt" | "reverse_charge" | "margin_scheme_art311" | "zero_rated" | "out_of_scope" | "other";
67
70
  /**
68
71
  * Create + emit an invoice from a booking. Returns the persisted row
69
72
  * after flipping the status from `draft` → `sent`. The status flip is
@@ -1 +1 @@
1
- {"version":3,"file":"service-issue.d.ts","sourceRoot":"","sources":["../src/service-issue.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,gCAAgC,EAEtC,MAAM,yBAAyB,CAAA;AAEhC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAE9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAEjE,OAAO,EAAE,KAAK,gBAAgB,EAA2B,MAAM,iBAAiB,CAAA;AAChF,OAAO,EAAyC,QAAQ,EAAY,MAAM,aAAa,CAAA;AACvF,OAAO,EAEL,KAAK,6BAA6B,EAElC,KAAK,sBAAsB,EAC5B,MAAM,cAAc,CAAA;AAErB;;;;;;;;GAQG;AAEH,MAAM,WAAW,mBAAoB,SAAQ,gBAAgB;IAC3D,QAAQ,CAAC,EAAE,QAAQ,CAAA;IACnB,mBAAmB,CAAC,EAAE,gCAAgC,CAAA;IACtD,+BAA+B,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAChD;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,6EAA6E;IAC7E,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,4DAA4D;IAC5D,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,4EAA4E;IAC5E,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,kFAAkF;IAClF,0BAA0B,CAAC,EAAE,MAAM,CAAA;IACnC,qDAAqD;IACrD,sBAAsB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,SAAS,CAAC,EAAE,qBAAqB,EAAE,CAAA;IACnC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,0BAA0B,CAAC,EAAE,OAAO,CAAA;IACpC,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,yBAAyB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1C;AAED,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAKD;;;;;GAKG;AACH,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,6BAA6B,EACpC,WAAW,EAAE,sBAAsB,EACnC,OAAO,GAAE,mBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAoClC;AAED;;;;;GAKG;AACH,wBAAsB,wBAAwB,CAC5C,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,6BAA6B,EACpC,WAAW,EAAE,sBAAsB,EACnC,OAAO,GAAE,mBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAoClC;AAkED;;;;;;;;;;;GAWG;AACH,wBAAsB,wBAAwB,CAC5C,EAAE,EAAE,kBAAkB,EACtB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE;IACP,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,CAAA;CACZ,EACN,OAAO,GAAE,mBAAwB,GAChC,OAAO,CACN;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,QAAQ,CAAC,YAAY,CAAA;CAAE,GACvD;IAAE,MAAM,EAAE,WAAW,CAAA;CAAE,GACvB;IAAE,MAAM,EAAE,cAAc,CAAA;CAAE,GAC1B;IAAE,MAAM,EAAE,mBAAmB,CAAA;CAAE,CAClC,CA0GA"}
1
+ {"version":3,"file":"service-issue.d.ts","sourceRoot":"","sources":["../src/service-issue.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,20 @@
1
1
  import { appendActionLedgerMutation, } from "@voyantjs/action-ledger";
2
- import { bookings } from "@voyantjs/bookings/schema";
3
- import { asc, eq } from "drizzle-orm";
2
+ import { bookingItems, bookings } from "@voyantjs/bookings/schema";
3
+ import { asc, eq, inArray } from "drizzle-orm";
4
+ import { resolveBookingSellTaxRate } from "./booking-tax.js";
4
5
  import { resolveInvoiceFxContext } from "./invoice-fx.js";
5
- import { invoiceLineItems, invoiceNumberSeries, invoices, payments } from "./schema.js";
6
+ import { bookingItemTaxLines, invoiceLineItems, invoiceNumberSeries, invoices, payments, } from "./schema.js";
6
7
  import { buildInvoiceIssuedActionLedgerInput, financeService, } from "./service.js";
8
+ const TAX_REGIME_CODES = new Set([
9
+ "standard",
10
+ "reduced",
11
+ "exempt",
12
+ "reverse_charge",
13
+ "margin_scheme_art311",
14
+ "zero_rated",
15
+ "out_of_scope",
16
+ "other",
17
+ ]);
7
18
  const ISSUED_EVENT = "invoice.issued";
8
19
  const PROFORMA_ISSUED_EVENT = "invoice.proforma.issued";
9
20
  /**
@@ -84,6 +95,7 @@ async function emitIssued(db, runtime, eventName, invoice) {
84
95
  .from(invoiceLineItems)
85
96
  .where(eq(invoiceLineItems.invoiceId, invoice.id))
86
97
  .orderBy(asc(invoiceLineItems.sortOrder));
98
+ const taxMetadataByBookingItemId = await loadLineTaxMetadata(db, lines);
87
99
  const payload = {
88
100
  invoiceId: invoice.id,
89
101
  invoiceNumber: invoice.invoiceNumber,
@@ -101,14 +113,20 @@ async function emitIssued(db, runtime, eventName, invoice) {
101
113
  clientCountry: booking?.contactCountry ?? null,
102
114
  clientVatCode: null,
103
115
  clientRegCom: null,
104
- lineItems: lines.map((line) => ({
105
- description: line.description,
106
- quantity: line.quantity,
107
- unitPrice: centsToMajor(line.unitPriceCents),
108
- currency: invoice.currency,
109
- ...(line.taxRate == null ? {} : { taxPercentage: line.taxRate }),
110
- isService: true,
111
- })),
116
+ lineItems: lines.map((line) => {
117
+ const taxMetadata = line.bookingItemId == null ? undefined : taxMetadataByBookingItemId.get(line.bookingItemId);
118
+ const taxPercentage = line.taxRate ?? taxMetadata?.taxPercentage;
119
+ return {
120
+ description: line.description,
121
+ quantity: line.quantity,
122
+ unitPrice: centsToMajor(line.unitPriceCents),
123
+ currency: invoice.currency,
124
+ ...(taxPercentage == null ? {} : { taxPercentage }),
125
+ ...(taxMetadata?.taxName == null ? {} : { taxName: taxMetadata.taxName }),
126
+ ...(taxMetadata?.taxRegimeCode == null ? {} : { taxRegimeCode: taxMetadata.taxRegimeCode }),
127
+ isService: true,
128
+ };
129
+ }),
112
130
  bookingNumber: booking?.bookingNumber ?? null,
113
131
  issueDate: toDateString(invoice.issueDate),
114
132
  dueDate: toDateString(invoice.dueDate),
@@ -125,6 +143,78 @@ async function emitIssued(db, runtime, eventName, invoice) {
125
143
  Object.assign(payload, fx);
126
144
  await runtime.eventBus.emit(eventName, payload);
127
145
  }
146
+ async function loadLineTaxMetadata(db, lines) {
147
+ const bookingItemIds = [
148
+ ...new Set(lines.map((line) => line.bookingItemId).filter((id) => Boolean(id))),
149
+ ];
150
+ if (bookingItemIds.length === 0)
151
+ return new Map();
152
+ const taxLines = await db
153
+ .select({
154
+ bookingItemId: bookingItemTaxLines.bookingItemId,
155
+ name: bookingItemTaxLines.name,
156
+ code: bookingItemTaxLines.code,
157
+ scope: bookingItemTaxLines.scope,
158
+ rateBasisPoints: bookingItemTaxLines.rateBasisPoints,
159
+ })
160
+ .from(bookingItemTaxLines)
161
+ .where(inArray(bookingItemTaxLines.bookingItemId, bookingItemIds))
162
+ .orderBy(asc(bookingItemTaxLines.bookingItemId), asc(bookingItemTaxLines.sortOrder), asc(bookingItemTaxLines.createdAt));
163
+ const taxLinesByBookingItemId = new Map();
164
+ for (const taxLine of taxLines) {
165
+ const existing = taxLinesByBookingItemId.get(taxLine.bookingItemId) ?? [];
166
+ existing.push(taxLine);
167
+ taxLinesByBookingItemId.set(taxLine.bookingItemId, existing);
168
+ }
169
+ const metadataByBookingItemId = new Map();
170
+ for (const bookingItemId of bookingItemIds) {
171
+ const taxLine = selectEventTaxLine(taxLinesByBookingItemId.get(bookingItemId) ?? []);
172
+ if (!taxLine)
173
+ continue;
174
+ metadataByBookingItemId.set(bookingItemId, {
175
+ ...(taxLine.rateBasisPoints == null
176
+ ? {}
177
+ : { taxPercentage: Math.round(taxLine.rateBasisPoints / 100) }),
178
+ taxName: taxLine.name,
179
+ taxRegimeCode: parseTaxRegimeCode(taxLine.code),
180
+ });
181
+ }
182
+ await backfillMissingLineTaxMetadata(db, bookingItemIds, metadataByBookingItemId);
183
+ return metadataByBookingItemId;
184
+ }
185
+ function selectEventTaxLine(taxLines) {
186
+ return (taxLines.find((taxLine) => taxLine.scope !== "withheld" && taxLine.rateBasisPoints != null) ??
187
+ taxLines.find((taxLine) => taxLine.scope !== "withheld") ??
188
+ null);
189
+ }
190
+ async function backfillMissingLineTaxMetadata(db, bookingItemIds, metadataByBookingItemId) {
191
+ const missingBookingItemIds = bookingItemIds.filter((id) => !metadataByBookingItemId.has(id));
192
+ if (missingBookingItemIds.length === 0)
193
+ return;
194
+ const productRows = await db
195
+ .select({
196
+ id: bookingItems.id,
197
+ productId: bookingItems.productId,
198
+ })
199
+ .from(bookingItems)
200
+ .where(inArray(bookingItems.id, missingBookingItemIds));
201
+ for (const row of productRows) {
202
+ if (!row.productId)
203
+ continue;
204
+ const taxRate = await resolveBookingSellTaxRate(db, { productId: row.productId });
205
+ if (!taxRate)
206
+ continue;
207
+ metadataByBookingItemId.set(row.id, {
208
+ taxPercentage: Math.round(taxRate.rate * 100),
209
+ taxName: taxRate.label,
210
+ taxRegimeCode: parseTaxRegimeCode(taxRate.code),
211
+ });
212
+ }
213
+ }
214
+ function parseTaxRegimeCode(code) {
215
+ const value = code?.split("/").at(-1);
216
+ return value && TAX_REGIME_CODES.has(value) ? value : null;
217
+ }
128
218
  /**
129
219
  * Convert an issued proforma into a final invoice. Copies the proforma's
130
220
  * line items verbatim (totals + taxes already match the booking the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyantjs/finance",
3
- "version": "0.77.4",
3
+ "version": "0.77.8",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -59,14 +59,14 @@
59
59
  "drizzle-orm": "^0.45.2",
60
60
  "hono": "^4.12.10",
61
61
  "zod": "^4.3.6",
62
- "@voyantjs/action-ledger": "0.77.4",
63
- "@voyantjs/bookings": "0.77.4",
64
- "@voyantjs/core": "0.77.4",
65
- "@voyantjs/db": "0.77.4",
66
- "@voyantjs/hono": "0.77.4",
67
- "@voyantjs/products": "0.77.4",
68
- "@voyantjs/utils": "0.77.4",
69
- "@voyantjs/storage": "0.77.4"
62
+ "@voyantjs/action-ledger": "0.77.8",
63
+ "@voyantjs/bookings": "0.77.8",
64
+ "@voyantjs/core": "0.77.8",
65
+ "@voyantjs/db": "0.77.8",
66
+ "@voyantjs/hono": "0.77.8",
67
+ "@voyantjs/products": "0.77.8",
68
+ "@voyantjs/utils": "0.77.8",
69
+ "@voyantjs/storage": "0.77.8"
70
70
  },
71
71
  "devDependencies": {
72
72
  "typescript": "^6.0.2",