@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 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 38 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.
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
- await sleep(retryAfterMs(response) ?? backoffMs * attempt);
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) {
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Contacts — customers and vendors alike.
3
3
  *
4
- * Search is fuzzy on the API side: "Nordlicht" finds "Nordlicht Consulting
5
- * GmbH", "Müller" may find three. Functions here return candidates and leave
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: "Nordlicht" finds "Nordlicht Consulting
5
- * GmbH", "Müller" may find three. Functions here return candidates and leave
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";
@@ -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
- return isDueImmediately(invoice)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kevludwig/lexware-office",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Typed client and domain logic for the Lexware Office Public API: contacts, receivables, payment reminder rules.",
5
5
  "type": "module",
6
6
  "license": "MIT",