@opencxh/domain 1.297.0 → 1.301.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.
Files changed (33) hide show
  1. package/dist/entities/agreement/types.d.ts +7 -2
  2. package/dist/entities/bookkeeping/types.d.ts +2 -1
  3. package/dist/entities/business-entity/types.d.ts +6 -0
  4. package/dist/entities/catalog/article.d.ts +28 -0
  5. package/dist/entities/catalog/article.test.d.ts +1 -0
  6. package/dist/entities/catalog/index.d.ts +1 -0
  7. package/dist/entities/catalog/pricing.d.ts +12 -6
  8. package/dist/entities/catalog/rating.d.ts +30 -0
  9. package/dist/entities/catalog/types.d.ts +133 -66
  10. package/dist/entities/catalog/usage-source.d.ts +11 -0
  11. package/dist/entities/company/address.d.ts +30 -0
  12. package/dist/entities/company/address.test.d.ts +1 -0
  13. package/dist/entities/company/index.d.ts +2 -0
  14. package/dist/entities/company/role.d.ts +33 -0
  15. package/dist/entities/company/types.d.ts +0 -22
  16. package/dist/entities/contact/segment.d.ts +1 -1
  17. package/dist/entities/purchase/feed.d.ts +132 -0
  18. package/dist/entities/purchase/feed.test.d.ts +1 -0
  19. package/dist/entities/purchase/index.d.ts +1 -0
  20. package/dist/entities/purchase/logic.d.ts +10 -4
  21. package/dist/entities/purchase/types.d.ts +28 -7
  22. package/dist/entities/transaction/booking.d.ts +1 -1
  23. package/dist/entities/transaction/convert.d.ts +2 -2
  24. package/dist/entities/transaction/document.d.ts +2 -0
  25. package/dist/entities/transaction/payment-term.d.ts +15 -1
  26. package/dist/entities/transaction/types.d.ts +18 -1
  27. package/dist/index.cjs +25 -23
  28. package/dist/index.d.ts +1 -0
  29. package/dist/index.js +3026 -2700
  30. package/dist/platform/bookkeeping.d.ts +4 -2
  31. package/dist/platform/csv.d.ts +12 -0
  32. package/dist/platform/csv.test.d.ts +1 -0
  33. package/package.json +1 -1
