@bisondesk/core-sdk 1.0.609 → 1.0.610

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 (36) hide show
  1. package/lib/constants.d.ts +1 -0
  2. package/lib/constants.d.ts.map +1 -1
  3. package/lib/constants.js +1 -0
  4. package/lib/constants.js.map +1 -1
  5. package/lib/types/leasing-debtors.d.ts +2 -0
  6. package/lib/types/leasing-debtors.d.ts.map +1 -1
  7. package/lib/types/leasing-debtors.js.map +1 -1
  8. package/lib/types/opportunities.d.ts +4 -0
  9. package/lib/types/opportunities.d.ts.map +1 -1
  10. package/lib/types/opportunities.js.map +1 -1
  11. package/lib/types/reports-leasing.d.ts +93 -0
  12. package/lib/types/reports-leasing.d.ts.map +1 -0
  13. package/lib/types/reports-leasing.js +11 -0
  14. package/lib/types/reports-leasing.js.map +1 -0
  15. package/lib/types/reports-sales.d.ts +6 -0
  16. package/lib/types/reports-sales.d.ts.map +1 -1
  17. package/lib/types/reports-sales.js.map +1 -1
  18. package/lib/types/vehicles.d.ts +1 -0
  19. package/lib/types/vehicles.d.ts.map +1 -1
  20. package/lib/types/vehicles.js.map +1 -1
  21. package/lib/utils/accounting.d.ts +1 -0
  22. package/lib/utils/accounting.d.ts.map +1 -1
  23. package/lib/utils/accounting.js +6 -0
  24. package/lib/utils/accounting.js.map +1 -1
  25. package/lib/utils/accounting.test.js +19 -1
  26. package/lib/utils/accounting.test.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/constants.ts +6 -0
  29. package/src/types/leasing-debtors.ts +3 -0
  30. package/src/types/opportunities.ts +11 -0
  31. package/src/types/reports-leasing.ts +186 -0
  32. package/src/types/reports-sales.ts +13 -3
  33. package/src/types/vehicles.ts +1 -0
  34. package/src/utils/accounting.test.ts +27 -1
  35. package/src/utils/accounting.ts +16 -0
  36. package/tsconfig.tsbuildinfo +1 -1
@@ -17,4 +17,10 @@ export const tenantHasAccounting = (tenant) => {
17
17
  }
18
18
  return tenant.branches.some((b) => branchHasAccounting(b));
19
19
  };
20
+ export const branchHasAccountingInTenant = (tenant, branchId) => {
21
+ if (tenant == null) {
22
+ return false;
23
+ }
24
+ return branchHasAccounting(tenant.branches.find((b) => b.id === branchId));
25
+ };
20
26
  //# sourceMappingURL=accounting.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"accounting.js","sourceRoot":"/","sources":["utils/accounting.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,MAA0B,EAAkC,EAAE;IAC3F,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;QACnB,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,MAAM,CAAC,UAAU,IAAI,IAAI,EAAE,CAAC;QAC9B,OAAO,MAAM,CAAC,UAAU,CAAC;IAC3B,CAAC;IACD,OAAO,MAAM,CAAC,eAAe,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;AACtD,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,MAA0B,EAAW,EAAE,CACzE,cAAc,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC;AAEjC,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,MAA0B,EAAW,EAAE;IACzE,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;QACnB,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,MAAM,CAAC,eAAe,EAAE,CAAC;QAC3B,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC;AAC7D,CAAC,CAAC","sourcesContent":["import { AccountingProvider, Branch, Tenant } from '../types/tenants.js';\n\nexport const branchProvider = (branch: Branch | undefined): AccountingProvider | undefined => {\n if (branch == null) {\n return undefined;\n }\n if (branch.accounting != null) {\n return branch.accounting;\n }\n return branch.exactAccounting ? 'exact' : undefined;\n};\n\nexport const branchHasAccounting = (branch: Branch | undefined): boolean =>\n branchProvider(branch) != null;\n\nexport const tenantHasAccounting = (tenant: Tenant | undefined): boolean => {\n if (tenant == null) {\n return false;\n }\n if (tenant.exactAccounting) {\n return true;\n }\n return tenant.branches.some((b) => branchHasAccounting(b));\n};\n"]}
