@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
Open a picked folder (`openDataFolder`), payment status in a separate `status.json`, sync-conflict and duplicate-number detection that blocks issuing, and `StorageExistsError` recognised by name so another package can implement the storage.
|
|
6
|
+
|
|
3
7
|
## 0.1.0
|
|
4
8
|
|
|
5
9
|
First release: storage interface, numbering, validation, VAT rules, JSON layouts, pdfmake rendering, yearly schedules, credit notes, quarter CSV export, legacy folder migration.
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ const { number, pdfPath } = await issueInvoice(storage, {
|
|
|
13
13
|
|
|
14
14
|
## Storage
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
`@dloizides/folder-store` implements this in the browser. Otherwise implement `InvoiceStorage` (`src/storage.ts`) for your platform: `readText`, `readBytes`, `list`, `writeNew` (must refuse an existing file with an error named `StorageExistsError`), `replace`, `remove`, `withLock`. Number claims run inside `withLock(NUMBER_LOCK, ...)`, so a browser implementation should back it with Web Locks.
|
|
17
17
|
|
|
18
18
|
## Folder format
|
|
19
19
|
|
|
@@ -26,6 +26,7 @@ layouts/<name>.json optional; overrides or adds to BUILT_I
|
|
|
26
26
|
schedules/<clientId>-<year>.json 12 months, each a draft until issued
|
|
27
27
|
invoices/<year>/<number>/invoice.json immutable record
|
|
28
28
|
invoices/<year>/<number>/<number>.pdf the PDF; never replaced
|
|
29
|
+
invoices/<year>/<number>/status.json payment date; the only file here that may change
|
|
29
30
|
```
|
|
30
31
|
|
|
31
32
|
`migrateLegacyFolder` moves `<year>/<number>.json` + `.pdf` into `invoices/`, never overwriting. Until then `nextNumber` counts both places.
|
|
@@ -41,3 +42,15 @@ A layout is plain data (`InvoiceLayout`): margins, sizes, three colours, title a
|
|
|
41
42
|
## Numbers
|
|
42
43
|
|
|
43
44
|
Invoices number `2030-001`, credit notes `CN-2030-001`, each gap-free per year of their date. `issueDueMonths` dates each invoice on the day it issues; have your accountant confirm that suits your jurisdiction. Built-in VAT rules are not tax advice.
|
|
45
|
+
|
|
46
|
+
## Opening a folder
|
|
47
|
+
|
|
48
|
+
`openDataFolder` returns `New` (empty; format written), `Existing`, `Legacy` (ours but no format; call `migrateLegacyFolder`) or `Foreign` (unrelated files; nothing written, ask the user).
|
|
49
|
+
|
|
50
|
+
## Conflicts
|
|
51
|
+
|
|
52
|
+
`findFolderConflicts(storage, year)` reports sync-conflict copies, one number written twice and unreadable records, in both layouts. Issuing refuses while the year holds any and names each path; the user removes the copy. Operating system files are ignored.
|
|
53
|
+
|
|
54
|
+
## Payment status
|
|
55
|
+
|
|
56
|
+
`markPaid`, `markUnpaid` and `readPaymentStatus` use `status.json` only. `paymentStateOf(record, status, today)` gives paid, unpaid or overdue; overdue starts the day after the due date, and no payment terms means never overdue.
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
declare const enum FolderConflictKind {
|
|
2
|
+
SyncCopy = "sync copy",
|
|
3
|
+
DuplicateNumber = "duplicate number",
|
|
4
|
+
UnreadableRecord = "unreadable record"
|
|
5
|
+
}
|
|
6
|
+
|
|
1
7
|
/**
|
|
2
8
|
* The data folder as the core sees it. Paths are relative, with forward slashes.
|
|
3
9
|
* Browser and Node implementations live outside this package.
|
|
@@ -22,6 +28,17 @@ interface InvoiceStorage {
|
|
|
22
28
|
declare class StorageExistsError extends Error {
|
|
23
29
|
constructor(path: string);
|
|
24
30
|
}
|
|
31
|
+
/** Whether an error means the file exists; matched by name, so another package's class of this name counts. */
|
|
32
|
+
declare function isStorageExistsError(error: unknown): boolean;
|
|
33
|
+
|
|
34
|
+
interface FolderConflict {
|
|
35
|
+
kind: FolderConflictKind;
|
|
36
|
+
path: string;
|
|
37
|
+
}
|
|
38
|
+
/** Finds sync-conflict copies, duplicate numbers and unreadable records in one year, in both layouts. */
|
|
39
|
+
declare function findFolderConflicts(storage: InvoiceStorage, year: string): Promise<FolderConflict[]>;
|
|
40
|
+
/** Refuses to continue if the year holds a conflict, naming each path. */
|
|
41
|
+
declare function assertNoConflicts(storage: InvoiceStorage, year: string): Promise<void>;
|
|
25
42
|
|
|
26
43
|
/** Whether an issued record is an invoice or a credit note. */
|
|
27
44
|
declare const enum InvoiceKind {
|
|
@@ -232,6 +249,13 @@ interface MigrationResult {
|
|
|
232
249
|
/** Moves <year>/<number>.json + .pdf to invoices/<year>/<number>/. Never overwrites. */
|
|
233
250
|
declare function migrateLegacyFolder(storage: InvoiceStorage): Promise<MigrationResult>;
|
|
234
251
|
|
|
252
|
+
declare const enum FolderOpenState {
|
|
253
|
+
New = "new",
|
|
254
|
+
Existing = "existing",
|
|
255
|
+
Legacy = "legacy",
|
|
256
|
+
Foreign = "foreign"
|
|
257
|
+
}
|
|
258
|
+
|
|
235
259
|
/** Where a layout places the document title. */
|
|
236
260
|
declare const enum LayoutAlign {
|
|
237
261
|
Left = "left",
|
|
@@ -308,11 +332,32 @@ declare function nextNumber(storage: InvoiceStorage, year: string, kind?: Invoic
|
|
|
308
332
|
/** Writes the record, refusing if that number was already issued. Issued records are never replaced. */
|
|
309
333
|
declare function claimInvoice(storage: InvoiceStorage, record: InvoiceRecord): Promise<string>;
|
|
310
334
|
|
|
335
|
+
/** Opens a picked folder: an empty one gets the format, anything else unrecognised is left untouched. */
|
|
336
|
+
declare function openDataFolder(storage: InvoiceStorage): Promise<FolderOpenState>;
|
|
337
|
+
|
|
311
338
|
/** Reads the request's company, client and VAT rule, refusing if any is missing. */
|
|
312
339
|
declare function loadParties(storage: InvoiceStorage, clientId: string): Promise<Parties>;
|
|
313
340
|
/** Checks a request and returns its parties; final mode also refuses blank required fields. */
|
|
314
341
|
declare function checkRequest(storage: InvoiceStorage, request: InvoiceRequest): Promise<Parties>;
|
|
315
342
|
|
|
343
|
+
declare const enum PaymentState {
|
|
344
|
+
Paid = "paid",
|
|
345
|
+
Unpaid = "unpaid",
|
|
346
|
+
Overdue = "overdue"
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
interface PaymentStatus {
|
|
350
|
+
paidOn: string | null;
|
|
351
|
+
}
|
|
352
|
+
/** Reads an invoice's payment status from its status.json; no file means unpaid. */
|
|
353
|
+
declare function readPaymentStatus(storage: InvoiceStorage, number: string): Promise<PaymentStatus>;
|
|
354
|
+
/** Records a payment date in status.json; the issued record is never touched. */
|
|
355
|
+
declare function markPaid(storage: InvoiceStorage, number: string, paidOn: string): Promise<void>;
|
|
356
|
+
/** Removes a recorded payment; the issued record is never touched. */
|
|
357
|
+
declare function markUnpaid(storage: InvoiceStorage, number: string): Promise<void>;
|
|
358
|
+
/** Says whether an invoice is paid, unpaid or overdue today; overdue starts the day after the due date. */
|
|
359
|
+
declare function paymentStateOf(record: InvoiceRecord, status: PaymentStatus, today: string): PaymentState;
|
|
360
|
+
|
|
316
361
|
/** The logo a PDF shows: an image data URL or SVG text. */
|
|
317
362
|
type PdfLogo = {
|
|
318
363
|
image: string;
|
|
@@ -396,4 +441,4 @@ declare function validateRequest(req: InvoiceRequest): FieldError[];
|
|
|
396
441
|
/** Lists required fields still blank; a final invoice is refused if any are. */
|
|
397
442
|
declare function missingForFinal(company: Company, client: Client, lines?: InvoiceLine[]): FieldError[];
|
|
398
443
|
|
|
399
|
-
export { BUILT_IN_LAYOUTS, BUILT_IN_RULES, type Client, type Company, type CreditNoteRequest, DEFAULT_LAYOUT, type FieldError, InvoiceError, InvoiceKind, type InvoiceLayout, type InvoiceLine, type InvoiceRecord, type InvoiceRequest, type InvoiceSchedule, type InvoiceStorage, type InvoiceView, type IssueDueOptions, type IssueDueResult, type IssueOptions, type IssuedInvoice, LayoutAlign, type MigrationResult, NUMBER_LOCK, type Parties, type PdfRender, type PrepareScheduleRequest, type RenderInput, type ResolvedLine, type ScheduleMonth, type ScheduleRef, StorageExistsError, type Totals, type VatRule, 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 };
|
|
444
|
+
export { BUILT_IN_LAYOUTS, BUILT_IN_RULES, type Client, type Company, type CreditNoteRequest, DEFAULT_LAYOUT, type FieldError, type FolderConflict, FolderConflictKind, FolderOpenState, InvoiceError, InvoiceKind, type InvoiceLayout, type InvoiceLine, type InvoiceRecord, type InvoiceRequest, type InvoiceSchedule, type InvoiceStorage, type InvoiceView, type IssueDueOptions, type IssueDueResult, type IssueOptions, type IssuedInvoice, LayoutAlign, type MigrationResult, NUMBER_LOCK, type Parties, PaymentState, type PaymentStatus, type PdfRender, type PrepareScheduleRequest, type RenderInput, type ResolvedLine, type ScheduleMonth, type ScheduleRef, StorageExistsError, type Totals, type VatRule, 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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
declare const enum FolderConflictKind {
|
|
2
|
+
SyncCopy = "sync copy",
|
|
3
|
+
DuplicateNumber = "duplicate number",
|
|
4
|
+
UnreadableRecord = "unreadable record"
|
|
5
|
+
}
|
|
6
|
+
|
|
1
7
|
/**
|
|
2
8
|
* The data folder as the core sees it. Paths are relative, with forward slashes.
|
|
3
9
|
* Browser and Node implementations live outside this package.
|
|
@@ -22,6 +28,17 @@ interface InvoiceStorage {
|
|
|
22
28
|
declare class StorageExistsError extends Error {
|
|
23
29
|
constructor(path: string);
|
|
24
30
|
}
|
|
31
|
+
/** Whether an error means the file exists; matched by name, so another package's class of this name counts. */
|
|
32
|
+
declare function isStorageExistsError(error: unknown): boolean;
|
|
33
|
+
|
|
34
|
+
interface FolderConflict {
|
|
35
|
+
kind: FolderConflictKind;
|
|
36
|
+
path: string;
|
|
37
|
+
}
|
|
38
|
+
/** Finds sync-conflict copies, duplicate numbers and unreadable records in one year, in both layouts. */
|
|
39
|
+
declare function findFolderConflicts(storage: InvoiceStorage, year: string): Promise<FolderConflict[]>;
|
|
40
|
+
/** Refuses to continue if the year holds a conflict, naming each path. */
|
|
41
|
+
declare function assertNoConflicts(storage: InvoiceStorage, year: string): Promise<void>;
|
|
25
42
|
|
|
26
43
|
/** Whether an issued record is an invoice or a credit note. */
|
|
27
44
|
declare const enum InvoiceKind {
|
|
@@ -232,6 +249,13 @@ interface MigrationResult {
|
|
|
232
249
|
/** Moves <year>/<number>.json + .pdf to invoices/<year>/<number>/. Never overwrites. */
|
|
233
250
|
declare function migrateLegacyFolder(storage: InvoiceStorage): Promise<MigrationResult>;
|
|
234
251
|
|
|
252
|
+
declare const enum FolderOpenState {
|
|
253
|
+
New = "new",
|
|
254
|
+
Existing = "existing",
|
|
255
|
+
Legacy = "legacy",
|
|
256
|
+
Foreign = "foreign"
|
|
257
|
+
}
|
|
258
|
+
|
|
235
259
|
/** Where a layout places the document title. */
|
|
236
260
|
declare const enum LayoutAlign {
|
|
237
261
|
Left = "left",
|
|
@@ -308,11 +332,32 @@ declare function nextNumber(storage: InvoiceStorage, year: string, kind?: Invoic
|
|
|
308
332
|
/** Writes the record, refusing if that number was already issued. Issued records are never replaced. */
|
|
309
333
|
declare function claimInvoice(storage: InvoiceStorage, record: InvoiceRecord): Promise<string>;
|
|
310
334
|
|
|
335
|
+
/** Opens a picked folder: an empty one gets the format, anything else unrecognised is left untouched. */
|
|
336
|
+
declare function openDataFolder(storage: InvoiceStorage): Promise<FolderOpenState>;
|
|
337
|
+
|
|
311
338
|
/** Reads the request's company, client and VAT rule, refusing if any is missing. */
|
|
312
339
|
declare function loadParties(storage: InvoiceStorage, clientId: string): Promise<Parties>;
|
|
313
340
|
/** Checks a request and returns its parties; final mode also refuses blank required fields. */
|
|
314
341
|
declare function checkRequest(storage: InvoiceStorage, request: InvoiceRequest): Promise<Parties>;
|
|
315
342
|
|
|
343
|
+
declare const enum PaymentState {
|
|
344
|
+
Paid = "paid",
|
|
345
|
+
Unpaid = "unpaid",
|
|
346
|
+
Overdue = "overdue"
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
interface PaymentStatus {
|
|
350
|
+
paidOn: string | null;
|
|
351
|
+
}
|
|
352
|
+
/** Reads an invoice's payment status from its status.json; no file means unpaid. */
|
|
353
|
+
declare function readPaymentStatus(storage: InvoiceStorage, number: string): Promise<PaymentStatus>;
|
|
354
|
+
/** Records a payment date in status.json; the issued record is never touched. */
|
|
355
|
+
declare function markPaid(storage: InvoiceStorage, number: string, paidOn: string): Promise<void>;
|
|
356
|
+
/** Removes a recorded payment; the issued record is never touched. */
|
|
357
|
+
declare function markUnpaid(storage: InvoiceStorage, number: string): Promise<void>;
|
|
358
|
+
/** Says whether an invoice is paid, unpaid or overdue today; overdue starts the day after the due date. */
|
|
359
|
+
declare function paymentStateOf(record: InvoiceRecord, status: PaymentStatus, today: string): PaymentState;
|
|
360
|
+
|
|
316
361
|
/** The logo a PDF shows: an image data URL or SVG text. */
|
|
317
362
|
type PdfLogo = {
|
|
318
363
|
image: string;
|
|
@@ -396,4 +441,4 @@ declare function validateRequest(req: InvoiceRequest): FieldError[];
|
|
|
396
441
|
/** Lists required fields still blank; a final invoice is refused if any are. */
|
|
397
442
|
declare function missingForFinal(company: Company, client: Client, lines?: InvoiceLine[]): FieldError[];
|
|
398
443
|
|
|
399
|
-
export { BUILT_IN_LAYOUTS, BUILT_IN_RULES, type Client, type Company, type CreditNoteRequest, DEFAULT_LAYOUT, type FieldError, InvoiceError, InvoiceKind, type InvoiceLayout, type InvoiceLine, type InvoiceRecord, type InvoiceRequest, type InvoiceSchedule, type InvoiceStorage, type InvoiceView, type IssueDueOptions, type IssueDueResult, type IssueOptions, type IssuedInvoice, LayoutAlign, type MigrationResult, NUMBER_LOCK, type Parties, type PdfRender, type PrepareScheduleRequest, type RenderInput, type ResolvedLine, type ScheduleMonth, type ScheduleRef, StorageExistsError, type Totals, type VatRule, 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 };
|
|
444
|
+
export { BUILT_IN_LAYOUTS, BUILT_IN_RULES, type Client, type Company, type CreditNoteRequest, DEFAULT_LAYOUT, type FieldError, type FolderConflict, FolderConflictKind, FolderOpenState, InvoiceError, InvoiceKind, type InvoiceLayout, type InvoiceLine, type InvoiceRecord, type InvoiceRequest, type InvoiceSchedule, type InvoiceStorage, type InvoiceView, type IssueDueOptions, type IssueDueResult, type IssueOptions, type IssuedInvoice, LayoutAlign, type MigrationResult, NUMBER_LOCK, type Parties, PaymentState, type PaymentStatus, type PdfRender, type PrepareScheduleRequest, type RenderInput, type ResolvedLine, type ScheduleMonth, type ScheduleRef, StorageExistsError, type Totals, type VatRule, 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 };
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,9 @@ var StorageExistsError = class extends Error {
|
|
|
7
7
|
this.name = "StorageExistsError";
|
|
8
8
|
}
|
|
9
9
|
};
|
|
10
|
+
function isStorageExistsError(error) {
|
|
11
|
+
return typeof error === "object" && error !== null && error.name === "StorageExistsError";
|
|
12
|
+
}
|
|
10
13
|
|
|
11
14
|
// src/types.ts
|
|
12
15
|
var InvoiceError = class extends Error {
|
|
@@ -23,6 +26,11 @@ var FORMAT_VERSION = 1;
|
|
|
23
26
|
var NUMBER_YEAR = /(\d{4})-\d+$/;
|
|
24
27
|
var LEGACY_YEAR = /^\d{4}$/;
|
|
25
28
|
var LEGACY_FILE = /^(\d{4}-\d+)\.(json|pdf)$/;
|
|
29
|
+
var OS_FILES = /* @__PURE__ */ new Set(["desktop.ini", "thumbs.db", ".ds_store"]);
|
|
30
|
+
var NUMBER_SHAPE = /^(CN-)?\d{4}-\d+$/;
|
|
31
|
+
function isOsFile(name) {
|
|
32
|
+
return OS_FILES.has(name.toLowerCase());
|
|
33
|
+
}
|
|
26
34
|
function toJson(value2) {
|
|
27
35
|
return JSON.stringify(value2, null, 2);
|
|
28
36
|
}
|
|
@@ -58,7 +66,7 @@ async function ensureFormat(storage) {
|
|
|
58
66
|
const text = await storage.readText(FORMAT_PATH);
|
|
59
67
|
if (text === null) {
|
|
60
68
|
await storage.writeNew(FORMAT_PATH, toJson({ version: FORMAT_VERSION })).catch((error) => {
|
|
61
|
-
if (!(error
|
|
69
|
+
if (!isStorageExistsError(error)) {
|
|
62
70
|
throw error;
|
|
63
71
|
}
|
|
64
72
|
});
|
|
@@ -101,6 +109,85 @@ async function migrateLegacyFolder(storage) {
|
|
|
101
109
|
return result;
|
|
102
110
|
}
|
|
103
111
|
|
|
112
|
+
// src/FolderConflictKind.ts
|
|
113
|
+
var FolderConflictKind = /* @__PURE__ */ ((FolderConflictKind2) => {
|
|
114
|
+
FolderConflictKind2["SyncCopy"] = "sync copy";
|
|
115
|
+
FolderConflictKind2["DuplicateNumber"] = "duplicate number";
|
|
116
|
+
FolderConflictKind2["UnreadableRecord"] = "unreadable record";
|
|
117
|
+
return FolderConflictKind2;
|
|
118
|
+
})(FolderConflictKind || {});
|
|
119
|
+
|
|
120
|
+
// src/conflicts.ts
|
|
121
|
+
var RECORD_FILE = "invoice.json";
|
|
122
|
+
var STATUS_FILE = "status.json";
|
|
123
|
+
var LEGACY_FILE2 = /^(\d{4}-\d+)\.(json|pdf)$/;
|
|
124
|
+
function seriesKey(number) {
|
|
125
|
+
const separator = number.lastIndexOf("-");
|
|
126
|
+
return `${number.slice(0, separator)}-${Number(number.slice(separator + 1))}`;
|
|
127
|
+
}
|
|
128
|
+
async function recordConflict(storage, dir, number) {
|
|
129
|
+
const path = `${dir}/${RECORD_FILE}`;
|
|
130
|
+
const text = await storage.readText(path);
|
|
131
|
+
if (text === null) {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
let parsed;
|
|
135
|
+
try {
|
|
136
|
+
parsed = JSON.parse(text);
|
|
137
|
+
} catch {
|
|
138
|
+
return [{ kind: "unreadable record" /* UnreadableRecord */, path }];
|
|
139
|
+
}
|
|
140
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
141
|
+
return [{ kind: "unreadable record" /* UnreadableRecord */, path }];
|
|
142
|
+
}
|
|
143
|
+
return parsed.number === number ? [] : [{ kind: "duplicate number" /* DuplicateNumber */, path }];
|
|
144
|
+
}
|
|
145
|
+
async function numberFolderConflicts(storage, dir, number) {
|
|
146
|
+
const allowed = /* @__PURE__ */ new Set([RECORD_FILE, STATUS_FILE, `${number}.pdf`]);
|
|
147
|
+
const copies = (await storage.list(dir)).filter((name) => !allowed.has(name) && !isOsFile(name)).map((name) => ({ kind: "sync copy" /* SyncCopy */, path: `${dir}/${name}` }));
|
|
148
|
+
return [...copies, ...await recordConflict(storage, dir, number)];
|
|
149
|
+
}
|
|
150
|
+
async function currentLayoutConflicts(storage, year) {
|
|
151
|
+
const conflicts = [];
|
|
152
|
+
const numbers = /* @__PURE__ */ new Set();
|
|
153
|
+
const keys = /* @__PURE__ */ new Set();
|
|
154
|
+
const yearDir = `invoices/${year}`;
|
|
155
|
+
for (const name of (await storage.list(yearDir)).filter((entry) => !isOsFile(entry))) {
|
|
156
|
+
const path = `${yearDir}/${name}`;
|
|
157
|
+
if (!NUMBER_SHAPE.test(name) || numberYear(name) !== year) {
|
|
158
|
+
conflicts.push({ kind: "sync copy" /* SyncCopy */, path });
|
|
159
|
+
} else if (keys.has(seriesKey(name))) {
|
|
160
|
+
conflicts.push({ kind: "duplicate number" /* DuplicateNumber */, path });
|
|
161
|
+
} else {
|
|
162
|
+
keys.add(seriesKey(name));
|
|
163
|
+
numbers.add(name);
|
|
164
|
+
conflicts.push(...await numberFolderConflicts(storage, path, name));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { conflicts, numbers };
|
|
168
|
+
}
|
|
169
|
+
async function legacyLayoutConflicts(storage, year, numbers) {
|
|
170
|
+
const names = (await storage.list(year)).filter((name) => !isOsFile(name));
|
|
171
|
+
return names.flatMap((name) => {
|
|
172
|
+
const number = LEGACY_FILE2.exec(name)?.[1];
|
|
173
|
+
const path = `${year}/${name}`;
|
|
174
|
+
if (number === void 0 || numberYear(number) !== year) {
|
|
175
|
+
return [{ kind: "sync copy" /* SyncCopy */, path }];
|
|
176
|
+
}
|
|
177
|
+
return numbers.has(number) ? [{ kind: "duplicate number" /* DuplicateNumber */, path }] : [];
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
async function findFolderConflicts(storage, year) {
|
|
181
|
+
const { conflicts, numbers } = await currentLayoutConflicts(storage, year);
|
|
182
|
+
return [...conflicts, ...await legacyLayoutConflicts(storage, year, numbers)];
|
|
183
|
+
}
|
|
184
|
+
async function assertNoConflicts(storage, year) {
|
|
185
|
+
const conflicts = await findFolderConflicts(storage, year);
|
|
186
|
+
if (conflicts.length > 0) {
|
|
187
|
+
throw new InvoiceError(conflicts.map(({ kind, path }) => ({ path: "folder", message: `${kind}: ${path}` })));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
104
191
|
// src/InvoiceKind.ts
|
|
105
192
|
var InvoiceKind = /* @__PURE__ */ ((InvoiceKind2) => {
|
|
106
193
|
InvoiceKind2["Invoice"] = "invoice";
|
|
@@ -270,7 +357,7 @@ async function claimInvoice(storage, record) {
|
|
|
270
357
|
try {
|
|
271
358
|
await storage.writeNew(path, toJson(record));
|
|
272
359
|
} catch (error) {
|
|
273
|
-
const message = error
|
|
360
|
+
const message = isStorageExistsError(error) ? `${record.number} is already issued` : `could not save ${record.number}: ${error.message}`;
|
|
274
361
|
throw new InvoiceError([{ path: "number", message }]);
|
|
275
362
|
}
|
|
276
363
|
return path;
|
|
@@ -656,7 +743,6 @@ async function renderInvoicePdf(storage, input) {
|
|
|
656
743
|
}
|
|
657
744
|
|
|
658
745
|
// src/issue.ts
|
|
659
|
-
var NUMBER_SHAPE = /^(CN-)?\d{4}-\d+$/;
|
|
660
746
|
var NUMBER_LOCK = "invoice-number";
|
|
661
747
|
function rendererFor(storage, options) {
|
|
662
748
|
return options.render ?? ((input) => renderInvoicePdf(storage, input));
|
|
@@ -678,6 +764,7 @@ async function issueInvoice(storage, request, options = {}) {
|
|
|
678
764
|
await ensureFormat(storage);
|
|
679
765
|
const render = rendererFor(storage, options);
|
|
680
766
|
return storage.withLock(NUMBER_LOCK, async () => {
|
|
767
|
+
await assertNoConflicts(storage, final.date.slice(0, 4));
|
|
681
768
|
const number = await nextNumber(storage, final.date.slice(0, 4));
|
|
682
769
|
const lines2 = resolveLines(final, parties.client);
|
|
683
770
|
const pdf = await render({ request: final, number, parties, lines: lines2 });
|
|
@@ -764,6 +851,7 @@ async function issueCreditNote(storage, input, options = {}) {
|
|
|
764
851
|
checkTarget(invoice, input);
|
|
765
852
|
const render = rendererFor(storage, options);
|
|
766
853
|
return storage.withLock(NUMBER_LOCK, async () => {
|
|
854
|
+
await assertNoConflicts(storage, input.date.slice(0, 4));
|
|
767
855
|
const remaining = invoice.totals.netCents - await creditedSoFar(storage, invoice, input.date);
|
|
768
856
|
const cents = creditCents(input, remaining);
|
|
769
857
|
const number = await nextNumber(storage, input.date.slice(0, 4), "credit-note" /* CreditNote */);
|
|
@@ -828,6 +916,15 @@ async function exportQuarterCsv(storage, year, quarter) {
|
|
|
828
916
|
return `${BOM}${[HEADER.join(","), ...records.map(toRow)].join(NEWLINE)}${NEWLINE}`;
|
|
829
917
|
}
|
|
830
918
|
|
|
919
|
+
// src/FolderOpenState.ts
|
|
920
|
+
var FolderOpenState = /* @__PURE__ */ ((FolderOpenState2) => {
|
|
921
|
+
FolderOpenState2["New"] = "new";
|
|
922
|
+
FolderOpenState2["Existing"] = "existing";
|
|
923
|
+
FolderOpenState2["Legacy"] = "legacy";
|
|
924
|
+
FolderOpenState2["Foreign"] = "foreign";
|
|
925
|
+
return FolderOpenState2;
|
|
926
|
+
})(FolderOpenState || {});
|
|
927
|
+
|
|
831
928
|
// src/memoryStorage.ts
|
|
832
929
|
function toBytes2(data) {
|
|
833
930
|
return typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
@@ -871,6 +968,69 @@ function createMemoryStorage(initial = {}) {
|
|
|
871
968
|
};
|
|
872
969
|
}
|
|
873
970
|
|
|
971
|
+
// src/openFolder.ts
|
|
972
|
+
var DATA_ENTRIES = /* @__PURE__ */ new Set(["company.json", "clients", "invoices", "rules", "layouts", "schedules"]);
|
|
973
|
+
var LEGACY_YEAR2 = /^\d{4}$/;
|
|
974
|
+
async function openDataFolder(storage) {
|
|
975
|
+
const names = (await storage.list("")).filter((name) => !isOsFile(name));
|
|
976
|
+
if (names.includes(FORMAT_PATH)) {
|
|
977
|
+
await ensureFormat(storage);
|
|
978
|
+
return "existing" /* Existing */;
|
|
979
|
+
}
|
|
980
|
+
if (names.length === 0) {
|
|
981
|
+
await ensureFormat(storage);
|
|
982
|
+
return "new" /* New */;
|
|
983
|
+
}
|
|
984
|
+
return names.some((name) => DATA_ENTRIES.has(name) || LEGACY_YEAR2.test(name)) ? "legacy" /* Legacy */ : "foreign" /* Foreign */;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// src/PaymentState.ts
|
|
988
|
+
var PaymentState = /* @__PURE__ */ ((PaymentState2) => {
|
|
989
|
+
PaymentState2["Paid"] = "paid";
|
|
990
|
+
PaymentState2["Unpaid"] = "unpaid";
|
|
991
|
+
PaymentState2["Overdue"] = "overdue";
|
|
992
|
+
return PaymentState2;
|
|
993
|
+
})(PaymentState || {});
|
|
994
|
+
|
|
995
|
+
// src/paymentStatus.ts
|
|
996
|
+
var STATUS_LOCK = "invoice-status";
|
|
997
|
+
function statusPath(number) {
|
|
998
|
+
return `${invoiceDir(number)}/status.json`;
|
|
999
|
+
}
|
|
1000
|
+
async function readPaymentStatus(storage, number) {
|
|
1001
|
+
const path = statusPath(number);
|
|
1002
|
+
if (await storage.readText(path) === null) {
|
|
1003
|
+
return { paidOn: null };
|
|
1004
|
+
}
|
|
1005
|
+
const { paidOn } = await readJsonFile(storage, path, "status");
|
|
1006
|
+
return { paidOn: typeof paidOn === "string" ? paidOn : null };
|
|
1007
|
+
}
|
|
1008
|
+
async function markPaid(storage, number, paidOn) {
|
|
1009
|
+
if (!isRealDate(paidOn)) {
|
|
1010
|
+
throw new InvoiceError([{ path: "paidOn", message: "must be a real date as YYYY-MM-DD" }]);
|
|
1011
|
+
}
|
|
1012
|
+
const record = await readInvoice(storage, number);
|
|
1013
|
+
if (record.kind !== "invoice" /* Invoice */) {
|
|
1014
|
+
throw new InvoiceError([{ path: "number", message: `${number} is a credit note; only an invoice can be paid` }]);
|
|
1015
|
+
}
|
|
1016
|
+
if (paidOn < record.request.date) {
|
|
1017
|
+
throw new InvoiceError([{ path: "paidOn", message: `must not be before the invoice date ${record.request.date}` }]);
|
|
1018
|
+
}
|
|
1019
|
+
const payment2 = { paidOn };
|
|
1020
|
+
await storage.withLock(STATUS_LOCK, () => storage.replace(statusPath(number), toJson(payment2)));
|
|
1021
|
+
}
|
|
1022
|
+
async function markUnpaid(storage, number) {
|
|
1023
|
+
await storage.withLock(STATUS_LOCK, () => storage.remove(statusPath(number)));
|
|
1024
|
+
}
|
|
1025
|
+
function paymentStateOf(record, status, today) {
|
|
1026
|
+
if (status.paidOn !== null) {
|
|
1027
|
+
return "paid" /* Paid */;
|
|
1028
|
+
}
|
|
1029
|
+
const terms = record.client.paymentTermsDays;
|
|
1030
|
+
const overdue = typeof terms === "number" && today > addDays(record.request.date, terms);
|
|
1031
|
+
return overdue ? "overdue" /* Overdue */ : "unpaid" /* Unpaid */;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
874
1034
|
// src/schedule.ts
|
|
875
1035
|
var MONTHS_IN_YEAR = 12;
|
|
876
1036
|
var YEAR3 = /^\d{4}$/;
|
|
@@ -901,7 +1061,7 @@ async function prepareYearSchedule(storage, request) {
|
|
|
901
1061
|
try {
|
|
902
1062
|
await storage.writeNew(schedulePath(schedule.id), toJson(schedule));
|
|
903
1063
|
} catch (error) {
|
|
904
|
-
if (error
|
|
1064
|
+
if (isStorageExistsError(error)) {
|
|
905
1065
|
throw new InvoiceError([{ path: "schedule", message: `${schedule.id} is already prepared` }]);
|
|
906
1066
|
}
|
|
907
1067
|
throw error;
|
|
@@ -982,12 +1142,16 @@ async function issueDueMonths(storage, id, options) {
|
|
|
982
1142
|
exports.BUILT_IN_LAYOUTS = BUILT_IN_LAYOUTS;
|
|
983
1143
|
exports.BUILT_IN_RULES = BUILT_IN_RULES;
|
|
984
1144
|
exports.DEFAULT_LAYOUT = DEFAULT_LAYOUT;
|
|
1145
|
+
exports.FolderConflictKind = FolderConflictKind;
|
|
1146
|
+
exports.FolderOpenState = FolderOpenState;
|
|
985
1147
|
exports.InvoiceError = InvoiceError;
|
|
986
1148
|
exports.InvoiceKind = InvoiceKind;
|
|
987
1149
|
exports.LayoutAlign = LayoutAlign;
|
|
988
1150
|
exports.NUMBER_LOCK = NUMBER_LOCK;
|
|
1151
|
+
exports.PaymentState = PaymentState;
|
|
989
1152
|
exports.StorageExistsError = StorageExistsError;
|
|
990
1153
|
exports.addDays = addDays;
|
|
1154
|
+
exports.assertNoConflicts = assertNoConflicts;
|
|
991
1155
|
exports.centsToAmount = centsToAmount;
|
|
992
1156
|
exports.checkRequest = checkRequest;
|
|
993
1157
|
exports.claimInvoice = claimInvoice;
|
|
@@ -998,24 +1162,31 @@ exports.editScheduleMonth = editScheduleMonth;
|
|
|
998
1162
|
exports.ensureFormat = ensureFormat;
|
|
999
1163
|
exports.expandDescription = expandDescription;
|
|
1000
1164
|
exports.exportQuarterCsv = exportQuarterCsv;
|
|
1165
|
+
exports.findFolderConflicts = findFolderConflicts;
|
|
1001
1166
|
exports.formatCents = formatCents;
|
|
1002
1167
|
exports.invoiceDir = invoiceDir;
|
|
1003
1168
|
exports.isBlank = isBlank;
|
|
1004
1169
|
exports.isRealDate = isRealDate;
|
|
1170
|
+
exports.isStorageExistsError = isStorageExistsError;
|
|
1005
1171
|
exports.issueCreditNote = issueCreditNote;
|
|
1006
1172
|
exports.issueDueMonths = issueDueMonths;
|
|
1007
1173
|
exports.issueInvoice = issueInvoice;
|
|
1008
1174
|
exports.listIssued = listIssued;
|
|
1009
1175
|
exports.loadLogo = loadLogo;
|
|
1010
1176
|
exports.loadParties = loadParties;
|
|
1177
|
+
exports.markPaid = markPaid;
|
|
1178
|
+
exports.markUnpaid = markUnpaid;
|
|
1011
1179
|
exports.migrateLegacyFolder = migrateLegacyFolder;
|
|
1012
1180
|
exports.missingForFinal = missingForFinal;
|
|
1013
1181
|
exports.nextNumber = nextNumber;
|
|
1182
|
+
exports.openDataFolder = openDataFolder;
|
|
1014
1183
|
exports.parseLayout = parseLayout;
|
|
1184
|
+
exports.paymentStateOf = paymentStateOf;
|
|
1015
1185
|
exports.pdfPath = pdfPath;
|
|
1016
1186
|
exports.periodTexts = periodTexts;
|
|
1017
1187
|
exports.prepareYearSchedule = prepareYearSchedule;
|
|
1018
1188
|
exports.readInvoice = readInvoice;
|
|
1189
|
+
exports.readPaymentStatus = readPaymentStatus;
|
|
1019
1190
|
exports.readSchedule = readSchedule;
|
|
1020
1191
|
exports.recordPath = recordPath;
|
|
1021
1192
|
exports.renderDraftPdf = renderDraftPdf;
|