@shipengine/connect-carrier-api 2.10.0 → 2.11.0-beta
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/app/build-native-rating-routes.d.ts +3 -0
- package/lib/app/build-native-rating-routes.js +103 -0
- package/lib/app/build-native-rating-routes.js.map +1 -0
- package/lib/app/carrier-app.js +5 -1
- package/lib/app/carrier-app.js.map +1 -1
- package/lib/app/create-get-rates-handler.d.ts +5 -0
- package/lib/app/create-get-rates-handler.js +95 -0
- package/lib/app/create-get-rates-handler.js.map +1 -0
- package/lib/app/index.d.ts +1 -0
- package/lib/app/index.js +1 -0
- package/lib/app/index.js.map +1 -1
- package/lib/app/internal/carrier-specification.d.ts +2 -4
- package/lib/app/internal/carrier-specification.js +9 -1
- package/lib/app/internal/carrier-specification.js.map +1 -1
- package/lib/app/internal/native-rating-specification.d.ts +5 -0
- package/lib/app/internal/native-rating-specification.js +3 -0
- package/lib/app/internal/native-rating-specification.js.map +1 -0
- package/lib/app/internal/route.d.ts +17 -3
- package/lib/app/internal/route.js.map +1 -1
- package/lib/app/metadata/carrier.d.ts +2 -6
- package/lib/app/metadata/carrier.js +4 -10
- package/lib/app/metadata/carrier.js.map +1 -1
- package/lib/app/metadata/custom-validators/file-exists.d.ts +1 -0
- package/lib/app/metadata/custom-validators/file-exists.js +12 -0
- package/lib/app/metadata/custom-validators/file-exists.js.map +1 -0
- package/lib/app/metadata/native-rating-configuration.d.ts +12 -0
- package/lib/app/metadata/native-rating-configuration.js +13 -0
- package/lib/app/metadata/native-rating-configuration.js.map +1 -0
- package/lib/app/metadata/rate-card.d.ts +9 -0
- package/lib/app/metadata/rate-card.js +10 -0
- package/lib/app/metadata/rate-card.js.map +1 -0
- package/lib/app/native-rating/base-rate-context.d.ts +3 -0
- package/lib/app/native-rating/base-rate-context.js +79 -0
- package/lib/app/native-rating/base-rate-context.js.map +1 -0
- package/lib/app/native-rating/context-results.d.ts +3 -0
- package/lib/app/native-rating/context-results.js +3 -0
- package/lib/app/native-rating/context-results.js.map +1 -0
- package/lib/app/native-rating/create-dynamic-carrier.d.ts +7 -0
- package/lib/app/native-rating/create-dynamic-carrier.js +63 -0
- package/lib/app/native-rating/create-dynamic-carrier.js.map +1 -0
- package/lib/app/native-rating/get-rates.d.ts +7 -0
- package/lib/app/native-rating/get-rates.js +3 -0
- package/lib/app/native-rating/get-rates.js.map +1 -0
- package/lib/app/native-rating/get-variables.d.ts +6 -0
- package/lib/app/native-rating/get-variables.js +3 -0
- package/lib/app/native-rating/get-variables.js.map +1 -0
- package/lib/app/native-rating/get-zone.d.ts +6 -0
- package/lib/app/native-rating/get-zone.js +3 -0
- package/lib/app/native-rating/get-zone.js.map +1 -0
- package/lib/app/native-rating/implementation-type.d.ts +31 -0
- package/lib/app/native-rating/implementation-type.js +8 -0
- package/lib/app/native-rating/implementation-type.js.map +1 -0
- package/lib/app/native-rating/index.d.ts +2 -0
- package/lib/app/native-rating/index.js +6 -0
- package/lib/app/native-rating/index.js.map +1 -0
- package/lib/app/native-rating/numeric-currency.d.ts +5 -0
- package/lib/app/native-rating/numeric-currency.js +3 -0
- package/lib/app/native-rating/numeric-currency.js.map +1 -0
- package/lib/app/native-rating/rating-context.d.ts +15 -0
- package/lib/app/native-rating/rating-context.js +3 -0
- package/lib/app/native-rating/rating-context.js.map +1 -0
- package/lib/models/rates/rate.d.ts +4 -2
- package/lib/models/rates/rate.js +2 -1
- package/lib/models/rates/rate.js.map +1 -1
- package/lib/models/shipment-item.d.ts +7 -0
- package/lib/models/shipment-item.js +3 -0
- package/lib/models/shipment-item.js.map +1 -0
- package/lib/requests/get-rates-request.d.ts +3 -0
- package/lib/requests/get-rates-request.js.map +1 -1
- package/package.json +5 -2
- package/spec.json +0 -12
- package/src/app/build-native-rating-routes.ts +117 -0
- package/src/app/carrier-app.ts +10 -2
- package/src/app/create-get-rates-handler.ts +119 -0
- package/src/app/index.ts +1 -0
- package/src/app/internal/carrier-specification.ts +16 -5
- package/src/app/internal/native-rating-specification.ts +5 -0
- package/src/app/internal/route.ts +20 -3
- package/src/app/metadata/carrier.ts +3 -15
- package/src/app/metadata/custom-validators/file-exists.ts +8 -0
- package/src/app/metadata/native-rating-configuration.ts +19 -0
- package/src/app/metadata/rate-card.ts +14 -0
- package/src/app/native-rating/base-rate-context.ts +97 -0
- package/src/app/native-rating/context-results.ts +3 -0
- package/src/app/native-rating/create-dynamic-carrier.ts +82 -0
- package/src/app/native-rating/get-rates.ts +9 -0
- package/src/app/native-rating/get-variables.ts +9 -0
- package/src/app/native-rating/get-zone.ts +9 -0
- package/src/app/native-rating/implementation-type.ts +34 -0
- package/src/app/native-rating/index.ts +2 -0
- package/src/app/native-rating/numeric-currency.ts +5 -0
- package/src/app/native-rating/rating-context.ts +19 -0
- package/src/models/rates/rate.ts +6 -3
- package/src/models/shipment-item.ts +7 -0
- package/src/requests/get-rates-request.ts +3 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/app/carrier-app.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { OpenApiSpecification } from '../spec';
|
|
|
5
5
|
|
|
6
6
|
import { Metadata } from './internal/metadata';
|
|
7
7
|
import { CarrierSpecification } from './internal/carrier-specification';
|
|
8
|
-
import { CarrierAppMetadataSchema } from './metadata';
|
|
8
|
+
import { CarrierAppMetadata, CarrierAppMetadataSchema } from './metadata';
|
|
9
9
|
import {
|
|
10
10
|
RegisterResponseSchema,
|
|
11
11
|
CancelNotificationResponseSchema,
|
|
@@ -26,6 +26,8 @@ import {
|
|
|
26
26
|
import { Schema } from 'joi';
|
|
27
27
|
import { DocumentTemplate } from './internal/document-template';
|
|
28
28
|
import fs from 'fs';
|
|
29
|
+
import { createGetRatesHandler } from './create-get-rates-handler';
|
|
30
|
+
import { buildNativeRatingRoutes } from './build-native-rating-routes';
|
|
29
31
|
|
|
30
32
|
const handleRequest = (implementation?: Function): any => {
|
|
31
33
|
if (implementation) {
|
|
@@ -67,6 +69,9 @@ export class CarrierApp implements ConnectRuntimeApp {
|
|
|
67
69
|
return results.error.details.map((detail) => `${detail.message}`);
|
|
68
70
|
}
|
|
69
71
|
};
|
|
72
|
+
|
|
73
|
+
const ratingImplementation = createGetRatesHandler(definition);
|
|
74
|
+
|
|
70
75
|
new Array<[Method, ApiEndpoints, any, Schema]>(
|
|
71
76
|
[
|
|
72
77
|
Method.POST,
|
|
@@ -89,7 +94,7 @@ export class CarrierApp implements ConnectRuntimeApp {
|
|
|
89
94
|
definition.CreateNotification,
|
|
90
95
|
CreateNotificationResponseSchema,
|
|
91
96
|
],
|
|
92
|
-
[Method.POST, ApiEndpoints.GetRates,
|
|
97
|
+
[Method.POST, ApiEndpoints.GetRates, ratingImplementation, GetRatesResponseSchema],
|
|
93
98
|
[
|
|
94
99
|
Method.POST,
|
|
95
100
|
ApiEndpoints.NormalizeTrackingData,
|
|
@@ -152,6 +157,9 @@ export class CarrierApp implements ConnectRuntimeApp {
|
|
|
152
157
|
return await this.getDocumentTemplate(request);
|
|
153
158
|
},
|
|
154
159
|
});
|
|
160
|
+
|
|
161
|
+
buildNativeRatingRoutes(definition.Metadata).forEach((x) => this.routes.push(x));
|
|
162
|
+
|
|
155
163
|
this.data = new Metadata(definition);
|
|
156
164
|
this.redoc = JSON.stringify(OpenApiSpecification);
|
|
157
165
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { GetRatesRequest } from '../requests';
|
|
4
|
+
import { CarrierAppDefinition } from './carrier-app-definition';
|
|
5
|
+
import { Carrier } from './metadata/carrier';
|
|
6
|
+
import { createContextBuilder } from './native-rating/base-rate-context';
|
|
7
|
+
import createDynamicCarrier from './native-rating/create-dynamic-carrier';
|
|
8
|
+
import { RateResultsAndId, RatingCarrier } from './native-rating/implementation-type';
|
|
9
|
+
|
|
10
|
+
export const rateLogicName = 'rate-shipments.js';
|
|
11
|
+
|
|
12
|
+
const runNativeRatingLocally = process.env.LOCAL_NATIVE_RATING
|
|
13
|
+
? process.env.LOCAL_NATIVE_RATING !== 'false'
|
|
14
|
+
: process.env.NODE_ENV !== 'production';
|
|
15
|
+
|
|
16
|
+
const createImplementation = (basePath: string): RatingCarrier | undefined => {
|
|
17
|
+
const logicPath = join(basePath, rateLogicName);
|
|
18
|
+
|
|
19
|
+
if (!existsSync(logicPath)) {
|
|
20
|
+
console.log(`Logic implementation does not exist at ${logicPath}`);
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const rawImplementation = readFileSync(logicPath, 'utf8');
|
|
25
|
+
return createDynamicCarrier(rawImplementation);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const createNativeRatingHandler = (carrier: Carrier) => {
|
|
29
|
+
if (!carrier.NativeRating) {
|
|
30
|
+
console.log('Carrier does NOT have a Native Rating property in its config');
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
console.dir(carrier.NativeRating);
|
|
34
|
+
const basePath = carrier.NativeRating.Path;
|
|
35
|
+
if (!basePath) {
|
|
36
|
+
console.log('Carrier does NOT have a Native Rating path set');
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!existsSync(basePath)) {
|
|
41
|
+
console.log(`Path for Native Rating '${basePath}' does not exist`);
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const buildContext = createContextBuilder(basePath);
|
|
47
|
+
const defaultImplementation = createImplementation(basePath);
|
|
48
|
+
const rateCards = Object.fromEntries(
|
|
49
|
+
carrier.NativeRating.RateCards?.map((card) => {
|
|
50
|
+
const impl = createImplementation(join(basePath, card.Id)) || defaultImplementation;
|
|
51
|
+
if (!impl) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Cannot find default nor card specific rating logic for rate card ${card.Id}`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const context = buildContext(card);
|
|
58
|
+
const rateShipments = (req: GetRatesRequest): Promise<RateResultsAndId[]> =>
|
|
59
|
+
impl.rateShipments(context, [{ id: 'rate-request', shipment: req }]);
|
|
60
|
+
|
|
61
|
+
return [card.Id, rateShipments];
|
|
62
|
+
}) || [],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const defaultRateCard = rateCards[carrier.NativeRating.DefaultRateCardId];
|
|
66
|
+
if (!defaultRateCard) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Could not find a rate card definition for default rate card ${carrier.NativeRating.DefaultRateCardId}`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return async (req: GetRatesRequest) => {
|
|
73
|
+
const rateCardId = req.metadata?.['native-rating-rate-card'];
|
|
74
|
+
const rateShipments = rateCardId ? rateCards[rateCardId] : defaultRateCard;
|
|
75
|
+
|
|
76
|
+
if (!rateShipments) {
|
|
77
|
+
throw new Error(`Could not find rate card ${rateCardId}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const results = await rateShipments(req);
|
|
81
|
+
return results.find((x) => x.id === 'rate-request');
|
|
82
|
+
};
|
|
83
|
+
} catch (err: any) {
|
|
84
|
+
console.warn(err?.message);
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const createGetRatesHandler = (definition: CarrierAppDefinition) => {
|
|
90
|
+
if (!runNativeRatingLocally) {
|
|
91
|
+
return definition.GetRates;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const nativeRatingCarriers = definition.Metadata.Carriers.map((x) => ({
|
|
95
|
+
apiCode: x.ApiCode || x.Name,
|
|
96
|
+
handler: createNativeRatingHandler(x) || definition.GetRates,
|
|
97
|
+
})).filter((x) => x.handler !== undefined);
|
|
98
|
+
|
|
99
|
+
// If there aren't any native rating implementations found, return whatever is defined in the definition
|
|
100
|
+
// This is what should happen in production because the Kong gateway should route any rate requests for NR
|
|
101
|
+
// carriers directly to the Native Rating service
|
|
102
|
+
if (nativeRatingCarriers.length === 0) {
|
|
103
|
+
console.log('No native rating implementations, returning GetRates implementation');
|
|
104
|
+
return definition.GetRates;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// If there's only a single carrier defined, just return the Native Rating handler so that we can avoid
|
|
108
|
+
// requiring the "carrier_code" property be set on the "metadata" object when testing
|
|
109
|
+
if (definition.Metadata.Carriers.length === 1) {
|
|
110
|
+
console.log('Only a single carrier and it uses Native Rating');
|
|
111
|
+
return nativeRatingCarriers[0].handler;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const implementationLookup = Object.fromEntries(
|
|
115
|
+
nativeRatingCarriers.map((x) => [x.apiCode, x.handler]),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return (req: GetRatesRequest) => implementationLookup[req.metadata?.['carrier_code']]?.(req);
|
|
119
|
+
};
|
package/src/app/index.ts
CHANGED
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
import { LabelFormatsEnum } from '../metadata/label-formats';
|
|
18
18
|
import { LabelSizesEnum } from '../metadata/label-sizes';
|
|
19
19
|
import { CarrierAttributeEnum } from '../metadata/carrier-attributes';
|
|
20
|
+
import { NativeRatingSpecification } from './native-rating-specification';
|
|
21
|
+
import { NativeRatingConfiguration } from '../metadata/native-rating-configuration';
|
|
20
22
|
|
|
21
23
|
export const mapShippingOptions = (
|
|
22
24
|
options?: ShippingOptionDictionary,
|
|
@@ -71,6 +73,18 @@ const mapAccountModal = (modal: AccountModals): AccountModals => {
|
|
|
71
73
|
};
|
|
72
74
|
};
|
|
73
75
|
|
|
76
|
+
const mapNativeRating = (
|
|
77
|
+
model?: NativeRatingConfiguration,
|
|
78
|
+
): NativeRatingSpecification | undefined => {
|
|
79
|
+
if (!model) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
DefaultRateCard: model.DefaultRateCardId,
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
|
|
74
88
|
/** @description This represents what we send to data manager */
|
|
75
89
|
export class CarrierSpecification {
|
|
76
90
|
Id: string;
|
|
@@ -92,10 +106,7 @@ export class CarrierSpecification {
|
|
|
92
106
|
LogoUrl: string;
|
|
93
107
|
IconUrl: string;
|
|
94
108
|
};
|
|
95
|
-
NativeRating?:
|
|
96
|
-
AppId: string;
|
|
97
|
-
DefaultRateCard: string;
|
|
98
|
-
};
|
|
109
|
+
NativeRating?: NativeRatingSpecification;
|
|
99
110
|
DocumentTemplate?: string;
|
|
100
111
|
|
|
101
112
|
constructor(definition: Carrier) {
|
|
@@ -118,7 +129,7 @@ export class CarrierSpecification {
|
|
|
118
129
|
this.TrackingUrl = definition.TrackingUrl;
|
|
119
130
|
this.CarrierUrl = definition.CarrierUrl;
|
|
120
131
|
this.Description = definition.Description;
|
|
121
|
-
this.NativeRating = definition.NativeRating;
|
|
132
|
+
this.NativeRating = mapNativeRating(definition.NativeRating);
|
|
122
133
|
this.DocumentTemplate = definition.DocumentTemplate;
|
|
123
134
|
}
|
|
124
135
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Request } from 'express';
|
|
2
|
+
|
|
1
3
|
export enum Method {
|
|
2
4
|
POST = 'post',
|
|
3
5
|
GET = 'get',
|
|
@@ -6,9 +8,24 @@ export enum Method {
|
|
|
6
8
|
DELETE = 'delete',
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
/** @description The definition for a route that will serve up the contents of a file */
|
|
12
|
+
export interface FileRoute {
|
|
13
|
+
/** @description The file that should be sent to the client. If this is set, handler will not be used. */
|
|
14
|
+
file: string | ((req: Request) => string | Promise<string>);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** @description The definition for a route and how it will be handled */
|
|
18
|
+
export interface HandlerRoute {
|
|
19
|
+
/** @description The method that will be called to handle this request */
|
|
12
20
|
handler?: (req: any) => any | Promise<any>;
|
|
21
|
+
/** @description The optional method used to validate the response for this route. Used for warning purposes. */
|
|
13
22
|
validateResponse?: (response: any) => string[] | undefined;
|
|
14
23
|
}
|
|
24
|
+
|
|
25
|
+
/** @description The definition for all types of routes */
|
|
26
|
+
export type Route = {
|
|
27
|
+
/** @description The path to the endpoint you wish to expose @example "/CreateLabel", "/sales_orders_export" */
|
|
28
|
+
path: string;
|
|
29
|
+
/** @description The http verb used for this endpoint @example "GET", "POST" */
|
|
30
|
+
method: Method;
|
|
31
|
+
} & (FileRoute | HandlerRoute);
|
|
@@ -7,8 +7,9 @@ import { CarrierAttributeEnum, CarrierAttributeEnumSchema } from './carrier-attr
|
|
|
7
7
|
import { LabelFormatsEnum, LabelFormatsEnumSchema } from './label-formats';
|
|
8
8
|
import { LabelSizesEnum, LabelSizesEnumSchema } from './label-sizes';
|
|
9
9
|
import { ConfirmationDictionary, ConfirmationDictionarySchema } from './confirmation-type';
|
|
10
|
-
import { existsSync } from 'fs';
|
|
11
10
|
import Joi from 'joi';
|
|
11
|
+
import { NativeRatingConfiguration } from './native-rating-configuration';
|
|
12
|
+
import { fileExists } from './custom-validators/file-exists';
|
|
12
13
|
|
|
13
14
|
import { ApiCodeRegex, ApiCodeValidationMessage } from '@shipengine/connect-runtime';
|
|
14
15
|
|
|
@@ -52,24 +53,11 @@ export interface Carrier {
|
|
|
52
53
|
Icon: string;
|
|
53
54
|
};
|
|
54
55
|
/** @description If this carrier uses Native Rating, details about the connection */
|
|
55
|
-
NativeRating?:
|
|
56
|
-
/** @description Id of the Native Rating app */
|
|
57
|
-
AppId: string;
|
|
58
|
-
|
|
59
|
-
/** @description Default rate card to use for rating */
|
|
60
|
-
DefaultRateCard: string;
|
|
61
|
-
};
|
|
56
|
+
NativeRating?: NativeRatingConfiguration;
|
|
62
57
|
/** @description This is the file path to the documents template, which is used by Rendering Service. Optional. If it's filled and template file provided, ApiCode must be filled. ApiCode will be used to identify the file in Rendering Service.*/
|
|
63
58
|
DocumentTemplate?: string;
|
|
64
59
|
}
|
|
65
60
|
|
|
66
|
-
const fileExists = (value: string, helpers: any) => {
|
|
67
|
-
if (existsSync(value)) {
|
|
68
|
-
return value;
|
|
69
|
-
}
|
|
70
|
-
throw new Error("the file doesn't exist");
|
|
71
|
-
};
|
|
72
|
-
|
|
73
61
|
export const CarrierSchema = Joi.object({
|
|
74
62
|
AccountModals: AccountModalsSchema.required(),
|
|
75
63
|
PackageTypes: Joi.array().optional().items(PackageTypeSchema).unique('Id'),
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import Joi from 'joi';
|
|
2
|
+
import { fileExists } from './custom-validators/file-exists';
|
|
3
|
+
import { RateCard, RateCardSchema } from './rate-card';
|
|
4
|
+
|
|
5
|
+
/** Configuration for attaching Native Rating to a carrier */
|
|
6
|
+
export interface NativeRatingConfiguration {
|
|
7
|
+
/** @description Default rate card to use for rating */
|
|
8
|
+
DefaultRateCardId: string;
|
|
9
|
+
/** Path to rating logic and rate card data for this carrier */
|
|
10
|
+
Path?: string;
|
|
11
|
+
/** Array of rate cards for the carrier */
|
|
12
|
+
RateCards?: RateCard[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const NativeRatingConfigurationSchema = Joi.object({
|
|
16
|
+
DefaultRateCard: Joi.string().required(),
|
|
17
|
+
Path: Joi.string().optional().custom(fileExists, 'implementation path exists'),
|
|
18
|
+
RateCards: Joi.array().optional().items(RateCardSchema).unique('Id'),
|
|
19
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import Joi from 'joi';
|
|
2
|
+
|
|
3
|
+
/** Configuration for a given rate card */
|
|
4
|
+
export interface RateCard {
|
|
5
|
+
/** Externally facing id of the rate card. */
|
|
6
|
+
Id: string;
|
|
7
|
+
/** Currency of the rate card. This uses the three-digit currency code format. */
|
|
8
|
+
Currency: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const RateCardSchema = Joi.object({
|
|
12
|
+
Id: Joi.string().required(),
|
|
13
|
+
Currency: Joi.string().required().length(3),
|
|
14
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { resolve } from 'path';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import winston, { format, transports } from 'winston';
|
|
4
|
+
import { NumericCurrency } from './numeric-currency';
|
|
5
|
+
import { RatingContext } from './rating-context';
|
|
6
|
+
import { RateCard } from '../metadata/rate-card';
|
|
7
|
+
|
|
8
|
+
interface DataLookup {
|
|
9
|
+
rates: (keys: string[]) => Promise<{ [key: string]: NumericCurrency }>;
|
|
10
|
+
variables: (keys: string[]) => Promise<{ [key: string]: any }>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type ZoneLookup = (keys: string[]) => Promise<{ [key: string]: any }>;
|
|
14
|
+
|
|
15
|
+
/** Predicate that returns true if the value is defined, false if not */
|
|
16
|
+
const valueIsUndefined = <T>([key, value]: [string, T | undefined]): boolean => {
|
|
17
|
+
const isIncluded = value !== undefined;
|
|
18
|
+
|
|
19
|
+
console.log(`${isIncluded ? '✔️' : '❌'} ${key}`);
|
|
20
|
+
|
|
21
|
+
return isIncluded;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Build a lookup function to find values for a list of keys */
|
|
25
|
+
const buildLookup =
|
|
26
|
+
<TIn, TOut>(name: string, items: { key: string; value: TIn }[], mapper?: (item: TIn) => TOut) =>
|
|
27
|
+
(keys: string[]): Promise<{ [dataKey: string]: TOut }> => {
|
|
28
|
+
console.log(`Getting ${name} keys (${keys.length} total):`);
|
|
29
|
+
console.group();
|
|
30
|
+
try {
|
|
31
|
+
const things = keys.map((key) => {
|
|
32
|
+
const foundValue = items.find((item) => key === item.key)?.value;
|
|
33
|
+
const mappedValue = foundValue && mapper ? mapper(foundValue) : foundValue;
|
|
34
|
+
return [key, mappedValue] as [string, TOut | undefined];
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const values = Object.fromEntries(things.filter(valueIsUndefined)) as {
|
|
38
|
+
[key: string]: TOut;
|
|
39
|
+
};
|
|
40
|
+
return Promise.resolve(values);
|
|
41
|
+
} finally {
|
|
42
|
+
console.groupEnd();
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const loadData = (basePath: string) => (dataType: string) => {
|
|
47
|
+
try {
|
|
48
|
+
const data = readFileSync(resolve(basePath, `${dataType}.json`));
|
|
49
|
+
const json = JSON.parse(data.toString());
|
|
50
|
+
return [dataType, json?.[dataType] || []];
|
|
51
|
+
} catch (err) {
|
|
52
|
+
return [dataType, []];
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const createContextBuilder = (basePath: string) => {
|
|
57
|
+
let zoneLookup: ZoneLookup;
|
|
58
|
+
const getZoneLookup = () => {
|
|
59
|
+
if (!zoneLookup) {
|
|
60
|
+
const zones = loadData(basePath)('zones')[1];
|
|
61
|
+
zoneLookup = buildLookup('zone', zones);
|
|
62
|
+
}
|
|
63
|
+
return zoneLookup;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const log = winston.createLogger({
|
|
67
|
+
format: format.combine(format.colorize(), format.timestamp(), format.metadata()),
|
|
68
|
+
transports: [new transports.Console({ level: 'debug' })],
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return (rateCard: RateCard): RatingContext => {
|
|
72
|
+
let dataLookup: DataLookup;
|
|
73
|
+
const getDataLookup = () => {
|
|
74
|
+
if (!dataLookup) {
|
|
75
|
+
const loadedData = Object.fromEntries(
|
|
76
|
+
['rates', 'variables'].map(loadData(resolve(basePath, rateCard.Id))),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
dataLookup = {
|
|
80
|
+
rates: buildLookup('rate', loadedData.rates, (x: any) => ({
|
|
81
|
+
amount: +x,
|
|
82
|
+
currency: rateCard.Currency,
|
|
83
|
+
})),
|
|
84
|
+
variables: buildLookup('variables', loadedData.variables),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return dataLookup;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
getRates: (keys: string[]) => getDataLookup().rates(keys),
|
|
92
|
+
getVariables: (keys: string[]) => getDataLookup().variables(keys),
|
|
93
|
+
getZone: (keys: string[]) => getZoneLookup()(keys),
|
|
94
|
+
log,
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { VM, VMScript } from 'vm2';
|
|
2
|
+
import { RateResultsAndId, RatingCarrier, ShipmentAndId } from './implementation-type';
|
|
3
|
+
import { RatingContext } from './rating-context';
|
|
4
|
+
|
|
5
|
+
/** Results of the carrier code validation **/
|
|
6
|
+
interface CodeValidationResults {
|
|
7
|
+
errors?: string[];
|
|
8
|
+
/** Does the carrier code contain the get zones function */
|
|
9
|
+
hasGetZonesFunction: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Ensure that the carrier module has the correct shape
|
|
13
|
+
* @param module Object into which code should be exported
|
|
14
|
+
*/
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
const validateCode = (module?: any): CodeValidationResults => {
|
|
17
|
+
if (module?.exports?.default === undefined) {
|
|
18
|
+
return {
|
|
19
|
+
errors: ['Code must have a default export'],
|
|
20
|
+
hasGetZonesFunction: false,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const errors = [];
|
|
25
|
+
const { rateShipments, getZones } = module.exports.default;
|
|
26
|
+
const hasGetZonesFunction = getZones !== undefined;
|
|
27
|
+
|
|
28
|
+
if (typeof rateShipments !== 'function') {
|
|
29
|
+
errors.push("Code must contain a function named 'rateShipments'");
|
|
30
|
+
} else if (rateShipments.length !== 2) {
|
|
31
|
+
errors.push("'rateShipments' function must have two parameters");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
errors,
|
|
36
|
+
hasGetZonesFunction,
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Create a dynamic carrier
|
|
41
|
+
* @param code Code that should be used for the carrier
|
|
42
|
+
* @returns Carrier with code ready to be executed
|
|
43
|
+
*/
|
|
44
|
+
const createDynamicCarrier = (code: string): RatingCarrier => {
|
|
45
|
+
// This is to make sure that the code we need to run doesn't get excluded because of dangling comments, etc.
|
|
46
|
+
const resetCode = code + ';\n /* */ \n';
|
|
47
|
+
|
|
48
|
+
const validationResults = new VM({
|
|
49
|
+
sandbox: { module: {}, validateCode },
|
|
50
|
+
}).run(`${resetCode}; validateCode(module);`);
|
|
51
|
+
if (validationResults.errors?.length > 0) {
|
|
52
|
+
throw new Error(validationResults.errors.join('; '));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const rateShipmentsScript = new VMScript(
|
|
56
|
+
`${resetCode} module.exports.default.rateShipments(context, shipment);`,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const executionVM = new VM({
|
|
60
|
+
sandbox: {
|
|
61
|
+
module: {},
|
|
62
|
+
context: {},
|
|
63
|
+
shipment: {},
|
|
64
|
+
zoneContext: {},
|
|
65
|
+
origin: {},
|
|
66
|
+
destination: {},
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const carrier: RatingCarrier = {
|
|
71
|
+
rateShipments: (
|
|
72
|
+
context: RatingContext,
|
|
73
|
+
shipment: ShipmentAndId[],
|
|
74
|
+
): Promise<RateResultsAndId[]> => {
|
|
75
|
+
return executionVM.setGlobals({ context, shipment }).run(rateShipmentsScript);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return carrier;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export default createDynamicCarrier;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ContextResults } from './context-results';
|
|
2
|
+
import { NumericCurrency } from './numeric-currency';
|
|
3
|
+
|
|
4
|
+
export type GetRatesResults = ContextResults<NumericCurrency>;
|
|
5
|
+
|
|
6
|
+
/** Function to get rates from the context */
|
|
7
|
+
export interface GetRates {
|
|
8
|
+
(dataKeys: string[]): Promise<GetRatesResults>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ContextResults } from './context-results';
|
|
2
|
+
|
|
3
|
+
export type GetVariableResults = ContextResults<any>;
|
|
4
|
+
|
|
5
|
+
/** Function to get rating variables from the context */
|
|
6
|
+
export interface GetVariables {
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
+
(dataKeys: string[]): Promise<GetVariableResults>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ContextResults } from './context-results';
|
|
2
|
+
|
|
3
|
+
export type GetZoneResults = ContextResults<any>;
|
|
4
|
+
|
|
5
|
+
/** Function to get zone data from the context */
|
|
6
|
+
export interface GetZone {
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
+
(dataKeys: string[]): Promise<GetZoneResults>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Rate } from '../../models/rates';
|
|
2
|
+
import { GetRatesRequest } from '../../requests/get-rates-request';
|
|
3
|
+
import { RatingContext } from './rating-context';
|
|
4
|
+
|
|
5
|
+
/** A rate request shipment with its corresponding id */
|
|
6
|
+
export interface ShipmentAndId {
|
|
7
|
+
/** rate_request_identifier for the rate request */
|
|
8
|
+
id: string;
|
|
9
|
+
/** Shipment for the rate request */
|
|
10
|
+
shipment: GetRatesRequest;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Rate results with its corresponding rate request id */
|
|
14
|
+
export interface RateResultsAndId {
|
|
15
|
+
/** rate_request_identifier for the corresponding rate request */
|
|
16
|
+
id: string;
|
|
17
|
+
/** Error, if any */
|
|
18
|
+
error?: unknown;
|
|
19
|
+
/** Rates for a given rate request */
|
|
20
|
+
rates: Rate[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Implementation of a carrier */
|
|
24
|
+
export interface RatingCarrier {
|
|
25
|
+
/** Rate shipments
|
|
26
|
+
* @param context Native Rating context that can be used by the implementation to interact with the underlying service
|
|
27
|
+
* @param shipment Shipments that should be rated
|
|
28
|
+
* @returns List of rates for the given shipments
|
|
29
|
+
*/
|
|
30
|
+
rateShipments: (context: RatingContext, shipment: ShipmentAndId[]) => Promise<RateResultsAndId[]>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Signify a validation error from the carrier */
|
|
34
|
+
export class CarrierValidationError extends Error {}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Logger } from 'winston';
|
|
2
|
+
import { GetVariables } from './get-variables';
|
|
3
|
+
import { GetRates } from './get-rates';
|
|
4
|
+
import { GetZone } from './get-zone';
|
|
5
|
+
|
|
6
|
+
/** Rates request context */
|
|
7
|
+
export interface RatingContext {
|
|
8
|
+
/** Function to get rates for given keys */
|
|
9
|
+
getRates: GetRates;
|
|
10
|
+
|
|
11
|
+
/** Function to get variables for given keys */
|
|
12
|
+
getVariables: GetVariables;
|
|
13
|
+
|
|
14
|
+
/** Function to get zone for given keys */
|
|
15
|
+
getZone: GetZone;
|
|
16
|
+
|
|
17
|
+
/** Logger that implementers can use */
|
|
18
|
+
log: Logger;
|
|
19
|
+
}
|
package/src/models/rates/rate.ts
CHANGED
|
@@ -26,8 +26,10 @@ export class Rate {
|
|
|
26
26
|
carrier_rate_id?: string;
|
|
27
27
|
/** @description DateTime after which the rate will no longer be accepted. ISO 8601 format, with local offset. Example: 2021-08-20T14:38:36.859237-05:00 */
|
|
28
28
|
expiration_datetime?: string;
|
|
29
|
-
/** @description
|
|
30
|
-
|
|
29
|
+
/** @description The Package type of this rate */
|
|
30
|
+
package_type?: string;
|
|
31
|
+
/** @description The zone this rate is for */
|
|
32
|
+
zone?: string;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
export const RateSchema = Joi.object({
|
|
@@ -42,5 +44,6 @@ export const RateSchema = Joi.object({
|
|
|
42
44
|
delivery_window: TimeWindowSchema.optional(),
|
|
43
45
|
carrier_rate_id: Joi.string().optional().empty(),
|
|
44
46
|
expiration_datetime: Joi.string().optional().empty().isoDate(),
|
|
45
|
-
|
|
47
|
+
package_type: Joi.string().optional().empty(),
|
|
48
|
+
zone: Joi.string().optional().empty(),
|
|
46
49
|
});
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
FulfillmentPlanDetails,
|
|
12
12
|
TimeWindow,
|
|
13
13
|
} from '../models';
|
|
14
|
+
import { ShipmentItem } from '../models/shipment-item';
|
|
14
15
|
|
|
15
16
|
/** @description Basic structure for a request to get rates */
|
|
16
17
|
export class GetRatesRequest extends BaseRequest {
|
|
@@ -30,4 +31,6 @@ export class GetRatesRequest extends BaseRequest {
|
|
|
30
31
|
fulfillment_plan_details?: FulfillmentPlanDetails;
|
|
31
32
|
/** @description The carrier pickup window is the time designated when the carrier will pickup your package from the initial location */
|
|
32
33
|
carrier_pickup_window?: TimeWindow;
|
|
34
|
+
/** Items included in the shipment */
|
|
35
|
+
items?: ShipmentItem[];
|
|
33
36
|
}
|