@bisondesk/core-sdk 1.0.591 → 1.0.593

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.
@@ -0,0 +1,97 @@
1
+ import { TENANT_ID_ADMIN_HEADER } from '@bisondesk/commons-sdk/constants';
2
+ import { cleanHeaders, fetchJson, getAdminAuth } from '@bisondesk/commons-sdk/fetch';
3
+ import { NextList } from '@bisondesk/commons-sdk/types';
4
+ import { BusinessEntityIds } from '../constants.js';
5
+ import { Activity, ActivityUpdate, ActivityV2Meta, NewActivity } from '../types/activities.js';
6
+ import { DataRecord } from '../types/utils.js';
7
+
8
+ export const createActivity = async <Meta = ActivityV2Meta>(
9
+ tenantId: string,
10
+ activity: NewActivity<Meta>,
11
+ ): Promise<DataRecord<Activity<Meta>>> => {
12
+ const auth = await getAdminAuth();
13
+ const response: DataRecord<Activity<Meta>> | undefined = await fetchJson(
14
+ `${process.env.CORE_API_ORIGIN}/api/activities`,
15
+ {
16
+ method: 'POST',
17
+ headers: cleanHeaders({
18
+ Authorization: auth,
19
+ [TENANT_ID_ADMIN_HEADER]: tenantId,
20
+ 'Content-Type': 'application/json',
21
+ }),
22
+ body: JSON.stringify(activity),
23
+ },
24
+ );
25
+
26
+ if (response == null) {
27
+ throw new Error('Activity not created');
28
+ }
29
+
30
+ return response;
31
+ };
32
+
33
+ export const listActivitiesByRecord = async (
34
+ tenantId: string,
35
+ businessEntityId: BusinessEntityIds,
36
+ recordId: string,
37
+ nextToken?: string,
38
+ ): Promise<NextList<DataRecord<Activity>>> => {
39
+ const auth = await getAdminAuth();
40
+ const url = new URL(
41
+ `/api/activities/${businessEntityId}/${recordId}`,
42
+ process.env.CORE_API_ORIGIN,
43
+ );
44
+ if (nextToken != null) {
45
+ url.searchParams.append('nextToken', nextToken);
46
+ }
47
+
48
+ const response: NextList<DataRecord<Activity>> | undefined = await fetchJson(url, {
49
+ headers: cleanHeaders({
50
+ Authorization: auth,
51
+ [TENANT_ID_ADMIN_HEADER]: tenantId,
52
+ }),
53
+ });
54
+
55
+ if (response == null) {
56
+ throw new Error('Failed to list activities');
57
+ }
58
+
59
+ return response;
60
+ };
61
+
62
+ export const updateActivity = async (
63
+ tenantId: string,
64
+ activityId: string,
65
+ update: ActivityUpdate,
66
+ ): Promise<DataRecord<Activity>> => {
67
+ const auth = await getAdminAuth();
68
+ const response: DataRecord<Activity> | undefined = await fetchJson(
69
+ `${process.env.CORE_API_ORIGIN}/api/activities/${activityId}`,
70
+ {
71
+ method: 'PATCH',
72
+ headers: cleanHeaders({
73
+ Authorization: auth,
74
+ [TENANT_ID_ADMIN_HEADER]: tenantId,
75
+ 'Content-Type': 'application/json',
76
+ }),
77
+ body: JSON.stringify(update),
78
+ },
79
+ );
80
+
81
+ if (response == null) {
82
+ throw new Error('Activity not updated');
83
+ }
84
+
85
+ return response;
86
+ };
87
+
88
+ export const deleteActivity = async (tenantId: string, activityId: string): Promise<void> => {
89
+ const auth = await getAdminAuth();
90
+ await fetchJson(`${process.env.CORE_API_ORIGIN}/api/activities/${activityId}`, {
91
+ method: 'DELETE',
92
+ headers: cleanHeaders({
93
+ Authorization: auth,
94
+ [TENANT_ID_ADMIN_HEADER]: tenantId,
95
+ }),
96
+ });
97
+ };
@@ -1,10 +1,25 @@
1
1
  import { Categories } from '../constants.js';
2
2
 