@@ -0,0 +1,132 @@
1
+ import { Cents } from '../money/money';
2
+ import { OfferAvailability, SupplierOffer } from './types';
3
+ /**
4
+ * A supplier's price and stock list as a file (plan 045 C): uploaded, mailed, or picked up from a
5
+ * storage folder. It only updates offers we already have; a line we do not know is listed, never
6
+ * turned into a product or an offer by itself.
7
+ */
8
+ /** The columns a feed can fill. `vendorSku` or `gtin` is the key; the rest is optional. */
9
+ export type FeedColumn = "vendorSku" | "gtin" | "description" | "cost" | "list" | "currency" | "stock" | "leadDays";
10
+ export declare const FEED_COLUMNS: readonly FeedColumn[];
11
+ /** Per column, the header it is read from. Set once (the AI proposes, a person approves). */
12
+ export type FeedMapping = Partial<Record<FeedColumn, string>>;
13
+ export type FeedSource = "upload" | "mail" | "storage";
14
+ /** How often a new file is expected; it decides how long a stock figure counts as fresh. */
15
+ export type FeedEvery = "day" | "week" | "month";
16
+ export declare const FEED_EVERY_MS: Record<FeedEvery, number>;
17
+ /** A price jump above this share is still applied, but said out loud. */
18
+ export declare const DEFAULT_JUMP_PERCENT = 10;
19
+ export interface SupplierFeed {
20
+ id: string;
21
+ organizationId: string;
22
+ businessEntityId: string;
23
+ supplierCompanyId: string;
24
+ name?: string;
25
+ source: FeedSource;
26
+ /** For `storage`: the folder it reads the newest csv from (a dated file each time is fine). */
27
+ storage?: {
28
+ mountId: string;
29
+ path: string;
30
+ };
31
+ /** The file the last storage run read, and how new it was: the same file is not read twice. */
32
+ lastFile?: {
33
+ id: string;
34
+ at: number;
35
+ };
36
+ mapping: FeedMapping;
37
+ /** When the file has no currency column. Absent = the administration's. */
38
+ currency?: string;
39
+ every: FeedEvery;
40
+ jumpPercent?: number;
41
+ lastRunAt?: number;
42
+ lastRunId?: string;
43
+ archived?: boolean;
44
+ createdBy: string;
45
+ createdAt?: number;
46
+ updatedAt?: number;
47
+ }
48
+ /** One line of the file, read. */
49
+ export interface FeedRow {
50
+ /** 1-based, the header included: what a person sees in the file. */
51
+ line: number;
52
+ vendorSku?: string;
53
+ gtin?: string;
54
+ description?: string;
55
+ costCents?: Cents;
56
+ listCents?: Cents;
57
+ currency?: string;
58
+ /** `null`: the file says nothing about stock, or "on request". */
59
+ stock?: number | null;
60
+ leadDays?: number;
61
+ }
62
+ export interface FeedReadResult {
63
+ rows: FeedRow[];
64
+ errors: {
65
+ line: number;
66
+ message: string;
67
+ }[];
68
+ /** A mapped header that is not in the file: the supplier changed the layout. */
69
+ missingHeaders: string[];
70
+ }
71
+ /** The file's cells, read through the mapping. Deterministic: the AI only proposed the mapping. */
72
+ export declare function readFeed(table: readonly string[][], mapping: FeedMapping): FeedReadResult;
73
+ /** Just enough of an article to match a feed line on its barcode. */
74
+ export interface FeedProduct {
75
+ id: string;
76
+ gtin?: string;
77
+ barcodes?: string[];
78
+ }
79
+ export type FeedMatch = {
80
+ row: FeedRow;
81
+ offer: SupplierOffer;
82
+ } | {
83
+ row: FeedRow;
84
+ product: FeedProduct;
85
+ } | {
86
+ row: FeedRow;
87
+ };
88
+ /**
89
+ * Each line to an offer of this supplier (their article number first, then the barcode of the
90
+ * article it is for), else to an article without an offer yet, else to nothing.
91
+ * `offers` are this supplier's in this administration.
92
+ */
93
+ export declare function matchFeed(rows: readonly FeedRow[], offers: readonly SupplierOffer[], products: readonly FeedProduct[]): FeedMatch[];
94
+ export interface OfferChange {
95
+ offerId: string;
96
+ set: Partial<Pick<SupplierOffer, "costCents" | "listCents" | "currency" | "vendorSku">> & {
97
+ availability: OfferAvailability;
98
+ };
99
+ /** A price moved more than the feed's threshold; applied, and said. */
100
+ jump?: {
101
+ fromCents: Cents;
102
+ toCents: Cents;
103
+ percent: number;
104
+ };
105
+ }
106
+ export interface FeedUnmatched {
107
+ line: number;
108
+ vendorSku?: string;
109
+ gtin?: string;
110
+ description?: string;
111
+ costCents?: Cents;
112
+ /** An article we have, with no offer from this supplier yet: "make an offer" is one click. */
113
+ productId?: string;
114
+ }
115
+ export interface FeedOutcome {
116
+ changes: OfferChange[];
117
+ /** Offers whose price and stock stayed as they were (only the freshness moved). */
118
+ unchanged: number;
119
+ /** Offers this feed filled before and no longer lists: stock 0, nothing deleted. */
120
+ gone: string[];
121
+ unmatched: FeedUnmatched[];
122
+ }
123
+ /**
124
+ * What the run writes. Every matched offer gets a fresh stock stamp — the freshness is the
125
+ * point of a feed; price, advice price and currency only when the file has them.
126
+ */
127
+ export declare function feedOutcome(matches: readonly FeedMatch[], offers: readonly SupplierOffer[], now: number, jumpPercent?: number): FeedOutcome;
128
+ /**
129
+ * A first mapping from the header alone: an exact word, then a header that starts with one.
130
+ * No AI, no guessing beyond the list — what it leaves open, the AI or a person fills.
131
+ */
132
+ export declare function guessFeedMapping(header: readonly string[]): FeedMapping;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,6 @@
1
1
  export * from './contract';
2
2
  export * from './demand';
3
+ export * from './feed';
3
4
  export * from './kinds';
4
5
  export * from './logic';
5
6
  export * from './match';
@@ -11,17 +11,23 @@ export interface GoodsReceiptProblem {
11
11
  }
12
12
  /**
13
13
  * Why a receipt cannot be booked: more in than still open, more back than came in, or serial
14
- * numbers that do not match the count. No serials is fine (bulk goods); some serials must be all.
14
+ * numbers that do not match the count in base units. No serials is fine (bulk goods); some serials
15
+ * must be all.
15
16
  */
