@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/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 instanceof StorageExistsError)) {
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 instanceof StorageExistsError ? `${record.number} is already issued` : `could not save ${record.number}: ${error.message}`;
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 instanceof StorageExistsError) {
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