3
+ type DeliveryFields =
4
+ | {
5
+ /** Flat cost in `currency` per vehicle category, added after conversion. */
6
+ deliveryCostByCategory: Partial<Record<Categories, number>>;
7
+ /** Human-readable, e.g., "Iquique (Chile)". */
8
+ deliveryLocation: string;
9
+ }
10
+ | { deliveryCostByCategory?: never; deliveryLocation?: never };
11
+
3
12
  export type CountryPricingRule = {
4
13
  countryCode: string; // lower-cased ISO 3166-1 alpha-2 (e.g., "cl")
5
14
  currency: string; // ISO 4217 (e.g., "USD")
6
- rate: number; // Fixed EUR→currency rate (e.g., 1.08)
7
- deliveryCostByCategory: Partial<Record<Categories, number>>; // Flat cost in target currency per vehicle category, added after conversion. At least one category is required.
8
- deliveryLocation: string; // Human-readable, e.g., "Iquique (Chile)".
15
+ /**
16
+ * Fixed multiplier applied to a price expressed in the owning branch's
17
+ * currency (`branch.currency`) to produce a price in `rule.currency`.
18
+ * For a tenant whose primary branch is EUR, a Bolivia rule with
19
+ * `currency: "USD"` carries `rate ≈ 1.08`. For a tenant whose primary
20
+ * branch is USD, a Belgium rule with `currency: "EUR"` carries
21
+ * `rate ≈ 0.92`.
22
+ */
23
+ rate: number;
9
24
  leasing?: boolean; // Override for leasing visibility
10
- };
25
+ } & DeliveryFields;
@@ -3,7 +3,7 @@ import { Organization } from './crm.js';
3
3
  import { Opportunity } from './opportunities.js';
4
4
  import { DeliveryTypes } from './quotes.js';
5
5
  import { BaseSearchRequest, SortOrder } from './search.js';
6
- import { DataRecord, ReferenceData } from './utils.js';
6
+ import { ReferenceData } from './utils.js';
7
7
  import { Vehicle, VehiclePurchase } from './vehicles.js';
8
8
 
