@dloizides/invoice-core 0.1.0 → 0.2.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/CHANGELOG.md +4 -0
- package/README.md +14 -1
- package/dist/index.d.mts +46 -1
- package/dist/index.d.ts +46 -1
- package/dist/index.js +175 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +165 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
package/dist/index.mjs
CHANGED
|
@@ -5,6 +5,9 @@ var StorageExistsError = class extends Error {
|
|
|
5
5
|
this.name = "StorageExistsError";
|
|
6
6
|
}
|
|
7
7
|
};
|
|
8
|
+
function isStorageExistsError(error) {
|
|
9
|
+
return typeof error === "object" && error !== null && error.name === "StorageExistsError";
|
|
10
|
+
}
|
|
8
11
|
|
|
9
12
|
// src/types.ts
|
|
10
13
|
var InvoiceError = class extends Error {
|
|
@@ -21,6 +24,11 @@ var FORMAT_VERSION = 1;
|
|
|
21
24
|
var NUMBER_YEAR = /(\d{4})-\d+$/;
|
|
22
25
|
var LEGACY_YEAR = /^\d{4}$/;
|
|
23
26
|
var LEGACY_FILE = /^(\d{4}-\d+)\.(json|pdf)$/;
|
|
27
|
+
var OS_FILES = /* @__PURE__ */ new Set(["desktop.ini", "thumbs.db", ".ds_store"]);
|
|
28
|
+
var NUMBER_SHAPE = /^(CN-)?\d{4}-\d+$/;
|
|
29
|
+
function isOsFile(name) {
|
|
30
|
+
return OS_FILES.has(name.toLowerCase());
|
|
31
|
+
}
|
|
24
32
|
function toJson(value2) {
|
|
25
33
|
return JSON.stringify(value2, null, 2);
|
|
26
34
|
}
|
|
@@ -56,7 +64,7 @@ async function ensureFormat(storage) {
|
|
|
56
64
|
const text = await storage.readText(FORMAT_PATH);
|
|
57
65
|
if (text === null) {
|
|
58
66
|
await storage.writeNew(FORMAT_PATH, toJson({ version: FORMAT_VERSION })).catch((error) => {
|
|
59
|
-
if (!(error
|
|
67
|
+
if (!isStorageExistsError(error)) {
|
|
60
68
|
throw error;
|
|
61
69
|
}
|
|
62
70
|
});
|
|
@@ -99,6 +107,85 @@ async function migrateLegacyFolder(storage) {
|
|
|
99
107
|
return result;
|
|
100
108
|
}
|
|
101
109
|
|
|
110
|
+
// src/FolderConflictKind.ts
|
|
111
|
+
var FolderConflictKind = /* @__PURE__ */ ((FolderConflictKind2) => {
|
|
112
|
+
FolderConflictKind2["SyncCopy"] = "sync copy";
|
|
113
|
+
FolderConflictKind2["DuplicateNumber"] = "duplicate number";
|
|
114
|
+
FolderConflictKind2["UnreadableRecord"] = "unreadable record";
|
|
115
|
+
return FolderConflictKind2;
|
|
116
|
+
})(FolderConflictKind || {});
|
|
117
|
+
|
|
118
|
+
// src/conflicts.ts
|
|
119
|
+
var RECORD_FILE = "invoice.json";
|
|
120
|
+
var STATUS_FILE = "status.json";
|
|
121
|
+
var LEGACY_FILE2 = /^(\d{4}-\d+)\.(json|pdf)$/;
|
|
122
|
+
function seriesKey(number) {
|
|
123
|
+
const separator = number.lastIndexOf("-");
|
|
124
|
+
return `${number.slice(0, separator)}-${Number(number.slice(separator + 1))}`;
|
|
125
|
+
}
|
|
126
|
+
async function recordConflict(storage, dir, number) {
|
|
127
|
+
const path = `${dir}/${RECORD_FILE}`;
|
|
128
|
+
const text = await storage.readText(path);
|
|
129
|
+
if (text === null) {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
let parsed;
|
|
133
|
+
try {
|
|
134
|
+
parsed = JSON.parse(text);
|
|
135
|
+
} catch {
|
|
136
|
+
return [{ kind: "unreadable record" /* UnreadableRecord */, path }];
|
|
137
|
+
}
|
|
138
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
139
|
+
return [{ kind: "unreadable record" /* UnreadableRecord */, path }];
|
|
140
|
+
}
|
|
141
|
+
return parsed.number === number ? [] : [{ kind: "duplicate number" /* DuplicateNumber */, path }];
|
|
142
|
+
}
|
|
143
|
+
async function numberFolderConflicts(storage, dir, number) {
|
|
144
|
+
const allowed = /* @__PURE__ */ new Set([RECORD_FILE, STATUS_FILE, `${number}.pdf`]);
|
|
145
|
+
const copies = (await storage.list(dir)).filter((name) => !allowed.has(name) && !isOsFile(name)).map((name) => ({ kind: "sync copy" /* SyncCopy */, path: `${dir}/${name}` }));
|
|
146
|
+
return [...copies, ...await recordConflict(storage, dir, number)];
|
|
147
|
+
}
|
|
148
|
+
async function currentLayoutConflicts(storage, year) {
|
|
149
|
+
const conflicts = [];
|
|
150
|
+
const numbers = /* @__PURE__ */ new Set();
|
|
151
|
+
const keys = /* @__PURE__ */ new Set();
|
|
152
|
+
const yearDir = `invoices/${year}`;
|
|
153
|
+
for (const name of (await storage.list(yearDir)).filter((entry) => !isOsFile(entry))) {
|
|
154
|
+
const path = `${yearDir}/${name}`;
|
|
155
|
+
if (!NUMBER_SHAPE.test(name) || numberYear(name) !== year) {
|
|
156
|
+
conflicts.push({ kind: "sync copy" /* SyncCopy */, path });
|
|
157
|
+
} else if (keys.has(seriesKey(name))) {
|
|
158
|
+
conflicts.push({ kind: "duplicate number" /* DuplicateNumber */, path });
|
|
159
|
+
} else {
|
|
160
|
+
keys.add(seriesKey(name));
|
|
161
|
+
numbers.add(name);
|
|
162
|
+
conflicts.push(...await numberFolderConflicts(storage, path, name));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { conflicts, numbers };
|
|
166
|
+
}
|
|
167
|
+
async function legacyLayoutConflicts(storage, year, numbers) {
|
|
168
|
+
const names = (await storage.list(year)).filter((name) => !isOsFile(name));
|
|
169
|
+
return names.flatMap((name) => {
|
|
170
|
+
const number = LEGACY_FILE2.exec(name)?.[1];
|
|
171
|
+
const path = `${year}/${name}`;
|
|
172
|
+
if (number === void 0 || numberYear(number) !== year) {
|
|
173
|
+
return [{ kind: "sync copy" /* SyncCopy */, path }];
|
|
174
|
+
}
|
|
175
|
+
return numbers.has(number) ? [{ kind: "duplicate number" /* DuplicateNumber */, path }] : [];
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
async function findFolderConflicts(storage, year) {
|
|
179
|
+
const { conflicts, numbers } = await currentLayoutConflicts(storage, year);
|
|
180
|
+
return [...conflicts, ...await legacyLayoutConflicts(storage, year, numbers)];
|
|
181
|
+
}
|
|
182
|
+
async function assertNoConflicts(storage, year) {
|
|
183
|
+
const conflicts = await findFolderConflicts(storage, year);
|
|
184
|
+
if (conflicts.length > 0) {
|
|
185
|
+
throw new InvoiceError(conflicts.map(({ kind, path }) => ({ path: "folder", message: `${kind}: ${path}` })));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
102
189
|
// src/InvoiceKind.ts
|
|
103
190
|
var InvoiceKind = /* @__PURE__ */ ((InvoiceKind2) => {
|
|
104
191
|
InvoiceKind2["Invoice"] = "invoice";
|
|
@@ -268,7 +355,7 @@ async function claimInvoice(storage, record) {
|
|
|
268
355
|
try {
|
|
269
356
|
await storage.writeNew(path, toJson(record));
|
|
270
357
|
} catch (error) {
|
|
271
|
-
const message = error
|
|
358
|
+
const message = isStorageExistsError(error) ? `${record.number} is already issued` : `could not save ${record.number}: ${error.message}`;
|
|
272
359
|
throw new InvoiceError([{ path: "number", message }]);
|
|
273
360
|
}
|
|
274
361
|
return path;
|
|
@@ -654,7 +741,6 @@ async function renderInvoicePdf(storage, input) {
|
|
|
654
741
|
}
|
|
655
742
|
|
|
656
743
|
// src/issue.ts
|
|
657
|
-
var NUMBER_SHAPE = /^(CN-)?\d{4}-\d+$/;
|
|
658
744
|
var NUMBER_LOCK = "invoice-number";
|
|
659
745
|
function rendererFor(storage, options) {
|
|
660
746
|
return options.render ?? ((input) => renderInvoicePdf(storage, input));
|
|
@@ -676,6 +762,7 @@ async function issueInvoice(storage, request, options = {}) {
|
|
|
676
762
|
await ensureFormat(storage);
|
|
677
763
|
const render = rendererFor(storage, options);
|
|
678
764
|
return storage.withLock(NUMBER_LOCK, async () => {
|
|
765
|
+
await assertNoConflicts(storage, final.date.slice(0, 4));
|
|
679
766
|
const number = await nextNumber(storage, final.date.slice(0, 4));
|
|
680
767
|
const lines2 = resolveLines(final, parties.client);
|
|
681
768
|
const pdf = await render({ request: final, number, parties, lines: lines2 });
|
|
@@ -762,6 +849,7 @@ async function issueCreditNote(storage, input, options = {}) {
|
|
|
762
849
|
checkTarget(invoice, input);
|
|
763
850
|
const render = rendererFor(storage, options);
|
|
764
851
|
return storage.withLock(NUMBER_LOCK, async () => {
|
|
852
|
+
await assertNoConflicts(storage, input.date.slice(0, 4));
|
|
765
853
|
const remaining = invoice.totals.netCents - await creditedSoFar(storage, invoice, input.date);
|
|
766
854
|
const cents = creditCents(input, remaining);
|
|
767
855
|
const number = await nextNumber(storage, input.date.slice(0, 4), "credit-note" /* CreditNote */);
|
|
@@ -826,6 +914,15 @@ async function exportQuarterCsv(storage, year, quarter) {
|
|
|
826
914
|
return `${BOM}${[HEADER.join(","), ...records.map(toRow)].join(NEWLINE)}${NEWLINE}`;
|
|
827
915
|
}
|
|
828
916
|
|
|
917
|
+
// src/FolderOpenState.ts
|
|
918
|
+
var FolderOpenState = /* @__PURE__ */ ((FolderOpenState2) => {
|
|
919
|
+
FolderOpenState2["New"] = "new";
|
|
920
|
+
FolderOpenState2["Existing"] = "existing";
|
|
921
|
+
FolderOpenState2["Legacy"] = "legacy";
|
|
922
|
+
FolderOpenState2["Foreign"] = "foreign";
|
|
923
|
+
return FolderOpenState2;
|
|
924
|
+
})(FolderOpenState || {});
|
|
925
|
+
|
|
829
926
|
// src/memoryStorage.ts
|
|
830
927
|
function toBytes2(data) {
|
|
831
928
|
return typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
@@ -869,6 +966,69 @@ function createMemoryStorage(initial = {}) {
|
|
|
869
966
|
};
|
|
870
967
|
}
|
|
871
968
|
|
|
969
|
+
// src/openFolder.ts
|
|
970
|
+
var DATA_ENTRIES = /* @__PURE__ */ new Set(["company.json", "clients", "invoices", "rules", "layouts", "schedules"]);
|
|
971
|
+
var LEGACY_YEAR2 = /^\d{4}$/;
|
|
972
|
+
async function openDataFolder(storage) {
|
|
973
|
+
const names = (await storage.list("")).filter((name) => !isOsFile(name));
|
|
974
|
+
if (names.includes(FORMAT_PATH)) {
|
|
975
|
+
await ensureFormat(storage);
|
|
976
|
+
return "existing" /* Existing */;
|
|
977
|
+
}
|
|
978
|
+
if (names.length === 0) {
|
|
979
|
+
await ensureFormat(storage);
|
|
980
|
+
return "new" /* New */;
|
|
981
|
+
}
|
|
982
|
+
return names.some((name) => DATA_ENTRIES.has(name) || LEGACY_YEAR2.test(name)) ? "legacy" /* Legacy */ : "foreign" /* Foreign */;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// src/PaymentState.ts
|
|
986
|
+
var PaymentState = /* @__PURE__ */ ((PaymentState2) => {
|
|
987
|
+
PaymentState2["Paid"] = "paid";
|
|
988
|
+
PaymentState2["Unpaid"] = "unpaid";
|
|
989
|
+
PaymentState2["Overdue"] = "overdue";
|
|
990
|
+
return PaymentState2;
|
|
991
|
+
})(PaymentState || {});
|
|
992
|
+
|
|
993
|
+
// src/paymentStatus.ts
|
|
994
|
+
var STATUS_LOCK = "invoice-status";
|
|
995
|
+
function statusPath(number) {
|
|
996
|
+
return `${invoiceDir(number)}/status.json`;
|
|
997
|
+
}
|
|
998
|
+
async function readPaymentStatus(storage, number) {
|
|
999
|
+
const path = statusPath(number);
|
|
1000
|
+
if (await storage.readText(path) === null) {
|
|
1001
|
+
return { paidOn: null };
|
|
1002
|
+
}
|
|
1003
|
+
const { paidOn } = await readJsonFile(storage, path, "status");
|
|
1004
|
+
return { paidOn: typeof paidOn === "string" ? paidOn : null };
|
|
1005
|
+
}
|
|
1006
|
+
async function markPaid(storage, number, paidOn) {
|
|
1007
|
+
if (!isRealDate(paidOn)) {
|
|
1008
|
+
throw new InvoiceError([{ path: "paidOn", message: "must be a real date as YYYY-MM-DD" }]);
|
|
1009
|
+
}
|
|
1010
|
+
const record = await readInvoice(storage, number);
|
|
1011
|
+
if (record.kind !== "invoice" /* Invoice */) {
|
|
1012
|
+
throw new InvoiceError([{ path: "number", message: `${number} is a credit note; only an invoice can be paid` }]);
|
|
1013
|
+
}
|
|
1014
|
+
if (paidOn < record.request.date) {
|
|
1015
|
+
throw new InvoiceError([{ path: "paidOn", message: `must not be before the invoice date ${record.request.date}` }]);
|
|
1016
|
+
}
|
|
1017
|
+
const payment2 = { paidOn };
|
|
1018
|
+
await storage.withLock(STATUS_LOCK, () => storage.replace(statusPath(number), toJson(payment2)));
|
|
1019
|
+
}
|
|
1020
|
+
async function markUnpaid(storage, number) {
|
|
1021
|
+
await storage.withLock(STATUS_LOCK, () => storage.remove(statusPath(number)));
|
|
1022
|
+
}
|
|
1023
|
+
function paymentStateOf(record, status, today) {
|
|
1024
|
+
if (status.paidOn !== null) {
|
|
1025
|
+
return "paid" /* Paid */;
|
|
1026
|
+
}
|
|
1027
|
+
const terms = record.client.paymentTermsDays;
|
|
1028
|
+
const overdue = typeof terms === "number" && today > addDays(record.request.date, terms);
|
|
1029
|
+
return overdue ? "overdue" /* Overdue */ : "unpaid" /* Unpaid */;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
872
1032
|
// src/schedule.ts
|
|
873
1033
|
var MONTHS_IN_YEAR = 12;
|
|
874
1034
|
var YEAR3 = /^\d{4}$/;
|
|
@@ -899,7 +1059,7 @@ async function prepareYearSchedule(storage, request) {
|
|
|
899
1059
|
try {
|
|
900
1060
|
await storage.writeNew(schedulePath(schedule.id), toJson(schedule));
|
|
901
1061
|
} catch (error) {
|
|
902
|
-
if (error
|
|
1062
|
+
if (isStorageExistsError(error)) {
|
|
903
1063
|
throw new InvoiceError([{ path: "schedule", message: `${schedule.id} is already prepared` }]);
|
|
904
1064
|
}
|
|
905
1065
|
throw error;
|
|
@@ -977,6 +1137,6 @@ async function issueDueMonths(storage, id, options) {
|
|
|
977
1137
|
return storage.withLock(lockName(id), async () => issueRun(storage, await reconcile(storage, await readSchedule(storage, id), options.today), options));
|
|
978
1138
|
}
|
|
979
1139
|
|
|
980
|
-
export { BUILT_IN_LAYOUTS, BUILT_IN_RULES, DEFAULT_LAYOUT, InvoiceError, InvoiceKind, LayoutAlign, NUMBER_LOCK, StorageExistsError, addDays, centsToAmount, checkRequest, claimInvoice, computeTotals, createMemoryStorage, displayDate, editScheduleMonth, ensureFormat, expandDescription, exportQuarterCsv, formatCents, invoiceDir, isBlank, isRealDate, issueCreditNote, issueDueMonths, issueInvoice, listIssued, loadLogo, loadParties, migrateLegacyFolder, missingForFinal, nextNumber, parseLayout, pdfPath, periodTexts, prepareYearSchedule, readInvoice, readSchedule, recordPath, renderDraftPdf, renderInvoicePdf, reprintInvoice, resolveLayout, resolveLines, selectLayoutName, toCents, validateLines, validateRequest, vatCents };
|
|
1140
|
+
export { BUILT_IN_LAYOUTS, BUILT_IN_RULES, DEFAULT_LAYOUT, FolderConflictKind, FolderOpenState, InvoiceError, InvoiceKind, LayoutAlign, NUMBER_LOCK, PaymentState, StorageExistsError, addDays, assertNoConflicts, centsToAmount, checkRequest, claimInvoice, computeTotals, createMemoryStorage, displayDate, editScheduleMonth, ensureFormat, expandDescription, exportQuarterCsv, findFolderConflicts, formatCents, invoiceDir, isBlank, isRealDate, isStorageExistsError, issueCreditNote, issueDueMonths, issueInvoice, listIssued, loadLogo, loadParties, markPaid, markUnpaid, migrateLegacyFolder, missingForFinal, nextNumber, openDataFolder, parseLayout, paymentStateOf, pdfPath, periodTexts, prepareYearSchedule, readInvoice, readPaymentStatus, readSchedule, recordPath, renderDraftPdf, renderInvoicePdf, reprintInvoice, resolveLayout, resolveLines, selectLayoutName, toCents, validateLines, validateRequest, vatCents };
|
|
981
1141
|
//# sourceMappingURL=index.mjs.map
|
|
982
1142
|
//# sourceMappingURL=index.mjs.map
|