@bisondesk/core-sdk 1.0.682 → 1.0.683
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/lib/types/leasing-debtors.d.ts +3 -0
- package/lib/types/leasing-debtors.d.ts.map +1 -1
- package/lib/types/leasing-debtors.js.map +1 -1
- package/lib/types/leasing-search.d.ts +2 -0
- package/lib/types/leasing-search.d.ts.map +1 -1
- package/lib/types/leasing-search.js.map +1 -1
- package/lib/types/mileage.d.ts +38 -0
- package/lib/types/mileage.d.ts.map +1 -0
- package/lib/types/mileage.js +15 -0
- package/lib/types/mileage.js.map +1 -0
- package/lib/types/vehicle-purchase-payments.d.ts +0 -15
- package/lib/types/vehicle-purchase-payments.d.ts.map +1 -1
- package/lib/types/vehicle-purchase-payments.js +0 -6
- package/lib/types/vehicle-purchase-payments.js.map +1 -1
- package/lib/utils/mileage.d.ts +18 -0
- package/lib/utils/mileage.d.ts.map +1 -0
- package/lib/utils/mileage.js +70 -0
- package/lib/utils/mileage.js.map +1 -0
- package/lib/utils/mileage.test.d.ts +2 -0
- package/lib/utils/mileage.test.d.ts.map +1 -0
- package/lib/utils/mileage.test.js +103 -0
- package/lib/utils/mileage.test.js.map +1 -0
- package/package.json +1 -1
- package/src/types/leasing-debtors.ts +4 -0
- package/src/types/leasing-search.ts +2 -0
- package/src/types/mileage.ts +51 -0
- package/src/types/vehicle-purchase-payments.ts +5 -36
- package/src/utils/mileage.test.ts +151 -0
- package/src/utils/mileage.ts +111 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/src/types/activities.js +0 -13
- package/src/types/activities.js.map +0 -1
- package/src/types/comparables.js +0 -2
- package/src/types/comparables.js.map +0 -1
- package/src/types/crm.js +0 -58
- package/src/types/crm.js.map +0 -1
- package/src/types/dashboard.js +0 -2
- package/src/types/dashboard.js.map +0 -1
- package/src/types/definitions.js +0 -2
- package/src/types/definitions.js.map +0 -1
- package/src/types/fields.js +0 -2
- package/src/types/fields.js.map +0 -1
- package/src/types/insights.js +0 -2
- package/src/types/insights.js.map +0 -1
- package/src/types/interests.js +0 -6
- package/src/types/interests.js.map +0 -1
- package/src/types/internal-events.js +0 -18
- package/src/types/internal-events.js.map +0 -1
- package/src/types/internet-opportunities.js +0 -2
- package/src/types/internet-opportunities.js.map +0 -1
- package/src/types/internet-vehicles.js +0 -2
- package/src/types/internet-vehicles.js.map +0 -1
- package/src/types/leasing-administration.js +0 -2
- package/src/types/leasing-administration.js.map +0 -1
- package/src/types/leasing-debtors.js +0 -2
- package/src/types/leasing-debtors.js.map +0 -1
- package/src/types/leasing-search.js +0 -2
- package/src/types/leasing-search.js.map +0 -1
- package/src/types/leasing-settings.js +0 -7
- package/src/types/leasing-settings.js.map +0 -1
- package/src/types/leasing.js +0 -38
- package/src/types/leasing.js.map +0 -1
- package/src/types/marketing.js +0 -35
- package/src/types/marketing.js.map +0 -1
- package/src/types/offers.js +0 -56
- package/src/types/opportunities.js +0 -107
- package/src/types/opportunities.js.map +0 -1
- package/src/types/payments.js +0 -7
- package/src/types/picklists.js +0 -2
- package/src/types/picklists.js.map +0 -1
- package/src/types/quotes.js +0 -42
- package/src/types/recently-viewed.js +0 -2
- package/src/types/recently-viewed.js.map +0 -1
- package/src/types/reservations.js +0 -5
- package/src/types/reservations.js.map +0 -1
- package/src/types/roi-ratings.js +0 -5
- package/src/types/roi-ratings.js.map +0 -1
- package/src/types/saved-filters.js +0 -9
- package/src/types/saved-filters.js.map +0 -1
- package/src/types/search.js +0 -2
- package/src/types/search.js.map +0 -1
- package/src/types/settings.js +0 -2
- package/src/types/settings.js.map +0 -1
- package/src/types/templates.js +0 -2
- package/src/types/templates.js.map +0 -1
- package/src/types/tenants.js +0 -21
- package/src/types/tenants.js.map +0 -1
- package/src/types/user-performance.js +0 -7
- package/src/types/user-performance.js.map +0 -1
- package/src/types/utils.js +0 -2
- package/src/types/validation.js +0 -2
- package/src/types/validation.js.map +0 -1
- package/src/types/vehicle-sales.js +0 -6
- package/src/types/vehicle-sales.js.map +0 -1
- package/src/types/vehicles.js +0 -98
- package/src/types/vehicles.js.map +0 -1
- package/src/utils/leasing.js +0 -6
- package/src/utils/leasing.js.map +0 -1
- package/src/utils/opportunities.js +0 -48
- package/src/utils/opportunities.js.map +0 -1
|
@@ -2,6 +2,7 @@ import { InsuranceEntry } from '@bisondesk/commons-sdk/insurance';
|
|
|
2
2
|
import { Contact, Organization } from './crm.js';
|
|
3
3
|
import { PublicSearchDefinition } from './definitions.js';
|
|
4
4
|
import { LeasingContract } from './leasing.js';
|
|
5
|
+
import { ContractMileage } from './mileage.js';
|
|
5
6
|
import { Vehicle } from './vehicles.js';
|
|
6
7
|
|
|
7
8
|
export type LeasingPublicSearchDefinitions = {
|
|
@@ -27,6 +28,7 @@ export type SearchLeasingContract = {
|
|
|
27
28
|
supplierName?: string;
|
|
28
29
|
hasActiveRecurrence?: boolean;
|
|
29
30
|
lastRecurrenceRunAt?: string;
|
|
31
|
+
mileage?: ContractMileage;
|
|
30
32
|
};
|
|
31
33
|
};
|
|
32
34
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export enum MileageStatus {
|
|
2
|
+
NoSignal = 'no-signal',
|
|
3
|
+
TooEarly = 'too-early',
|
|
4
|
+
Low = 'low',
|
|
5
|
+
Normal = 'normal',
|
|
6
|
+
High = 'high',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type MileageAlert = MileageStatus.Low | MileageStatus.High | MileageStatus.NoSignal;
|
|
10
|
+
|
|
11
|
+
export enum MileageTrendDirection {
|
|
12
|
+
Up = 'up',
|
|
13
|
+
Down = 'down',
|
|
14
|
+
Flat = 'flat',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type MileageTrend = {
|
|
18
|
+
direction: MileageTrendDirection;
|
|
19
|
+
/** The last 30 days against the 12-month average, in percent. */
|
|
20
|
+
percent: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Km driven by a contract's vehicle, read from its wired track-and-trace device. */
|
|
24
|
+
export type ContractMileage = {
|
|
25
|
+
contractId: string;
|
|
26
|
+
stockNumber?: string;
|
|
27
|
+
status: MileageStatus;
|
|
28
|
+
/** The status, when it is outside the limits and the contract has overdue invoices. */
|
|
29
|
+
alert?: MileageAlert;
|
|
30
|
+
last30DaysKm: number;
|
|
31
|
+
/** Absent while the device document predates the yearly figures: unknown, not zero. */
|
|
32
|
+
currentYearKm?: number;
|
|
33
|
+
/** Km per 30 days over the past 12 months; absent under 60 tracked days. */
|
|
34
|
+
yearAvg30DaysKm?: number;
|
|
35
|
+
trend?: MileageTrend;
|
|
36
|
+
lastReadingAt?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** A debtor dossier's tracked vehicles at a glance, for the dossier lists and boards. */
|
|
40
|
+
export type MileageSummary = {
|
|
41
|
+
vehicles: number;
|
|
42
|
+
/** Vehicles flagged per alert: outside the limits on a contract with overdue invoices. */
|
|
43
|
+
noSignal: number;
|
|
44
|
+
high: number;
|
|
45
|
+
low: number;
|
|
46
|
+
issues: number;
|
|
47
|
+
/** Km over the last 30 days of the vehicles still reporting; absent when none is. */
|
|
48
|
+
last30DaysKm?: number;
|
|
49
|
+
/** Those vehicles' km against their combined 12-month average. */
|
|
50
|
+
trend?: MileageTrend;
|
|
51
|
+
};
|
|
@@ -105,10 +105,9 @@ export type VehiclePurchasePaymentBff = {
|
|
|
105
105
|
};
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
|
-
* The group an unpaid purchase
|
|
109
|
-
* (whether or not it has arrived), arrived on our park and not sold, or still expected —
|
|
110
|
-
* sold nor arrived.
|
|
111
|
-
* `group` filter and `meta.counts` serve the client that paged each group on its own.
|
|
108
|
+
* The group an unpaid purchase is listed under, in the order the Unpaid list shows them: sold to a
|
|
109
|
+
* client (whether or not it has arrived), arrived on our park and not sold, or still expected —
|
|
110
|
+
* neither sold nor arrived.
|
|
112
111
|
*/
|
|
113
112
|
export enum UnpaidPurchaseGroup {
|
|
114
113
|
Sold = 'sold',
|
|
@@ -116,34 +115,6 @@ export enum UnpaidPurchaseGroup {
|
|
|
116
115
|
Expected = 'expected',
|
|
117
116
|
}
|
|
118
117
|
|
|
119
|
-
/**
|
|
120
|
-
* Where the vehicle behind an unpaid purchase stands, in the order the Unpaid list ranks its sales:
|
|
121
|
-
* delivered to the client, arrived on our park, or still expected from the supplier. Delivered comes
|
|
122
|
-
* from the sale side, so only a sold vehicle is delivered, whether or not it passed our park.
|
|
123
|
-
*/
|
|
124
|
-
export enum UnpaidPurchaseLogisticStatus {
|
|
125
|
-
Delivered = 'delivered',
|
|
126
|
-
Arrived = 'arrived',
|
|
127
|
-
Expected = 'expected',
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** The filters on the Unpaid list. A filter left out keeps every purchase. */
|
|
131
|
-
export type UnpaidPurchasesFilters = {
|
|
132
|
-
/**
|
|
133
|
-
* Free text, case-insensitive: every word must appear in the stock number, VIN, vehicle title or
|
|
134
|
-
* supplier name.
|
|
135
|
-
*/
|
|
136
|
-
q?: string;
|
|
137
|
-
sold?: boolean;
|
|
138
|
-
logisticStatus?: UnpaidPurchaseLogisticStatus;
|
|
139
|
-
/** Purchasers (`purchase.purchasedBy`); a purchase matches any of them. */
|
|
140
|
-
purchasedBy?: string[];
|
|
141
|
-
branchId?: string;
|
|
142
|
-
/** The vehicle's condition (`external.condition.used`); a vehicle without one matches neither. */
|
|
143
|
-
used?: boolean;
|
|
144
|
-
group?: UnpaidPurchaseGroup;
|
|
145
|
-
};
|
|
146
|
-
|
|
147
118
|
/**
|
|
148
119
|
* A row on the Unpaid tab: an agreed purchase that still has an amount to authorise for payment.
|
|
149
120
|
* Derived from the purchase deal and the payments already authorised, not a payment record.
|
|
@@ -166,13 +137,11 @@ export type UnpaidPurchase = {
|
|
|
166
137
|
supplierName?: string;
|
|
167
138
|
};
|
|
168
139
|
|
|
169
|
-
/** What a page of
|
|
140
|
+
/** What a page of one Unpaid group carries besides its rows. */
|
|
170
141
|
export type UnpaidPurchasesMeta = {
|
|
171
142
|
/** Whether the requester may authorise a payment, as on the vehicle purchase tab. */
|
|
172
143
|
canAllowPayment: boolean;
|
|
173
|
-
/**
|
|
174
|
-
purchasers: string[];
|
|
175
|
-
/** How many purchases each group holds, unfiltered. */
|
|
144
|
+
/** How many purchases each group holds, for the group headers. */
|
|
176
145
|
counts: Record<UnpaidPurchaseGroup, number>;
|
|
177
146
|
};
|
|
178
147
|
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { Categories } from '../constants.js';
|
|
3
|
+
import { ContractMileage, MileageStatus, MileageTrendDirection } from '../types/mileage.js';
|
|
4
|
+
import {
|
|
5
|
+
getMileageAlert,
|
|
6
|
+
getMileageStatus,
|
|
7
|
+
getMileageSummary,
|
|
8
|
+
getMileageTrend,
|
|
9
|
+
} from './mileage.js';
|
|
10
|
+
|
|
11
|
+
const now = new Date('2026-09-30T12:00:00Z');
|
|
12
|
+
const lastReadingAt = '2026-09-30T06:00:00Z';
|
|
13
|
+
|
|
14
|
+
describe('getMileageStatus', () => {
|
|
15
|
+
test.each([
|
|
16
|
+
{ name: 'silent for more than three days', lastReadingAt: '2026-09-26T09:00:00Z' },
|
|
17
|
+
{ name: 'that never reported', lastReadingAt: undefined },
|
|
18
|
+
])('a device $name has no signal, whatever its last figures say', ({ lastReadingAt }) => {
|
|
19
|
+
expect(
|
|
20
|
+
getMileageStatus({ category: Categories.Van, last30DaysKm: 8_950, lastReadingAt }, now),
|
|
21
|
+
).toBe(MileageStatus.NoSignal);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('a device quiet for three days still has a signal', () => {
|
|
25
|
+
expect(
|
|
26
|
+
getMileageStatus(
|
|
27
|
+
{ category: Categories.Van, last30DaysKm: 3_000, lastReadingAt: '2026-09-27T09:00:00Z' },
|
|
28
|
+
now,
|
|
29
|
+
),
|
|
30
|
+
).toBe(MileageStatus.Normal);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('a device installed less than 30 days ago is too early to judge', () => {
|
|
34
|
+
expect(
|
|
35
|
+
getMileageStatus(
|
|
36
|
+
{ last30DaysKm: 40, lastReadingAt, trackedSince: '2026-09-15T08:00:00Z' },
|
|
37
|
+
now,
|
|
38
|
+
),
|
|
39
|
+
).toBe(MileageStatus.TooEarly);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test.each([
|
|
43
|
+
{ category: Categories.Van, last30DaysKm: 249, expected: MileageStatus.Low },
|
|
44
|
+
{ category: Categories.Van, last30DaysKm: 250, expected: MileageStatus.Normal },
|
|
45
|
+
{ category: Categories.Van, last30DaysKm: 7_500, expected: MileageStatus.Normal },
|
|
46
|
+
{ category: Categories.Van, last30DaysKm: 7_501, expected: MileageStatus.High },
|
|
47
|
+
{ category: Categories.Car, last30DaysKm: 7_501, expected: MileageStatus.High },
|
|
48
|
+
{ category: Categories.TractorHead, last30DaysKm: 12_000, expected: MileageStatus.Normal },
|
|
49
|
+
{ category: Categories.TractorHead, last30DaysKm: 12_001, expected: MileageStatus.High },
|
|
50
|
+
{ category: Categories.Truck, last30DaysKm: 12_001, expected: MileageStatus.High },
|
|
51
|
+
{ category: Categories.Machine, last30DaysKm: 50_000, expected: MileageStatus.Normal },
|
|
52
|
+
{ category: undefined, last30DaysKm: 50_000, expected: MileageStatus.Normal },
|
|
53
|
+
])('$category at $last30DaysKm km is $expected', ({ category, last30DaysKm, expected }) => {
|
|
54
|
+
expect(
|
|
55
|
+
getMileageStatus(
|
|
56
|
+
{ category, last30DaysKm, lastReadingAt, trackedSince: '2025-01-01T00:00:00Z' },
|
|
57
|
+
now,
|
|
58
|
+
),
|
|
59
|
+
).toBe(expected);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('getMileageAlert', () => {
|
|
64
|
+
test.each([
|
|
65
|
+
MileageStatus.Low,
|
|
66
|
+
MileageStatus.High,
|
|
67
|
+
MileageStatus.NoSignal,
|
|
68
|
+
])('%s on an overdue contract is an alert', (status) => {
|
|
69
|
+
expect(getMileageAlert(status, true)).toBe(status);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test.each([MileageStatus.Normal, MileageStatus.TooEarly])('%s is never an alert', (status) => {
|
|
73
|
+
expect(getMileageAlert(status, true)).toBeUndefined();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('a contract without overdue invoices has no alert', () => {
|
|
77
|
+
expect(getMileageAlert(MileageStatus.Low, false)).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('getMileageTrend', () => {
|
|
82
|
+
test('has no trend without a yearly average', () => {
|
|
83
|
+
expect(getMileageTrend(1_000, undefined)).toBeUndefined();
|
|
84
|
+
expect(getMileageTrend(1_000, 0)).toBeUndefined();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test.each([
|
|
88
|
+
{ last30DaysKm: 13_200, direction: MileageTrendDirection.Up, percent: 32 },
|
|
89
|
+
{ last30DaysKm: 11_500, direction: MileageTrendDirection.Up, percent: 15 },
|
|
90
|
+
{ last30DaysKm: 11_400, direction: MileageTrendDirection.Flat, percent: 14 },
|
|
91
|
+
{ last30DaysKm: 8_600, direction: MileageTrendDirection.Flat, percent: -14 },
|
|
92
|
+
{ last30DaysKm: 8_500, direction: MileageTrendDirection.Down, percent: -15 },
|
|
93
|
+
{ last30DaysKm: 300, direction: MileageTrendDirection.Down, percent: -97 },
|
|
94
|
+
])('$last30DaysKm km against 10 000 is $direction $percent%', ({
|
|
95
|
+
last30DaysKm,
|
|
96
|
+
direction,
|
|
97
|
+
percent,
|
|
98
|
+
}) => {
|
|
99
|
+
expect(getMileageTrend(last30DaysKm, 10_000)).toEqual({ direction, percent });
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('getMileageSummary', () => {
|
|
104
|
+
const vehicle = (
|
|
105
|
+
contractId: string,
|
|
106
|
+
status: MileageStatus,
|
|
107
|
+
alert: ContractMileage['alert'],
|
|
108
|
+
last30DaysKm: number,
|
|
109
|
+
yearAvg30DaysKm?: number,
|
|
110
|
+
): ContractMileage => ({ contractId, status, alert, last30DaysKm, yearAvg30DaysKm });
|
|
111
|
+
|
|
112
|
+
test('counts flagged vehicles per alert and leaves silent devices out of the km', () => {
|
|
113
|
+
expect(
|
|
114
|
+
getMileageSummary([
|
|
115
|
+
vehicle('a', MileageStatus.NoSignal, MileageStatus.NoSignal, 9_000, 9_000),
|
|
116
|
+
vehicle('b', MileageStatus.NoSignal, undefined, 4_000),
|
|
117
|
+
vehicle('c', MileageStatus.High, MileageStatus.High, 13_000, 10_000),
|
|
118
|
+
vehicle('d', MileageStatus.Low, MileageStatus.Low, 100, 2_000),
|
|
119
|
+
vehicle('e', MileageStatus.Low, undefined, 200),
|
|
120
|
+
vehicle('f', MileageStatus.TooEarly, undefined, 1_500),
|
|
121
|
+
]),
|
|
122
|
+
).toEqual({
|
|
123
|
+
vehicles: 6,
|
|
124
|
+
noSignal: 1,
|
|
125
|
+
high: 1,
|
|
126
|
+
low: 1,
|
|
127
|
+
issues: 3,
|
|
128
|
+
last30DaysKm: 14_800,
|
|
129
|
+
trend: { direction: MileageTrendDirection.Flat, percent: 9 },
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('has no km and no trend when no device is reporting', () => {
|
|
134
|
+
const summary = getMileageSummary([
|
|
135
|
+
vehicle('a', MileageStatus.NoSignal, MileageStatus.NoSignal, 9_000, 9_000),
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
expect(summary.last30DaysKm).toBeUndefined();
|
|
139
|
+
expect(summary.trend).toBeUndefined();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('is all zeros for a dossier without tracked vehicles', () => {
|
|
143
|
+
expect(getMileageSummary([])).toEqual({
|
|
144
|
+
vehicles: 0,
|
|
145
|
+
noSignal: 0,
|
|
146
|
+
high: 0,
|
|
147
|
+
low: 0,
|
|
148
|
+
issues: 0,
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { differenceInCalendarDays, parseISO } from 'date-fns';
|
|
2
|
+
import { sumBy } from 'lodash-es';
|
|
3
|
+
import { Categories } from '../constants.js';
|
|
4
|
+
import {
|
|
5
|
+
ContractMileage,
|
|
6
|
+
MileageAlert,
|
|
7
|
+
MileageStatus,
|
|
8
|
+
MileageSummary,
|
|
9
|
+
MileageTrend,
|
|
10
|
+
MileageTrendDirection,
|
|
11
|
+
} from '../types/mileage.js';
|
|
12
|
+
|
|
13
|
+
export const MILEAGE_LOW_KM_30_DAYS = 250;
|
|
14
|
+
export const MILEAGE_TREND_BAND = 0.15;
|
|
15
|
+
export const KM_FILTER_MAX = 15_000;
|
|
16
|
+
export const KM_FILTER_STEP = 250;
|
|
17
|
+
|
|
18
|
+
const MILEAGE_WINDOW_DAYS = 30;
|
|
19
|
+
const MILEAGE_SIGNAL_LOST_AFTER_DAYS = 3;
|
|
20
|
+
|
|
21
|
+
export const MILEAGE_HIGH_KM_30_DAYS: { [category: string]: number } = {
|
|
22
|
+
[Categories.Van]: 7_500,
|
|
23
|
+
[Categories.Car]: 7_500,
|
|
24
|
+
[Categories.TractorHead]: 12_000,
|
|
25
|
+
[Categories.Truck]: 12_000,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A device that has not reported for more than three days, or never did, has no signal: its km
|
|
30
|
+
* figures stop refreshing, so they say nothing about how much the vehicle drives.
|
|
31
|
+
*/
|
|
32
|
+
export const getMileageStatus = (
|
|
33
|
+
input: { category?: string; last30DaysKm: number; lastReadingAt?: string; trackedSince?: string },
|
|
34
|
+
now: Date = new Date(),
|
|
35
|
+
): MileageStatus => {
|
|
36
|
+
if (
|
|
37
|
+
input.lastReadingAt == null ||
|
|
38
|
+
differenceInCalendarDays(now, parseISO(input.lastReadingAt)) > MILEAGE_SIGNAL_LOST_AFTER_DAYS
|
|
39
|
+
) {
|
|
40
|
+
return MileageStatus.NoSignal;
|
|
41
|
+
}
|
|
42
|
+
if (
|
|
43
|
+
input.trackedSince != null &&
|
|
44
|
+
differenceInCalendarDays(now, parseISO(input.trackedSince)) < MILEAGE_WINDOW_DAYS
|
|
45
|
+
) {
|
|
46
|
+
return MileageStatus.TooEarly;
|
|
47
|
+
}
|
|
48
|
+
if (input.last30DaysKm < MILEAGE_LOW_KM_30_DAYS) {
|
|
49
|
+
return MileageStatus.Low;
|
|
50
|
+
}
|
|
51
|
+
const highLimit = input.category != null ? MILEAGE_HIGH_KM_30_DAYS[input.category] : undefined;
|
|
52
|
+
if (highLimit != null && input.last30DaysKm > highLimit) {
|
|
53
|
+
return MileageStatus.High;
|
|
54
|
+
}
|
|
55
|
+
return MileageStatus.Normal;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const isOutsideLimits = (status: MileageStatus): status is MileageAlert =>
|
|
59
|
+
status === MileageStatus.Low ||
|
|
60
|
+
status === MileageStatus.High ||
|
|
61
|
+
status === MileageStatus.NoSignal;
|
|
62
|
+
|
|
63
|
+
/** Debt follow-up only cares about mileage outside the limits on a contract that is overdue. */
|
|
64
|
+
export const getMileageAlert = (
|
|
65
|
+
status: MileageStatus,
|
|
66
|
+
overdue: boolean,
|
|
67
|
+
): MileageAlert | undefined => (overdue && isOutsideLimits(status) ? status : undefined);
|
|
68
|
+
|
|
69
|
+
export const getMileageTrend = (
|
|
70
|
+
last30DaysKm: number,
|
|
71
|
+
yearAvg30DaysKm: number | undefined,
|
|
72
|
+
): MileageTrend | undefined => {
|
|
73
|
+
if (yearAvg30DaysKm == null || yearAvg30DaysKm === 0) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const change = (last30DaysKm - yearAvg30DaysKm) / yearAvg30DaysKm;
|
|
77
|
+
const percent = Math.round(change * 100);
|
|
78
|
+
if (change >= MILEAGE_TREND_BAND) {
|
|
79
|
+
return { direction: MileageTrendDirection.Up, percent };
|
|
80
|
+
}
|
|
81
|
+
if (change <= -MILEAGE_TREND_BAND) {
|
|
82
|
+
return { direction: MileageTrendDirection.Down, percent };
|
|
83
|
+
}
|
|
84
|
+
return { direction: MileageTrendDirection.Flat, percent };
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Counts a dossier's flagged vehicles and adds up the km of the ones still reporting. A silent
|
|
89
|
+
* device's figures stay frozen at its last reading, so they are left out of the total and the trend.
|
|
90
|
+
*/
|
|
91
|
+
export const getMileageSummary = (mileage: ContractMileage[]): MileageSummary => {
|
|
92
|
+
const countAlert = (alert: MileageAlert) => mileage.filter((item) => item.alert === alert).length;
|
|
93
|
+
const noSignal = countAlert(MileageStatus.NoSignal);
|
|
94
|
+
const high = countAlert(MileageStatus.High);
|
|
95
|
+
const low = countAlert(MileageStatus.Low);
|
|
96
|
+
const reporting = mileage.filter((item) => item.status !== MileageStatus.NoSignal);
|
|
97
|
+
const withAverage = reporting.filter((item) => item.yearAvg30DaysKm != null);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
vehicles: mileage.length,
|
|
101
|
+
noSignal,
|
|
102
|
+
high,
|
|
103
|
+
low,
|
|
104
|
+
issues: noSignal + high + low,
|
|
105
|
+
last30DaysKm: reporting.length > 0 ? sumBy(reporting, (item) => item.last30DaysKm) : undefined,
|
|
106
|
+
trend: getMileageTrend(
|
|
107
|
+
sumBy(withAverage, (item) => item.last30DaysKm),
|
|
108
|
+
sumBy(withAverage, (item) => item.yearAvg30DaysKm ?? 0),
|
|
109
|
+
),
|
|
110
|
+
};
|
|
111
|
+
};
|