@shopickup/adapters-foxpost 0.0.1
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/README.md +48 -0
- package/dist/capabilities/index.d.ts +9 -0
- package/dist/capabilities/index.d.ts.map +1 -0
- package/dist/capabilities/index.js +8 -0
- package/dist/capabilities/label.d.ts +27 -0
- package/dist/capabilities/label.d.ts.map +1 -0
- package/dist/capabilities/label.js +370 -0
- package/dist/capabilities/parcels.d.ts +21 -0
- package/dist/capabilities/parcels.d.ts.map +1 -0
- package/dist/capabilities/parcels.js +233 -0
- package/dist/capabilities/pickup-points.d.ts +38 -0
- package/dist/capabilities/pickup-points.d.ts.map +1 -0
- package/dist/capabilities/pickup-points.js +225 -0
- package/dist/capabilities/track.d.ts +16 -0
- package/dist/capabilities/track.d.ts.map +1 -0
- package/dist/capabilities/track.js +99 -0
- package/dist/client/index.d.ts +17 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +30 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +165 -0
- package/dist/index.d.ts +119 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +151 -0
- package/dist/mappers/index.d.ts +108 -0
- package/dist/mappers/index.d.ts.map +1 -0
- package/dist/mappers/index.js +270 -0
- package/dist/mappers/trackStatus.d.ts +58 -0
- package/dist/mappers/trackStatus.d.ts.map +1 -0
- package/dist/mappers/trackStatus.js +290 -0
- package/dist/types/generated.d.ts +177 -0
- package/dist/types/generated.d.ts.map +1 -0
- package/dist/types/generated.js +9 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +6 -0
- package/dist/utils/httpUtils.d.ts +18 -0
- package/dist/utils/httpUtils.d.ts.map +1 -0
- package/dist/utils/httpUtils.js +33 -0
- package/dist/utils/resolveBaseUrl.d.ts +23 -0
- package/dist/utils/resolveBaseUrl.d.ts.map +1 -0
- package/dist/utils/resolveBaseUrl.js +19 -0
- package/dist/validation.d.ts +1723 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +799 -0
- package/package.json +68 -0
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# @shopickup/adapters-foxpost
|
|
2
|
+
|
|
3
|
+
Foxpost adapter for Shopickup.
|
|
4
|
+
|
|
5
|
+
[GitHub repo](https://github.com/shopickup/shopickup-integration-layer)
|
|
6
|
+
[Issues](https://github.com/shopickup/shopickup-integration-layer/issues)
|
|
7
|
+
|
|
8
|
+
## What it does
|
|
9
|
+
|
|
10
|
+
- `CREATE_PARCEL`
|
|
11
|
+
- `CREATE_LABEL`
|
|
12
|
+
- `TRACK`
|
|
13
|
+
- `LIST_PICKUP_POINTS`
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add @shopickup/adapters-foxpost @shopickup/core
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { FoxpostAdapter } from '@shopickup/adapters-foxpost';
|
|
25
|
+
import { executeCreateLabelFlow } from '@shopickup/core';
|
|
26
|
+
|
|
27
|
+
const adapter = new FoxpostAdapter('https://webapi.foxpost.hu');
|
|
28
|
+
|
|
29
|
+
const result = await executeCreateLabelFlow({
|
|
30
|
+
adapter,
|
|
31
|
+
shipment: {
|
|
32
|
+
id: 'order-001',
|
|
33
|
+
sender: { name: 'Shop', street: 'Main', city: 'Budapest', postalCode: '1011', country: 'HU' },
|
|
34
|
+
recipient: { name: 'Customer', street: 'Fo utca 2', city: 'Siofok', postalCode: '8600', country: 'HU' },
|
|
35
|
+
service: 'standard',
|
|
36
|
+
totalWeight: 1200,
|
|
37
|
+
createdAt: new Date(),
|
|
38
|
+
updatedAt: new Date(),
|
|
39
|
+
},
|
|
40
|
+
parcels: [{ id: 'parcel-1', weight: 1200 }],
|
|
41
|
+
credentials: { apiKey: 'your-foxpost-api-key' },
|
|
42
|
+
context: { http: yourHttpClient, logger: console },
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Status
|
|
47
|
+
|
|
48
|
+
Published as `0.x.x` while the adapter API is still evolving.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Foxpost Adapter: Capabilities Export
|
|
3
|
+
* Re-exports all capability methods for clean imports
|
|
4
|
+
*/
|
|
5
|
+
export { createParcel, createParcels } from './parcels.js';
|
|
6
|
+
export { createLabel, createLabels } from './label.js';
|
|
7
|
+
export { track } from './track.js';
|
|
8
|
+
export { fetchPickupPoints } from './pickup-points.js';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/capabilities/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Foxpost Adapter: Capabilities Export
|
|
3
|
+
* Re-exports all capability methods for clean imports
|
|
4
|
+
*/
|
|
5
|
+
export { createParcel, createParcels } from './parcels.js';
|
|
6
|
+
export { createLabel, createLabels } from './label.js';
|
|
7
|
+
export { track } from './track.js';
|
|
8
|
+
export { fetchPickupPoints } from './pickup-points.js';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Foxpost Adapter: Label Generation Capability
|
|
3
|
+
* Handles CREATE_LABEL and CREATE_LABELS operations
|
|
4
|
+
*/
|
|
5
|
+
import type { AdapterContext, CreateLabelResponse, CreateLabelsResponse } from "@shopickup/core";
|
|
6
|
+
import type { CreateLabelRequestFoxpost, CreateLabelsRequestFoxpost } from "../validation.js";
|
|
7
|
+
import type { ResolveBaseUrl } from "../utils/resolveBaseUrl.js";
|
|
8
|
+
/**
|
|
9
|
+
* Create a label (generate PDF) for a single parcel
|
|
10
|
+
* Delegates to createLabels to reuse batching logic
|
|
11
|
+
*
|
|
12
|
+
* Returns Promise<LabelResult> with file mapping and metadata
|
|
13
|
+
*/
|
|
14
|
+
export declare function createLabel(req: CreateLabelRequestFoxpost, ctx: AdapterContext, resolveBaseUrl: ResolveBaseUrl): Promise<CreateLabelResponse>;
|
|
15
|
+
/**
|
|
16
|
+
* Create labels for multiple parcels in one call
|
|
17
|
+
*
|
|
18
|
+
* Foxpost POST /api/label/{pageSize} endpoint:
|
|
19
|
+
* - Takes array of parcel IDs (barcodes)
|
|
20
|
+
* - Returns PDF with all labels (optionally concatenated based on pageSize)
|
|
21
|
+
* - For A7 size on A4 page, supports startPos parameter (1-7)
|
|
22
|
+
*
|
|
23
|
+
* Returns structured response with files array and per-item results
|
|
24
|
+
* Foxpost returns one PDF (combined), so all results reference the same file
|
|
25
|
+
*/
|
|
26
|
+
export declare function createLabels(req: CreateLabelsRequestFoxpost, ctx: AdapterContext, resolveBaseUrl: ResolveBaseUrl): Promise<CreateLabelsResponse>;
|
|
27
|
+
//# sourceMappingURL=label.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"label.d.ts","sourceRoot":"","sources":["../../src/capabilities/label.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAET,cAAc,EACf,mBAAmB,EAClB,oBAAoB,EAGtB,MAAM,iBAAiB,CAAC;AASzB,OAAO,KAAK,EACV,yBAAyB,EACzB,0BAA0B,EAC3B,MAAM,kBAAkB,CAAC;AAE1B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAkEjE;;;;;GAKG;AACH,wBAAsB,WAAW,CAC/B,GAAG,EAAE,yBAAyB,EAC9B,GAAG,EAAE,cAAc,EACnB,cAAc,EAAE,cAAc,GAC7B,OAAO,CAAC,mBAAmB,CAAC,CAkD9B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,0BAA0B,EAC/B,GAAG,EAAE,cAAc,EACnB,cAAc,EAAE,cAAc,GAC7B,OAAO,CAAC,oBAAoB,CAAC,CAqS/B"}
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Foxpost Adapter: Label Generation Capability
|
|
3
|
+
* Handles CREATE_LABEL and CREATE_LABELS operations
|
|
4
|
+
*/
|
|
5
|
+
import { CarrierError, errorToLog, serializeForLog } from "@shopickup/core";
|
|
6
|
+
import { translateFoxpostError } from '../errors.js';
|
|
7
|
+
import { safeValidateCreateLabelRequest, safeValidateCreateLabelsRequest, safeValidateFoxpostLabelPdfRaw, safeValidateFoxpostApiError, } from "../validation.js";
|
|
8
|
+
import { buildFoxpostBinaryHeaders } from '../utils/httpUtils.js';
|
|
9
|
+
import { URLSearchParams } from "node:url";
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
const BLOB_FIELDS = new Set(['label', 'labelBase64']);
|
|
12
|
+
function isSerializedBuffer(value) {
|
|
13
|
+
return (!!value &&
|
|
14
|
+
typeof value === 'object' &&
|
|
15
|
+
value.type === 'Buffer' &&
|
|
16
|
+
Array.isArray(value.data));
|
|
17
|
+
}
|
|
18
|
+
function summarizeBufferLike(value) {
|
|
19
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
|
|
20
|
+
return {
|
|
21
|
+
omittedBinary: true,
|
|
22
|
+
byteLength: value.byteLength,
|
|
23
|
+
note: 'binary payload omitted from rawCarrierResponse',
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
omittedBinary: true,
|
|
28
|
+
byteLength: value.data.length,
|
|
29
|
+
note: 'binary payload omitted from rawCarrierResponse',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function sanitizeRawValue(value, keyHint) {
|
|
33
|
+
if (typeof value === 'string') {
|
|
34
|
+
if (keyHint && BLOB_FIELDS.has(keyHint)) {
|
|
35
|
+
return `[truncated ${keyHint}; length=${value.length}]`;
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
|
|
40
|
+
return summarizeBufferLike(value);
|
|
41
|
+
}
|
|
42
|
+
if (isSerializedBuffer(value)) {
|
|
43
|
+
return summarizeBufferLike(value);
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
return value.map((item) => sanitizeRawValue(item));
|
|
47
|
+
}
|
|
48
|
+
if (value && typeof value === 'object') {
|
|
49
|
+
const out = {};
|
|
50
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
51
|
+
out[key] = sanitizeRawValue(nested, key);
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
function sanitizeRawCarrierResponse(raw) {
|
|
58
|
+
return sanitizeRawValue(raw);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Create a label (generate PDF) for a single parcel
|
|
62
|
+
* Delegates to createLabels to reuse batching logic
|
|
63
|
+
*
|
|
64
|
+
* Returns Promise<LabelResult> with file mapping and metadata
|
|
65
|
+
*/
|
|
66
|
+
export async function createLabel(req, ctx, resolveBaseUrl) {
|
|
67
|
+
// Validate request format and credentials
|
|
68
|
+
const validated = safeValidateCreateLabelRequest(req);
|
|
69
|
+
if (!validated.success) {
|
|
70
|
+
throw new CarrierError(`Invalid request: ${validated.error.message}`, "Validation", { raw: serializeForLog(validated.error) });
|
|
71
|
+
}
|
|
72
|
+
// Convert single label request to batch request
|
|
73
|
+
const batchReq = {
|
|
74
|
+
parcelCarrierIds: [validated.data.parcelCarrierId],
|
|
75
|
+
credentials: req.credentials,
|
|
76
|
+
options: req.options,
|
|
77
|
+
};
|
|
78
|
+
// Delegate to batch implementation
|
|
79
|
+
const response = await createLabels(batchReq, ctx, resolveBaseUrl);
|
|
80
|
+
// Extract first (only) result
|
|
81
|
+
if (!response || !Array.isArray(response.results)) {
|
|
82
|
+
throw new CarrierError("Unexpected response shape from createLabels", "Transient", { raw: serializeForLog(response) });
|
|
83
|
+
}
|
|
84
|
+
const results = response.results;
|
|
85
|
+
if (results.length === 0) {
|
|
86
|
+
throw new CarrierError("createLabels returned an empty results array", "Transient", { raw: serializeForLog(response) });
|
|
87
|
+
}
|
|
88
|
+
// Return the first (only) label result
|
|
89
|
+
const result = results[0];
|
|
90
|
+
const file = result.fileId
|
|
91
|
+
? response.files?.find((candidate) => candidate.id === result.fileId)
|
|
92
|
+
: undefined;
|
|
93
|
+
return {
|
|
94
|
+
...result,
|
|
95
|
+
file,
|
|
96
|
+
rawCarrierResponse: response.rawCarrierResponse,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Create labels for multiple parcels in one call
|
|
101
|
+
*
|
|
102
|
+
* Foxpost POST /api/label/{pageSize} endpoint:
|
|
103
|
+
* - Takes array of parcel IDs (barcodes)
|
|
104
|
+
* - Returns PDF with all labels (optionally concatenated based on pageSize)
|
|
105
|
+
* - For A7 size on A4 page, supports startPos parameter (1-7)
|
|
106
|
+
*
|
|
107
|
+
* Returns structured response with files array and per-item results
|
|
108
|
+
* Foxpost returns one PDF (combined), so all results reference the same file
|
|
109
|
+
*/
|
|
110
|
+
export async function createLabels(req, ctx, resolveBaseUrl) {
|
|
111
|
+
try {
|
|
112
|
+
// Validate request format and credentials
|
|
113
|
+
const validated = safeValidateCreateLabelsRequest(req);
|
|
114
|
+
if (!validated.success) {
|
|
115
|
+
throw new CarrierError(`Invalid request: ${validated.error.message}`, "Validation", { raw: validated.error });
|
|
116
|
+
}
|
|
117
|
+
if (!ctx.http) {
|
|
118
|
+
throw new CarrierError("HTTP client not provided in context", "Permanent");
|
|
119
|
+
}
|
|
120
|
+
if (!Array.isArray(req.parcelCarrierIds) || req.parcelCarrierIds.length === 0) {
|
|
121
|
+
return {
|
|
122
|
+
results: [],
|
|
123
|
+
files: [],
|
|
124
|
+
successCount: 0,
|
|
125
|
+
failureCount: 0,
|
|
126
|
+
totalCount: 0,
|
|
127
|
+
allSucceeded: false,
|
|
128
|
+
allFailed: false,
|
|
129
|
+
someFailed: false,
|
|
130
|
+
summary: "No parcels to process",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// Normalize public request options to adapter internal options.
|
|
134
|
+
const internalOptions = {
|
|
135
|
+
useTestApi: validated.data.options?.useTestApi ?? false,
|
|
136
|
+
size: validated.data.options?.size ?? "A7",
|
|
137
|
+
startPos: validated.data.options?.foxpost?.startPos,
|
|
138
|
+
isPortrait: validated.data.options?.foxpost?.isPortrait ?? false,
|
|
139
|
+
};
|
|
140
|
+
const baseUrl = resolveBaseUrl({ useTestApi: internalOptions.useTestApi });
|
|
141
|
+
// Construct URL with page size and optional params
|
|
142
|
+
const params = new URLSearchParams();
|
|
143
|
+
if (internalOptions.startPos !== undefined && internalOptions.startPos !== null) {
|
|
144
|
+
params.set('startPos', String(internalOptions.startPos));
|
|
145
|
+
}
|
|
146
|
+
if (internalOptions.isPortrait !== undefined && internalOptions.isPortrait !== null) {
|
|
147
|
+
params.set('isPortrait', String(internalOptions.isPortrait));
|
|
148
|
+
}
|
|
149
|
+
const url = `${baseUrl}/api/label/${internalOptions.size}${params.toString() ? `?${params.toString()}` : ''}`;
|
|
150
|
+
ctx.logger?.debug("Foxpost: Creating labels batch", {
|
|
151
|
+
testMode: internalOptions.useTestApi,
|
|
152
|
+
count: req.parcelCarrierIds.length,
|
|
153
|
+
size: internalOptions.size,
|
|
154
|
+
startPos: internalOptions.startPos,
|
|
155
|
+
isPortrait: internalOptions.isPortrait,
|
|
156
|
+
});
|
|
157
|
+
try {
|
|
158
|
+
// Make request to Foxpost label API
|
|
159
|
+
// Response is PDF binary data
|
|
160
|
+
const httpResponse = await ctx.http.post(url, validated.data.parcelCarrierIds, {
|
|
161
|
+
headers: buildFoxpostBinaryHeaders(validated.data.credentials),
|
|
162
|
+
responseType: "arraybuffer",
|
|
163
|
+
});
|
|
164
|
+
// Extract buffer from normalized HttpResponse
|
|
165
|
+
const pdfBuffer = httpResponse.body;
|
|
166
|
+
// Validate PDF binary response is non-empty
|
|
167
|
+
const pdfValidation = safeValidateFoxpostLabelPdfRaw(pdfBuffer);
|
|
168
|
+
if (!pdfValidation.success) {
|
|
169
|
+
throw new CarrierError(`Invalid PDF response: ${pdfValidation.error.message}`, "Transient", { raw: serializeForLog(pdfValidation.error) });
|
|
170
|
+
}
|
|
171
|
+
// Get byte length (works for Buffer and Uint8Array)
|
|
172
|
+
const byteLength = pdfBuffer instanceof Buffer ? pdfBuffer.byteLength :
|
|
173
|
+
pdfBuffer instanceof Uint8Array ? pdfBuffer.byteLength :
|
|
174
|
+
0;
|
|
175
|
+
// Create file resource for the single PDF
|
|
176
|
+
const fileId = randomUUID();
|
|
177
|
+
const file = {
|
|
178
|
+
id: fileId,
|
|
179
|
+
contentType: "application/pdf",
|
|
180
|
+
byteLength,
|
|
181
|
+
pages: req.parcelCarrierIds.length, // One page per label (Foxpost behavior)
|
|
182
|
+
orientation: internalOptions.isPortrait === false ? 'landscape' : 'portrait',
|
|
183
|
+
metadata: {
|
|
184
|
+
size: internalOptions.size,
|
|
185
|
+
isPortrait: internalOptions.isPortrait,
|
|
186
|
+
barcodeCount: req.parcelCarrierIds.length,
|
|
187
|
+
combined: true, // All labels in one file
|
|
188
|
+
},
|
|
189
|
+
// Attach raw bytes so dev-server responses can include file bytes directly
|
|
190
|
+
rawBytes: pdfBuffer,
|
|
191
|
+
};
|
|
192
|
+
ctx.logger?.info("Foxpost: Labels created successfully", {
|
|
193
|
+
count: req.parcelCarrierIds.length,
|
|
194
|
+
size: internalOptions.size,
|
|
195
|
+
testMode: internalOptions.useTestApi,
|
|
196
|
+
});
|
|
197
|
+
// Create per-item results, all referencing the same file
|
|
198
|
+
const results = req.parcelCarrierIds.map((barcode, idx) => ({
|
|
199
|
+
inputId: barcode,
|
|
200
|
+
status: "created",
|
|
201
|
+
fileId,
|
|
202
|
+
pageRange: { start: idx + 1, end: idx + 1 }, // One page per label
|
|
203
|
+
raw: {
|
|
204
|
+
barcode,
|
|
205
|
+
format: "PDF",
|
|
206
|
+
pageSize: internalOptions.size,
|
|
207
|
+
startPos: internalOptions.startPos,
|
|
208
|
+
pageNumber: idx + 1,
|
|
209
|
+
},
|
|
210
|
+
}));
|
|
211
|
+
return {
|
|
212
|
+
results,
|
|
213
|
+
files: [file],
|
|
214
|
+
successCount: results.length,
|
|
215
|
+
failureCount: 0,
|
|
216
|
+
totalCount: results.length,
|
|
217
|
+
allSucceeded: true,
|
|
218
|
+
allFailed: false,
|
|
219
|
+
someFailed: false,
|
|
220
|
+
summary: `All ${results.length} labels generated successfully`,
|
|
221
|
+
// Preserve status/headers but strip embedded label/blob payloads.
|
|
222
|
+
rawCarrierResponse: sanitizeRawCarrierResponse(serializeForLog(httpResponse)),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
catch (labelError) {
|
|
226
|
+
// Try to parse error response as Foxpost ApiError
|
|
227
|
+
let errorMessage = `Failed to generate label: ${labelError?.message || "Unknown error"}`;
|
|
228
|
+
let errorCategory = 'Transient';
|
|
229
|
+
// If labelError is from Foxpost API and contains status/error info, extract it
|
|
230
|
+
if (labelError instanceof CarrierError) {
|
|
231
|
+
// Already a CarrierError from validation, propagate it
|
|
232
|
+
throw labelError;
|
|
233
|
+
}
|
|
234
|
+
// Try to extract HTTP status from axios-like error
|
|
235
|
+
const httpStatus = labelError?.response?.status;
|
|
236
|
+
ctx.logger?.debug("Foxpost error analysis", {
|
|
237
|
+
hasResponse: !!labelError?.response,
|
|
238
|
+
httpStatus,
|
|
239
|
+
dataType: labelError?.response?.data?.constructor?.name,
|
|
240
|
+
isBuffer: Buffer.isBuffer(labelError?.response?.data),
|
|
241
|
+
});
|
|
242
|
+
if (httpStatus) {
|
|
243
|
+
// Attempt to parse error body as JSON if available
|
|
244
|
+
try {
|
|
245
|
+
let errorBody = labelError?.response?.data;
|
|
246
|
+
if (errorBody) {
|
|
247
|
+
ctx.logger?.debug("Raw error body before parsing", {
|
|
248
|
+
type: errorBody?.constructor?.name,
|
|
249
|
+
isBuffer: Buffer.isBuffer(errorBody),
|
|
250
|
+
isUint8Array: errorBody instanceof Uint8Array,
|
|
251
|
+
byteLength: errorBody?.length || errorBody?.byteLength,
|
|
252
|
+
});
|
|
253
|
+
// If error body is a Buffer, decode it to string first
|
|
254
|
+
if (Buffer.isBuffer(errorBody)) {
|
|
255
|
+
const decoded = errorBody.toString('utf-8');
|
|
256
|
+
ctx.logger?.debug("Decoded buffer to string", { decoded });
|
|
257
|
+
errorBody = JSON.parse(decoded);
|
|
258
|
+
}
|
|
259
|
+
else if (errorBody instanceof Uint8Array) {
|
|
260
|
+
errorBody = JSON.parse(new TextDecoder().decode(errorBody));
|
|
261
|
+
}
|
|
262
|
+
ctx.logger?.debug("Parsed error body", { errorBody });
|
|
263
|
+
// Try to parse as Foxpost ApiError
|
|
264
|
+
const apiErrorValidation = safeValidateFoxpostApiError(errorBody);
|
|
265
|
+
if (apiErrorValidation.success) {
|
|
266
|
+
const apiError = apiErrorValidation.data;
|
|
267
|
+
// Use error code as message if available
|
|
268
|
+
if (apiError.error) {
|
|
269
|
+
errorMessage = apiError.error;
|
|
270
|
+
ctx.logger?.debug("Extracted error message from API response", { message: errorMessage });
|
|
271
|
+
}
|
|
272
|
+
// Map HTTP status to error category
|
|
273
|
+
if (httpStatus === 400) {
|
|
274
|
+
errorCategory = /not[_ -]?found/i.test(String(apiError.error || errorMessage))
|
|
275
|
+
? 'NotFound'
|
|
276
|
+
: 'Validation';
|
|
277
|
+
}
|
|
278
|
+
else if (httpStatus === 401 || httpStatus === 403) {
|
|
279
|
+
errorCategory = 'Auth';
|
|
280
|
+
}
|
|
281
|
+
else if (httpStatus >= 500) {
|
|
282
|
+
errorCategory = 'Transient';
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
// Validation failed, log the issue
|
|
287
|
+
ctx.logger?.debug("Failed to validate Foxpost API error response", {
|
|
288
|
+
validation: apiErrorValidation.error,
|
|
289
|
+
attemptedBody: errorBody,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
catch (parseError) {
|
|
295
|
+
// If parsing fails, log and use default error category based on status
|
|
296
|
+
ctx.logger?.debug("Failed to parse error body", {
|
|
297
|
+
error: parseError?.message,
|
|
298
|
+
});
|
|
299
|
+
if (httpStatus === 400) {
|
|
300
|
+
errorCategory = 'Validation';
|
|
301
|
+
}
|
|
302
|
+
else if (httpStatus === 401 || httpStatus === 403) {
|
|
303
|
+
errorCategory = 'Auth';
|
|
304
|
+
}
|
|
305
|
+
else if (httpStatus >= 500) {
|
|
306
|
+
errorCategory = 'Transient';
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// If PDF generation fails, return error results for all barcodes
|
|
311
|
+
ctx.logger?.error("Foxpost: Label generation failed", {
|
|
312
|
+
count: req.parcelCarrierIds.length,
|
|
313
|
+
size: internalOptions.size,
|
|
314
|
+
error: errorToLog(labelError),
|
|
315
|
+
});
|
|
316
|
+
// Return failed results for all parcels
|
|
317
|
+
const errorObj = {
|
|
318
|
+
code: "LABEL_GENERATION_FAILED",
|
|
319
|
+
message: errorMessage,
|
|
320
|
+
};
|
|
321
|
+
ctx.logger?.debug("Creating error object", {
|
|
322
|
+
code: errorObj.code,
|
|
323
|
+
message: errorObj.message,
|
|
324
|
+
stringified: JSON.stringify(errorObj),
|
|
325
|
+
});
|
|
326
|
+
const results = req.parcelCarrierIds.map((barcode) => {
|
|
327
|
+
const result = {
|
|
328
|
+
inputId: barcode,
|
|
329
|
+
status: "failed",
|
|
330
|
+
errors: [errorObj],
|
|
331
|
+
raw: { barcode, error: serializeForLog(labelError) },
|
|
332
|
+
};
|
|
333
|
+
ctx.logger?.debug("Result object created", {
|
|
334
|
+
inputId: result.inputId,
|
|
335
|
+
status: result.status,
|
|
336
|
+
errorsLength: result.errors?.length,
|
|
337
|
+
firstError: result.errors?.[0],
|
|
338
|
+
});
|
|
339
|
+
return result;
|
|
340
|
+
});
|
|
341
|
+
ctx.logger?.debug("Error results being returned", {
|
|
342
|
+
sample: results[0],
|
|
343
|
+
errorMessage,
|
|
344
|
+
errorCategory,
|
|
345
|
+
});
|
|
346
|
+
return {
|
|
347
|
+
results,
|
|
348
|
+
files: [],
|
|
349
|
+
successCount: 0,
|
|
350
|
+
failureCount: results.length,
|
|
351
|
+
totalCount: results.length,
|
|
352
|
+
allSucceeded: false,
|
|
353
|
+
allFailed: true,
|
|
354
|
+
someFailed: false,
|
|
355
|
+
summary: `All ${results.length} labels failed`,
|
|
356
|
+
rawCarrierResponse: sanitizeRawCarrierResponse({ error: serializeForLog(labelError) }),
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
if (error instanceof CarrierError) {
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
364
|
+
ctx.logger?.error("Foxpost: Error creating labels", {
|
|
365
|
+
count: req.parcelCarrierIds.length,
|
|
366
|
+
error: errorToLog(error),
|
|
367
|
+
});
|
|
368
|
+
throw translateFoxpostError(error);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Foxpost Adapter: Parcel Creation Capabilities
|
|
3
|
+
* Handles CREATE_PARCEL and CREATE_PARCELS operations
|
|
4
|
+
*/
|
|
5
|
+
import type { CarrierResource, AdapterContext, CreateParcelRequest, CreateParcelsRequest, CreateParcelsResponse } from "@shopickup/core";
|
|
6
|
+
import type { ResolveBaseUrl } from '../utils/resolveBaseUrl.js';
|
|
7
|
+
/**
|
|
8
|
+
* Create a single parcel in Foxpost
|
|
9
|
+
* Delegates to createParcels to reuse batching logic
|
|
10
|
+
*/
|
|
11
|
+
export declare function createParcel(req: CreateParcelRequest, ctx: AdapterContext, createParcelsImpl: (req: CreateParcelsRequest, ctx: AdapterContext) => Promise<CreateParcelsResponse>): Promise<CarrierResource>;
|
|
12
|
+
/**
|
|
13
|
+
* Create multiple parcels in one call
|
|
14
|
+
* Maps canonical Parcel array to Foxpost CreateParcelRequest and calls the
|
|
15
|
+
* Foxpost batch endpoint which accepts an array. Returns per-item CarrierResource
|
|
16
|
+
* so callers can handle partial failures.
|
|
17
|
+
*
|
|
18
|
+
* @returns CreateParcelsResponse with summary and per-item results
|
|
19
|
+
*/
|
|
20
|
+
export declare function createParcels(req: CreateParcelsRequest, ctx: AdapterContext, resolveBaseUrl: ResolveBaseUrl): Promise<CreateParcelsResponse>;
|
|
21
|
+
//# sourceMappingURL=parcels.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parcels.d.ts","sourceRoot":"","sources":["../../src/capabilities/parcels.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,eAAe,EAGf,cAAc,EACd,mBAAmB,EACnB,oBAAoB,EACpB,qBAAqB,EACtB,MAAM,iBAAiB,CAAC;AASzB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAEjE;;;GAGG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,mBAAmB,EACxB,GAAG,EAAE,cAAc,EACnB,iBAAiB,EAAE,CAAC,GAAG,EAAE,oBAAoB,EAAE,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,qBAAqB,CAAC,GACpG,OAAO,CAAC,eAAe,CAAC,CA2C1B;AAED;;;;;;;GAOG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,oBAAoB,EACzB,GAAG,EAAE,cAAc,EACnB,cAAc,EAAE,cAAc,GAC7B,OAAO,CAAC,qBAAqB,CAAC,CAmOhC"}
|