easypost-mcp 2.1.0 → 3.1.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/package.json +1 -1
- package/src/audit/AuditLogger.js +20 -0
- package/src/constants/toolRisk.js +36 -0
- package/src/elicitation/fallback.js +37 -0
- package/src/elicitation/fields/catalog.js +171 -0
- package/src/pipeline/ExecutionPipeline.js +226 -0
- package/src/resources/ResourceManager.js +244 -0
- package/src/resources/cache.js +41 -0
- package/src/resources/fuzzy.js +57 -0
- package/src/resources/staticResources.js +89 -0
- package/src/schemas/addressSchemas.js +3 -1
- package/src/schemas/batchSchemas.js +2 -1
- package/src/schemas/commonSchemas.js +5 -1
- package/src/schemas/pickupSchemas.js +4 -1
- package/src/schemas/resourceSchemas.js +16 -0
- package/src/schemas/returnSchemas.js +5 -1
- package/src/schemas/shipmentSchemas.js +8 -6
- package/src/server/createServer.js +25 -1
- package/src/services/AddressService.js +53 -3
- package/src/services/BatchService.js +8 -1
- package/src/services/ConfirmationService.js +45 -0
- package/src/services/ElicitationService.js +230 -0
- package/src/services/IdempotencyStore.js +40 -0
- package/src/services/PickupService.js +47 -20
- package/src/services/ReturnService.js +37 -7
- package/src/services/ShipmentService.js +149 -17
- package/src/services/WorkflowStateStore.js +70 -0
- package/src/services/index.js +20 -5
- package/src/tools/ToolDefinition.js +13 -2
- package/src/tools/definitions/pickups.js +2 -2
- package/src/tools/definitions/resources.js +59 -0
- package/src/tools/definitions/shipments.js +3 -3
- package/src/tools/index.js +2 -0
- package/src/validators/antiHallucination.js +109 -8
- package/src/validators/validate.js +2 -2
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { mapAddress } from "../adapters/easypost/responseMappers.js";
|
|
2
|
+
import { createFallbackResponse } from "../elicitation/fallback.js";
|
|
2
3
|
|
|
3
4
|
class AddressService {
|
|
4
|
-
constructor(easypostClient) {
|
|
5
|
+
constructor(easypostClient, { confirmations } = {}) {
|
|
5
6
|
this.easypost = easypostClient;
|
|
7
|
+
this.confirmations = confirmations;
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
async verifyAddress(input, context) {
|
|
@@ -14,9 +16,32 @@ class AddressService {
|
|
|
14
16
|
context
|
|
15
17
|
);
|
|
16
18
|
|
|
19
|
+
const mapped = mapAddress(address);
|
|
20
|
+
if (hasAddressCorrection(input.address, mapped) && input.confirm !== true) {
|
|
21
|
+
try {
|
|
22
|
+
await this.confirmations?.requireConfirmed(input, {
|
|
23
|
+
action: "confirm_normalized_address",
|
|
24
|
+
message: "EasyPost returned a normalized address. Did you mean this corrected address?",
|
|
25
|
+
details: { original_address: input.address, normalized_address: mapped },
|
|
26
|
+
}, context);
|
|
27
|
+
} catch {
|
|
28
|
+
return createFallbackResponse({
|
|
29
|
+
errorCode: "ADDRESS_CONFIRMATION_REQUIRED",
|
|
30
|
+
message: "EasyPost returned a normalized address. Confirm before using it.",
|
|
31
|
+
availableOptions: [{ value: "normalized_address", address: mapped }],
|
|
32
|
+
examples: { confirm: [true] },
|
|
33
|
+
nextAction: "Retry with confirm=true if the normalized address is correct.",
|
|
34
|
+
metadata: {
|
|
35
|
+
original_address: input.address,
|
|
36
|
+
normalized_address: mapped,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
17
42
|
return {
|
|
18
43
|
ok: true,
|
|
19
|
-
address:
|
|
44
|
+
address: mapped,
|
|
20
45
|
verification_status: address.verifications?.delivery?.success === true ? "verified" : "review_required",
|
|
21
46
|
messages: address.verifications?.delivery?.errors || [],
|
|
22
47
|
};
|
|
@@ -29,8 +54,33 @@ class AddressService {
|
|
|
29
54
|
context
|
|
30
55
|
);
|
|
31
56
|
|
|
32
|
-
|
|
57
|
+
const mapped = mapAddress(address);
|
|
58
|
+
if (input.verify && hasAddressCorrection(input.address, mapped) && input.confirm !== true) {
|
|
59
|
+
return createFallbackResponse({
|
|
60
|
+
errorCode: "ADDRESS_CONFIRMATION_REQUIRED",
|
|
61
|
+
message: "EasyPost returned a normalized address. Confirm before using it.",
|
|
62
|
+
availableOptions: [{ value: "normalized_address", address: mapped }],
|
|
63
|
+
examples: { confirm: [true] },
|
|
64
|
+
nextAction: "Retry with confirm=true if the normalized address is correct.",
|
|
65
|
+
metadata: {
|
|
66
|
+
original_address: input.address,
|
|
67
|
+
normalized_address: mapped,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { ok: true, address: mapped };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function hasAddressCorrection(original, normalized) {
|
|
77
|
+
if (!original || !normalized) return false;
|
|
78
|
+
for (const field of ["street1", "street2", "city", "state", "zip", "country"]) {
|
|
79
|
+
const before = String(original[field] || "").trim().toUpperCase();
|
|
80
|
+
const after = String(normalized[field] || "").trim().toUpperCase();
|
|
81
|
+
if (before && after && before !== after) return true;
|
|
33
82
|
}
|
|
83
|
+
return false;
|
|
34
84
|
}
|
|
35
85
|
|
|
36
86
|
export { AddressService };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
class BatchService {
|
|
2
|
-
constructor(easypostClient) {
|
|
2
|
+
constructor(easypostClient, { confirmations } = {}) {
|
|
3
3
|
this.easypost = easypostClient;
|
|
4
|
+
this.confirmations = confirmations;
|
|
4
5
|
}
|
|
5
6
|
|
|
6
7
|
async createBatch(input, context) {
|
|
@@ -17,6 +18,12 @@ class BatchService {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
async buyBatch(input, context) {
|
|
21
|
+
await this.confirmations?.requireConfirmed(input, {
|
|
22
|
+
action: "buy_batch",
|
|
23
|
+
message: `You are about to buy labels for batch ${input.batch_id}. Proceed?`,
|
|
24
|
+
details: { batch_id: input.batch_id },
|
|
25
|
+
}, context);
|
|
26
|
+
|
|
20
27
|
const batch = await this.easypost.execute(
|
|
21
28
|
"batch.buy",
|
|
22
29
|
(client) => client.Batch.buy(input.batch_id),
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { AppError } from "../errors/AppError.js";
|
|
2
|
+
|
|
3
|
+
class ConfirmationRequiredError extends AppError {
|
|
4
|
+
constructor({ action, message, details }) {
|
|
5
|
+
super(message, {
|
|
6
|
+
code: "CONFIRMATION_REQUIRED",
|
|
7
|
+
statusCode: 409,
|
|
8
|
+
details: {
|
|
9
|
+
action,
|
|
10
|
+
confirmation_required: true,
|
|
11
|
+
confirmation_field: "confirm",
|
|
12
|
+
expected_value: true,
|
|
13
|
+
...details,
|
|
14
|
+
},
|
|
15
|
+
safeMessage: message,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class ConfirmationService {
|
|
21
|
+
async requireConfirmed(input, { action, message, details = {}, typedConfirmation }, context = {}) {
|
|
22
|
+
if (input.confirm === true) return;
|
|
23
|
+
|
|
24
|
+
const elicited = await context.elicitation?.confirm({
|
|
25
|
+
server: context.server,
|
|
26
|
+
message,
|
|
27
|
+
title: "Confirm operation",
|
|
28
|
+
description: "Required before this high-risk operation can continue.",
|
|
29
|
+
typedConfirmation,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (elicited?.confirmed) {
|
|
33
|
+
input.confirm = true;
|
|
34
|
+
context.audit?.record("confirmation_elicited", {
|
|
35
|
+
correlationId: context.correlationId,
|
|
36
|
+
action,
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
throw new ConfirmationRequiredError({ action, message, details });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { ConfirmationService, ConfirmationRequiredError };
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { schemaForField } from "../elicitation/fields/catalog.js";
|
|
2
|
+
|
|
3
|
+
class ElicitationService {
|
|
4
|
+
supportsForm(server) {
|
|
5
|
+
return Boolean(server?.getClientCapabilities?.()?.elicitation?.form);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async requestForm({ server, message, properties, required = [], timeoutMs = 120000 }) {
|
|
9
|
+
if (!this.supportsForm(server)) {
|
|
10
|
+
return { accepted: false, reason: "FORM_ELICITATION_UNAVAILABLE" };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const result = await server.elicitInput(
|
|
15
|
+
{
|
|
16
|
+
mode: "form",
|
|
17
|
+
message,
|
|
18
|
+
requestedSchema: {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties,
|
|
21
|
+
required,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
{ timeout: timeoutMs }
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
if (result.action !== "accept") {
|
|
28
|
+
return {
|
|
29
|
+
accepted: false,
|
|
30
|
+
reason: `ELICITATION_${String(result.action || "UNKNOWN").toUpperCase()}`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { accepted: true, content: result.content || {} };
|
|
35
|
+
} catch (error) {
|
|
36
|
+
return {
|
|
37
|
+
accepted: false,
|
|
38
|
+
reason: "ELICITATION_FAILED",
|
|
39
|
+
error: { name: error.name, message: error.message },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async singleSelect({ server, message, field = "selection", title = "Selection", description, options, required = true, timeoutMs }) {
|
|
45
|
+
const result = await this.requestForm({
|
|
46
|
+
server,
|
|
47
|
+
message,
|
|
48
|
+
timeoutMs,
|
|
49
|
+
properties: {
|
|
50
|
+
[field]: {
|
|
51
|
+
type: "string",
|
|
52
|
+
title,
|
|
53
|
+
description,
|
|
54
|
+
oneOf: options.map((option) => ({
|
|
55
|
+
const: String(option.value),
|
|
56
|
+
title: option.label,
|
|
57
|
+
})),
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
required: required ? [field] : [],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (!result.accepted) return { selected: false, ...result };
|
|
64
|
+
return {
|
|
65
|
+
selected: true,
|
|
66
|
+
value: result.content[field],
|
|
67
|
+
content: result.content,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async multiSelect({ server, message, field = "selections", title = "Selections", description, options, timeoutMs }) {
|
|
72
|
+
const result = await this.requestForm({
|
|
73
|
+
server,
|
|
74
|
+
message,
|
|
75
|
+
timeoutMs,
|
|
76
|
+
properties: {
|
|
77
|
+
[field]: {
|
|
78
|
+
type: "array",
|
|
79
|
+
title,
|
|
80
|
+
description,
|
|
81
|
+
items: {
|
|
82
|
+
type: "string",
|
|
83
|
+
enum: options.map((option) => String(option.value)),
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
required: [field],
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (!result.accepted) return { selected: false, ...result };
|
|
91
|
+
return {
|
|
92
|
+
selected: true,
|
|
93
|
+
values: Array.isArray(result.content[field]) ? result.content[field] : [],
|
|
94
|
+
content: result.content,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async confirm({ server, message, field = "confirm", title = "Confirm", description, typedConfirmation, timeoutMs }) {
|
|
99
|
+
const properties = {
|
|
100
|
+
[field]: {
|
|
101
|
+
type: "boolean",
|
|
102
|
+
title,
|
|
103
|
+
description,
|
|
104
|
+
default: false,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
const required = [field];
|
|
108
|
+
|
|
109
|
+
if (typedConfirmation) {
|
|
110
|
+
properties.confirmation_text = {
|
|
111
|
+
type: "string",
|
|
112
|
+
title: `Type ${typedConfirmation}`,
|
|
113
|
+
description: `Type ${typedConfirmation} to confirm.`,
|
|
114
|
+
minLength: typedConfirmation.length,
|
|
115
|
+
maxLength: typedConfirmation.length,
|
|
116
|
+
};
|
|
117
|
+
required.push("confirmation_text");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const result = await this.requestForm({ server, message, properties, required, timeoutMs });
|
|
121
|
+
if (!result.accepted) return { confirmed: false, ...result };
|
|
122
|
+
|
|
123
|
+
const typedOk = typedConfirmation
|
|
124
|
+
? result.content.confirmation_text === typedConfirmation
|
|
125
|
+
: true;
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
confirmed: result.content[field] === true && typedOk,
|
|
129
|
+
content: result.content,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async requestMissingFields({ server, toolName, missingFields, partialInput, timeoutMs }) {
|
|
134
|
+
const properties = Object.fromEntries(
|
|
135
|
+
missingFields.map((path) => [path, schemaForField(path)])
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const result = await this.requestForm({
|
|
139
|
+
server,
|
|
140
|
+
timeoutMs,
|
|
141
|
+
message: `Provide the missing fields for ${toolName}. Already supplied values will be preserved.`,
|
|
142
|
+
properties,
|
|
143
|
+
required: missingFields,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (!result.accepted) return { completed: false, ...result };
|
|
147
|
+
return {
|
|
148
|
+
completed: true,
|
|
149
|
+
input: applyFlatPaths(partialInput, result.content),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async clarify({ server, message, field, suggestions, timeoutMs }) {
|
|
154
|
+
return this.singleSelect({
|
|
155
|
+
server,
|
|
156
|
+
message,
|
|
157
|
+
field,
|
|
158
|
+
title: "Clarification",
|
|
159
|
+
description: "Choose one exact suggested value, or cancel and provide a different exact value.",
|
|
160
|
+
options: suggestions.map((suggestion) => ({
|
|
161
|
+
value: suggestion.code || suggestion.value || suggestion.id || suggestion.name,
|
|
162
|
+
label: suggestionLabel(suggestion),
|
|
163
|
+
})),
|
|
164
|
+
timeoutMs,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async selectRateForLabel({ server, shipmentId, rates, confirmDefault = false, timeoutMs = 120000 }) {
|
|
169
|
+
if (!this.supportsForm(server) || !rates.length) {
|
|
170
|
+
return { selected: false, reason: "FORM_ELICITATION_UNAVAILABLE" };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const result = await this.requestForm({
|
|
174
|
+
server,
|
|
175
|
+
timeoutMs,
|
|
176
|
+
message: `Select the rate to buy for shipment ${shipmentId}. This will purchase a shipping label only if you also confirm.`,
|
|
177
|
+
properties: {
|
|
178
|
+
rate_option: {
|
|
179
|
+
type: "string",
|
|
180
|
+
title: "Rate",
|
|
181
|
+
description: "Choose one exact returned shipment rate.",
|
|
182
|
+
oneOf: rates.map((rate, index) => ({
|
|
183
|
+
const: String(index + 1),
|
|
184
|
+
title: `${index + 1}. ${rate.carrier} ${rate.service} - ${rate.rate} ${rate.currency || "USD"}`,
|
|
185
|
+
})),
|
|
186
|
+
},
|
|
187
|
+
confirm: {
|
|
188
|
+
type: "boolean",
|
|
189
|
+
title: "Confirm label purchase",
|
|
190
|
+
description: "Required to buy the selected label.",
|
|
191
|
+
default: confirmDefault,
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
required: ["rate_option", "confirm"],
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (!result.accepted) {
|
|
198
|
+
return { selected: false, reason: result.reason, error: result.error };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const option = Number(result.content.rate_option);
|
|
202
|
+
return {
|
|
203
|
+
selected: Number.isInteger(option),
|
|
204
|
+
rate_option: option,
|
|
205
|
+
confirm: result.content.confirm === true,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function suggestionLabel(suggestion) {
|
|
211
|
+
const primary = suggestion.name || suggestion.code || suggestion.value || suggestion.id;
|
|
212
|
+
const score = suggestion.score !== undefined ? ` (${suggestion.score})` : "";
|
|
213
|
+
return `${primary}${score}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function applyFlatPaths(base, flatValues) {
|
|
217
|
+
const output = structuredClone(base || {});
|
|
218
|
+
for (const [path, value] of Object.entries(flatValues || {})) {
|
|
219
|
+
const parts = path.split(".");
|
|
220
|
+
let target = output;
|
|
221
|
+
for (const part of parts.slice(0, -1)) {
|
|
222
|
+
target[part] = target[part] && typeof target[part] === "object" ? target[part] : {};
|
|
223
|
+
target = target[part];
|
|
224
|
+
}
|
|
225
|
+
target[parts.at(-1)] = value;
|
|
226
|
+
}
|
|
227
|
+
return output;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export { ElicitationService, applyFlatPaths };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
class IdempotencyStore {
|
|
2
|
+
constructor({ ttlMs = 10 * 60 * 1000 } = {}) {
|
|
3
|
+
this.ttlMs = ttlMs;
|
|
4
|
+
this.entries = new Map();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
_purgeExpired() {
|
|
8
|
+
const now = Date.now();
|
|
9
|
+
for (const [key, entry] of this.entries.entries()) {
|
|
10
|
+
if (entry.expiresAt <= now) this.entries.delete(key);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
get(key) {
|
|
15
|
+
if (!key) return null;
|
|
16
|
+
this._purgeExpired();
|
|
17
|
+
const entry = this.entries.get(key);
|
|
18
|
+
return entry?.result || null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
set(key, result) {
|
|
22
|
+
if (!key) return result;
|
|
23
|
+
this._purgeExpired();
|
|
24
|
+
this.entries.set(key, {
|
|
25
|
+
result,
|
|
26
|
+
expiresAt: Date.now() + this.ttlMs,
|
|
27
|
+
});
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async run(key, operation) {
|
|
32
|
+
const existing = this.get(key);
|
|
33
|
+
if (existing) return { reused: true, result: existing };
|
|
34
|
+
const result = await operation();
|
|
35
|
+
this.set(key, result);
|
|
36
|
+
return { reused: false, result };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export { IdempotencyStore };
|
|
@@ -1,35 +1,62 @@
|
|
|
1
1
|
class PickupService {
|
|
2
|
-
constructor(easypostClient) {
|
|
2
|
+
constructor(easypostClient, { confirmations, idempotency } = {}) {
|
|
3
3
|
this.easypost = easypostClient;
|
|
4
|
+
this.confirmations = confirmations;
|
|
5
|
+
this.idempotency = idempotency;
|
|
4
6
|
}
|
|
5
7
|
|
|
6
8
|
async schedulePickup(input, context) {
|
|
7
|
-
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
await this.confirmations?.requireConfirmed(input, {
|
|
10
|
+
action: "schedule_pickup",
|
|
11
|
+
message: `You are about to create a pickup window from ${input.min_datetime} to ${input.max_datetime} for ${input.shipment_ids.length} shipment(s). Proceed?`,
|
|
12
|
+
details: {
|
|
13
|
+
shipment_ids: input.shipment_ids,
|
|
12
14
|
min_datetime: input.min_datetime,
|
|
13
15
|
max_datetime: input.max_datetime,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}),
|
|
17
|
-
context
|
|
18
|
-
);
|
|
16
|
+
},
|
|
17
|
+
}, context);
|
|
19
18
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
const operation = async () => {
|
|
20
|
+
const pickup = await this.easypost.execute(
|
|
21
|
+
"pickup.create",
|
|
22
|
+
(client) => client.Pickup.create({
|
|
23
|
+
shipment: { id: input.shipment_ids[0] },
|
|
24
|
+
address: input.address,
|
|
25
|
+
min_datetime: input.min_datetime,
|
|
26
|
+
max_datetime: input.max_datetime,
|
|
27
|
+
instructions: input.instructions,
|
|
28
|
+
carrier_accounts: input.carrier_accounts,
|
|
29
|
+
}),
|
|
30
|
+
context
|
|
31
|
+
);
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
context.audit?.record("pickup_created", {
|
|
34
|
+
correlationId: context.correlationId,
|
|
35
|
+
pickup_id: pickup.id,
|
|
36
|
+
pickup_rate_count: Array.isArray(pickup.pickup_rates) ? pickup.pickup_rates.length : 0,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
ok: true,
|
|
41
|
+
pickup,
|
|
42
|
+
pickup_rates: pickup.pickup_rates || [],
|
|
43
|
+
next_step: "Review returned pickup_rates. This tool does not auto-buy a pickup rate.",
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
if (!this.idempotency || !input.idempotency_key) return operation();
|
|
48
|
+
const { reused, result } = await this.idempotency.run(`schedule_pickup:${input.idempotency_key}`, operation);
|
|
49
|
+
return reused ? { ...result, idempotent_replay: true } : result;
|
|
30
50
|
}
|
|
31
51
|
|
|
32
52
|
async cancelPickup(input, context) {
|
|
53
|
+
await this.confirmations?.requireConfirmed(input, {
|
|
54
|
+
action: "cancel_pickup",
|
|
55
|
+
message: `You are about to cancel pickup ${input.pickup_id}. Proceed?`,
|
|
56
|
+
details: { pickup_id: input.pickup_id },
|
|
57
|
+
typedConfirmation: "CANCEL",
|
|
58
|
+
}, context);
|
|
59
|
+
|
|
33
60
|
const pickup = await this.easypost.execute(
|
|
34
61
|
"pickup.cancel",
|
|
35
62
|
(client) => client.Pickup.cancel(input.pickup_id),
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { mapShipment, mapRate } from "../adapters/easypost/responseMappers.js";
|
|
2
2
|
|
|
3
3
|
class ReturnService {
|
|
4
|
-
constructor(easypostClient) {
|
|
4
|
+
constructor(easypostClient, { confirmations, elicitation } = {}) {
|
|
5
5
|
this.easypost = easypostClient;
|
|
6
|
+
this.confirmations = confirmations;
|
|
7
|
+
this.elicitation = elicitation;
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
async createReturnLabel(input, context) {
|
|
@@ -18,13 +20,41 @@ class ReturnService {
|
|
|
18
20
|
context
|
|
19
21
|
);
|
|
20
22
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
const rates = shipment.rates || [];
|
|
24
|
+
if (!input.rate_id && !input.rate_option) {
|
|
25
|
+
return {
|
|
26
|
+
ok: true,
|
|
27
|
+
shipment: mapShipment(shipment),
|
|
28
|
+
rates: rates.map((rate, index) => ({ option: index + 1, ...mapRate(rate) })),
|
|
29
|
+
next_step: "Choose a numbered rate_option or exact rate_id to buy the return label. The server will not auto-select a return rate.",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
24
32
|
|
|
25
|
-
const
|
|
26
|
-
?
|
|
27
|
-
:
|
|
33
|
+
const rate = input.rate_id
|
|
34
|
+
? rates.find((candidate) => candidate.id === input.rate_id)
|
|
35
|
+
: rates[input.rate_option - 1];
|
|
36
|
+
|
|
37
|
+
if (!rate) {
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
success: false,
|
|
41
|
+
error_code: "RETURN_RATE_SELECTION_REQUIRED",
|
|
42
|
+
message: "The requested return rate selection is unavailable. Select one returned option.",
|
|
43
|
+
missing_fields: [],
|
|
44
|
+
ambiguous_fields: [],
|
|
45
|
+
available_options: rates.map((candidate, index) => ({ option: index + 1, ...mapRate(candidate) })),
|
|
46
|
+
examples: { rate_option: [1] },
|
|
47
|
+
next_action: "Retry with a valid rate_option or rate_id from available_options.",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await this.confirmations?.requireConfirmed(input, {
|
|
52
|
+
action: "create_return_label",
|
|
53
|
+
message: `You are about to buy a return label using ${rate.carrier} ${rate.service} for ${rate.rate} ${rate.currency || "USD"}. Proceed?`,
|
|
54
|
+
details: { rate: mapRate(rate), shipment_id: shipment.id },
|
|
55
|
+
}, context);
|
|
56
|
+
|
|
57
|
+
const bought = await this.easypost.execute("return.buy", (client) => client.Shipment.buy(shipment.id, rate), context);
|
|
28
58
|
|
|
29
59
|
return { ok: true, shipment: mapShipment(bought), purchased_rate: mapRate(rate) };
|
|
30
60
|
}
|