@opencxh/domain 1.248.0 → 1.249.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.
@@ -45,6 +45,14 @@ export interface Company {
45
45
  * recipient block is the first, and the issue gate refuses a document with a hole in it.
46
46
  */
47
47
  address?: PostalAddress;
48
+ /**
49
+ * This customer's default payment condition, as a code `apps/sales` owns.
50
+ *
51
+ * An opaque key, exactly like {@link Company.locale} is an opaque language: crm stores it and
52
+ * never interprets it. It is a default and not a rule — a document copies the condition when it
53
+ * is created and keeps it from then on, so changing this never rewrites what already went out.
54
+ */
55
+ paymentTermCode?: string;
48
56
  /** Raw, human-readable phone/website as entered, for display. */
49
57
  phone?: string;
50
58
  website?: string;
@@ -23,8 +23,8 @@ export type TransactionDraft = Omit<Transaction, "id" | "organizationId" | "crea
23
23
  * formula in `lineNet` keeps holding and a negative amount stays the outcome of a calculation
24
24
  * rather than a second code path.
25
25
  *
26
- * What is deliberately dropped: the number, the issue date, the file, the external ids and the
27
- * dunning history. Those belong to the document that went out, and this is a new one.
26
+ * What is deliberately dropped: the number, the issue date, the due date, the file, the external
27
+ * ids and the dunning history. Those belong to the document that went out, and this is a new one.
28
28
  */
29
29
  export declare function copyFrom(source: Transaction, kindKey: string): TransactionDraft;
30
30
  /**
@@ -1,5 +1,6 @@
1
1
  import { DocumentBlock } from '../../platform/document-blocks';
2
2
  import { DocumentSlots, TemplateContext } from '../document-template/tokens';
3
+ import { TemplateCondition } from '../document-template/types';
3
4
  import { TaxRate } from '../money/tax';
4
5
  import { PostalAddress } from '../organization/types';
5
6
  import { Transaction } from './types';
@@ -59,7 +60,7 @@ export interface SlotLabels {
59
60
  }
60
61
  export declare const DUTCH_SLOT_LABELS: SlotLabels;
61
62
  /**
62
- * The default quote template, as a **seed** — not a renderer.
63
+ * The default template for one kind, as a **seed** — not a renderer.
63
64
  *
64
65
  * Note the split inside it: the token **paths** are English because they address data, the
65
66
  * **text** around them is Dutch because a customer reads it.
@@ -68,7 +69,18 @@ export declare const DUTCH_SLOT_LABELS: SlotLabels;
68
69
  * `DocumentTemplate` row the first time an organisation needs one, and from then on the row is
69
70
  * the truth. An administrator moves the terms above the lines and that sticks; nobody deploys.
70
71
  *
72
+ * Built per kind rather than one constant, because the tokens are namespaced by the kind key —
73
+ * an invoice seeded with `{quote.number}` renders a hole where its number should be. The
74
+ * head fields differ too: a quote is valid until, an invoice falls due.
75
+ *
71
76
  * The two empty placeholders are the slots {@link transactionSlots} fills. Their `block_id` is
72
77
  * the whole contract.
73
78
  */
79
+ export declare function defaultTemplateBlocks(kindKey: string): DocumentBlock[];
80
+ /** The conditions a seeded template ships with: a block that is empty does not appear. */
81
+ export declare const DEFAULT_TEMPLATE_CONDITIONS: (kindKey: string) => Record<string, TemplateCondition>;
82
+ /**
83
+ * The quote seed, kept as a constant because it is what every existing organisation was seeded
84
+ * with — and what the tests pin.
85
+ */
74
86
  export declare const DEFAULT_QUOTE_TEMPLATE_BLOCKS: DocumentBlock[];
@@ -2,4 +2,6 @@ export * from './convert';
2
2
  export * from './document';
3
3
  export * from './keys';
4
4
  export * from './kinds';
5
+ export * from './number-series';
6
+ export * from './payment-term';
5
7
  export * from './types';
@@ -20,8 +20,38 @@ export interface TransactionKind {
20
20
  /** Invert amounts: a credit note is an invoice with a minus sign. */
21
21
  signFlip: boolean;
22
22
  statuses: readonly TransactionStatus[];
23
+ /**
24
+ * The route segment this kind lives under: `quotes`, `orders`, `invoices`.
25
+ *
26
+ * A field and not `${key}s`: a credit note is filed with the invoices, and string arithmetic on
27
+ * a key is how you end up with `/credit_notes` that nobody registered.
28
+ */
29
+ path: string;
30
+ /**
31
+ * Where it lands when issued.
32
+ *
33
+ * A row rather than an `if` in the handler — and it fixes a bug that is live today: the issue
34
+ * handler writes `status: "sent"` for everything, and `sent` is not in the status list of an
35
+ * invoice or a credit note.
36
+ */
37
+ issuedStatus: TransactionStatus;
23
38
  }
24
39
  export declare const TRANSACTION_KINDS: readonly TransactionKind[];
40
+ /**
41
+ * Which kinds the app actually offers today.
42
+ *
43
+ * Here and not in the server app, because both sides need the same answer and two lists drift:
44
+ * the save handler refuses what is not in here, and the navigation column draws a row for what is.
45
+ * A product decision, like the kinds themselves — §12.4: *"transactiesoorten zijn van ons, niet
46
+ * van de organisatie"*.
47
+ *
48
+ * `credit_note` is deliberately absent. Crediting an invoice is its own small feature — which
49
+ * lines, partial credits, the reference back — and a kind you can create but not fill correctly is
50
+ * worse than one you cannot create at all.
51
+ */
52
+ export declare const ENABLED_TRANSACTION_KINDS: readonly string[];
53
+ /** The kinds a reader can browse, one per route segment. A credit note files with the invoices. */
54
+ export declare function listedTransactionKinds(): TransactionKind[];
25
55
  /**
26
56
  * The kind for a key, or a safe stand-in.
27
57
  *
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The running number behind an invoice.
3
+ *
4
+ * One row per (administration, kind), and that row **is** the counter — there is no kv mirror and
5
+ * no cold-start reseed. `apps/work/server/src/item/sequence.ts` needs both because its counter
6
+ * lives on a project row it does not want to write on every issue; this row exists for nothing
7
+ * else, so writing it every time is the cheap answer.
8
+ *
9
+ * What is copied from work verbatim, because it was learned the hard way there:
10
+ *
11
+ * - **The lock is the optimisation; the unique index is the guarantee.** `Bridge.kv` has no
12
+ * compare-and-swap, so a serialised read-modify-write is a speed bump. The unique index on
13
+ * `(organizationId, businessEntityId, number)` is what actually refuses a duplicate.
14
+ * - **Gaps are accepted.** A number drawn for a write that then fails is burned. Nothing in the
15
+ * product may derive a count from the highest number.
16
+ */
17
+ export interface NumberSeries {
18
+ id: string;
19
+ organizationId: string;
20
+ /**
21
+ * Which administration. A series never reaches across one — two administrations each have a
22
+ * right to invoice 0001, which is exactly what the unique index is scoped to.
23
+ */
24
+ businessEntityId: string;
25
+ /** `invoice`, `credit_note`. One per kind: a bookkeeper reads them as separate books. */
26
+ kindKey: string;
27
+ /** What comes in front: `F`, or nothing. The year is not a prefix — that is the period. */
28
+ prefix: string;
29
+ /** `year` starts again at 1 in January; `never` runs on. */
30
+ resetOn: NumberSeriesReset;
31
+ /** Digits the counter is padded to. `4` → `0001`. */
32
+ padding: number;
33
+ /** The period the counter is in: `"2027"`, or `""` when it never resets. */
34
+ period: string;
35
+ /** The number the next document gets. */
36
+ next: number;
37
+ createdBy: string;
38
+ createdAt?: number;
39
+ updatedAt?: number;
40
+ }
41
+ export type NumberSeriesReset = "year" | "never";
42
+ /** Four digits and a yearly reset: the Dutch default, and what a seeded series starts on. */
43
+ export declare const DEFAULT_NUMBER_PADDING = 4;
44
+ /**
45
+ * `F2027-0001`.
46
+ *
47
+ * Pure, so a settings screen can show the format **before** the first invoice goes out — after
48
+ * that it is fixed, and a format nobody previewed is a format somebody finds out about from a
49
+ * customer.
50
+ *
51
+ * `padStart` never truncates, so a counter that outgrows its padding simply gets longer. Losing a
52
+ * digit would make two invoices share a number, which the index would then refuse — a numbering
53
+ * scheme has to bend rather than break.
54
+ */
55
+ export declare function formatSeriesNumber(series: Pick<NumberSeries, "prefix" | "period" | "padding">, sequence: number): string;
56
+ /** The period a series should be in right now. `""` when it never resets. */
57
+ export declare function currentPeriod(resetOn: NumberSeriesReset, at?: number): string;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,80 @@
1
+ /**
2
+ * How and when a document is paid.
3
+ *
4
+ * A table, not an engine — the same shape as `TaxRate`, and for the same reason: a document refers
5
+ * to a condition **by code**, never by its wording, so renaming "Binnen 14 dagen" does not rewrite
6
+ * what a customer already received.
7
+ */
8
+ export type PaymentTermMethod = "cash" | "credit" | "direct_debit" | "card" | "other";
9
+ /**
10
+ * `PaymentTermMethod` and not `PaymentMethod`: that name is taken by
11
+ * `entities/organization/billing.ts` for the card or mandate **we** charge a customer's
12
+ * subscription on. Two different questions — how our customer pays us, and how their customer
13
+ * pays them — and one of them had the name first.
14
+ */
15
+ export declare const PAYMENT_TERM_METHODS: readonly PaymentTermMethod[];
16
+ export interface PaymentTerm {
17
+ id: string;
18
+ organizationId: string;
19
+ /**
20
+ * Empty = shared with every administration; an administration's own row shadows it on `code`.
21
+ * Exactly how `TaxRate` works, so there is one rule to learn instead of two.
22
+ */
23
+ businessEntityId?: string;
24
+ /** Stable and fixed once it exists — documents point at it. `14D`, `CONTANT`, `INCASSO`. */
25
+ code: string;
26
+ /** What the customer reads. Plain text, not an i18n key: an organisation writes its own. */
27
+ label: string;
28
+ method: PaymentTermMethod;
29
+ /** Days after issue until it falls due. `0` is payable immediately. */
30
+ dueDays: number;
31
+ /** The one a new document starts with when neither the request nor the customer says. */
32
+ isDefault?: boolean;
33
+ archived?: boolean;
34
+ order?: number;
35
+ createdBy: string;
36
+ createdAt?: number;
37
+ updatedAt?: number;
38
+ }
39
+ /**
40
+ * The due date of a document issued at `issuedAt` under a term of `dueDays`.
41
+ *
42
+ * Whole days on the clock, deliberately not calendar arithmetic: an invoice issued at 23:00 falls
43
+ * due 30×24 hours later and nobody has ever argued about the hour. Doing it in local calendar days
44
+ * would make the answer depend on which timezone the server happened to be in.
45
+ */
46
+ export declare function dueDateFrom(issuedAt: number, dueDays: number): number;
47
+ /**
48
+ * The term that applies, out of the shared table and one administration's own.
49
+ *
50
+ * Shared rows first, then the administration's over them, by code — the same `shadowByKey` the VAT
51
+ * table uses. Kept in domain rather than in the app because both the server and the screen have to
52
+ * land on the same answer.
53
+ */
54
+ export declare function shadowTermsByCode(rows: readonly PaymentTerm[], businessEntityId?: string): PaymentTerm[];
55
+ /**
56
+ * The condition as it was copied onto a document.
57
+ *
58
+ * One object and not four columns: it is one concept, `dueAt` needs `dueDays` at issue time, and
59
+ * a `Transaction` already keeps `lines`, `totals` and `source` this way. Nothing queries on a
60
+ * payment condition, which is the only thing a flat column would buy.
61
+ */
62
+ export interface TransactionPaymentTerm {
63
+ code: string;
64
+ label: string;
65
+ method: PaymentTermMethod;
66
+ dueDays: number;
67
+ }
68
+ /** What a term looks like once it is on a document. */
69
+ export declare function toTransactionTerm(term: PaymentTerm): TransactionPaymentTerm;
70
+ /**
71
+ * The Dutch starting set, seeded on first read.
72
+ *
73
+ * Seeded and not hardcoded, exactly like `NL_TAX_RATES`: these three cover the practice and an
74
+ * organisation that needs a fourth may not have to wait for a deploy.
75
+ *
76
+ * The labels are **plain text and not i18n keys**, unlike the tax table. They are printed on the
77
+ * paper by the server, which has no `t()` in hand — and an organisation that writes to Germany
78
+ * edits three rows rather than waiting for a translation it cannot supply.
79
+ */
80
+ export declare const NL_PAYMENT_TERMS: readonly Omit<PaymentTerm, "id" | "organizationId" | "createdBy">[];
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,6 @@
1
1
  import { Cents } from '../money/money';
2
2
  import { OwnerScope } from '../scope/types';
3
+ import { TransactionPaymentTerm } from './payment-term';
3
4
  /**
4
5
  * The engine: a quote, an order, an invoice, a credit note.
5
6
  *
@@ -54,8 +55,41 @@ export interface Transaction {
54
55
  totals: TransactionTotals;
55
56
  /** Reverse charge or exemption: ticked, never derived. Replaces every line's rate. */
56
57
  taxOverrideKey?: string;
58
+ /**
59
+ * The remark above the lines, and the conditions under them.
60
+ *
61
+ * Declared in phase 0 and dead until phase 5: nothing wrote them and the document layer did not
62
+ * read them. They are the "opmerking" a person types on a document, and they render through
63
+ * `{<kind>.note}` and `{<kind>.terms}`.
64
+ */
57
65
  introText?: string;
58
66
  termsText?: string;
67
+ /**
68
+ * Which payment condition applied, **copied** at save — code, wording, method and days.
69
+ *
70
+ * Copied and not looked up, for the same reason a line copies its product name: a condition that
71
+ * is renamed or retired next year must not rewrite an invoice that already went out. The days
72
+ * ride along because the due date is computed at issue, and by then the row may be gone.
73
+ */
74
+ paymentTerm?: TransactionPaymentTerm;
75
+ /**
76
+ * When it falls due. Computed at issue from the condition, and only for a numbered kind: a quote
77
+ * does not fall due, it expires — that is `expiresAt`.
78
+ */
79
+ dueAt?: number;
80
+ /** When it was delivered. Set by a person; we have no way of knowing. */
81
+ deliveredAt?: number;
82
+ /**
83
+ * The service period this document covers: *"Periode 01-01-2027 t/m 31-03-2027"*.
84
+ *
85
+ * Empty on a one-off sale, filled on anything periodic — which is what a subscription invoice
86
+ * is. The field is here now rather than with `Agreement` in phase 6 because it passes the test
87
+ * from the plan: it records a **moment** that a frozen invoice states and that cannot be
88
+ * reconstructed afterwards. Which quarter was billed is not derivable from a contract that has
89
+ * since been changed or ended.
90
+ */
91
+ periodFrom?: number;
92
+ periodTo?: number;
59
93
  /** Epoch ms. `issuedAt` set = frozen. */
60
94
  issuedAt?: number;
61
95
  expiresAt?: number;