@dotdev/harmony-sdk 1.30.1 → 1.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/errors/index.d.ts +9 -3
- package/dist/errors/index.js +9 -5
- package/dist/errors/index.spec.d.ts +1 -0
- package/dist/errors/index.spec.js +56 -0
- package/dist/helpers/index.js +24 -6
- package/dist/helpers/index.spec.js +129 -0
- package/dist/modules/stock-lookup/mappings/stock-lookup.mapper.js +1 -0
- package/dist/modules/stock-lookup/stock-lookup.module.spec.js +2 -0
- package/dist/modules/stock-lookup/types/stock-lookup.interface.d.ts +2 -0
- package/package.json +1 -1
package/dist/errors/index.d.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
+
export type HarmonyErrorDetails = {
|
|
2
|
+
status?: number;
|
|
3
|
+
code?: string;
|
|
4
|
+
};
|
|
1
5
|
export declare class HarmonyAPIError extends Error {
|
|
2
|
-
|
|
6
|
+
readonly status?: number;
|
|
7
|
+
readonly code?: string;
|
|
8
|
+
constructor(message: string, details?: HarmonyErrorDetails);
|
|
3
9
|
}
|
|
4
10
|
export declare class AuthenticationError extends HarmonyAPIError {
|
|
5
|
-
constructor(message: string);
|
|
11
|
+
constructor(message: string, details?: HarmonyErrorDetails);
|
|
6
12
|
}
|
|
7
13
|
export declare class RequestError extends HarmonyAPIError {
|
|
8
|
-
constructor(message: string);
|
|
14
|
+
constructor(message: string, details?: HarmonyErrorDetails);
|
|
9
15
|
}
|
|
10
16
|
export interface ApiError {
|
|
11
17
|
status?: string;
|
package/dist/errors/index.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
export class HarmonyAPIError extends Error {
|
|
2
|
-
|
|
2
|
+
status;
|
|
3
|
+
code;
|
|
4
|
+
constructor(message, details) {
|
|
3
5
|
super(message);
|
|
4
6
|
this.name = "HarmonyAPIError";
|
|
7
|
+
this.status = details?.status;
|
|
8
|
+
this.code = details?.code;
|
|
5
9
|
}
|
|
6
10
|
}
|
|
7
11
|
export class AuthenticationError extends HarmonyAPIError {
|
|
8
|
-
constructor(message) {
|
|
9
|
-
super(message);
|
|
12
|
+
constructor(message, details) {
|
|
13
|
+
super(message, details);
|
|
10
14
|
this.name = "AuthenticationError";
|
|
11
15
|
}
|
|
12
16
|
}
|
|
13
17
|
export class RequestError extends HarmonyAPIError {
|
|
14
|
-
constructor(message) {
|
|
15
|
-
super(message);
|
|
18
|
+
constructor(message, details) {
|
|
19
|
+
super(message, details);
|
|
16
20
|
this.name = "RequestError";
|
|
17
21
|
}
|
|
18
22
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { AuthenticationError, HarmonyAPIError, RequestError, } from "./index";
|
|
2
|
+
describe("HarmonyAPIError", () => {
|
|
3
|
+
it("constructs with message only (backward compat)", () => {
|
|
4
|
+
const e = new HarmonyAPIError("boom");
|
|
5
|
+
expect(e.message).toBe("boom");
|
|
6
|
+
expect(e.name).toBe("HarmonyAPIError");
|
|
7
|
+
expect(e.status).toBeUndefined();
|
|
8
|
+
expect(e.code).toBeUndefined();
|
|
9
|
+
});
|
|
10
|
+
it("accepts optional status and code via details arg", () => {
|
|
11
|
+
const e = new HarmonyAPIError("boom", { status: 400, code: "S:Server" });
|
|
12
|
+
expect(e.message).toBe("boom");
|
|
13
|
+
expect(e.status).toBe(400);
|
|
14
|
+
expect(e.code).toBe("S:Server");
|
|
15
|
+
});
|
|
16
|
+
it("accepts partial details (status only, code only)", () => {
|
|
17
|
+
const s = new HarmonyAPIError("a", { status: 500 });
|
|
18
|
+
expect(s.status).toBe(500);
|
|
19
|
+
expect(s.code).toBeUndefined();
|
|
20
|
+
const c = new HarmonyAPIError("b", { code: "X" });
|
|
21
|
+
expect(c.status).toBeUndefined();
|
|
22
|
+
expect(c.code).toBe("X");
|
|
23
|
+
});
|
|
24
|
+
it("is instanceof Error", () => {
|
|
25
|
+
expect(new HarmonyAPIError("x")).toBeInstanceOf(Error);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
describe("RequestError", () => {
|
|
29
|
+
it("inherits HarmonyAPIError and exposes status/code", () => {
|
|
30
|
+
const e = new RequestError("req fail", { status: 502, code: "" });
|
|
31
|
+
expect(e).toBeInstanceOf(HarmonyAPIError);
|
|
32
|
+
expect(e).toBeInstanceOf(Error);
|
|
33
|
+
expect(e.name).toBe("RequestError");
|
|
34
|
+
expect(e.status).toBe(502);
|
|
35
|
+
expect(e.code).toBe("");
|
|
36
|
+
});
|
|
37
|
+
it("constructs with message only (backward compat)", () => {
|
|
38
|
+
const e = new RequestError("just msg");
|
|
39
|
+
expect(e.message).toBe("just msg");
|
|
40
|
+
expect(e.status).toBeUndefined();
|
|
41
|
+
expect(e.code).toBeUndefined();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
describe("AuthenticationError", () => {
|
|
45
|
+
it("inherits HarmonyAPIError and supports details arg", () => {
|
|
46
|
+
const e = new AuthenticationError("no token", { status: 401 });
|
|
47
|
+
expect(e).toBeInstanceOf(HarmonyAPIError);
|
|
48
|
+
expect(e.name).toBe("AuthenticationError");
|
|
49
|
+
expect(e.status).toBe(401);
|
|
50
|
+
});
|
|
51
|
+
it("constructs with message only (backward compat)", () => {
|
|
52
|
+
const e = new AuthenticationError("no token");
|
|
53
|
+
expect(e.message).toBe("no token");
|
|
54
|
+
expect(e.status).toBeUndefined();
|
|
55
|
+
});
|
|
56
|
+
});
|
package/dist/helpers/index.js
CHANGED
|
@@ -99,33 +99,42 @@ export class ApiHelper {
|
|
|
99
99
|
}
|
|
100
100
|
static async parseError(error) {
|
|
101
101
|
if (axios.isAxiosError(error)) {
|
|
102
|
+
const status = error?.response?.status;
|
|
102
103
|
const childLogger = logger.child({
|
|
103
104
|
error: {
|
|
104
105
|
config: error?.config,
|
|
105
106
|
request: error?.request,
|
|
106
107
|
data: error?.response?.data,
|
|
107
|
-
status
|
|
108
|
+
status,
|
|
108
109
|
},
|
|
109
110
|
});
|
|
110
111
|
if (error?.response) {
|
|
112
|
+
const statusCategory = statusToCategory(status);
|
|
111
113
|
if (Utils.isValidXml(error?.response?.data)) {
|
|
112
114
|
const parsedError = await ApiHelper.parseHarmonyErrorResponse(error?.response?.data);
|
|
113
|
-
childLogger.error(
|
|
114
|
-
|
|
115
|
+
childLogger.error({
|
|
116
|
+
statusCategory,
|
|
117
|
+
harmonyCode: parsedError?.code,
|
|
118
|
+
harmonyName: parsedError?.name,
|
|
119
|
+
harmonyMessage: parsedError?.message,
|
|
120
|
+
}, `Harmony request error`);
|
|
121
|
+
return new RequestError(`API request failed with status ${error.response.status}: {code: ${parsedError?.code}, name: ${parsedError?.name}, message: ${parsedError?.message}}`, { status, code: parsedError?.code });
|
|
115
122
|
}
|
|
116
123
|
else {
|
|
117
|
-
childLogger.error(`API request error due to response not in Harmony XML format`);
|
|
118
|
-
return new RequestError(`API request failed but error response is not in Harmony XML format. Please check the logs for more details
|
|
124
|
+
childLogger.error({ statusCategory }, `API request error due to response not in Harmony XML format`);
|
|
125
|
+
return new RequestError(`API request failed but error response is not in Harmony XML format. Please check the logs for more details.`, { status });
|
|
119
126
|
}
|
|
120
127
|
}
|
|
121
128
|
else if (error?.request) {
|
|
129
|
+
childLogger.error({ statusCategory: "no_response" }, `No response received from Harmony API`);
|
|
122
130
|
return new RequestError("No response received from the server during API call");
|
|
123
131
|
}
|
|
124
132
|
else {
|
|
133
|
+
childLogger.error({ statusCategory: "setup_failure" }, `Harmony API request setup failed`);
|
|
125
134
|
return new RequestError(`API request setup failed: ${error?.message}`);
|
|
126
135
|
}
|
|
127
136
|
}
|
|
128
|
-
logger.error(error, `⛔️ Unexpected error occurred during API request`);
|
|
137
|
+
logger.error({ err: error, statusCategory: "unexpected" }, `⛔️ Unexpected error occurred during API request`);
|
|
129
138
|
return new RequestError(`Unexpected error occurred during API request`);
|
|
130
139
|
}
|
|
131
140
|
/**
|
|
@@ -161,6 +170,15 @@ export class ApiHelper {
|
|
|
161
170
|
};
|
|
162
171
|
}
|
|
163
172
|
}
|
|
173
|
+
function statusToCategory(status) {
|
|
174
|
+
if (status === undefined)
|
|
175
|
+
return "unknown";
|
|
176
|
+
if (status >= 400 && status < 500)
|
|
177
|
+
return "client_error";
|
|
178
|
+
if (status >= 500 && status < 600)
|
|
179
|
+
return "server_error";
|
|
180
|
+
return "unknown";
|
|
181
|
+
}
|
|
164
182
|
/**
|
|
165
183
|
* Check whether the provided parameters meet the specified limits. If any parameter exceeds the limit, an error will be thrown.
|
|
166
184
|
* If all checks passed, return true.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import axios from "axios";
|
|
2
2
|
import { ApiHelper, checkParamLimits, TEST_HEADERS, Utils } from ".";
|
|
3
|
+
import { RequestError } from "../errors";
|
|
3
4
|
import { ServiceAlias } from "../modules/shared/types";
|
|
4
5
|
// Increase timeout due to Harmony API having long response time
|
|
5
6
|
jest.setTimeout(999999);
|
|
@@ -146,3 +147,131 @@ describe("parseRequestError", () => {
|
|
|
146
147
|
expect(await ApiHelper.parseHarmonyErrorResponse(errResp)).toEqual(expected);
|
|
147
148
|
});
|
|
148
149
|
});
|
|
150
|
+
describe("ApiHelper.parseError", () => {
|
|
151
|
+
// Note: XML declaration must be at position 0 — no leading whitespace, or
|
|
152
|
+
// fast-xml-parser's XMLValidator rejects it and parseError takes the non-XML branch.
|
|
153
|
+
const HARMONY_XML_FAULT = `<?xml version='1.0' encoding='UTF-8'?><S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/"><S:Body><S:Fault xmlns:ns4="http://www.w3.org/2003/05/soap-envelope"><faultcode>S:Server</faultcode><faultstring>Failed Test Request</faultstring><detail><ns2:ServiceFault xmlns:ns2="http://pos.ws.fbsaust.com.au"><errorDetails>[204-104-2408071537-0000851206] (Invalid value)</errorDetails></ns2:ServiceFault></detail></S:Fault></S:Body></S:Envelope>`;
|
|
154
|
+
const GIFT_CARD_SERIAL_FAULT = `<?xml version='1.0' encoding='UTF-8'?><S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/"><S:Body><S:Fault><faultcode>S:Server</faultcode><faultstring>Validation</faultstring><detail><ns2:ServiceFault xmlns:ns2="http://gv.ws.fbsaust.com.au"><errorDetails>SN00001 is not a valid Serial Number value</errorDetails></ns2:ServiceFault></detail></S:Fault></S:Body></S:Envelope>`;
|
|
155
|
+
const makeAxiosError = (overrides) => {
|
|
156
|
+
const err = new Error(overrides.message ?? "Request failed");
|
|
157
|
+
err.isAxiosError = true;
|
|
158
|
+
err.config = {};
|
|
159
|
+
if (overrides.response)
|
|
160
|
+
err.response = overrides.response;
|
|
161
|
+
if (overrides.request)
|
|
162
|
+
err.request = overrides.request;
|
|
163
|
+
return err;
|
|
164
|
+
};
|
|
165
|
+
// axios.isAxiosError is auto-mocked by jest.mock("axios") at top of file.
|
|
166
|
+
// Point it at real behaviour per-test so constructed errors pass isAxiosError().
|
|
167
|
+
// Also silence the pino logger and capture structured context passed to it.
|
|
168
|
+
const { logger: realLogger } = require("./logger");
|
|
169
|
+
let childErrorSpy;
|
|
170
|
+
beforeEach(() => {
|
|
171
|
+
mAxios.isAxiosError.mockImplementation((e) => !!e?.isAxiosError);
|
|
172
|
+
childErrorSpy = jest.fn();
|
|
173
|
+
jest.spyOn(realLogger, "error").mockImplementation(jest.fn());
|
|
174
|
+
jest.spyOn(realLogger, "child").mockReturnValue({
|
|
175
|
+
error: childErrorSpy,
|
|
176
|
+
debug: jest.fn(),
|
|
177
|
+
info: jest.fn(),
|
|
178
|
+
warn: jest.fn(),
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
afterEach(() => {
|
|
182
|
+
jest.restoreAllMocks();
|
|
183
|
+
});
|
|
184
|
+
it("populates status and code for 4xx with Harmony XML body", async () => {
|
|
185
|
+
const err = makeAxiosError({
|
|
186
|
+
response: { status: 400, data: HARMONY_XML_FAULT },
|
|
187
|
+
});
|
|
188
|
+
const result = await ApiHelper.parseError(err);
|
|
189
|
+
expect(result).toBeInstanceOf(RequestError);
|
|
190
|
+
expect(result.status).toBe(400);
|
|
191
|
+
expect(result.code).toBe("S:Server");
|
|
192
|
+
expect(result.message).toBe(`API request failed with status 400: {code: S:Server, name: Failed Test Request, message: [204-104-2408071537-0000851206] (Invalid value)}`);
|
|
193
|
+
});
|
|
194
|
+
it("populates status for 5xx with Harmony XML body", async () => {
|
|
195
|
+
const err = makeAxiosError({
|
|
196
|
+
response: { status: 500, data: HARMONY_XML_FAULT },
|
|
197
|
+
});
|
|
198
|
+
const result = await ApiHelper.parseError(err);
|
|
199
|
+
expect(result.status).toBe(500);
|
|
200
|
+
expect(result.code).toBe("S:Server");
|
|
201
|
+
});
|
|
202
|
+
it("populates status but no code for non-XML 4xx response", async () => {
|
|
203
|
+
const err = makeAxiosError({
|
|
204
|
+
response: { status: 404, data: "Not Found" },
|
|
205
|
+
});
|
|
206
|
+
const result = await ApiHelper.parseError(err);
|
|
207
|
+
expect(result.status).toBe(404);
|
|
208
|
+
expect(result.code).toBeUndefined();
|
|
209
|
+
expect(result.message).toBe(`API request failed but error response is not in Harmony XML format. Please check the logs for more details.`);
|
|
210
|
+
});
|
|
211
|
+
it("populates status but no code for non-XML 5xx response", async () => {
|
|
212
|
+
const err = makeAxiosError({
|
|
213
|
+
response: { status: 502, data: "502 Bad Gateway" },
|
|
214
|
+
});
|
|
215
|
+
const result = await ApiHelper.parseError(err);
|
|
216
|
+
expect(result.status).toBe(502);
|
|
217
|
+
expect(result.code).toBeUndefined();
|
|
218
|
+
});
|
|
219
|
+
it("returns no status when request was sent but no response received", async () => {
|
|
220
|
+
const err = makeAxiosError({ request: {} });
|
|
221
|
+
const result = await ApiHelper.parseError(err);
|
|
222
|
+
expect(result.status).toBeUndefined();
|
|
223
|
+
expect(result.code).toBeUndefined();
|
|
224
|
+
expect(result.message).toBe("No response received from the server during API call");
|
|
225
|
+
});
|
|
226
|
+
it("returns no status for axios setup failure", async () => {
|
|
227
|
+
const err = makeAxiosError({ message: "bad URL" });
|
|
228
|
+
const result = await ApiHelper.parseError(err);
|
|
229
|
+
expect(result.status).toBeUndefined();
|
|
230
|
+
expect(result.message).toBe("API request setup failed: bad URL");
|
|
231
|
+
});
|
|
232
|
+
it("returns a RequestError for non-axios errors (fallthrough)", async () => {
|
|
233
|
+
const result = await ApiHelper.parseError(new Error("random"));
|
|
234
|
+
expect(result).toBeInstanceOf(RequestError);
|
|
235
|
+
expect(result.status).toBeUndefined();
|
|
236
|
+
expect(result.message).toBe("Unexpected error occurred during API request");
|
|
237
|
+
});
|
|
238
|
+
it("preserves gift-card serial-number substring in message (backward-compat)", async () => {
|
|
239
|
+
const err = makeAxiosError({
|
|
240
|
+
response: { status: 400, data: GIFT_CARD_SERIAL_FAULT },
|
|
241
|
+
});
|
|
242
|
+
const result = await ApiHelper.parseError(err);
|
|
243
|
+
// core-v2 services/gift-cards/strategies/harmony.ts:158 does
|
|
244
|
+
// err.message.includes("is not a valid Serial Number value") — must remain true.
|
|
245
|
+
expect(result.message.includes("is not a valid Serial Number value")).toBe(true);
|
|
246
|
+
});
|
|
247
|
+
it("logs structured fields for 4xx XML (statusCategory=client_error + harmonyCode)", async () => {
|
|
248
|
+
const err = makeAxiosError({
|
|
249
|
+
response: { status: 400, data: HARMONY_XML_FAULT },
|
|
250
|
+
});
|
|
251
|
+
await ApiHelper.parseError(err);
|
|
252
|
+
expect(childErrorSpy).toHaveBeenCalledTimes(1);
|
|
253
|
+
const [ctx, msg] = childErrorSpy.mock.calls[0];
|
|
254
|
+
expect(ctx).toMatchObject({
|
|
255
|
+
statusCategory: "client_error",
|
|
256
|
+
harmonyCode: "S:Server",
|
|
257
|
+
harmonyName: "Failed Test Request",
|
|
258
|
+
harmonyMessage: "[204-104-2408071537-0000851206] (Invalid value)",
|
|
259
|
+
});
|
|
260
|
+
expect(msg).toBe("Harmony request error");
|
|
261
|
+
});
|
|
262
|
+
it("logs statusCategory=server_error for 5xx", async () => {
|
|
263
|
+
const err = makeAxiosError({
|
|
264
|
+
response: { status: 500, data: HARMONY_XML_FAULT },
|
|
265
|
+
});
|
|
266
|
+
await ApiHelper.parseError(err);
|
|
267
|
+
const [ctx] = childErrorSpy.mock.calls[0];
|
|
268
|
+
expect(ctx).toMatchObject({ statusCategory: "server_error" });
|
|
269
|
+
});
|
|
270
|
+
it("logs statusCategory=no_response when request never got a response", async () => {
|
|
271
|
+
const err = makeAxiosError({ request: {} });
|
|
272
|
+
await ApiHelper.parseError(err);
|
|
273
|
+
const [ctx, msg] = childErrorSpy.mock.calls[0];
|
|
274
|
+
expect(ctx).toMatchObject({ statusCategory: "no_response" });
|
|
275
|
+
expect(msg).toBe("No response received from Harmony API");
|
|
276
|
+
});
|
|
277
|
+
});
|
|
@@ -102,6 +102,7 @@ export function mapStock(src) {
|
|
|
102
102
|
sellEndDate: (src?.sell_end_date ?? [])[0],
|
|
103
103
|
deliverFromDate: (src?.deliver_from_date ?? [])[0],
|
|
104
104
|
isoCountryOfOrigin: (src?.iso_country_of_origin ?? [])[0],
|
|
105
|
+
tariffCode: (src?.tariff_code ?? [])[0],
|
|
105
106
|
stockActive: (src?.stock_active ?? [])[0],
|
|
106
107
|
alternateNamekey: (src?.alternate_namekey ?? [])[0],
|
|
107
108
|
size: (src?.size ?? [])[0],
|
|
@@ -367,6 +367,7 @@ describe("StockLookup", () => {
|
|
|
367
367
|
<sell_end_date></sell_end_date>
|
|
368
368
|
<deliver_from_date></deliver_from_date>
|
|
369
369
|
<iso_country_of_origin></iso_country_of_origin>
|
|
370
|
+
<tariff_code>6204.33.00</tariff_code>
|
|
370
371
|
<size>6</size>
|
|
371
372
|
<pack></pack>
|
|
372
373
|
</Stock>
|
|
@@ -425,6 +426,7 @@ describe("StockLookup", () => {
|
|
|
425
426
|
sellEndDate: "",
|
|
426
427
|
deliverFromDate: "",
|
|
427
428
|
isoCountryOfOrigin: "",
|
|
429
|
+
tariffCode: "6204.33.00",
|
|
428
430
|
size: "6",
|
|
429
431
|
pack: "",
|
|
430
432
|
stockActive: undefined,
|
|
@@ -181,6 +181,7 @@ export interface Stock {
|
|
|
181
181
|
sellEndDate: string;
|
|
182
182
|
deliverFromDate: string;
|
|
183
183
|
isoCountryOfOrigin: string;
|
|
184
|
+
tariffCode: string;
|
|
184
185
|
size?: string;
|
|
185
186
|
pack?: string;
|
|
186
187
|
stockActive?: string;
|
|
@@ -232,6 +233,7 @@ export interface StockRaw {
|
|
|
232
233
|
sell_end_date: string[];
|
|
233
234
|
deliver_from_date: string[];
|
|
234
235
|
iso_country_of_origin: string[];
|
|
236
|
+
tariff_code: string[];
|
|
235
237
|
size: string[];
|
|
236
238
|
pack: string[];
|
|
237
239
|
stock_active: string[];
|