@kevludwig/lexware-office 0.4.0 → 0.4.2
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/README.md +1 -1
- package/dist/client.js +20 -1
- package/dist/contacts.d.ts +2 -2
- package/dist/contacts.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/reminders.d.ts +13 -0
- package/dist/reminders.js +52 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -90,7 +90,7 @@ The ledger needs somewhere to keep one small JSON record per invoice. `JsonStore
|
|
|
90
90
|
|
|
91
91
|
## Tests
|
|
92
92
|
|
|
93
|
-
`npm test` runs
|
|
93
|
+
`npm test` runs 59 tests against a fake API — no account, no network. `test/fake-api.ts` shows the pattern: routes answer by method and path, and every call is recorded.
|
|
94
94
|
|
|
95
95
|
## Not affiliated with Lexware
|
|
96
96
|
|
package/dist/client.js
CHANGED
|
@@ -55,6 +55,24 @@ export function query(params) {
|
|
|
55
55
|
return rendered ? `?${rendered}` : "";
|
|
56
56
|
}
|
|
57
57
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
58
|
+
/** Waits `ms`, or rejects with the signal's reason as soon as it aborts. */
|
|
59
|
+
function abortableSleep(ms, signal) {
|
|
60
|
+
if (!signal)
|
|
61
|
+
return sleep(ms).then(() => undefined);
|
|
62
|
+
if (signal.aborted)
|
|
63
|
+
return Promise.reject(signal.reason);
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const onAbort = () => {
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
reject(signal.reason);
|
|
68
|
+
};
|
|
69
|
+
const timer = setTimeout(() => {
|
|
70
|
+
signal.removeEventListener("abort", onAbort);
|
|
71
|
+
resolve();
|
|
72
|
+
}, ms);
|
|
73
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
74
|
+
});
|
|
75
|
+
}
|
|
58
76
|
/** The server's Retry-After in ms (seconds or an HTTP date), capped at 10 s; null without one. */
|
|
59
77
|
function retryAfterMs(response) {
|
|
60
78
|
const header = response.headers.get("retry-after")?.trim();
|
|
@@ -109,7 +127,8 @@ export function createLexwareClient(options) {
|
|
|
109
127
|
let response = await fire();
|
|
110
128
|
for (let attempt = 1; response.status === 429 && attempt <= retries; attempt++) {
|
|
111
129
|
await response.body?.cancel().catch(() => undefined);
|
|
112
|
-
|
|
130
|
+
// The caller's cancellation also ends the wait between retries.
|
|
131
|
+
await abortableSleep(retryAfterMs(response) ?? backoffMs * attempt, init.signal);
|
|
113
132
|
response = await fire();
|
|
114
133
|
}
|
|
115
134
|
if (!response.ok) {
|
package/dist/contacts.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Contacts — customers and vendors alike.
|
|
3
3
|
*
|
|
4
|
-
* Search is fuzzy on the API side: "
|
|
5
|
-
*
|
|
4
|
+
* Search is fuzzy on the API side: "Beispiel" finds "Beispiel GmbH",
|
|
5
|
+
* "Müller" may find three. Functions here return candidates and leave
|
|
6
6
|
* the choice to the caller; guessing one of several is the worse mistake.
|
|
7
7
|
*/
|
|
8
8
|
import { type LexwareClient } from "./client.ts";
|
package/dist/contacts.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Contacts — customers and vendors alike.
|
|
3
3
|
*
|
|
4
|
-
* Search is fuzzy on the API side: "
|
|
5
|
-
*
|
|
4
|
+
* Search is fuzzy on the API side: "Beispiel" finds "Beispiel GmbH",
|
|
5
|
+
* "Müller" may find three. Functions here return candidates and leave
|
|
6
6
|
* the choice to the caller; guessing one of several is the worse mistake.
|
|
7
7
|
*/
|
|
8
8
|
import { query } from "./client.js";
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { DEFAULT_APP_URL, DEFAULT_BASE_URL, LexwareApiError, createLexwareClient
|
|
|
2
2
|
export { contactEmail, createCustomer, findContactByNumber, findContactsByName, MIN_NAME_SEARCH_LENGTH, getContact, resolveContactByName, type Contact, type CreatedCustomer, type NewCustomer, } from "./contacts.ts";
|
|
3
3
|
export { addDays, berlinDay, daysSince } from "./dates.ts";
|
|
4
4
|
export { downloadInvoicePdf, findOverdueInvoice, findOverdueInvoices, type OverdueInvoice, } from "./receivables.ts";
|
|
5
|
-
export { DEFAULT_REMINDER_POLICY, daysPastReminderDue, earliestReminderDate, isDueImmediately, isOpenForReminder, reminderDueDate, reminderLedger, selectDueReminders, type DueReminder, type ReminderHold, type ReminderLedger, type ReminderPolicy, type ReminderRecord, type ReminderSelection, type ReminderStatus, mailContentSha256, paymentReminderEffect, sendReminder, type PaymentReminderEffect, type ReminderTarget, type SendReminderOutcome, } from "./reminders.ts";
|
|
5
|
+
export { DEFAULT_REMINDER_POLICY, daysPastReminderDue, earliestReminderDate, isDueImmediately, isOpenForReminder, reminderDueDate, nextPaymentDay, germanNationwideHolidays, germanPaymentDay, reminderLedger, selectDueReminders, type DueReminder, type ReminderHold, type ReminderLedger, type ReminderPolicy, type ReminderRecord, type ReminderSelection, type ReminderStatus, mailContentSha256, paymentReminderEffect, sendReminder, type PaymentReminderEffect, type ReminderTarget, type SendReminderOutcome, } from "./reminders.ts";
|
|
6
6
|
export { MailNotSentError, defaultReminderMail, reminderSubject, type ReminderMail, type ReminderMailData, type ReminderSender, type RenderReminderMail, type SendReminderMail, } from "./reminder-mail.ts";
|
|
7
7
|
export { keySegment, memoryStore, type JsonStore } from "./store.ts";
|
|
8
8
|
export { DUPLICATE_TOTAL_TOLERANCE, ITEMS_TOTAL_TOLERANCE, lineNet, netTotal, round2, taxFromGross, taxFromNet, type PricedLine, } from "./money.ts";
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ export { DEFAULT_APP_URL, DEFAULT_BASE_URL, LexwareApiError, createLexwareClient
|
|
|
2
2
|
export { contactEmail, createCustomer, findContactByNumber, findContactsByName, MIN_NAME_SEARCH_LENGTH, getContact, resolveContactByName, } from "./contacts.js";
|
|
3
3
|
export { addDays, berlinDay, daysSince } from "./dates.js";
|
|
4
4
|
export { downloadInvoicePdf, findOverdueInvoice, findOverdueInvoices, } from "./receivables.js";
|
|
5
|
-
export { DEFAULT_REMINDER_POLICY, daysPastReminderDue, earliestReminderDate, isDueImmediately, isOpenForReminder, reminderDueDate, reminderLedger, selectDueReminders, mailContentSha256, paymentReminderEffect, sendReminder, } from "./reminders.js";
|
|
5
|
+
export { DEFAULT_REMINDER_POLICY, daysPastReminderDue, earliestReminderDate, isDueImmediately, isOpenForReminder, reminderDueDate, nextPaymentDay, germanNationwideHolidays, germanPaymentDay, reminderLedger, selectDueReminders, mailContentSha256, paymentReminderEffect, sendReminder, } from "./reminders.js";
|
|
6
6
|
export { MailNotSentError, defaultReminderMail, reminderSubject, } from "./reminder-mail.js";
|
|
7
7
|
export { keySegment, memoryStore } from "./store.js";
|
|
8
8
|
export { DUPLICATE_TOTAL_TOLERANCE, ITEMS_TOTAL_TOLERANCE, lineNet, netTotal, round2, taxFromGross, taxFromNet, } from "./money.js";
|
package/dist/reminders.d.ts
CHANGED
|
@@ -26,7 +26,20 @@ export interface ReminderPolicy {
|
|
|
26
26
|
pendingValidHours: number;
|
|
27
27
|
/** A declined reminder stays quiet this many days. Default: 7. */
|
|
28
28
|
declinedQuietDays: number;
|
|
29
|
+
/**
|
|
30
|
+
* Whether a day (yyyy-MM-dd) counts as a day by which money can arrive.
|
|
31
|
+
* Set, a due date on another day moves to the next one that does — as § 193
|
|
32
|
+
* BGB moves a deadline off weekends and holidays. Not set: due dates count
|
|
33
|
+
* as they are. `germanPaymentDay` covers weekends and nationwide holidays.
|
|
34
|
+
*/
|
|
35
|
+
isPaymentDay?: (day: string) => boolean;
|
|
29
36
|
}
|
|
37
|
+
/** The day itself, or the next one `isPaymentDay` accepts (at most two weeks on). */
|
|
38
|
+
export declare function nextPaymentDay(day: string, isPaymentDay?: (day: string) => boolean): string;
|
|
39
|
+
/** Germany's nationwide public holidays of a year (yyyy-MM-dd). Regional ones are left to the caller. */
|
|
40
|
+
export declare function germanNationwideHolidays(year: number): string[];
|
|
41
|
+
/** A weekday that is no nationwide public holiday in Germany — when banks book. */
|
|
42
|
+
export declare function germanPaymentDay(day: string): boolean;
|
|
30
43
|
export declare const DEFAULT_REMINDER_POLICY: ReminderPolicy;
|
|
31
44
|
interface Dated {
|
|
32
45
|
voucherDate: string;
|
package/dist/reminders.js
CHANGED
|
@@ -19,6 +19,56 @@ import { downloadInvoicePdf, findOverdueInvoice, findOverdueInvoices } from "./r
|
|
|
19
19
|
import { createHash } from "node:crypto";
|
|
20
20
|
import { MailNotSentError } from "./reminder-mail.js";
|
|
21
21
|
import { keySegment } from "./store.js";
|
|
22
|
+
/** The day itself, or the next one `isPaymentDay` accepts (at most two weeks on). */
|
|
23
|
+
export function nextPaymentDay(day, isPaymentDay) {
|
|
24
|
+
if (!isPaymentDay)
|
|
25
|
+
return day;
|
|
26
|
+
let current = day;
|
|
27
|
+
for (let step = 0; step < 14 && !isPaymentDay(current); step++)
|
|
28
|
+
current = addDays(current, 1);
|
|
29
|
+
return current;
|
|
30
|
+
}
|
|
31
|
+
/** Easter Sunday of a year (Gregorian, anonymous algorithm), yyyy-MM-dd. */
|
|
32
|
+
function easterSunday(year) {
|
|
33
|
+
const a = year % 19;
|
|
34
|
+
const b = Math.floor(year / 100);
|
|
35
|
+
const c = year % 100;
|
|
36
|
+
const d = Math.floor(b / 4);
|
|
37
|
+
const e = b % 4;
|
|
38
|
+
const f = Math.floor((b + 8) / 25);
|
|
39
|
+
const g = Math.floor((b - f + 1) / 3);
|
|
40
|
+
const h = (19 * a + b - d - g + 15) % 30;
|
|
41
|
+
const i = Math.floor(c / 4);
|
|
42
|
+
const k = c % 4;
|
|
43
|
+
const l = (32 + 2 * e + 2 * i - h - k) % 7;
|
|
44
|
+
const m = Math.floor((a + 11 * h + 22 * l) / 451);
|
|
45
|
+
const month = Math.floor((h + l - 7 * m + 114) / 31);
|
|
46
|
+
const dayOfMonth = ((h + l - 7 * m + 114) % 31) + 1;
|
|
47
|
+
return `${year}-${String(month).padStart(2, "0")}-${String(dayOfMonth).padStart(2, "0")}`;
|
|
48
|
+
}
|
|
49
|
+
/** Germany's nationwide public holidays of a year (yyyy-MM-dd). Regional ones are left to the caller. */
|
|
50
|
+
export function germanNationwideHolidays(year) {
|
|
51
|
+
const easter = easterSunday(year);
|
|
52
|
+
return [
|
|
53
|
+
`${year}-01-01`,
|
|
54
|
+
addDays(easter, -2),
|
|
55
|
+
addDays(easter, 1),
|
|
56
|
+
`${year}-05-01`,
|
|
57
|
+
addDays(easter, 39),
|
|
58
|
+
addDays(easter, 50),
|
|
59
|
+
`${year}-10-03`,
|
|
60
|
+
`${year}-12-25`,
|
|
61
|
+
`${year}-12-26`,
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
/** A weekday that is no nationwide public holiday in Germany — when banks book. */
|
|
65
|
+
export function germanPaymentDay(day) {
|
|
66
|
+
const date = new Date(`${day.slice(0, 10)}T00:00:00Z`);
|
|
67
|
+
const weekday = date.getUTCDay();
|
|
68
|
+
if (weekday === 0 || weekday === 6)
|
|
69
|
+
return false;
|
|
70
|
+
return !germanNationwideHolidays(date.getUTCFullYear()).includes(day.slice(0, 10));
|
|
71
|
+
}
|
|
22
72
|
export const DEFAULT_REMINDER_POLICY = {
|
|
23
73
|
minDaysOverdue: 3,
|
|
24
74
|
immediatePaymentTermDays: 14,
|
|
@@ -31,9 +81,10 @@ export function isDueImmediately(invoice) {
|
|
|
31
81
|
}
|
|
32
82
|
/** The day from which lateness counts (yyyy-MM-dd). */
|
|
33
83
|
export function reminderDueDate(invoice, policy = DEFAULT_REMINDER_POLICY) {
|
|
34
|
-
|
|
84
|
+
const due = isDueImmediately(invoice)
|
|
35
85
|
? addDays(invoice.voucherDate, policy.immediatePaymentTermDays)
|
|
36
86
|
: invoice.dueDate.slice(0, 10);
|
|
87
|
+
return nextPaymentDay(due, policy.isPaymentDay);
|
|
37
88
|
}
|
|
38
89
|
/** Days past the reminder due date. */
|
|
39
90
|
export function daysPastReminderDue(invoice, policy = DEFAULT_REMINDER_POLICY, today) {
|
package/package.json
CHANGED