1
+ {"version":3,"file":"accounting.js","sourceRoot":"/","sources":["utils/accounting.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,MAA0B,EAAkC,EAAE;IAC3F,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;QACnB,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,MAAM,CAAC,UAAU,IAAI,IAAI,EAAE,CAAC;QAC9B,OAAO,MAAM,CAAC,UAAU,CAAC;IAC3B,CAAC;IACD,OAAO,MAAM,CAAC,eAAe,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;AACtD,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,MAA0B,EAAW,EAAE,CACzE,cAAc,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC;AAEjC,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,MAA0B,EAAW,EAAE;IACzE,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;QACnB,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,MAAM,CAAC,eAAe,EAAE,CAAC;QAC3B,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC;AAC7D,CAAC,CAAC;AAQF,MAAM,CAAC,MAAM,2BAA2B,GAAG,CACzC,MAA0B,EAC1B,QAAgB,EACP,EAAE;IACX,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;QACnB,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,mBAAmB,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC;AAC7E,CAAC,CAAC","sourcesContent":["import { AccountingProvider, Branch, Tenant } from '../types/tenants.js';\n\nexport const branchProvider = (branch: Branch | undefined): AccountingProvider | undefined => {\n if (branch == null) {\n return undefined;\n }\n if (branch.accounting != null) {\n return branch.accounting;\n }\n return branch.exactAccounting ? 'exact' : undefined;\n};\n\nexport const branchHasAccounting = (branch: Branch | undefined): boolean =>\n branchProvider(branch) != null;\n\nexport const tenantHasAccounting = (tenant: Tenant | undefined): boolean => {\n if (tenant == null) {\n return false;\n }\n if (tenant.exactAccounting) {\n return true;\n }\n return tenant.branches.some((b) => branchHasAccounting(b));\n};\n\n/**\n * Whether a specific branch books through an accounting integration. Resolves\n * purely per branch — a tenant can mix accounting and non-accounting branches.\n * Accounting is carried on the branch (`accounting`, or the deprecated\n * `exactAccounting`), so the tenant-wide flag is not consulted.\n */\nexport const branchHasAccountingInTenant = (\n tenant: Tenant | undefined,\n branchId: string,\n): boolean => {\n if (tenant == null) {\n return false;\n }\n return branchHasAccounting(tenant.branches.find((b) => b.id === branchId));\n};\n"]}
@@ -1,4 +1,4 @@
1
- import { branchHasAccounting, branchProvider, tenantHasAccounting } from './accounting.js';
1
+ import { branchHasAccounting, branchHasAccountingInTenant, branchProvider, tenantHasAccounting, } from './accounting.js';
2
2
  const makeBranch = (overrides = {}) => ({
3
3
  id: 'b1',
4
4
  name: 'Branch 1',
@@ -72,4 +72,22 @@ describe('tenantHasAccounting', () => {
72
72
  expect(tenantHasAccounting(undefined)).toBe(false);
73
73
  });
74
74
  });
75
+ describe('branchHasAccountingInTenant', () => {
76
+ it('resolves per branch in a mixed tenant', () => {
77
+ const tenant = makeTenant([
78
+ makeBranch({ id: 'odoo-branch', accounting: 'odoo' }),
79
+ makeBranch({ id: 'plain-branch' }),
80
+ ]);
81
+ expect(branchHasAccountingInTenant(tenant, 'odoo-branch')).toBe(true);
82
+ expect(branchHasAccountingInTenant(tenant, 'plain-branch')).toBe(false);
83
+ });
84
+ it('ignores the deprecated tenant-wide flag and resolves per branch', () => {
85
+ const tenant = makeTenant([makeBranch({ id: 'plain-branch' })], { exactAccounting: true });
86
+ expect(branchHasAccountingInTenant(tenant, 'plain-branch')).toBe(false);
87
+ });
88
+ it('is false for an unknown branch or missing tenant', () => {
89
+ expect(branchHasAccountingInTenant(makeTenant([makeBranch()]), 'nope')).toBe(false);
90
+ expect(branchHasAccountingInTenant(undefined, 'b1')).toBe(false);
91
+ });
92
+ });
75
93
  //# sourceMappingURL=accounting.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"accounting.test.js","sourceRoot":"/","sources":["utils/accounting.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { Branch, Tenant } from '../types/tenants.js';\nimport { branchHasAccounting, branchProvider, tenantHasAccounting } from './accounting.js';\n\nconst makeBranch = (overrides: Partial<Branch> = {}): Branch => ({\n id: 'b1',\n name: 'Branch 1',\n country: 'ci',\n language: 'fr',\n currency: 'XOF',\n adminNumberPrefix: '',\n stockNumberPrefix: '',\n vatNumber: '',\n vatRates: { standard: '18' },\n location: { country: 'ci' },\n ...overrides,\n});\n\nconst makeTenant = (branches: Branch[], overrides: Partial<Tenant> = {}): Tenant =>\n ({\n id: 't',\n name: 't',\n region: 'eu-west-1',\n defaultCountry: 'ci',\n defaultCurrency: 'XOF',\n defaultLanguage: 'fr',\n subdomains: ['t'],\n languages: ['fr'],\n emailDomain: 'example.com',\n branches: branches as [Branch, ...Branch[]],\n modules: [],\n opportunities: { types: [] },\n ...overrides,\n }) as Tenant;\n\ndescribe('branchProvider', () => {\n it('returns undefined for missing branch', () => {\n expect(branchProvider(undefined)).toBeUndefined();\n });\n\n it('prefers the new discriminator over the legacy boolean', () => {\n const branch = makeBranch({ accounting: 'odoo', exactAccounting: true });\n expect(branchProvider(branch)).toBe('odoo');\n });\n\n it('falls back to legacy boolean as exact', () => {\n const branch = makeBranch({ exactAccounting: true });\n expect(branchProvider(branch)).toBe('exact');\n });\n\n it('returns undefined when neither field is set', () => {\n expect(branchProvider(makeBranch())).toBeUndefined();\n });\n\n it('returns undefined when legacy boolean is false', () => {\n expect(branchProvider(makeBranch({ exactAccounting: false }))).toBeUndefined();\n });\n});\n\ndescribe('branchHasAccounting', () => {\n it('is true when accounting is set', () => {\n expect(branchHasAccounting(makeBranch({ accounting: 'odoo' }))).toBe(true);\n });\n\n it('is true when legacy exactAccounting is true', () => {\n expect(branchHasAccounting(makeBranch({ exactAccounting: true }))).toBe(true);\n });\n\n it('is false otherwise', () => {\n expect(branchHasAccounting(makeBranch())).toBe(false);\n expect(branchHasAccounting(undefined)).toBe(false);\n });\n});\n\ndescribe('tenantHasAccounting', () => {\n it('is true when tenant-level legacy flag is set', () => {\n const tenant = makeTenant([makeBranch()], { exactAccounting: true });\n expect(tenantHasAccounting(tenant)).toBe(true);\n });\n\n it('is true when any branch has accounting', () => {\n const tenant = makeTenant([makeBranch(), makeBranch({ id: 'b2', accounting: 'odoo' })]);\n expect(tenantHasAccounting(tenant)).toBe(true);\n });\n\n it('is false when no branch has accounting and tenant flag is not set', () => {\n expect(tenantHasAccounting(makeTenant([makeBranch()]))).toBe(false);\n expect(tenantHasAccounting(undefined)).toBe(false);\n });\n});\n"]}
1
+ {"version":3,"file":"accounting.test.js","sourceRoot":"/","sources":["utils/accounting.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { Branch, Tenant } from '../types/tenants.js';\nimport {\n branchHasAccounting,\n branchHasAccountingInTenant,\n branchProvider,\n tenantHasAccounting,\n} from './accounting.js';\n\nconst makeBranch = (overrides: Partial<Branch> = {}): Branch => ({\n id: 'b1',\n name: 'Branch 1',\n country: 'ci',\n language: 'fr',\n currency: 'XOF',\n adminNumberPrefix: '',\n stockNumberPrefix: '',\n vatNumber: '',\n vatRates: { standard: '18' },\n location: { country: 'ci' },\n ...overrides,\n});\n\nconst makeTenant = (branches: Branch[], overrides: Partial<Tenant> = {}): Tenant =>\n ({\n id: 't',\n name: 't',\n region: 'eu-west-1',\n defaultCountry: 'ci',\n defaultCurrency: 'XOF',\n defaultLanguage: 'fr',\n subdomains: ['t'],\n languages: ['fr'],\n emailDomain: 'example.com',\n branches: branches as [Branch, ...Branch[]],\n modules: [],\n opportunities: { types: [] },\n ...overrides,\n }) as Tenant;\n\ndescribe('branchProvider', () => {\n it('returns undefined for missing branch', () => {\n expect(branchProvider(undefined)).toBeUndefined();\n });\n\n it('prefers the new discriminator over the legacy boolean', () => {\n const branch = makeBranch({ accounting: 'odoo', exactAccounting: true });\n expect(branchProvider(branch)).toBe('odoo');\n });\n\n it('falls back to legacy boolean as exact', () => {\n const branch = makeBranch({ exactAccounting: true });\n expect(branchProvider(branch)).toBe('exact');\n });\n\n it('returns undefined when neither field is set', () => {\n expect(branchProvider(makeBranch())).toBeUndefined();\n });\n\n it('returns undefined when legacy boolean is false', () => {\n expect(branchProvider(makeBranch({ exactAccounting: false }))).toBeUndefined();\n });\n});\n\ndescribe('branchHasAccounting', () => {\n it('is true when accounting is set', () => {\n expect(branchHasAccounting(makeBranch({ accounting: 'odoo' }))).toBe(true);\n });\n\n it('is true when legacy exactAccounting is true', () => {\n expect(branchHasAccounting(makeBranch({ exactAccounting: true }))).toBe(true);\n });\n\n it('is false otherwise', () => {\n expect(branchHasAccounting(makeBranch())).toBe(false);\n expect(branchHasAccounting(undefined)).toBe(false);\n });\n});\n\ndescribe('tenantHasAccounting', () => {\n it('is true when tenant-level legacy flag is set', () => {\n const tenant = makeTenant([makeBranch()], { exactAccounting: true });\n expect(tenantHasAccounting(tenant)).toBe(true);\n });\n\n it('is true when any branch has accounting', () => {\n const tenant = makeTenant([makeBranch(), makeBranch({ id: 'b2', accounting: 'odoo' })]);\n expect(tenantHasAccounting(tenant)).toBe(true);\n });\n\n it('is false when no branch has accounting and tenant flag is not set', () => {\n expect(tenantHasAccounting(makeTenant([makeBranch()]))).toBe(false);\n expect(tenantHasAccounting(undefined)).toBe(false);\n });\n});\n\ndescribe('branchHasAccountingInTenant', () => {\n it('resolves per branch in a mixed tenant', () => {\n const tenant = makeTenant([\n makeBranch({ id: 'odoo-branch', accounting: 'odoo' }),\n makeBranch({ id: 'plain-branch' }),\n ]);\n expect(branchHasAccountingInTenant(tenant, 'odoo-branch')).toBe(true);\n expect(branchHasAccountingInTenant(tenant, 'plain-branch')).toBe(false);\n });\n\n it('ignores the deprecated tenant-wide flag and resolves per branch', () => {\n const tenant = makeTenant([makeBranch({ id: 'plain-branch' })], { exactAccounting: true });\n expect(branchHasAccountingInTenant(tenant, 'plain-branch')).toBe(false);\n });\n\n it('is false for an unknown branch or missing tenant', () => {\n expect(branchHasAccountingInTenant(makeTenant([makeBranch()]), 'nope')).toBe(false);\n expect(branchHasAccountingInTenant(undefined, 'b1')).toBe(false);\n });\n});\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bisondesk/core-sdk",
3
- "version": "1.0.609",
3
+ "version": "1.0.610",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "author": "TheTruckCompany",
package/src/constants.ts CHANGED
@@ -1000,8 +1000,14 @@ export const AXLE_CONFIG: { [config: string]: VehicleAxle[] } = {
1000
1000
  ],
1001
1001
  };
1002
1002
 
1003
+ // The operational debtors index / browse table keeps 6 months. The leasing report's
1004
+ // deeper history lives in a separate `debtors_history` index (see DEBTORS_HISTORY_MONTHS).
1003
1005
  export const DEBTORS_MONTHS_UNTIL_TODAY_COUNT = 6;
1004
1006
 
1007
+ // Depth of the separate `debtors_history` index that backs the leasing report. 24 covers
1008
+ // "last year" and its YoY (which needs the December of two years ago, up to 24 months back).
1009
+ export const DEBTORS_HISTORY_MONTHS = 24;
1010
+
1005
1011
  // Email Template IDs
1006
1012
  export const VEHICLE_SHARE_TEMPLATE_ID = 'vehicle-share';
1007
1013
  export const VEHICLE_MARKETING_CAMPAIGN_TEMPLATE_ID = 'vehicle-marketing-campaign';
@@ -14,6 +14,9 @@ export type MonthData = {
14
14
  overdueInvoiceIds: string[];
15
15
  riskLevel: number;
16
16
  monthlyInvoicedAmount: string;
17
+ // Non-leasing ("extra") debt for this month, computed as-of month-end.
18
+ nonLeasingOverdueAmount: string;
19
+ nonLeasingUnpaidAmount: string;
17
20
  };
18
21
 
19
22
  export type DebtorInfo = {
@@ -316,6 +316,14 @@ export type QuoteInfo = {
316
316
  status: QuoteStatus;
317
317
  roi: string;
318
318
  id: string;
319
+ // Leasing-only metrics, denormalized from the active leasing quote so the leasing report
320
+ // can take medians without re-reading each quote. Absent on non-leasing quotes.
321
+ /** Leasing term in months (`calculatorInput.leasing.duration`). */
322
+ durationMonths?: number;
323
+ /** Yearly ROI as a percentage 0–100 (`calculatorOutput.roi.perYear`). */
324
+ roiPerYear?: number;
325
+ /** Advance (deposit) as a percentage 0–100 (`calculatorInput.leasing.depositPercentage`). */
326
+ advancePercentage?: number;
319
327
  };
320
328
 
321
329
  export type SearchOpportunity = {
@@ -341,6 +349,9 @@ export type SearchOpportunity = {
341
349
  // Calendar days from min(marketingAt, evaluationAt, purchaseAgreedAt) until validatedAt.
342
350
  // Mirrors `daysInVirtualInventory` on the vehicle Business Overview page.
343
351
  daysAvailable?: number;
352
+ // Operational supplier for reporting. For SLB clones this is the source vehicle supplier,
353
+ // not the bank supplier on the clone purchase.
354
+ realSupplierId?: string;
344
355
  };
345
356
  };
346
357
 
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Leasing report — the payload for the `Reports → Leasing` tab. Computed entirely from
3
+ * OpenSearch: receivables (`finance_docs`), contracts (`leasing_contracts`), leasing
4
+ * opportunities (`opportunities`) and client risk (`debtors`).
5
+ *
6
+ * Every metric except the fixed cumulative chart is evaluated over — or as of the end
7
+ * of — the selected whole-month window. `asOf = window.end`: `now` for `mtd`/`ytd`, and
8
+ * the period boundary for historical ranges (so "last year" shows the state at the end
9
+ * of last year). Flow metrics (invoiced, contracts started/ended, opportunities) sum over
10
+ * the window; point-in-time metrics (overdue, aging, active contracts, risk) are
11
+ * reconstructed as of `asOf`.
12
+ *
13
+ * The API returns raw numbers only; the client formats (€, %) and colors the deltas.
14
+ */
15
+
16
+ import { YoyMetric } from './reports-sales.js';
17
+
18
+ /**
19
+ * Period presets driving every metric except the fixed cumulative chart, in selector
20
+ * order. All are month-aligned (contracts start on the 1st of a month), so there are no
21
+ * day-based ranges. `mtd`/`ytd` are to-date; `last{3,6,12}Months` are complete months
22
+ * from the first of a month back to the start of the current month; `lastMonth`/
23
+ * `lastYear` are the previous complete calendar month/year.
24
+ */
25
+ export const LEASING_PERIOD_KEYS = [
26
+ 'mtd',
27
+ 'ytd',
28
+ 'lastMonth',
29
+ 'last3Months',
30
+ 'last6Months',
31
+ 'last12Months',
32
+ 'lastYear',
33
+ ] as const;
34
+
35
+ export type LeasingPeriodKey = (typeof LEASING_PERIOD_KEYS)[number];
36
+
37
+ export const DEFAULT_LEASING_PERIOD: LeasingPeriodKey = 'mtd';
38
+
39
+ export type LeasingReportRequest = { period: LeasingPeriodKey };
40
+
41
+ export type { YoyMetric };
42
+
43
+ /** Outstanding-invoice age bands, in calendar months from issue date as of the window end. */
44
+ export type LeasingAgingBucketKey = '0-1' | '1-2' | '2-3' | '3-12' | '1y+';
45
+
46
+ export type LeasingAgingBucket = {
47
+ bucket: LeasingAgingBucketKey;
48
+ /** Outstanding amount (excl VAT) in this age band, as of the window end. */
49
+ amount: number;
50
+ };
51
+
52
+ export type LeasingCategorySplit = {
53
+ /** Vehicle category from `vehicle.external.general.category` (e.g. "Tractor Head"). */
54
+ category: string;
55
+ count: number;
56
+ /** Share of opportunities into preparation in the current window, 0–100. */
57
+ sharePct: number;
58
+ };
59
+
60
+ export type LeasingSalesperson = {
61
+ /** User id from `opportunity.accountManager`; the client resolves name + avatar. */
62
+ id: string;
63
+ units: number;
64
+ yoyPct: number | null;
65
+ };
66
+
67
+ export type LeasingOppCountry = {
68
+ /** ISO country code (lowercase) from `org.countryCode`. */
69
+ code: string;
70
+ units: number;
71
+ /** Share of opportunities into preparation in the current window, 0–100. */
72
+ sharePct: number;
73
+ };
74
+
75
+ export type LeasingRealSupplier = {
76
+ /** CRM organization id for the operational supplier. */
77
+ id: string;
78
+ /** CRM organization name at report generation time; falls back to `id` when unavailable. */
79
+ name: string;
80
+ units: number;
81
+ /** Share of opportunities with a resolved real supplier in the current window, 0–100. */
82
+ sharePct: number;
83
+ yoyPct: number | null;
84
+ };
85
+
86
+ export type LeasingCumulativeBucket = {
87
+ /** Calendar month, 1–12. */
88
+ month: number;
89
+ /** Cumulative new contracts started this calendar year through this month. */
90
+ thisYear: number;
91
+ /** Cumulative new contracts started last calendar year through this month. */
92
+ lastYear: number;
93
+ };
94
+
95
+ export type LeasingOverdueBucket = {
96
+ /** `YYYY-MM` month. */
97
+ month: string;
98
+ /** Overdue rent amount (excl VAT) as of the end of that month. */
99
+ amount: number;
100
+ };
101
+
102
+ export type LeasingReportResponse = {
103
+ period: {
104
+ key: LeasingPeriodKey;
105
+ /** Current window, half-open `[start, end)` (ISO strings). `end` is `asOf`. */
106
+ start: string;
107
+ end: string;
108
+ /** Number of days in the window. */
109
+ days: number;
110
+ /** Year-ago window `[yoyStart, yoyEnd)`. */
111
+ yoyStart: string;
112
+ yoyEnd: string;
113
+ };
114
+ receivables: {
115
+ /** Net invoiced (invoices − credit notes), excl VAT, over the window. */
116
+ invoiced: YoyMetric;
117
+ /** Overdue (excl VAT) as of the window end (+ as of end − 1y). */
118
+ overdue: YoyMetric;
119
+ /**
120
+ * `overdue / total outstanding` as a fraction 0–1, both from finance_docs as of the window
121
+ * end (overdue ÷ all unpaid rent). Null when nothing is outstanding.
122
+ */
123
+ overdueShareOfOutstanding: number | null;
124
+ /**
125
+ * Share of total outstanding rent held by the 10 largest debtors (concentration), as a
126
+ * fraction 0–1. Null when nothing is outstanding.
127
+ */
128
+ top10ClientShare: number | null;
129
+ };
130
+ /** Outstanding leasing invoices by issue-date age, as of the window end. */
131
+ aging: LeasingAgingBucket[];
132
+ contracts: {
133
+ /** Active contracts as of the window end (+ as of end − 1y). */
134
+ active: YoyMetric;
135
+ /** New contracts started over the window. */
136
+ started: YoyMetric;
137
+ /** Contracts that ended over the window (actual end date in range, excludes discarded). */
138
+ ended: YoyMetric;
139
+ };
140
+ opportunities: {
141
+ /** Leasing opportunities entering Preparation over the window. */
142
+ intoPreparation: YoyMetric;
143
+ /** Median opportunity amount over the window. */
144
+ medianPrice: YoyMetric;
145
+ /** Median leasing term in months, from each opp's active leasing quote. */
146
+ medianDuration: YoyMetric;
147
+ /** Median yearly ROI as a percentage 0–100, from each opp's active leasing quote. */
148
+ medianRoiPerYear: YoyMetric;
149
+ /** 75th-percentile yearly ROI (%) over the current window; null if no quotes. */
150
+ roiPerYearP75: number | null;
151
+ /** Median advance (deposit) as a percentage 0–100, from each opp's active leasing quote. */
152
+ medianAdvancePct: YoyMetric;
153
+ /** Vehicle category split of opportunities into Preparation (current window). */
154
+ categorySplit: LeasingCategorySplit[];
155
+ /** Sellers ranked by opportunities into Preparation (current units desc), with YoY. */
156
+ salespeople: LeasingSalesperson[];
157
+ /** Real suppliers ranked by opportunities into Preparation (current units desc), with YoY. */
158
+ realSuppliers: LeasingRealSupplier[];
159
+ /** Org-country split of opportunities into Preparation (current window). */
160
+ countries: LeasingOppCountry[];
161
+ };
162
+ // Client risk, as of the snapshot month. `riskLevel` is zero-inflated (most clients
163
+ // are 0), so this is the at-risk tail shape, not a central statistic.
164
+ risk: {
165
+ /** Active-contract clients with non-zero risk, with YoY. */
166
+ clientsAtRisk: YoyMetric;
167
+ /** Total active-contract clients (the at-risk denominator). */
168
+ activeClients: number;
169
+ /** Severity-band counts of at-risk clients (`riskLevel` floored: levels 1/2/3/4+). */
170
+ bands: { band: string; count: number }[];
171
+ };
172
+ /** Fixed 12-month cumulative new contracts, this calendar year vs last. */
173
+ cumulativeContracts: LeasingCumulativeBucket[];
174
+ /** Fixed trailing-6-month overdue rent amount (excl VAT), as of each month-end. */
175
+ overdueHistory: LeasingOverdueBucket[];
176
+ /** Track-and-trace fleet usage over the window. */
177
+ fleet: {
178
+ /**
179
+ * Median `last30DaysKm` across wired track-and-trace devices that pinged within the window;
180
+ * null when no such device. From the `track_and_trace_devices` index.
181
+ */
182
+ medianKm30d: number | null;
183
+ /** Number of wired devices (carrying a km value) the median was taken over. */
184
+ deviceCount: number;
185
+ };
186
+ };
@@ -1,8 +1,9 @@
1
1
  /**
2
- * Sales report — the payload for the `Reports → Sales` tab. Everything is computed
3
- * from the per-tenant `opportunities` OpenSearch index, keyed on
2
+ * Sales report — the payload for the `Reports → Sales` tab. Sales-side metrics
3
+ * are computed from the per-tenant `opportunities` OpenSearch index, keyed on
4
4
  * `opportunity.reviewedAt` (the moment an opportunity ENTERS Preparation — our
5
- * "sale" event; `preparedAt` is when it leaves Preparation into Delivery).
5
+ * "sale" event; `preparedAt` is when it leaves Preparation into Delivery). The
6
+ * buyer ranking comes from the vehicles index, keyed on `purchase.purchaseAgreedAt`.
6
7
  *
7
8
  * The API returns raw numbers only; the client formats (€, %, pp, "younger") and
8
9
  * colors the deltas.
@@ -66,6 +67,13 @@ export type SalesReportSalesperson = {
66
67
  yoyPct: number | null;
67
68
  };
68
69
 
70
+ export type SalesReportBuyer = {
71
+ /** User id from `purchase.purchasedBy`; the client resolves name + avatar. */
72
+ id: string;
73
+ units: number;
74
+ yoyPct: number | null;
75
+ };
76
+
69
77
  export type SalesReportMake = {
70
78
  /** Vehicle make name from `vehicle.external.general.make` (stored verbatim, e.g. "DAF"). */
71
79
  make: string;
@@ -127,6 +135,8 @@ export type SalesReportResponse = {
127
135
  makes: SalesReportMake[];
128
136
  /** All sales people with units in either window (capped; the client list scrolls). */
129
137
  salespeople: SalesReportSalesperson[];
138
+ /** All purchasers with vehicles bought in the current window (capped; the client list scrolls). */
139
+ buyers: SalesReportBuyer[];
130
140
  /** Returning-customer tenure shares (4 buckets). */
131
141
  returning: TenureBucket[];
132
142
  conglomerate: SalesReportConglomerate;
@@ -260,6 +260,7 @@ export type SearchVehicle = {
260
260
  dealStatus?: VehiclePurchaseDealStatus;
261
261
  logisticsStatus?: VehiclePurchaseLogisticsStatus;
262
262
  purchasedBy?: string;
263
+ purchaseAgreedAt?: string;
263
264
  parkingName?: string;
264
265
  organizationId: string;
265
266
  organizationExternalId?: number;
@@ -1,5 +1,10 @@
1
1
  import { Branch, Tenant } from '../types/tenants.js';
2
- import { branchHasAccounting, branchProvider, tenantHasAccounting } from './accounting.js';
2
+ import {
3
+ branchHasAccounting,
4
+ branchHasAccountingInTenant,
5
+ branchProvider,
6
+ tenantHasAccounting,
7
+ } from './accounting.js';
3
8
 
4
9
  const makeBranch = (overrides: Partial<Branch> = {}): Branch => ({
5
10
  id: 'b1',
@@ -87,3 +92,24 @@ describe('tenantHasAccounting', () => {
87
92
  expect(tenantHasAccounting(undefined)).toBe(false);
88
93
  });
89
94
  });
95
+
96
+ describe('branchHasAccountingInTenant', () => {
97
+ it('resolves per branch in a mixed tenant', () => {
98
+ const tenant = makeTenant([
99
+ makeBranch({ id: 'odoo-branch', accounting: 'odoo' }),
100
+ makeBranch({ id: 'plain-branch' }),
101
+ ]);
102
+ expect(branchHasAccountingInTenant(tenant, 'odoo-branch')).toBe(true);
103
+ expect(branchHasAccountingInTenant(tenant, 'plain-branch')).toBe(false);
104
+ });
105
+
106
+ it('ignores the deprecated tenant-wide flag and resolves per branch', () => {
107
+ const tenant = makeTenant([makeBranch({ id: 'plain-branch' })], { exactAccounting: true });
108
+ expect(branchHasAccountingInTenant(tenant, 'plain-branch')).toBe(false);
109
+ });
110
+
111
+ it('is false for an unknown branch or missing tenant', () => {
112
+ expect(branchHasAccountingInTenant(makeTenant([makeBranch()]), 'nope')).toBe(false);
113
+ expect(branchHasAccountingInTenant(undefined, 'b1')).toBe(false);
114
+ });
115
+ });
@@ -22,3 +22,19 @@ export const tenantHasAccounting = (tenant: Tenant | undefined): boolean => {
22
22
  }
23
23
  return tenant.branches.some((b) => branchHasAccounting(b));
24
24
  };
25
+
26
+ /**
27
+ * Whether a specific branch books through an accounting integration. Resolves
28
+ * purely per branch — a tenant can mix accounting and non-accounting branches.
29
+ * Accounting is carried on the branch (`accounting`, or the deprecated
30
+ * `exactAccounting`), so the tenant-wide flag is not consulted.
31
+ */
32
+ export const branchHasAccountingInTenant = (
33
+ tenant: Tenant | undefined,
34
+ branchId: string,
35
+ ): boolean => {
36
+ if (tenant == null) {
37
+ return false;
38
+ }
39
+ return branchHasAccounting(tenant.branches.find((b) => b.id === branchId));
40
+ };