16
- export declare function goodsReceiptProblems(receipt: Pick<GoodsReceipt, "movement" | "lines">, poLines: readonly Pick<PurchaseLine, "id" | "quantity">[], booked: readonly GoodsReceipt[]): GoodsReceiptProblem[];
17
+ export declare function goodsReceiptProblems(receipt: Pick<GoodsReceipt, "movement" | "lines">, poLines: readonly Pick<PurchaseLine, "id" | "quantity" | "factor">[], booked: readonly GoodsReceipt[]): GoodsReceiptProblem[];
17
18
  /** Lines of a receipt worth keeping: a positive quantity, serials trimmed and unique. */
18
19
  export declare function cleanReceiptLines(lines: readonly GoodsReceiptLine[]): GoodsReceiptLine[];
19
20
  /** Valid on `at`. */
20
21
  export declare function offerValid(offer: Pick<SupplierOffer, "validFrom" | "validTo">, at: number): boolean;
21
22
  /** The offer that prices the product: the marked default if valid, else the first valid one. */
22
23
  export declare function defaultOffer<T extends SupplierOffer>(offers: readonly T[], at: number): T | null;
23
- /** Purchase units → product units: two rolls of 500 m are 1,000 m. */
24
- export declare function toProductUnits(offer: Pick<SupplierOffer, "unitsPerPurchaseUnit">, qty: number): number;
24
+ /**
25
+ * Purchase units → base units: two rolls of 500 m are 1,000 m. Takes an offer or an order line —
26
+ * a receipt counts with the factor the line was ordered at, never today's offer (plan 046 #14).
27
+ */
28
+ export declare function toProductUnits(source: {
29
+ factor?: number;
30
+ }, qty: number): number;
25
31
  /**
26
32
  * The offer's cost per product unit, as amount, currency and period — what is written back to the
27
33
  * product. Never a monthly equivalent: € 222 per year stays € 222 per year.
@@ -1,4 +1,5 @@
1
1
  import { ProvisioningAction, ProvisioningState } from '../../platform/provisioning';
2
+ import { LineRhythm } from '../agreement/types';
2
3
  import { Cents } from '../money/money';
3
4
  import { PricePer } from '../money/period-price';
4
5
  import { PostalAddress } from '../organization/types';
@@ -24,14 +25,17 @@ export interface SupplierOffer {
24
25
  costCents: Cents;
25
26
  currency: string;
26
27
  per?: PricePer;
27
- /** The supplier's term: an NCE yearly licence is `year` even when billed monthly. */
28
- term?: "month" | "year" | "3year";
28
+ /**
29
+ * The supplier's commitment in months (1, 12, 24, 36), in sales' words (`Commitment.termMonths`):
30
+ * an NCE yearly licence is 12 even when billed monthly. Absent = cancel any time.
31
+ */
32
+ termMonths?: number;
29
33
  /** How often they invoice; `costCents` is per `per`, not per invoice. */
30
- billedEvery?: "month" | "year" | "once";
31
- /** What you order in: a roll, a pallet, a box of 50. Absent = the product's own unit. */
32
- purchaseUnit?: string;
33
- /** Product units in one purchase unit: a roll of 500 m is 500. */
34
- unitsPerPurchaseUnit?: number;
34
+ billedEvery?: LineRhythm;
35
+ /** What you order in: a roll, a pallet, a box of 50. Absent = the article's base unit. */
36
+ unitKey?: string;
37
+ /** Base units in one `unitKey` (a roll of 500 m is 500). Absent = the article's own factor, else 1. */
38
+ factor?: number;
35
39
  validFrom?: number;
36
40
  validTo?: number;
37
41
  minQty?: number;
@@ -51,11 +55,25 @@ export interface SupplierOffer {
51
55
  app: string;
52
56
  offer: string;
53
57
  };
58
+ /** The supplier's advised price (the Microsoft ERP price), from a feed (plan 045 G). */
59
+ listCents?: Cents;
60
+ /** The last known stock; only the latest, no history (plan 045 B.1). */
61
+ availability?: OfferAvailability;
54
62
  isDefault: boolean;
55
63
  createdBy: string;
56
64
  createdAt?: number;
57
65
  updatedAt?: number;
58
66
  }
