@opencxh/domain 1.240.0 → 1.242.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/entities/catalog/codes.d.ts +32 -0
- package/dist/entities/catalog/codes.test.d.ts +1 -0
- package/dist/entities/catalog/index.d.ts +3 -0
- package/dist/entities/catalog/pricing.d.ts +40 -0
- package/dist/entities/catalog/pricing.test.d.ts +1 -0
- package/dist/entities/catalog/types.d.ts +107 -0
- package/dist/entities/document-template/tokens.d.ts +1 -1
- package/dist/entities/document-template/types.d.ts +1 -1
- package/dist/entities/money/index.d.ts +3 -0
- package/dist/entities/money/money.d.ts +66 -0
- package/dist/entities/money/money.test.d.ts +1 -0
- package/dist/entities/money/tax.d.ts +33 -0
- package/dist/entities/money/totals.d.ts +34 -0
- package/dist/entities/money/totals.test.d.ts +1 -0
- package/dist/entities/transaction/convert.d.ts +38 -0
- package/dist/entities/transaction/convert.test.d.ts +1 -0
- package/dist/entities/transaction/document.d.ts +71 -0
- package/dist/entities/transaction/document.test.d.ts +1 -0
- package/dist/entities/transaction/index.d.ts +5 -0
- package/dist/entities/transaction/keys.d.ts +16 -0
- package/dist/entities/transaction/kinds.d.ts +32 -0
- package/dist/entities/transaction/kinds.test.d.ts +1 -0
- package/dist/entities/transaction/types.d.ts +126 -0
- package/dist/index.cjs +21 -19
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2009 -1623
- package/package.json +1 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Three codes, three owners — and only one of them may be normalised.
|
|
3
|
+
*
|
|
4
|
+
* | Field | Whose | Shape |
|
|
5
|
+
* |---|---|---|
|
|
6
|
+
* | `sku` | ours | free, unique per organisation |
|
|
7
|
+
* | `gtin` | GS1's | 8, 12, 13 or 14 digits + check digit |
|
|
8
|
+
* | `barcodes` | mixed | as scanned |
|
|
9
|
+
*
|
|
10
|
+
* `gtin` and not `ean`: EAN-13 is one of four lengths, GTIN is the umbrella and the word GS1 uses
|
|
11
|
+
* itself. The screen says "EAN / barcode", because that is what people say.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* A GTIN as one key, or `undefined` when it is not one.
|
|
15
|
+
*
|
|
16
|
+
* The same box arrives as UPC-12 `012345678905` from an American feed and as EAN-13
|
|
17
|
+
* `0012345678905` from Shopify. Left-padding to 14 digits makes those one key; without it the
|
|
18
|
+
* second import creates a second product and the reporting axis is broken.
|
|
19
|
+
*
|
|
20
|
+
* **Refuses rather than quietly cleaning up.** The check digit costs ten lines and catches the
|
|
21
|
+
* typo that otherwise only shows in the webshop — and a product with an invented EAN is worse
|
|
22
|
+
* than a product without one.
|
|
23
|
+
*/
|
|
24
|
+
export declare function normalizeGtin(raw: string): string | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* A filter that finds a product by whatever was typed or scanned.
|
|
27
|
+
*
|
|
28
|
+
* One helper and not three queries in three screens, because of the trap in the middle:
|
|
29
|
+
* `barcodes` is a string **array**, and `{ barcodes: "x" }` matches nothing on it — it has to be
|
|
30
|
+
* `$in`. That mistake is silent; it returns an empty list, not an error.
|
|
31
|
+
*/
|
|
32
|
+
export declare function codeQuery(organizationId: string, code: string): Record<string, unknown>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Cents } from '../money/money';
|
|
2
|
+
import { PriceRule, Product } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* The ladder: the most specific rule wins, whole. No sum, no stacking.
|
|
5
|
+
*
|
|
6
|
+
* | Rank | Rule | Source |
|
|
7
|
+
* |---|---|---|
|
|
8
|
+
* | 1 | contract + product | `contract` |
|
|
9
|
+
* | 2 | customer + product | `customer` |
|
|
10
|
+
* | 3 | customer + category | `customer_category` |
|
|
11
|
+
* | 4 | segment + product | `segment` |
|
|
12
|
+
* | 5 | segment + category | `segment_category` |
|
|
13
|
+
* | 6 | the product's list price | `list` |
|
|
14
|
+
*
|
|
15
|
+
* A tier (`minQuantity`) is not a rung of its own but a tie-break within one: of the rules that
|
|
16
|
+
* pass, the highest threshold that the quantity reaches wins, and the source then reads `tier`
|
|
17
|
+
* — because that is the answer the salesperson is looking for.
|
|
18
|
+
*/
|
|
19
|
+
export interface PriceContext {
|
|
20
|
+
companyId?: string;
|
|
21
|
+
segmentKey?: string;
|
|
22
|
+
contractId?: string;
|
|
23
|
+
quantity: number;
|
|
24
|
+
/** Epoch ms; the validity window is judged against this, not against "now". */
|
|
25
|
+
at: number;
|
|
26
|
+
}
|
|
27
|
+
export interface ResolvedPrice {
|
|
28
|
+
unitPriceCents: Cents;
|
|
29
|
+
/** Where this amount came from. Almost free, and the only question a salesperson asks. */
|
|
30
|
+
sourceKey: string;
|
|
31
|
+
ruleId?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The price for this product, for this customer, at this quantity, on this date.
|
|
35
|
+
*
|
|
36
|
+
* Pure, so the screen can compute along while you type and land on the same cent the server
|
|
37
|
+
* stores. A rule that names neither the product nor its category does not apply, however
|
|
38
|
+
* specific its customer is: a discount on "everything" is a decision, not a price rule.
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolvePrice(product: Product, ctx: PriceContext, rules: readonly PriceRule[]): ResolvedPrice;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Cents } from '../money/money';
|
|
2
|
+
/**
|
|
3
|
+
* What we sell, variants included.
|
|
4
|
+
*
|
|
5
|
+
* One self-referencing table instead of a product/variant pair: a variant has exactly the same
|
|
6
|
+
* fields as its parent and misses none of them. What it adds is {@link Product.options} — and that
|
|
7
|
+
* is a column, not an entity.
|
|
8
|
+
*/
|
|
9
|
+
export interface Product {
|
|
10
|
+
id: string;
|
|
11
|
+
organizationId: string;
|
|
12
|
+
/** The parent this is a variant of. Absent = a product in its own right. */
|
|
13
|
+
parentId?: string;
|
|
14
|
+
/** What makes this variant different: `[{ key: "colour", value: "black" }]`. A label, nothing more. */
|
|
15
|
+
options?: {
|
|
16
|
+
key: string;
|
|
17
|
+
value: string;
|
|
18
|
+
}[];
|
|
19
|
+
name: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
/** Our own article number. Unique per organisation — see `codes.ts`. */
|
|
22
|
+
sku?: string;
|
|
23
|
+
/** The global code: EAN-13, UPC-12, GTIN-14. Stored normalised — see `normalizeGtin`. */
|
|
24
|
+
gtin?: string;
|
|
25
|
+
/** Whatever else is scannable (outer case, our own Code128), as scanned. */
|
|
26
|
+
barcodes?: string[];
|
|
27
|
+
/**
|
|
28
|
+
* The category this falls under — a key from a small table of its own, not free text.
|
|
29
|
+
*
|
|
30
|
+
* Having to be stable is the whole requirement: together with {@link Product.taxRateKey} this is
|
|
31
|
+
* the key the bookkeeping bridge picks a ledger account on. A free text field where one person
|
|
32
|
+
* types "Services" and another "services " cannot carry that.
|
|
33
|
+
*/
|
|
34
|
+
categoryKey?: string;
|
|
35
|
+
/** References to this product elsewhere (`moneybird:412`). A reference, never a copy. */
|
|
36
|
+
externalIds?: string[];
|
|
37
|
+
/** See {@link FIXED_UNITS}. Drives the form and the word after the quantity. */
|
|
38
|
+
unitKey: string;
|
|
39
|
+
/** Key into the organisation's VAT table, never the percentage itself. */
|
|
40
|
+
taxRateKey: string;
|
|
41
|
+
listPriceCents: Cents;
|
|
42
|
+
currency: string;
|
|
43
|
+
/**
|
|
44
|
+
* What we normally pay for this. **Not** a cost price out of an inventory valuation.
|
|
45
|
+
*
|
|
46
|
+
* Copied onto the line when it is added, and shielded: margin is not for everyone holding
|
|
47
|
+
* `sales.product.read`. It never reaches a template context either — `{line.cost}` on a quote
|
|
48
|
+
* would be one typo away.
|
|
49
|
+
*/
|
|
50
|
+
costPriceCents?: Cents;
|
|
51
|
+
/** Who we buy it from. A `Company` in crm, so free — and all a later purchase order needs. */
|
|
52
|
+
supplierCompanyId?: string;
|
|
53
|
+
archived?: boolean;
|
|
54
|
+
order?: number;
|
|
55
|
+
createdBy: string;
|
|
56
|
+
createdAt?: number;
|
|
57
|
+
updatedAt?: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Units follow work's `FIXED_TYPES` pattern: a fixed list plus a namespaced fallback, so an
|
|
61
|
+
* unknown key reads as itself instead of breaking.
|
|
62
|
+
*/
|
|
63
|
+
export declare const FIXED_UNITS: readonly ["piece", "hour", "day", "month", "km", "license"];
|
|
64
|
+
export type FixedUnit = (typeof FIXED_UNITS)[number];
|
|
65
|
+
/**
|
|
66
|
+
* A price for a customer, a segment or a contract.
|
|
67
|
+
*
|
|
68
|
+
* Deliberately no price expression language: no formulas, no conditions, no ordering field. The
|
|
69
|
+
* moment a price cannot be explained to the customer it is the wrong price.
|
|
70
|
+
*/
|
|
71
|
+
export interface PriceRule {
|
|
72
|
+
id: string;
|
|
73
|
+
organizationId: string;
|
|
74
|
+
/** Either a specific product or a whole category. One of the two, never both. */
|
|
75
|
+
productId?: string;
|
|
76
|
+
categoryKey?: string;
|
|
77
|
+
/** For whom. Absent = for everyone. */
|
|
78
|
+
companyId?: string;
|
|
79
|
+
/** A free label on the customer card ("reseller", "education"). An attribute in crm. */
|
|
80
|
+
segmentKey?: string;
|
|
81
|
+
/** From which quantity. Absent = from one. */
|
|
82
|
+
minQuantity?: number;
|
|
83
|
+
/** Exactly one of the two. A fixed price beats a discount when both would apply. */
|
|
84
|
+
unitPriceCents?: Cents;
|
|
85
|
+
discountPercent?: number;
|
|
86
|
+
currency: string;
|
|
87
|
+
validFrom?: number;
|
|
88
|
+
validTo?: number;
|
|
89
|
+
/**
|
|
90
|
+
* The contract that placed this rule. Declared now because it is the top rung of the ladder: a
|
|
91
|
+
* contract price is not a second pricing mechanism, it is a price rule with an owner and a term.
|
|
92
|
+
*/
|
|
93
|
+
contractId?: string;
|
|
94
|
+
createdBy: string;
|
|
95
|
+
createdAt?: number;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* What the product form sends. Everything optional but the name: the server fills defaults, and
|
|
99
|
+
* a form that has to send a complete product cannot add a variant with two fields.
|
|
100
|
+
*/
|
|
101
|
+
export interface SaveProductRequest extends Partial<Omit<Product, "id" | "organizationId" | "createdBy" | "createdAt" | "updatedAt">> {
|
|
102
|
+
id?: string;
|
|
103
|
+
name: string;
|
|
104
|
+
}
|
|
105
|
+
export interface SavePriceRuleRequest extends Partial<Omit<PriceRule, "id" | "organizationId" | "createdBy" | "createdAt">> {
|
|
106
|
+
id?: string;
|
|
107
|
+
}
|
|
@@ -33,7 +33,7 @@ export type DocumentSlots = Record<string, readonly DocumentBlock[]>;
|
|
|
33
33
|
* text lives.
|
|
34
34
|
*
|
|
35
35
|
* Every text-bearing field is walked, not just `text` — a token in a table cell or a letterhead
|
|
36
|
-
* value is the ordinary case for an invoice, and missing one would leave `{
|
|
36
|
+
* value is the ordinary case for an invoice, and missing one would leave `{invoice.number}`
|
|
37
37
|
* printed on a document that went out the door.
|
|
38
38
|
*/
|
|
39
39
|
export declare function fillDocument(blocks: readonly DocumentBlock[], context: TemplateContext, conditions?: Record<string, TemplateCondition>, slots?: DocumentSlots): FillDocumentResult;
|
|
@@ -59,7 +59,7 @@ export interface DocumentTemplate {
|
|
|
59
59
|
* app that decides it.
|
|
60
60
|
*/
|
|
61
61
|
export interface TemplateCondition {
|
|
62
|
-
/** A path into the same context the tokens read: `
|
|
62
|
+
/** A path into the same context the tokens read: `customer.direct_debit`. */
|
|
63
63
|
key: string;
|
|
64
64
|
/** Show when the value equals this. */
|
|
65
65
|
equals?: unknown;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Money, as whole cents. There is no `number` in euros anywhere in this platform.
|
|
3
|
+
*
|
|
4
|
+
* Not caution but necessity: `0.1 + 0.2 !== 0.3`, and a quote of eleven lines adds up eleven
|
|
5
|
+
* times. One cent between the screen and the invoice is not a rounding error, it is a customer
|
|
6
|
+
* on the phone.
|
|
7
|
+
*
|
|
8
|
+
* The currency lives on the transaction, never on the line: adding lines in two currencies does
|
|
9
|
+
* not produce an amount.
|
|
10
|
+
*/
|
|
11
|
+
export type Cents = number;
|
|
12
|
+
/**
|
|
13
|
+
* Half a cent goes up, below zero too.
|
|
14
|
+
*
|
|
15
|
+
* `Math.round(-0.5)` is `-0`, not `-1`: JS rounds towards +infinity. On a credit note — where
|
|
16
|
+
* every amount is negative — that is systematically one cent too little refunded per line, and it
|
|
17
|
+
* only shows when someone puts the credit note next to the invoice.
|
|
18
|
+
*/
|
|
19
|
+
export declare function roundHalfUp(value: number): Cents;
|
|
20
|
+
/**
|
|
21
|
+
* Split `total` over `weights` so the parts sum to exactly `total`.
|
|
22
|
+
*
|
|
23
|
+
* VAT is computed per rate group and not per line (see `totals.ts`), but a journal entry books it
|
|
24
|
+
* per line. Dividing proportionally and rounding each part independently loses or gains cents;
|
|
25
|
+
* largest-remainder gives every part its floor and hands the leftover cents to the largest
|
|
26
|
+
* fractions. Works for negative totals — a credit note is the whole point.
|
|
27
|
+
*
|
|
28
|
+
* Weights that sum to zero cannot be divided proportionally, so the whole amount lands on the
|
|
29
|
+
* first part rather than disappearing.
|
|
30
|
+
*/
|
|
31
|
+
export declare function allocate(total: Cents, weights: readonly number[]): Cents[];
|
|
32
|
+
/** Grouping and decimal marks. Defaults are Dutch, because the organisation is. */
|
|
33
|
+
export interface AmountFormat {
|
|
34
|
+
decimal?: string;
|
|
35
|
+
group?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Cents as a readable amount. Always two decimals, never a currency symbol.
|
|
39
|
+
*
|
|
40
|
+
* Hand-rolled instead of `Intl.NumberFormat`: this module is imported by `apps/*/server`, which
|
|
41
|
+
* runs on QuickJS, and `Intl` is not something to bet an invoice on. The symbol is the caller's
|
|
42
|
+
* job — it belongs next to the amount in the UI, not inside the number.
|
|
43
|
+
*/
|
|
44
|
+
export declare function formatCents(cents: Cents, format?: AmountFormat): string;
|
|
45
|
+
/**
|
|
46
|
+
* A typed amount to cents, or `undefined` when it is not an amount.
|
|
47
|
+
*
|
|
48
|
+
* Both "1.234,56" and "1,234.56" have to work, and they differ only in which separator came last.
|
|
49
|
+
* So: the **last** separator is the decimal mark when one or two digits follow it; anything else
|
|
50
|
+
* is grouping and gets dropped. "1.234" is therefore one thousand two hundred and thirty-four,
|
|
51
|
+
* not 1,234 — the reading a Dutch typist intends, and the one a three-digit group implies.
|
|
52
|
+
*
|
|
53
|
+
* `undefined` rather than `0` on junk: a silent zero is a line that quietly costs nothing.
|
|
54
|
+
*/
|
|
55
|
+
export declare function parseAmount(input: string): Cents | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* What a line earns: its net minus what the goods cost us.
|
|
58
|
+
*
|
|
59
|
+
* Derived, never stored — see the plan's §2.2c. A line without a cost price counts as zero margin
|
|
60
|
+
* and not as a loss; "unknown" and "free" are not the same thing, and the screen says which.
|
|
61
|
+
*/
|
|
62
|
+
export declare function marginCents(line: {
|
|
63
|
+
netCents: Cents;
|
|
64
|
+
quantity: number;
|
|
65
|
+
costPriceCents?: Cents;
|
|
66
|
+
}): Cents;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VAT rates: a table, not an engine.
|
|
3
|
+
*
|
|
4
|
+
* The rate lives on the organisation's settings and a line refers to it **by key**, never by
|
|
5
|
+
* percentage. A rate that changes (21 became 21 in 2019, 19 before that) must not rewrite history,
|
|
6
|
+
* and a percentage copied onto a thousand lines cannot be corrected.
|
|
7
|
+
*/
|
|
8
|
+
export interface TaxRate {
|
|
9
|
+
key: string;
|
|
10
|
+
/** i18n key, not text — the same rule as `FIXED_TYPES` in work. */
|
|
11
|
+
label: string;
|
|
12
|
+
/** Whole percent as written on the invoice: `21`, not `0.21`. */
|
|
13
|
+
percent: number;
|
|
14
|
+
kind: TaxRateKind;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Why a rate is what it is.
|
|
18
|
+
*
|
|
19
|
+
* `zero`, `reverse_charge` and `exempt` all compute to nothing and are still three different
|
|
20
|
+
* sentences on an invoice, which is the only reason they are separate: the template picks its
|
|
21
|
+
* line from this, and the bookkeeping connector maps it onto its own tax code.
|
|
22
|
+
*/
|
|
23
|
+
export type TaxRateKind = "standard" | "reduced" | "zero" | "reverse_charge" | "exempt";
|
|
24
|
+
/** The Dutch defaults an organisation starts with. It may edit them; it may not compute them. */
|
|
25
|
+
export declare const NL_TAX_RATES: readonly TaxRate[];
|
|
26
|
+
/**
|
|
27
|
+
* The rate for a key, or a zero-percent stand-in.
|
|
28
|
+
*
|
|
29
|
+
* Never `undefined`: an unknown key on a saved line must not make a total uncomputable. Charging
|
|
30
|
+
* nothing is the safe direction — too little VAT is a correction, a crash is a document nobody
|
|
31
|
+
* can open.
|
|
32
|
+
*/
|
|
33
|
+
export declare function taxRateFor(key: string, rates: readonly TaxRate[]): TaxRate;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { TransactionLine, TransactionTotals } from '../transaction/types';
|
|
2
|
+
import { Cents } from './money';
|
|
3
|
+
import { TaxRate } from './tax';
|
|
4
|
+
/**
|
|
5
|
+
* The rounding order. Fixed, and not a setting.
|
|
6
|
+
*
|
|
7
|
+
* 1. Line net = `roundHalfUp(quantity * unitPrice * (1 - discount))` — once, per line.
|
|
8
|
+
* 2. Net per rate = the sum of those line amounts. Exact, they are already integers.
|
|
9
|
+
* 3. VAT = `roundHalfUp(netPerRate * rate)` — **once per rate, not per line.**
|
|
10
|
+
* 4. Total = sum of nets + sum of VAT.
|
|
11
|
+
*
|
|
12
|
+
* Step 3 is where billing packages diverge. Rounding per line differs by up to ten cents on twenty
|
|
13
|
+
* lines from every bookkeeping system that rounds per rate group — which is what Moneybird, Exact
|
|
14
|
+
* and the tax office do.
|
|
15
|
+
*
|
|
16
|
+
* `quantity` may be fractional (3.5 hours); a *price* never is.
|
|
17
|
+
*/
|
|
18
|
+
export declare function lineNet(line: TransactionLine): Cents;
|
|
19
|
+
export interface PricedTransaction {
|
|
20
|
+
/** The same lines with `netCents` and `taxCents` filled in. */
|
|
21
|
+
lines: TransactionLine[];
|
|
22
|
+
totals: TransactionTotals;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The four steps, plus the one thing they leave open: VAT per line.
|
|
26
|
+
*
|
|
27
|
+
* The group amount from step 3 is handed back to its own lines with {@link allocate}, so
|
|
28
|
+
* `sum(line.taxCents) === totals.taxCents` exactly. A journal entry books VAT per line, and a
|
|
29
|
+
* split that is off by a cent is a journal entry that does not balance.
|
|
30
|
+
*
|
|
31
|
+
* `overrideKey` is the "reverse charge / exempt" tick on the transaction: it replaces every line's
|
|
32
|
+
* rate, because that is a property of the sale and not of the product.
|
|
33
|
+
*/
|
|
34
|
+
export declare function transactionTotals(lines: readonly TransactionLine[], rates: readonly TaxRate[], overrideKey?: string): PricedTransaction;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Transaction } from './types';
|
|
2
|
+
/** A transaction with `issuedAt` is frozen. One question, one answer, one place. */
|
|
3
|
+
export declare function isFrozen(transaction: Pick<Transaction, "issuedAt">): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* What a frozen transaction still accepts.
|
|
6
|
+
*
|
|
7
|
+
* `status`, `fileRef` and `dunning` are what happens *to* a document after it left. `number` and
|
|
8
|
+
* `externalIds` are there because Moneybird, Exact and AFAS issue the invoice number themselves
|
|
9
|
+
* and it arrives after issuing — a freeze rule that refuses that forces the bookkeeping bridge
|
|
10
|
+
* into a second column, and then the real invoice number is not on the invoice. `postedAt` is the
|
|
11
|
+
* push marking its own work done.
|
|
12
|
+
*
|
|
13
|
+
* Everything else is a `BadRequestError`. Whoever wants to change a sent quote gets v2.
|
|
14
|
+
*/
|
|
15
|
+
export declare const FROZEN_WRITABLE_FIELDS: readonly (keyof Transaction)[];
|
|
16
|
+
/** The fields a draft carries over; the server owns id, organisation and author. */
|
|
17
|
+
export type TransactionDraft = Omit<Transaction, "id" | "organizationId" | "createdBy" | "createdAt" | "updatedAt">;
|
|
18
|
+
/**
|
|
19
|
+
* The conversion: quote -> order -> invoice -> credit note, as one function.
|
|
20
|
+
*
|
|
21
|
+
* Amounts are not copied but recomputed by the caller (`transactionTotals`), because the target
|
|
22
|
+
* kind may invert them. Inverting happens on **`quantity`** and not on the price, so the one
|
|
23
|
+
* formula in `lineNet` keeps holding and a negative amount stays the outcome of a calculation
|
|
24
|
+
* rather than a second code path.
|
|
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.
|
|
28
|
+
*/
|
|
29
|
+
export declare function copyFrom(source: Transaction, kindKey: string): TransactionDraft;
|
|
30
|
+
/**
|
|
31
|
+
* The next version of the same quote.
|
|
32
|
+
*
|
|
33
|
+
* Whoever wants to change a sent quote does not edit it: the previous one stays, read-only, and
|
|
34
|
+
* stays retrievable — because that is what the customer saw. Easy to build, painful to introduce
|
|
35
|
+
* afterwards: without this rule you have, a year from now, a quote that was changed after it was
|
|
36
|
+
* signed and no way left to see that.
|
|
37
|
+
*/
|
|
38
|
+
export declare function nextVersion(source: Transaction): TransactionDraft;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { DocumentBlock } from '../../platform/document-blocks';
|
|
2
|
+
import { DocumentSlots, TemplateContext } from '../document-template/tokens';
|
|
3
|
+
import { TaxRate } from '../money/tax';
|
|
4
|
+
import { Transaction } from './types';
|
|
5
|
+
/**
|
|
6
|
+
* The seam between a transaction and the paper it is printed on.
|
|
7
|
+
*
|
|
8
|
+
* Two functions and no renderer. The block order lives in a `DocumentTemplate` row, not in code:
|
|
9
|
+
* the moment it is hardcoded here, the template layer is decoration and every house-style change
|
|
10
|
+
* is a deploy. What a template *cannot* express is the line table and the totals — a token
|
|
11
|
+
* replaces text, it cannot build a table out of an array — so those arrive as slots.
|
|
12
|
+
*
|
|
13
|
+
* The consequence for a second consumer (a work order, a timesheet) is that it costs one context,
|
|
14
|
+
* one slot function and one seeded template row. No renderer, no screen, no export.
|
|
15
|
+
*/
|
|
16
|
+
/** What a template author addresses. */
|
|
17
|
+
export interface TransactionContextSources {
|
|
18
|
+
companyName?: string;
|
|
19
|
+
/** `Company` carries no address today; the token exists so a template survives when it does. */
|
|
20
|
+
companyCity?: string;
|
|
21
|
+
contactName?: string;
|
|
22
|
+
organizationName?: string;
|
|
23
|
+
/** Who the recipient reads; drives which template row is picked. */
|
|
24
|
+
locale?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The tokens: `{customer.name}`, `{quote.number}`, `{totals.gross}`.
|
|
28
|
+
*
|
|
29
|
+
* English, like every other identifier in this repo. A token is a **path into data**, not a
|
|
30
|
+
* sentence — the Dutch on a quote is the text a template author writes around it.
|
|
31
|
+
*
|
|
32
|
+
* **The cost price and the margin are not in here, and that is load-bearing.** A template author
|
|
33
|
+
* writes `{regel.bedrag}` by hand; if a cost ever landed in this object, `{regel.inkoop}` on a
|
|
34
|
+
* quote would be one typo away from the customer. The shield on the way out of the server
|
|
35
|
+
* (`transactionView`) is the second lock; this one is the first.
|
|
36
|
+
*/
|
|
37
|
+
export declare function transactionContext(transaction: Transaction, sources?: TransactionContextSources): TemplateContext;
|
|
38
|
+
/**
|
|
39
|
+
* The blocks a template cannot write itself: the line table and the totals.
|
|
40
|
+
*
|
|
41
|
+
* Keyed by the `block_id` the template author puts on an empty placeholder — `lines` and `totals`.
|
|
42
|
+
* That pairing is a **convention**, not a contract: an author cannot invent a third slot and
|
|
43
|
+
* expect sales to fill it, because the consumer decides what it hands over.
|
|
44
|
+
*
|
|
45
|
+
* Amounts arrive already formatted. The blocks layer never computes; that is the whole reason a
|
|
46
|
+
* totals row carries a string.
|
|
47
|
+
*/
|
|
48
|
+
export declare function transactionSlots(transaction: Transaction, rates?: readonly TaxRate[], labels?: SlotLabels): DocumentSlots;
|
|
49
|
+
export interface SlotLabels {
|
|
50
|
+
description: string;
|
|
51
|
+
quantity: string;
|
|
52
|
+
unitPrice: string;
|
|
53
|
+
net: string;
|
|
54
|
+
tax: string;
|
|
55
|
+
gross: string;
|
|
56
|
+
}
|
|
57
|
+
export declare const DUTCH_SLOT_LABELS: SlotLabels;
|
|
58
|
+
/**
|
|
59
|
+
* The default quote template, as a **seed** — not a renderer.
|
|
60
|
+
*
|
|
61
|
+
* Note the split inside it: the token **paths** are English because they address data, the
|
|
62
|
+
* **text** around them is Dutch because a customer reads it.
|
|
63
|
+
*
|
|
64
|
+
* The distinction is the whole point of the template layer: this array is written into a
|
|
65
|
+
* `DocumentTemplate` row the first time an organisation needs one, and from then on the row is
|
|
66
|
+
* the truth. An administrator moves the terms above the lines and that sticks; nobody deploys.
|
|
67
|
+
*
|
|
68
|
+
* The two empty placeholders are the slots {@link transactionSlots} fills. Their `block_id` is
|
|
69
|
+
* the whole contract.
|
|
70
|
+
*/
|
|
71
|
+
export declare const DEFAULT_QUOTE_TEMPLATE_BLOCKS: DocumentBlock[];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scope kinds `apps/sales` owns — **the kinds, not the engine**.
|
|
3
|
+
*
|
|
4
|
+
* Every other app in this platform claims concrete nouns: `contact` + `company` (crm),
|
|
5
|
+
* `interaction` + `activity` (comms), `artifact` + `file` (storage). More importantly a scopeKey
|
|
6
|
+
* is *stored* — in relation edges, attributes, memory rows, pins. `invoice:412` stays
|
|
7
|
+
* `invoice:412` even if invoices ever move to a finance app; one branded `sales_document:412`
|
|
8
|
+
* would mean a backfill across four apps.
|
|
9
|
+
*/
|
|
10
|
+
export declare const TRANSACTION_SCOPE_KINDS: readonly string[];
|
|
11
|
+
export declare const PRODUCT_SCOPE_KIND = "product";
|
|
12
|
+
/** `invoice:412`. The kind of a transaction *is* its kind key — no mapping table. */
|
|
13
|
+
export declare function transactionScopeKey(kindKey: string, id: string): string;
|
|
14
|
+
export declare function productScopeKey(id: string): string;
|
|
15
|
+
/** Is this scopeKey one of ours, and which id does it point at? */
|
|
16
|
+
export declare function transactionIdFromScope(scopeKey: string): string | undefined;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { TransactionStatus } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* A kind is a row of flags, not a subclass.
|
|
4
|
+
*
|
|
5
|
+
* Switching on `order` and `invoice` later is a row here plus a number series — no second screen,
|
|
6
|
+
* no second line model, no second renderer. That is the whole reason to build the engine generic
|
|
7
|
+
* in one go.
|
|
8
|
+
*
|
|
9
|
+
* There is no `direction`: everything `apps/sales` issues goes outward. A purchase invoice is not
|
|
10
|
+
* a sale — no source quote, a different approval flow, a different user — and belongs in a finance
|
|
11
|
+
* app, on the same type and its own table. A flag that is always `out` in one app and always `in`
|
|
12
|
+
* in the other is an app boundary, not a flag.
|
|
13
|
+
*/
|
|
14
|
+
export interface TransactionKind {
|
|
15
|
+
key: string;
|
|
16
|
+
/** i18n key, not text. */
|
|
17
|
+
label: string;
|
|
18
|
+
/** Gets a number from a series when issued. */
|
|
19
|
+
numbered: boolean;
|
|
20
|
+
/** Invert amounts: a credit note is an invoice with a minus sign. */
|
|
21
|
+
signFlip: boolean;
|
|
22
|
+
statuses: readonly TransactionStatus[];
|
|
23
|
+
}
|
|
24
|
+
export declare const TRANSACTION_KINDS: readonly TransactionKind[];
|
|
25
|
+
/**
|
|
26
|
+
* The kind for a key, or a safe stand-in.
|
|
27
|
+
*
|
|
28
|
+
* Never `undefined`: a stored row with a kind we no longer know must still open. The stand-in is
|
|
29
|
+
* the conservative one — unnumbered and unflipped, because handing out a number or inverting
|
|
30
|
+
* amounts on a guess is worse than showing a document with a strange label.
|
|
31
|
+
*/
|
|
32
|
+
export declare function transactionKind(key: string): TransactionKind;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|