@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.
- package/dist/service-issue.d.ts +3 -0
- package/dist/service-issue.d.ts.map +1 -1
- package/dist/service-issue.js +101 -11
- package/package.json +9 -9
package/dist/service-issue.d.ts
CHANGED
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"service-issue.d.ts","sourceRoot":"","sources":["../src/service-issue.ts"],"names":[],"mappings":"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"}
|
package/dist/service-issue.js
CHANGED
|
@@ -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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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.
|
|
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.
|
|
63
|
-
"@voyantjs/bookings": "0.77.
|
|
64
|
-
"@voyantjs/core": "0.77.
|
|
65
|
-
"@voyantjs/db": "0.77.
|
|
66
|
-
"@voyantjs/hono": "0.77.
|
|
67
|
-
"@voyantjs/products": "0.77.
|
|
68
|
-
"@voyantjs/utils": "0.77.
|
|
69
|
-
"@voyantjs/storage": "0.77.
|
|
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",
|