67
+ /**
68
+ * What the supplier has, as last heard. `qty: null` = "can deliver, number unknown". `at` says
69
+ * how old it is: a stand older than the feed's rhythm counts as unknown in the choice.
70
+ */
71
+ export interface OfferAvailability {
72
+ qty: number | null;
73
+ at: number;
74
+ source: "feed" | "api" | "manual";
75
+ leadDays?: number;
76
+ }
59
77
  /** Ours to keep, or sold on to a customer — decides where the cost books (fase 6). */
60
78
  export type PurchaseUse = "own" | "resale";
61
79
  export interface PurchaseLine extends TransactionLine {
@@ -111,6 +129,9 @@ export interface LineProvisioning {
111
129
  export interface DeliverTo {
112
130
  kind: "own" | "customer" | "site";
113
131
  companyId?: string;
132
+ /** A crm `CompanyAddress` (plan 050); absent = the company's primary delivery address. */
133
+ addressId?: string;
134
+ /** Frozen when the order is sent: what the supplier was told, whatever crm says later. */
114
135
  address?: PostalAddress;
115
136
  }
116
137
  /** A purchase order: a `Transaction` of kind `purchase_order`; `companyId` is the supplier. */
@@ -11,4 +11,4 @@ export declare function isBookable(tx: Pick<Transaction, "kindKey" | "issuedAt">
11
11
  * Headings and options carry no amount, so they are not lines here. A reverse charge or
12
12
  * exemption ticked on the document replaces every line's code, as it did when it was priced.
13
13
  */
14
- export declare function bookingDocumentOf(tx: Transaction, company: Pick<Company, "id" | "name" | "vatNumber" | "kvkNumber" | "address"> | null, sourceApp?: string): BookingDocument;
14
+ export declare function bookingDocumentOf(tx: Transaction, company: Pick<Company, "id" | "name" | "vatNumber" | "kvkNumber"> | null, sourceApp?: string): BookingDocument;
@@ -22,12 +22,12 @@ export declare function awaitsNumber(tx: Pick<Transaction, "issuedAt" | "kindKey
22
22
  * What a save may set on a draft. `status` is not here: it moves through `issue` and the status
23
23
  * route, which check the transition and announce it.
24
24
  */
25
- export declare const DRAFT_FIELDS: readonly ["companyId", "contactId", "dealItemId", "title", "reference", "currency", "taxOverrideKey", "introText", "termsText", "expiresAt", "deliveredAt", "periodFrom", "periodTo", "paymentTerm", "lines"];
25
+ export declare const DRAFT_FIELDS: readonly ["companyId", "contactId", "dealItemId", "title", "reference", "currency", "taxOverrideKey", "introText", "termsText", "expiresAt", "deliveredAt", "periodFrom", "periodTo", "paymentTerm", "billingAddressId", "lines"];
26
26
  /**
27
27
  * The draft fields a save may empty, named in `clear`. A list and not `null` in the body:
28
28
  * `invoke` strips `null`, so "empty this" never reaches the server any other way.
29
29
  */
30
- export declare const CLEARABLE_FIELDS: readonly ["contactId", "dealItemId", "title", "reference", "taxOverrideKey", "introText", "termsText", "expiresAt", "deliveredAt", "periodFrom", "periodTo"];
30
+ export declare const CLEARABLE_FIELDS: readonly ["contactId", "dealItemId", "title", "reference", "taxOverrideKey", "introText", "termsText", "expiresAt", "deliveredAt", "periodFrom", "periodTo", "billingAddressId"];
31
31
  export type ClearableField = (typeof CLEARABLE_FIELDS)[number];
32
32
  /** The fields a draft carries over; the server owns id, organisation and author. */
33
33
  export type TransactionDraft = Omit<Transaction, "id" | "organizationId" | "createdBy" | "createdAt" | "updatedAt">;
@@ -59,6 +59,8 @@ export interface SlotLabels {
59
59
  unitPrice: string;
60
60
  /** After a period price: "222,00 per jaar". */
61
61
  per: Record<PricePer, string>;
62
+ /** A meter's display unit after a rated quantity: `minute` → "min". Unknown = the key. */
63
+ units: Record<string, string>;
62
64
  net: string;
63
65
  tax: string;
64
66
  gross: string;
@@ -28,7 +28,9 @@ export interface PaymentTerm {
28
28
  method: PaymentTermMethod;
29
29
  /** Days after issue until it falls due. `0` is payable immediately. */
30
30
  dueDays: number;
31
- /** The one a new document starts with when neither the request nor the customer says. */
31
+ /** Who pays under it (plan 050): our customer (`sales`), or we a supplier. Absent = both. */
32
+ direction?: PaymentTermDirection;
33
+ /** The one a new document starts with when neither the request nor the role says. */
32
34
  isDefault?: boolean;
33
35
  archived?: boolean;
34
36
  order?: number;
@@ -36,6 +38,18 @@ export interface PaymentTerm {
36
38
  createdAt?: number;
37
39
  updatedAt?: number;
38
40
  }
41
+ export type PaymentTermDirection = "sales" | "purchase";
42
+ /** Whether a term may be picked for a document in `direction`. */
43
+ export declare const termApplies: (term: Pick<PaymentTerm, "direction">, direction: PaymentTermDirection) => boolean;
44
+ /**
45
+ * The term a new document takes: the one asked for, else the role's, else the default in that
46
+ * direction (a direction's own default beats a shared one). Archived or wrong-way codes are
47
+ * skipped, never an error — the next rung answers. `null` = the organisation has no terms.
48
+ */
49
+ export declare function pickPaymentTerm(terms: readonly PaymentTerm[], direction: PaymentTermDirection, wanted?: {
50
+ explicit?: string;
51
+ role?: string;
52
+ }): PaymentTerm | null;
39
53
  /**
40
54
  * The due date of a document issued at `issuedAt` under a term of `dueDays`.
41
55
  *
@@ -5,6 +5,7 @@ import { DocumentSender } from '../document-template/types';
5
5
  import { Cents } from '../money/money';
6
6
  import { PricePer } from '../money/period-price';
7
7
  import { TaxRateKind } from '../money/tax';
8
+ import { PostalAddress } from '../organization/types';
8
9
  import { OwnerScope } from '../scope/types';
9
10
  import { TransactionPaymentTerm } from './payment-term';
10
11
  /**
@@ -83,6 +84,11 @@ export interface Transaction {
83
84
  * ride along because the due date is computed at issue, and by then the row may be gone.
84
85
  */
85
86
  paymentTerm?: TransactionPaymentTerm;
87
+ /**
88
+ * The customer's crm address this document goes to (plan 050); absent = its primary billing
89
+ * address. Read while it is a draft, frozen as `issuedPaper.billingAddress` when it goes out.
90
+ */
91
+ billingAddressId?: string;
86
92
  /**
87
93
  * When it falls due. Computed at issue from the condition, and only for a numbered kind: a quote
88
94
  * does not fall due, it expires — that is `expiresAt`.
@@ -170,6 +176,8 @@ export interface IssuedPaper {
170
176
  sender: DocumentSender;
171
177
  templateId?: string;
172
178
  customerName?: string;
179
+ /** The billing address as it went out (plan 050); the books take this one, not crm's today. */
180
+ billingAddress?: PostalAddress;
173
181
  }
174
182
  export interface TransactionLine {
175
183
  /** Stable within the transaction, so a version diff can follow a line. */
@@ -181,7 +189,13 @@ export interface TransactionLine {
181
189
  name: string;
182
190
  description?: string;
183
191
  unitKey: string;
192
+ /**
193
+ * Base units in one `unitKey`, fixed when the line was added (a box of 10 is 10). Absent = 1.
194
+ * Receipt, delivery and invoice count with this, never with today's article (plan 046 #14).
195
+ */
196
+ factor?: number;
184
197
  quantity: number;
198
+ /** Per `unitKey`: a box of 10 at € 3 a piece is € 30. */
185
199
  unitPriceCents: Cents;
186
200
  /** The period the unit price is for ("€ 25 per month"). Absent = once, per unit. */
187
201
  pricePer?: PricePer;
@@ -193,7 +207,10 @@ export interface TransactionLine {
193
207
  netCents: Cents;
194
208
  /** This line's share of its rate group's VAT. Shielded, like the cost price. */
195
209
  taxCents: Cents;
196
- /** The cost price as it stood when the line was added. Shielded by `sales.product.cost`. */
210
+ /**
211
+ * The cost price as it stood when the line was added, per `unitKey`, **in the document's
212
+ * currency** (converted on that day, plan 046 #16). Shielded by `catalog.product.cost`.
213
+ */
197
214
  costPriceCents?: Cents;
198
215
  /** The mapping axis onto a chart of accounts, copied from the product's category. */
199
216
  ledgerCategoryKey?: string;