9
9
  export enum TransportType {
@@ -360,6 +360,7 @@ export type VehicleInternalInfo = {
360
360
  };
361
361
 
362
362
  export type VehicleOriginalInfo = {
363
+ /** IMMEDIATE source tenant (the predecessor in a sync chain). */
363
364
  tenantId: string;
364
365
  tenantChain?: string[];
365
366
  identification: {
@@ -367,6 +368,7 @@ export type VehicleOriginalInfo = {
367
368
  branchId: string;
368
369
  stockNumber: string;
369
370
  metaId?: string;
371
+ country: string;
370
372
  };
371
373
  salesConditions: {
372
374
  leasing?: boolean;
@@ -378,6 +380,16 @@ export type VehicleOriginalInfo = {
378
380
  available?: boolean;
379
381
  };
380
382
  };
383
+ /**
384
+ * Deepest-origin tenant/branch in a multi-hop sync chain. Carries forward
385
+ * verbatim across hops so consumers can answer "where did this vehicle
386
+ * actually originate?" without walking the chain.
387
+ */
388
+ owner: {
389
+ tenantId: string;
390
+ branchId: string;
391
+ country: string;
392
+ };
381
393
  };
382
394
 
383
395
  export type VehicleAxlesInfo = {
@@ -523,7 +535,10 @@ export type VehicleExternalInfo = {
523
535
  remarks?: string;
524
536
  available?: boolean;
525
537
  };
526
- };
538
+ } & (
539
+ | { deliveryCost: number; deliveryLocation: string }
540
+ | { deliveryCost?: never; deliveryLocation?: never }
541
+ );
527
542
  superstructure?: VehicleSuperstructure;
528
543
  weights?: {
529
544
  gvw?: number;
@@ -670,6 +685,7 @@ export enum VehiclePurchaseDeliveryType {
670
685
 
671
686
  export type VehicleStatus = {
672
687
  deleted?: boolean;
688
+ deletedReason?: string;
673
689
  };
674
690
 
675
691
  export type VehiclePriceList = {
@@ -0,0 +1,82 @@
1
+ import { Categories } from '../constants.js';
2
+ import { CountryPricingRule } from '../types/country-pricing.js';
3
+ import {
4
+ convertFromBranchCurrency,
5
+ getApplicableDeliveryCost,
6
+ shouldApplyDeliveryCost,
7
+ } from './country-pricing.js';
8
+
9
+ const baseRule: CountryPricingRule = {
10
+ countryCode: 'bo',
11
+ currency: 'USD',
12
+ rate: 1.08,
13
+ deliveryCostByCategory: { [Categories.Truck]: 500 },
14
+ deliveryLocation: 'La Paz (Bolivia)',
15
+ };
16
+
17
+ describe('convertFromBranchCurrency', () => {
18
+ test('multiplies by rate', () => {
19
+ expect(convertFromBranchCurrency(1000, { rate: 1.08 })).toBe(1080);
20
+ });
21
+
22
+ test('uses Math.ceil to avoid underselling on partial cents', () => {
23
+ expect(convertFromBranchCurrency(999.5, { rate: 1.0 })).toBe(1000);
24
+ });
25
+ });
26
+
27
+ describe('shouldApplyDeliveryCost', () => {
28
+ test('returns false when origin equals rule country', () => {
29
+ expect(shouldApplyDeliveryCost({ countryCode: 'bo' }, 'bo')).toBe(false);
30
+ });
31
+
32
+ test('is case-insensitive', () => {
33
+ expect(shouldApplyDeliveryCost({ countryCode: 'bo' }, 'BO')).toBe(false);
34
+ expect(shouldApplyDeliveryCost({ countryCode: 'BO' }, 'bo')).toBe(false);
35
+ });
36
+
37
+ test('returns true when origin differs from rule country', () => {
38
+ expect(shouldApplyDeliveryCost({ countryCode: 'bo' }, 'be')).toBe(true);
39
+ });
40
+
41
+ test('returns true when origin is undefined (safe default)', () => {
42
+ expect(shouldApplyDeliveryCost({ countryCode: 'bo' }, undefined)).toBe(true);
43
+ });
44
+ });
45
+
46
+ describe('getApplicableDeliveryCost', () => {
47
+ test('returns undefined when same country, even with a configured cost', () => {
48
+ expect(getApplicableDeliveryCost(baseRule, Categories.Truck, 'bo')).toBeUndefined();
49
+ });
50
+
51
+ test('returns undefined when category is undefined', () => {
52
+ expect(getApplicableDeliveryCost(baseRule, undefined, 'be')).toBeUndefined();
53
+ });
54
+
55
+ test('returns the matching cost for a different country and known category', () => {
56
+ expect(getApplicableDeliveryCost(baseRule, Categories.Truck, 'be')).toBe(500);
57
+ });
58
+
59
+ test('returns 0 when the category is explicitly mapped to 0 (opt-in with no fee)', () => {
60
+ const zeroFeeRule: CountryPricingRule = {
61
+ countryCode: 'bo',
62
+ currency: 'USD',
63
+ rate: 1.08,
64
+ deliveryCostByCategory: { [Categories.Truck]: 0 },
65
+ deliveryLocation: 'Bolivia',
66
+ };
67
+ expect(getApplicableDeliveryCost(zeroFeeRule, Categories.Truck, 'be')).toBe(0);
68
+ });
69
+
70
+ test('returns undefined when category has no configured cost', () => {
71
+ expect(getApplicableDeliveryCost(baseRule, Categories.Van, 'be')).toBeUndefined();
72
+ });
73
+
74
+ test('returns undefined when rule has no deliveryCostByCategory at all', () => {
75
+ const pureRateRule: CountryPricingRule = {
76
+ countryCode: 'bo',
77
+ currency: 'USD',
78
+ rate: 1.08,
79
+ };
80
+ expect(getApplicableDeliveryCost(pureRateRule, Categories.Truck, 'be')).toBeUndefined();
81
+ });
82
+ });
@@ -0,0 +1,44 @@
1
+ import { Categories } from '../constants.js';
2
+ import { CountryPricingRule } from '../types/country-pricing.js';
3
+
4
+ /**
5
+ * Should the delivery cost defined by `rule` be applied for a vehicle whose
6
+ * origin branch is in `originCountry`? Returns `false` when the vehicle is
7
+ * already located in the rule's country (case-insensitive ISO-2 compare).
8
+ */
9
+ export const shouldApplyDeliveryCost = (
10
+ rule: Pick<CountryPricingRule, 'countryCode'>,
11
+ originCountry: string | undefined,
12
+ ): boolean => {
13
+ if (originCountry == null) {
14
+ return true;
15
+ }
16
+ return originCountry.toLowerCase() !== rule.countryCode.toLowerCase();
17
+ };
18
+
19
+ /**
20
+ * Look up the per-category delivery cost on the rule. Returns `undefined` when delivery is
21
+ * not applicable (same-country suppression, missing category, or category not in the rule's
22
+ * map). Returns the configured number — including `0` — when the rule explicitly covers the
23
+ * category. A `0` is a legitimate value: the operator has opted in for this category but is
24
+ * charging no fee on top (e.g. for vehicles already at the rule's `deliveryLocation`).
25
+ */
26
+ export const getApplicableDeliveryCost = (
27
+ rule: CountryPricingRule,
28
+ category: string | undefined,
29
+ originCountry: string | undefined,
30
+ ): number | undefined => {
31
+ if (!shouldApplyDeliveryCost(rule, originCountry)) {
32
+ return undefined;
33
+ }
34
+ if (category == null) {
35
+ return undefined;
36
+ }
37
+ return rule.deliveryCostByCategory?.[category as Categories];
38
+ };
39
+
40
+ /** Convert an amount in the owning branch's currency to `rule.currency`. */
41
+ export const convertFromBranchCurrency = (
42
+ branchAmount: number,
43
+ rule: Pick<CountryPricingRule, 'rate'>,
44
+ ): number => Math.ceil(branchAmount * rule.rate);