@alexkroman1/aai-cli 5.3.0 → 5.5.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/README.md +51 -0
- package/dist/{_agent-D8zBb3M5.mjs → _agent-DMyOab9_.mjs} +2 -2
- package/dist/{_bundler-DFS4xxqE.mjs → _bundler-Cjaxa2wi.mjs} +1 -1
- package/dist/{_config-D3F9km8X.mjs → _config-5AEqhh-O.mjs} +2 -2
- package/dist/{_dev-server-BBUWo0sb.mjs → _dev-server-vV05Fnki.mjs} +3 -3
- package/dist/{_init-DU-sXH6S.mjs → _init-BZ9t_Kz-.mjs} +3 -56
- package/dist/{_slug-api-19R5kZ0U.mjs → _slug-api-DaqQJHk8.mjs} +1 -1
- package/dist/_templates-Bv8CR800.mjs +69 -0
- package/dist/_templates.d.ts +6 -0
- package/dist/{build-Xq1xxoSg.mjs → build-D_PgQOD4.mjs} +1 -1
- package/dist/cli.mjs +59 -17
- package/dist/{client-bundler-BWDkUeEP.mjs → client-bundler-yiWoXrgb.mjs} +3 -0
- package/dist/client-bundler.d.ts +9 -0
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-kSXFLxek.mjs → delete-DXilFBb1.mjs} +1 -1
- package/dist/{deploy-Ci0X1gp2.mjs → deploy-1eaXcfUw.mjs} +3 -3
- package/dist/{dev-g7sGt8qX.mjs → dev-gVNdGFYY.mjs} +1 -1
- package/dist/{init-BjeK8crW.mjs → init-DoU4_txp.mjs} +3 -3
- package/dist/login-C59ZHzuO.mjs +109 -0
- package/dist/login.d.ts +34 -0
- package/dist/scaffold/CLAUDE.md +190 -120
- package/dist/scaffold/package.json +3 -3
- package/dist/{secret-DOva9OGk.mjs → secret-CGAIAbUx.mjs} +1 -1
- package/dist/{storage-vM6HjDZZ.mjs → storage-CnhOayhm.mjs} +1 -1
- package/dist/templates/code-interpreter/agent.ts +1 -2
- package/dist/templates/dispatch-center/agent.test.ts +3 -3
- package/dist/templates/dispatch-center/agent.ts +7 -7
- package/dist/templates/dispatch-center/client.tsx +7 -2
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +1 -1
- package/dist/templates/dispatch-center/tools/incident_create.ts +1 -1
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +1 -1
- package/dist/templates/dispatch-center/tools/incident_get.ts +1 -1
- package/dist/templates/dispatch-center/tools/incident_triage.ts +1 -1
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +1 -1
- package/dist/templates/dispatch-center/tools/ops_protocols.ts +1 -1
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +1 -1
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +1 -1
- package/dist/templates/dispatch-center/tools/resources_get_available.ts +1 -1
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +1 -1
- package/dist/templates/embedded-assets/agent.ts +2 -3
- package/dist/templates/health-assistant/agent.ts +3 -4
- package/dist/templates/infocom-adventure/agent.ts +10 -12
- package/dist/templates/infocom-adventure/client.tsx +5 -5
- package/dist/templates/math-buddy/agent.ts +7 -6
- package/dist/templates/night-owl/agent.ts +2 -3
- package/dist/templates/personal-finance/agent.ts +1 -2
- package/dist/templates/pipeline-simple/agent.test.ts +13 -13
- package/dist/templates/pipeline-simple/agent.ts +5 -6
- package/dist/templates/pizza-ordering/agent.test.ts +13 -11
- package/dist/templates/pizza-ordering/agent.ts +5 -6
- package/dist/templates/retail/address.ts +29 -0
- package/dist/templates/retail/agent.test.ts +1030 -0
- package/dist/templates/retail/agent.ts +71 -0
- package/dist/templates/retail/authenticate.ts +34 -0
- package/dist/templates/retail/client.tsx +459 -0
- package/dist/templates/retail/refund.ts +19 -0
- package/dist/templates/retail/registry.test.ts +182 -0
- package/dist/templates/retail/resolve.test.ts +158 -0
- package/dist/templates/retail/resolve.ts +158 -0
- package/dist/templates/retail/seed.json +4260 -0
- package/dist/templates/retail/seed.test.ts +224 -0
- package/dist/templates/retail/shared.test.ts +192 -0
- package/dist/templates/retail/shared.ts +430 -0
- package/dist/templates/retail/store.test.ts +256 -0
- package/dist/templates/retail/store.ts +243 -0
- package/dist/templates/retail/swap.test.ts +186 -0
- package/dist/templates/retail/swap.ts +138 -0
- package/dist/templates/retail/system-prompt.md +111 -0
- package/dist/templates/retail/tools/cancel_pending_order.ts +82 -0
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +83 -0
- package/dist/templates/retail/tools/find_user_id_by_email.ts +32 -0
- package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +38 -0
- package/dist/templates/retail/tools/get_item_details.ts +29 -0
- package/dist/templates/retail/tools/get_order_details.ts +41 -0
- package/dist/templates/retail/tools/get_product_details.ts +33 -0
- package/dist/templates/retail/tools/get_user_details.ts +44 -0
- package/dist/templates/retail/tools/list_all_product_types.ts +24 -0
- package/dist/templates/retail/tools/modify_pending_order_address.ts +46 -0
- package/dist/templates/retail/tools/modify_pending_order_items.ts +102 -0
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +103 -0
- package/dist/templates/retail/tools/modify_user_address.ts +38 -0
- package/dist/templates/retail/tools/return_delivered_order_items.ts +101 -0
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +22 -0
- package/dist/templates/simple/agent.test.ts +18 -0
- package/dist/templates/simple/agent.ts +3 -0
- package/dist/templates/solo-rpg/agent.test.ts +2 -2
- package/dist/templates/solo-rpg/agent.ts +1 -2
- package/dist/templates/solo-rpg/tools/action_roll.ts +1 -1
- package/dist/templates/solo-rpg/tools/load_game.ts +1 -1
- package/dist/templates/solo-rpg/tools/oracle.ts +1 -1
- package/dist/templates/solo-rpg/tools/save_game.ts +1 -1
- package/dist/templates/solo-rpg/tools/setup_character.ts +1 -1
- package/dist/templates/solo-rpg/tools/update_state.ts +1 -1
- package/dist/templates/web-researcher/agent.ts +1 -2
- package/dist/typecheck.d.ts +18 -0
- package/dist/typecheck.mjs +8 -0
- package/dist/worker-bundler.d.ts +9 -1
- package/dist/worker-bundler.mjs +8 -0
- package/package.json +3 -3
|
@@ -0,0 +1,1030 @@
|
|
|
1
|
+
import type { ToolContext } from "@alexkroman1/aai";
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
|
+
import type { AuthResult } from "./authenticate.ts";
|
|
4
|
+
import type { Address } from "./shared.ts";
|
|
5
|
+
import type { ErrorResult } from "./store.ts";
|
|
6
|
+
import { getState, isError } from "./store.ts";
|
|
7
|
+
import { cancelPendingOrder } from "./tools/cancel_pending_order.ts";
|
|
8
|
+
import { exchangeDeliveredOrderItems } from "./tools/exchange_delivered_order_items.ts";
|
|
9
|
+
import { findUserIdByEmail } from "./tools/find_user_id_by_email.ts";
|
|
10
|
+
import { findUserIdByNameZip } from "./tools/find_user_id_by_name_zip.ts";
|
|
11
|
+
import { getItemDetails } from "./tools/get_item_details.ts";
|
|
12
|
+
import { getOrderDetails } from "./tools/get_order_details.ts";
|
|
13
|
+
import { getProductDetails } from "./tools/get_product_details.ts";
|
|
14
|
+
import { getUserDetails } from "./tools/get_user_details.ts";
|
|
15
|
+
import { listAllProductTypes } from "./tools/list_all_product_types.ts";
|
|
16
|
+
import { modifyPendingOrderAddress } from "./tools/modify_pending_order_address.ts";
|
|
17
|
+
import { modifyPendingOrderItems } from "./tools/modify_pending_order_items.ts";
|
|
18
|
+
import { modifyPendingOrderPayment } from "./tools/modify_pending_order_payment.ts";
|
|
19
|
+
import { modifyUserAddress } from "./tools/modify_user_address.ts";
|
|
20
|
+
import { returnDeliveredOrderItems } from "./tools/return_delivered_order_items.ts";
|
|
21
|
+
import { transferToHumanAgents } from "./tools/transfer_to_human_agents.ts";
|
|
22
|
+
|
|
23
|
+
let sessionCounter = 0;
|
|
24
|
+
|
|
25
|
+
function makeCtx(): ToolContext {
|
|
26
|
+
return {
|
|
27
|
+
sessionId: `retail-test-${++sessionCounter}`,
|
|
28
|
+
send: () => {},
|
|
29
|
+
env: {},
|
|
30
|
+
state: {},
|
|
31
|
+
messages: [],
|
|
32
|
+
} as unknown as ToolContext;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A context already authenticated as `userId`, via the real tool. */
|
|
36
|
+
async function authedCtx(email: string): Promise<ToolContext> {
|
|
37
|
+
const ctx = makeCtx();
|
|
38
|
+
// `ToolDef["execute"]`'s public signature always returns `unknown` (the
|
|
39
|
+
// wire type is fixed so any tool is assignable to `ToolDef`, regardless of
|
|
40
|
+
// what its own `execute` body actually returns), so a cast is needed
|
|
41
|
+
// anywhere a test reads a success field back off the result — matching the
|
|
42
|
+
// `as {...}` convention other templates already use for the same reason.
|
|
43
|
+
const result = (await findUserIdByEmail.execute({ email }, ctx)) as AuthResult | ErrorResult;
|
|
44
|
+
if (isError(result)) throw new Error(`fixture failed to authenticate: ${result.error}`);
|
|
45
|
+
return ctx;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe("authentication", () => {
|
|
49
|
+
test("find_user_id_by_email is case-insensitive and authenticates the session", async () => {
|
|
50
|
+
const ctx = makeCtx();
|
|
51
|
+
const result = (await findUserIdByEmail.execute(
|
|
52
|
+
{ email: "OLIVIA.ITO5204@EXAMPLE.COM" },
|
|
53
|
+
ctx,
|
|
54
|
+
)) as AuthResult | ErrorResult;
|
|
55
|
+
expect(isError(result) ? null : result.user_id).toBe("olivia_ito_3591");
|
|
56
|
+
expect(getState(ctx).authenticatedUserId).toBe("olivia_ito_3591");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("an unknown email is refused and leaves the session unauthenticated", async () => {
|
|
60
|
+
const ctx = makeCtx();
|
|
61
|
+
const result = await findUserIdByEmail.execute({ email: "nobody@example.com" }, ctx);
|
|
62
|
+
expect(isError(result)).toBe(true);
|
|
63
|
+
expect(getState(ctx).authenticatedUserId).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("find_user_id_by_name_zip is case-insensitive on names and exact on zip", async () => {
|
|
67
|
+
const ctx = makeCtx();
|
|
68
|
+
const ok = (await findUserIdByNameZip.execute(
|
|
69
|
+
{ first_name: "aarav", last_name: "ANDERSON", zip: "19031" },
|
|
70
|
+
ctx,
|
|
71
|
+
)) as AuthResult | ErrorResult;
|
|
72
|
+
expect(isError(ok) ? null : ok.user_id).toBe("aarav_anderson_8794");
|
|
73
|
+
|
|
74
|
+
const wrongZip = await findUserIdByNameZip.execute(
|
|
75
|
+
{ first_name: "Aarav", last_name: "Anderson", zip: "78268" },
|
|
76
|
+
makeCtx(),
|
|
77
|
+
);
|
|
78
|
+
expect(isError(wrongZip)).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("two customers share a first name — the zip is what separates them", async () => {
|
|
82
|
+
const a = (await findUserIdByNameZip.execute(
|
|
83
|
+
{ first_name: "Aarav", last_name: "Anderson", zip: "19031" },
|
|
84
|
+
makeCtx(),
|
|
85
|
+
)) as AuthResult | ErrorResult;
|
|
86
|
+
const b = (await findUserIdByNameZip.execute(
|
|
87
|
+
{ first_name: "Aarav", last_name: "Gonzalez", zip: "78268" },
|
|
88
|
+
makeCtx(),
|
|
89
|
+
)) as AuthResult | ErrorResult;
|
|
90
|
+
expect(isError(a) ? null : a.user_id).toBe("aarav_anderson_8794");
|
|
91
|
+
expect(isError(b) ? null : b.user_id).toBe("aarav_gonzalez_5113");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("re-authenticating as the same customer is a no-op success", async () => {
|
|
95
|
+
const ctx = await authedCtx("olivia.ito5204@example.com");
|
|
96
|
+
const again = await findUserIdByEmail.execute({ email: "olivia.ito5204@example.com" }, ctx);
|
|
97
|
+
expect(isError(again)).toBe(false);
|
|
98
|
+
expect(getState(ctx).authenticatedUserId).toBe("olivia_ito_3591");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("switching to a DIFFERENT customer mid-conversation is refused", async () => {
|
|
102
|
+
const ctx = await authedCtx("olivia.ito5204@example.com");
|
|
103
|
+
const switched = await findUserIdByEmail.execute(
|
|
104
|
+
{ email: "aarav.anderson9752@example.com" },
|
|
105
|
+
ctx,
|
|
106
|
+
);
|
|
107
|
+
expect(isError(switched) && switched.error.toLowerCase()).toContain("one customer");
|
|
108
|
+
expect(getState(ctx).authenticatedUserId).toBe("olivia_ito_3591");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("switching via name + zip is refused too — both doors, one lock", async () => {
|
|
112
|
+
const ctx = await authedCtx("olivia.ito5204@example.com");
|
|
113
|
+
const switched = await findUserIdByNameZip.execute(
|
|
114
|
+
{ first_name: "Aarav", last_name: "Anderson", zip: "19031" },
|
|
115
|
+
ctx,
|
|
116
|
+
);
|
|
117
|
+
expect(isError(switched)).toBe(true);
|
|
118
|
+
expect(getState(ctx).authenticatedUserId).toBe("olivia_ito_3591");
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
interface UserDetailsResult {
|
|
123
|
+
user_id: string;
|
|
124
|
+
email: string;
|
|
125
|
+
orders: { order_id: string; status: string }[];
|
|
126
|
+
payment_methods: { payment_method_id: string; source: string; balance?: number }[];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
interface OrderDetailsResult {
|
|
130
|
+
order_id: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface ProductDetailsResult {
|
|
134
|
+
name: string;
|
|
135
|
+
variants: { item_id: string }[];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
interface ItemDetailsResult {
|
|
139
|
+
price: number;
|
|
140
|
+
product_name: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
interface ProductTypesResult {
|
|
144
|
+
products: Record<string, string>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
describe("read tools", () => {
|
|
148
|
+
test("get_user_details returns the caller's profile with gift-card balances", async () => {
|
|
149
|
+
const ctx = await authedCtx("olivia.ito5204@example.com");
|
|
150
|
+
const result = (await getUserDetails.execute({ user_id: "olivia_ito_3591" }, ctx)) as
|
|
151
|
+
| UserDetailsResult
|
|
152
|
+
| ErrorResult;
|
|
153
|
+
if (isError(result)) throw new Error(result.error);
|
|
154
|
+
expect(result.email).toBe("olivia.ito5204@example.com");
|
|
155
|
+
expect(result.orders).toHaveLength(5);
|
|
156
|
+
const card = result.payment_methods.find((m) => m.payment_method_id === "gift_card_7794233");
|
|
157
|
+
expect(card?.balance).toBe(56);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("get_user_details refuses a different user id", async () => {
|
|
161
|
+
const ctx = await authedCtx("olivia.ito5204@example.com");
|
|
162
|
+
const result = await getUserDetails.execute({ user_id: "aarav_anderson_8794" }, ctx);
|
|
163
|
+
expect(isError(result)).toBe(true);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("get_order_details resolves shorthand and sets focus", async () => {
|
|
167
|
+
const ctx = await authedCtx("olivia.ito5204@example.com");
|
|
168
|
+
const result = (await getOrderDetails.execute({ order_id: "the delivered one" }, ctx)) as
|
|
169
|
+
| OrderDetailsResult
|
|
170
|
+
| ErrorResult;
|
|
171
|
+
if (isError(result)) throw new Error(result.error);
|
|
172
|
+
expect(result.order_id).toBe("#W5866402");
|
|
173
|
+
expect(getState(ctx).focus.orderId).toBe("#W5866402");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("get_order_details refuses another customer's order", async () => {
|
|
177
|
+
const ctx = await authedCtx("olivia.ito5204@example.com");
|
|
178
|
+
const result = await getOrderDetails.execute({ order_id: "#W4316152" }, ctx);
|
|
179
|
+
expect(isError(result)).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("get_product_details lists variants and needs no authentication", async () => {
|
|
183
|
+
const result = (await getProductDetails.execute({ product_id: "9832717871" }, makeCtx())) as
|
|
184
|
+
| ProductDetailsResult
|
|
185
|
+
| ErrorResult;
|
|
186
|
+
if (isError(result)) throw new Error(result.error);
|
|
187
|
+
expect(result.name).toBe("Tea Kettle");
|
|
188
|
+
expect(result.variants.length).toBeGreaterThan(1);
|
|
189
|
+
expect(result.variants[0]).toHaveProperty("item_id");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("get_product_details rejects an item id passed as a product id, and says so", async () => {
|
|
193
|
+
const result = await getProductDetails.execute({ product_id: "3909406921" }, makeCtx());
|
|
194
|
+
expect(isError(result) && result.error).toContain("item id");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("get_item_details resolves an item without knowing its product", async () => {
|
|
198
|
+
const result = (await getItemDetails.execute({ item_id: "3909406921" }, makeCtx())) as
|
|
199
|
+
| ItemDetailsResult
|
|
200
|
+
| ErrorResult;
|
|
201
|
+
if (isError(result)) throw new Error(result.error);
|
|
202
|
+
expect(result.price).toBe(98.25);
|
|
203
|
+
expect(result.product_name).toBe("Tea Kettle");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("list_all_product_types returns all 50, sorted by name", async () => {
|
|
207
|
+
const result = (await listAllProductTypes.execute({}, makeCtx())) as
|
|
208
|
+
| ProductTypesResult
|
|
209
|
+
| ErrorResult;
|
|
210
|
+
if (isError(result)) throw new Error(result.error);
|
|
211
|
+
expect(Object.keys(result.products)).toHaveLength(50);
|
|
212
|
+
const names = Object.keys(result.products);
|
|
213
|
+
// localeCompare, not the default lexicographic sort — the tool sorts for a
|
|
214
|
+
// human reading the catalog, e.g. "Laptop" before "LED Light Bulb" ('a' < 'E'
|
|
215
|
+
// by locale, but not by UTF-16 code unit).
|
|
216
|
+
expect(names).toEqual([...names].sort((a, b) => a.localeCompare(b)));
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
interface CancelOrderResult {
|
|
221
|
+
order_id: string;
|
|
222
|
+
status: string;
|
|
223
|
+
cancel_reason: string;
|
|
224
|
+
refunded: number;
|
|
225
|
+
refund_immediate: boolean;
|
|
226
|
+
message: string;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
describe("cancel_pending_order", () => {
|
|
230
|
+
test("cancels a pending order and credits a gift card immediately", async () => {
|
|
231
|
+
const ctx = await authedCtx("aarav.anderson9752@example.com");
|
|
232
|
+
const result = (await cancelPendingOrder.execute(
|
|
233
|
+
{ order_id: "#W9300146", reason: "ordered by mistake" },
|
|
234
|
+
ctx,
|
|
235
|
+
)) as CancelOrderResult | ErrorResult;
|
|
236
|
+
if (isError(result)) throw new Error(result.error);
|
|
237
|
+
expect(result.status).toBe("cancelled");
|
|
238
|
+
expect(result.refund_immediate).toBe(true);
|
|
239
|
+
|
|
240
|
+
const state = getState(ctx);
|
|
241
|
+
expect(state.store.orders["#W9300146"]?.status).toBe("cancelled");
|
|
242
|
+
expect(state.store.orders["#W9300146"]?.cancel_reason).toBe("ordered by mistake");
|
|
243
|
+
// $17.00 card + the $153.23 order total.
|
|
244
|
+
const card = state.store.users.aarav_anderson_8794?.payment_methods.gift_card_7245904;
|
|
245
|
+
expect(card?.source === "gift_card" && card.balance).toBe(170.23);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("appends a matching refund to the payment history", async () => {
|
|
249
|
+
const ctx = await authedCtx("aarav.anderson9752@example.com");
|
|
250
|
+
await cancelPendingOrder.execute({ order_id: "#W9300146", reason: "no longer needed" }, ctx);
|
|
251
|
+
const history = getState(ctx).store.orders["#W9300146"]?.payment_history ?? [];
|
|
252
|
+
expect(history).toHaveLength(2);
|
|
253
|
+
expect(history[1]).toMatchObject({
|
|
254
|
+
transaction_type: "refund",
|
|
255
|
+
amount: 153.23,
|
|
256
|
+
payment_method_id: "gift_card_7245904",
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("a non-gift-card refund takes 5-7 days and moves no balance", async () => {
|
|
261
|
+
const ctx = await authedCtx("olivia.ito5204@example.com");
|
|
262
|
+
const result = (await cancelPendingOrder.execute(
|
|
263
|
+
{ order_id: "#W5442520", reason: "no longer needed" },
|
|
264
|
+
ctx,
|
|
265
|
+
)) as CancelOrderResult | ErrorResult;
|
|
266
|
+
if (isError(result)) throw new Error(result.error);
|
|
267
|
+
expect(result.refund_immediate).toBe(false);
|
|
268
|
+
expect(result.message).toContain("5 to 7");
|
|
269
|
+
const card = getState(ctx).store.users.olivia_ito_3591?.payment_methods.gift_card_7794233;
|
|
270
|
+
expect(card?.source === "gift_card" && card.balance).toBe(56);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("refuses a processed order", async () => {
|
|
274
|
+
const ctx = await authedCtx("olivia.ito5204@example.com");
|
|
275
|
+
const result = await cancelPendingOrder.execute(
|
|
276
|
+
{ order_id: "#W5353646", reason: "no longer needed" },
|
|
277
|
+
ctx,
|
|
278
|
+
);
|
|
279
|
+
expect(isError(result) && result.error).toContain("processed");
|
|
280
|
+
expect(getState(ctx).store.orders["#W5353646"]?.status).toBe("processed");
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("refuses a delivered order", async () => {
|
|
284
|
+
const ctx = await authedCtx("olivia.ito5204@example.com");
|
|
285
|
+
const result = await cancelPendingOrder.execute(
|
|
286
|
+
{ order_id: "#W5866402", reason: "no longer needed" },
|
|
287
|
+
ctx,
|
|
288
|
+
);
|
|
289
|
+
expect(isError(result)).toBe(true);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("refuses a reason outside tau2's two accepted values", async () => {
|
|
293
|
+
const ctx = await authedCtx("aarav.anderson9752@example.com");
|
|
294
|
+
// The schema's `reason` field is a two-value enum, so a well-typed caller
|
|
295
|
+
// can never construct this input — this simulates the wire boundary
|
|
296
|
+
// (an LLM tool call is untyped) rather than a call TS would allow.
|
|
297
|
+
const result = await cancelPendingOrder.execute(
|
|
298
|
+
{ order_id: "#W9300146", reason: "changed my mind" as unknown as "no longer needed" },
|
|
299
|
+
ctx,
|
|
300
|
+
);
|
|
301
|
+
expect(isError(result) && result.error).toContain("no longer needed");
|
|
302
|
+
expect(getState(ctx).store.orders["#W9300146"]?.status).toBe("pending");
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("refuses cancelling twice", async () => {
|
|
306
|
+
const ctx = await authedCtx("aarav.anderson9752@example.com");
|
|
307
|
+
await cancelPendingOrder.execute({ order_id: "#W9300146", reason: "no longer needed" }, ctx);
|
|
308
|
+
const second = await cancelPendingOrder.execute(
|
|
309
|
+
{ order_id: "#W9300146", reason: "no longer needed" },
|
|
310
|
+
ctx,
|
|
311
|
+
);
|
|
312
|
+
expect(isError(second)).toBe(true);
|
|
313
|
+
const card = getState(ctx).store.users.aarav_anderson_8794?.payment_methods.gift_card_7245904;
|
|
314
|
+
expect(card?.source === "gift_card" && card.balance).toBe(170.23);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("resolves spoken shorthand to the single pending order", async () => {
|
|
318
|
+
const ctx = await authedCtx("aarav.anderson9752@example.com");
|
|
319
|
+
const result = (await cancelPendingOrder.execute(
|
|
320
|
+
{ order_id: "my pending order", reason: "ordered by mistake" },
|
|
321
|
+
ctx,
|
|
322
|
+
)) as CancelOrderResult | ErrorResult;
|
|
323
|
+
expect(isError(result) ? null : result.order_id).toBe("#W9300146");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("refuses ambiguous shorthand rather than cancelling the wrong order", async () => {
|
|
327
|
+
const ctx = await authedCtx("olivia.ito5204@example.com");
|
|
328
|
+
const result = await cancelPendingOrder.execute(
|
|
329
|
+
{ order_id: "my pending order", reason: "no longer needed" },
|
|
330
|
+
ctx,
|
|
331
|
+
);
|
|
332
|
+
expect(isError(result)).toBe(true);
|
|
333
|
+
const statuses = ["#W5442520", "#W7941031", "#W3657213"].map(
|
|
334
|
+
(id) => getState(ctx).store.orders[id]?.status,
|
|
335
|
+
);
|
|
336
|
+
expect(statuses).toEqual(["pending", "pending", "pending"]);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const NEW_ADDRESS = {
|
|
341
|
+
address1: "742 Evergreen Terrace",
|
|
342
|
+
address2: "Apt 4",
|
|
343
|
+
city: "Springfield",
|
|
344
|
+
state: "OR",
|
|
345
|
+
country: "USA",
|
|
346
|
+
zip: "97477",
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
interface AddressToolResult {
|
|
350
|
+
order_id: string;
|
|
351
|
+
address: Address;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
describe("modify_pending_order_address", () => {
|
|
355
|
+
test("rewrites a pending order's shipping address", async () => {
|
|
356
|
+
const ctx = await authedCtx("aarav.anderson9752@example.com");
|
|
357
|
+
const result = (await modifyPendingOrderAddress.execute(
|
|
358
|
+
{ order_id: "#W9300146", ...NEW_ADDRESS },
|
|
359
|
+
ctx,
|
|
360
|
+
)) as AddressToolResult | ErrorResult;
|
|
361
|
+
if (isError(result)) throw new Error(result.error);
|
|
362
|
+
expect(result.address).toEqual(NEW_ADDRESS);
|
|
363
|
+
expect(getState(ctx).store.orders["#W9300146"]?.address).toEqual(NEW_ADDRESS);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("leaves the customer's default address untouched", async () => {
|
|
367
|
+
const ctx = await authedCtx("aarav.anderson9752@example.com");
|
|
368
|
+
await modifyPendingOrderAddress.execute({ order_id: "#W9300146", ...NEW_ADDRESS }, ctx);
|
|
369
|
+
expect(getState(ctx).store.users.aarav_anderson_8794?.address.zip).toBe("19031");
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("accepts a 'pending (item modified)' order — unlike cancel", async () => {
|
|
373
|
+
const ctx = await authedCtx("aarav.anderson9752@example.com");
|
|
374
|
+
const order = getState(ctx).store.orders["#W9300146"];
|
|
375
|
+
if (!order) throw new Error("fixture missing");
|
|
376
|
+
order.status = "pending (item modified)";
|
|
377
|
+
const result = await modifyPendingOrderAddress.execute(
|
|
378
|
+
{ order_id: "#W9300146", ...NEW_ADDRESS },
|
|
379
|
+
ctx,
|
|
380
|
+
);
|
|
381
|
+
expect(isError(result)).toBe(false);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("refuses a delivered order", async () => {
|
|
385
|
+
const ctx = await authedCtx("olivia.ito5204@example.com");
|
|
386
|
+
const result = await modifyPendingOrderAddress.execute(
|
|
387
|
+
{ order_id: "#W5866402", ...NEW_ADDRESS },
|
|
388
|
+
ctx,
|
|
389
|
+
);
|
|
390
|
+
expect(isError(result)).toBe(true);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test("refuses another customer's order", async () => {
|
|
394
|
+
const ctx = await authedCtx("olivia.ito5204@example.com");
|
|
395
|
+
const result = await modifyPendingOrderAddress.execute(
|
|
396
|
+
{ order_id: "#W9300146", ...NEW_ADDRESS },
|
|
397
|
+
ctx,
|
|
398
|
+
);
|
|
399
|
+
expect(isError(result)).toBe(true);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
describe("modify_user_address", () => {
|
|
404
|
+
test("rewrites the customer's default address", async () => {
|
|
405
|
+
const ctx = await authedCtx("emma.smith3991@example.com");
|
|
406
|
+
const result = await modifyUserAddress.execute(
|
|
407
|
+
{ user_id: "emma_smith_8564", ...NEW_ADDRESS },
|
|
408
|
+
ctx,
|
|
409
|
+
);
|
|
410
|
+
if (isError(result)) throw new Error(result.error);
|
|
411
|
+
expect(getState(ctx).store.users.emma_smith_8564?.address).toEqual(NEW_ADDRESS);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("leaves existing orders' addresses untouched", async () => {
|
|
415
|
+
const ctx = await authedCtx("emma.smith3991@example.com");
|
|
416
|
+
const before = structuredClone(getState(ctx).store.orders["#W2417020"]?.address);
|
|
417
|
+
await modifyUserAddress.execute({ user_id: "emma_smith_8564", ...NEW_ADDRESS }, ctx);
|
|
418
|
+
expect(getState(ctx).store.orders["#W2417020"]?.address).toEqual(before);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test("refuses a different user id", async () => {
|
|
422
|
+
const ctx = await authedCtx("emma.smith3991@example.com");
|
|
423
|
+
const result = await modifyUserAddress.execute(
|
|
424
|
+
{ user_id: "olivia_ito_3591", ...NEW_ADDRESS },
|
|
425
|
+
ctx,
|
|
426
|
+
);
|
|
427
|
+
expect(isError(result)).toBe(true);
|
|
428
|
+
expect(getState(ctx).store.users.olivia_ito_3591?.address.zip).toBe("80218");
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("requires authentication", async () => {
|
|
432
|
+
const result = await modifyUserAddress.execute(
|
|
433
|
+
{ user_id: "emma_smith_8564", ...NEW_ADDRESS },
|
|
434
|
+
makeCtx(),
|
|
435
|
+
);
|
|
436
|
+
expect(isError(result) && result.error).toContain("find_user_id_by_email");
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
interface ModifyItemsResult {
|
|
441
|
+
order_id: string;
|
|
442
|
+
status: string;
|
|
443
|
+
price_difference: number;
|
|
444
|
+
items: { name: string; item_id: string; options: Record<string, string>; price: number }[];
|
|
445
|
+
message: string;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
describe("modify_pending_order_items", () => {
|
|
449
|
+
test("swaps an item, charges the difference to a gift card, and goes terminal", async () => {
|
|
450
|
+
const ctx = await authedCtx("emma.smith3991@example.com");
|
|
451
|
+
const result = (await modifyPendingOrderItems.execute(
|
|
452
|
+
{
|
|
453
|
+
order_id: "#W2417020",
|
|
454
|
+
item_ids: ["8997785118"],
|
|
455
|
+
new_item_ids: ["6017636844"],
|
|
456
|
+
payment_method_id: "gift_card_8541487",
|
|
457
|
+
},
|
|
458
|
+
ctx,
|
|
459
|
+
)) as ModifyItemsResult | ErrorResult;
|
|
460
|
+
if (isError(result)) throw new Error(result.error);
|
|
461
|
+
expect(result.price_difference).toBe(-382.03);
|
|
462
|
+
expect(result.status).toBe("pending (item modified)");
|
|
463
|
+
|
|
464
|
+
const state = getState(ctx);
|
|
465
|
+
const order = state.store.orders["#W2417020"];
|
|
466
|
+
expect(order?.items[0]?.item_id).toBe("6017636844");
|
|
467
|
+
expect(order?.items[0]?.price).toBe(2292.37);
|
|
468
|
+
// $62.00 card refunded $382.03.
|
|
469
|
+
const card = state.store.users.emma_smith_8564?.payment_methods.gift_card_8541487;
|
|
470
|
+
expect(card?.source === "gift_card" && card.balance).toBe(444.03);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test("records the difference as a refund entry when the new item is cheaper", async () => {
|
|
474
|
+
const ctx = await authedCtx("emma.smith3991@example.com");
|
|
475
|
+
await modifyPendingOrderItems.execute(
|
|
476
|
+
{
|
|
477
|
+
order_id: "#W2417020",
|
|
478
|
+
item_ids: ["8997785118"],
|
|
479
|
+
new_item_ids: ["6017636844"],
|
|
480
|
+
payment_method_id: "gift_card_8541487",
|
|
481
|
+
},
|
|
482
|
+
ctx,
|
|
483
|
+
);
|
|
484
|
+
const history = getState(ctx).store.orders["#W2417020"]?.payment_history ?? [];
|
|
485
|
+
expect(history[1]).toMatchObject({
|
|
486
|
+
transaction_type: "refund",
|
|
487
|
+
amount: 382.03,
|
|
488
|
+
payment_method_id: "gift_card_8541487",
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test("refuses a second modification — the status is terminal", async () => {
|
|
493
|
+
const ctx = await authedCtx("emma.smith3991@example.com");
|
|
494
|
+
await modifyPendingOrderItems.execute(
|
|
495
|
+
{
|
|
496
|
+
order_id: "#W2417020",
|
|
497
|
+
item_ids: ["8997785118"],
|
|
498
|
+
new_item_ids: ["6017636844"],
|
|
499
|
+
payment_method_id: "gift_card_8541487",
|
|
500
|
+
},
|
|
501
|
+
ctx,
|
|
502
|
+
);
|
|
503
|
+
const second = await modifyPendingOrderItems.execute(
|
|
504
|
+
{
|
|
505
|
+
order_id: "#W2417020",
|
|
506
|
+
item_ids: ["6017636844"],
|
|
507
|
+
new_item_ids: ["9844888101"],
|
|
508
|
+
payment_method_id: "gift_card_8541487",
|
|
509
|
+
},
|
|
510
|
+
ctx,
|
|
511
|
+
);
|
|
512
|
+
expect(isError(second) && second.error).toContain("pending (item modified)");
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test("a modified order can no longer be cancelled", async () => {
|
|
516
|
+
const ctx = await authedCtx("emma.smith3991@example.com");
|
|
517
|
+
await modifyPendingOrderItems.execute(
|
|
518
|
+
{
|
|
519
|
+
order_id: "#W2417020",
|
|
520
|
+
item_ids: ["8997785118"],
|
|
521
|
+
new_item_ids: ["6017636844"],
|
|
522
|
+
payment_method_id: "gift_card_8541487",
|
|
523
|
+
},
|
|
524
|
+
ctx,
|
|
525
|
+
);
|
|
526
|
+
const cancelled = await cancelPendingOrder.execute(
|
|
527
|
+
{ order_id: "#W2417020", reason: "no longer needed" },
|
|
528
|
+
ctx,
|
|
529
|
+
);
|
|
530
|
+
expect(isError(cancelled)).toBe(true);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test("refuses when the gift card cannot cover the difference, changing nothing", async () => {
|
|
534
|
+
// anya_garcia holds a $51.00 gift card. Upgrading the Laptop in her pending
|
|
535
|
+
// order #W6436609 from 6017636844 ($2292.37) to 9844888101 ($2459.74) is
|
|
536
|
+
// +$167.37 — the NEAREST available upgrade, so the seed trim always keeps
|
|
537
|
+
// it, and it is well past the balance.
|
|
538
|
+
const ctx = await authedCtx("anya.garcia2061@example.com");
|
|
539
|
+
const result = await modifyPendingOrderItems.execute(
|
|
540
|
+
{
|
|
541
|
+
order_id: "#W6436609",
|
|
542
|
+
item_ids: ["6017636844"],
|
|
543
|
+
new_item_ids: ["9844888101"],
|
|
544
|
+
payment_method_id: "gift_card_4374071",
|
|
545
|
+
},
|
|
546
|
+
ctx,
|
|
547
|
+
);
|
|
548
|
+
expect(isError(result) && result.error.toLowerCase()).toContain("balance");
|
|
549
|
+
|
|
550
|
+
const state = getState(ctx);
|
|
551
|
+
const order = state.store.orders["#W6436609"];
|
|
552
|
+
expect(order?.status).toBe("pending");
|
|
553
|
+
expect(order?.items.map((i) => i.item_id)).toContain("6017636844");
|
|
554
|
+
expect(order?.payment_history).toHaveLength(1);
|
|
555
|
+
const card = state.store.users.anya_garcia_3271?.payment_methods.gift_card_4374071;
|
|
556
|
+
expect(card?.source === "gift_card" && card.balance).toBe(51);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
test("the same upgrade succeeds on a payment method with no balance to run out of", async () => {
|
|
560
|
+
const ctx = await authedCtx("anya.garcia2061@example.com");
|
|
561
|
+
const result = (await modifyPendingOrderItems.execute(
|
|
562
|
+
{
|
|
563
|
+
order_id: "#W6436609",
|
|
564
|
+
item_ids: ["6017636844"],
|
|
565
|
+
new_item_ids: ["9844888101"],
|
|
566
|
+
payment_method_id: "credit_card_8955149",
|
|
567
|
+
},
|
|
568
|
+
ctx,
|
|
569
|
+
)) as ModifyItemsResult | ErrorResult;
|
|
570
|
+
if (isError(result)) throw new Error(result.error);
|
|
571
|
+
expect(result.price_difference).toBe(167.37);
|
|
572
|
+
const history = getState(ctx).store.orders["#W6436609"]?.payment_history ?? [];
|
|
573
|
+
expect(history[1]).toMatchObject({
|
|
574
|
+
transaction_type: "payment",
|
|
575
|
+
amount: 167.37,
|
|
576
|
+
payment_method_id: "credit_card_8955149",
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
test("refuses a delivered order", async () => {
|
|
581
|
+
const ctx = await authedCtx("olivia.ito5204@example.com");
|
|
582
|
+
const result = await modifyPendingOrderItems.execute(
|
|
583
|
+
{
|
|
584
|
+
order_id: "#W5866402",
|
|
585
|
+
item_ids: ["6242772310"],
|
|
586
|
+
new_item_ids: ["6200867091"],
|
|
587
|
+
payment_method_id: "gift_card_7794233",
|
|
588
|
+
},
|
|
589
|
+
ctx,
|
|
590
|
+
);
|
|
591
|
+
expect(isError(result)).toBe(true);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test("refuses a payment method belonging to another customer", async () => {
|
|
595
|
+
const ctx = await authedCtx("emma.smith3991@example.com");
|
|
596
|
+
const result = await modifyPendingOrderItems.execute(
|
|
597
|
+
{
|
|
598
|
+
order_id: "#W2417020",
|
|
599
|
+
item_ids: ["8997785118"],
|
|
600
|
+
new_item_ids: ["6017636844"],
|
|
601
|
+
payment_method_id: "gift_card_7245904",
|
|
602
|
+
},
|
|
603
|
+
ctx,
|
|
604
|
+
);
|
|
605
|
+
expect(isError(result)).toBe(true);
|
|
606
|
+
expect(getState(ctx).store.orders["#W2417020"]?.status).toBe("pending");
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
interface ModifyPaymentResult {
|
|
611
|
+
order_id: string;
|
|
612
|
+
status: string;
|
|
613
|
+
amount: number;
|
|
614
|
+
paid_with: string;
|
|
615
|
+
refunded_to: string;
|
|
616
|
+
message: string;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
describe("modify_pending_order_payment", () => {
|
|
620
|
+
test("moves an order to a different method, refunding the old one", async () => {
|
|
621
|
+
const ctx = await authedCtx("olivia.ito5204@example.com");
|
|
622
|
+
// #W5442520 is $663.85 on credit_card_9753331. Pay by PayPal instead.
|
|
623
|
+
const result = (await modifyPendingOrderPayment.execute(
|
|
624
|
+
{ order_id: "#W5442520", payment_method_id: "paypal_8049766" },
|
|
625
|
+
ctx,
|
|
626
|
+
)) as ModifyPaymentResult | ErrorResult;
|
|
627
|
+
if (isError(result)) throw new Error(result.error);
|
|
628
|
+
expect(result.status).toBe("pending");
|
|
629
|
+
|
|
630
|
+
const history = getState(ctx).store.orders["#W5442520"]?.payment_history ?? [];
|
|
631
|
+
expect(history).toHaveLength(3);
|
|
632
|
+
expect(history[1]).toMatchObject({
|
|
633
|
+
transaction_type: "payment",
|
|
634
|
+
amount: 663.85,
|
|
635
|
+
payment_method_id: "paypal_8049766",
|
|
636
|
+
});
|
|
637
|
+
expect(history[2]).toMatchObject({
|
|
638
|
+
transaction_type: "refund",
|
|
639
|
+
amount: 663.85,
|
|
640
|
+
payment_method_id: "credit_card_9753331",
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
test("refunds a gift card that was the original method", async () => {
|
|
645
|
+
const ctx = await authedCtx("aarav.gonzalez9269@example.com");
|
|
646
|
+
// #W9160732 is $1011.54 on gift_card_5979071 ($96.00). Move to PayPal.
|
|
647
|
+
const result = await modifyPendingOrderPayment.execute(
|
|
648
|
+
{ order_id: "#W9160732", payment_method_id: "paypal_6121064" },
|
|
649
|
+
ctx,
|
|
650
|
+
);
|
|
651
|
+
expect(isError(result)).toBe(false);
|
|
652
|
+
const card = getState(ctx).store.users.aarav_gonzalez_5113?.payment_methods.gift_card_5979071;
|
|
653
|
+
expect(card?.source === "gift_card" && card.balance).toBe(1107.54);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
test("refuses a gift card that cannot cover the whole order", async () => {
|
|
657
|
+
const ctx = await authedCtx("aarav.gonzalez9269@example.com");
|
|
658
|
+
// #W6979932 is $1291.82 on PayPal; the gift card holds $96.00.
|
|
659
|
+
const result = await modifyPendingOrderPayment.execute(
|
|
660
|
+
{ order_id: "#W6979932", payment_method_id: "gift_card_5979071" },
|
|
661
|
+
ctx,
|
|
662
|
+
);
|
|
663
|
+
expect(isError(result) && result.error.toLowerCase()).toContain("balance");
|
|
664
|
+
const card = getState(ctx).store.users.aarav_gonzalez_5113?.payment_methods.gift_card_5979071;
|
|
665
|
+
expect(card?.source === "gift_card" && card.balance).toBe(96);
|
|
666
|
+
expect(getState(ctx).store.orders["#W6979932"]?.payment_history).toHaveLength(1);
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
test("refuses the method the order already uses", async () => {
|
|
670
|
+
const ctx = await authedCtx("olivia.ito5204@example.com");
|
|
671
|
+
const result = await modifyPendingOrderPayment.execute(
|
|
672
|
+
{ order_id: "#W5442520", payment_method_id: "credit_card_9753331" },
|
|
673
|
+
ctx,
|
|
674
|
+
);
|
|
675
|
+
expect(isError(result) && result.error.toLowerCase()).toContain("different");
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
test("refuses an order whose payment history is not a single payment", async () => {
|
|
679
|
+
const ctx = await authedCtx("olivia.ito5204@example.com");
|
|
680
|
+
const order = getState(ctx).store.orders["#W5442520"];
|
|
681
|
+
if (!order) throw new Error("fixture missing");
|
|
682
|
+
// No seeded pending order has a second history entry, so construct one —
|
|
683
|
+
// tau2 guards this case and the guard should still be covered.
|
|
684
|
+
order.payment_history.push({
|
|
685
|
+
transaction_type: "refund",
|
|
686
|
+
amount: 10,
|
|
687
|
+
payment_method_id: "credit_card_9753331",
|
|
688
|
+
});
|
|
689
|
+
const result = await modifyPendingOrderPayment.execute(
|
|
690
|
+
{ order_id: "#W5442520", payment_method_id: "paypal_8049766" },
|
|
691
|
+
ctx,
|
|
692
|
+
);
|
|
693
|
+
expect(isError(result) && result.error.toLowerCase()).toContain("exactly one payment");
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
test("refuses a delivered order", async () => {
|
|
697
|
+
const ctx = await authedCtx("olivia.ito5204@example.com");
|
|
698
|
+
const result = await modifyPendingOrderPayment.execute(
|
|
699
|
+
{ order_id: "#W5866402", payment_method_id: "gift_card_7794233" },
|
|
700
|
+
ctx,
|
|
701
|
+
);
|
|
702
|
+
expect(isError(result)).toBe(true);
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
test("refuses a method not on the customer's profile", async () => {
|
|
706
|
+
const ctx = await authedCtx("olivia.ito5204@example.com");
|
|
707
|
+
const result = await modifyPendingOrderPayment.execute(
|
|
708
|
+
{ order_id: "#W5442520", payment_method_id: "gift_card_7245904" },
|
|
709
|
+
ctx,
|
|
710
|
+
);
|
|
711
|
+
expect(isError(result)).toBe(true);
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
interface ReturnResult {
|
|
716
|
+
order_id: string;
|
|
717
|
+
status: string;
|
|
718
|
+
return_items: string[];
|
|
719
|
+
refund_to: string;
|
|
720
|
+
message: string;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
interface ExchangeResult {
|
|
724
|
+
order_id: string;
|
|
725
|
+
status: string;
|
|
726
|
+
price_difference: number;
|
|
727
|
+
exchange_items: string[];
|
|
728
|
+
exchange_new_items: string[];
|
|
729
|
+
message: string;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
describe("return_delivered_order_items", () => {
|
|
733
|
+
test("requests a return refunded to the original method", async () => {
|
|
734
|
+
const ctx = await authedCtx("harper.brown3965@example.com");
|
|
735
|
+
const result = (await returnDeliveredOrderItems.execute(
|
|
736
|
+
{
|
|
737
|
+
order_id: "#W1840144",
|
|
738
|
+
item_ids: ["8590708195"],
|
|
739
|
+
payment_method_id: "paypal_2306935",
|
|
740
|
+
},
|
|
741
|
+
ctx,
|
|
742
|
+
)) as ReturnResult | ErrorResult;
|
|
743
|
+
if (isError(result)) throw new Error(result.error);
|
|
744
|
+
expect(result.status).toBe("return requested");
|
|
745
|
+
const order = getState(ctx).store.orders["#W1840144"];
|
|
746
|
+
expect(order?.return_items).toEqual(["8590708195"]);
|
|
747
|
+
expect(order?.return_payment_method_id).toBe("paypal_2306935");
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
test("accepts a gift card that was NOT the original method", async () => {
|
|
751
|
+
const ctx = await authedCtx("olivia.ito5204@example.com");
|
|
752
|
+
// #W5866402 was paid by PayPal; olivia also holds a gift card.
|
|
753
|
+
const result = await returnDeliveredOrderItems.execute(
|
|
754
|
+
{
|
|
755
|
+
order_id: "#W5866402",
|
|
756
|
+
item_ids: ["9727387530"],
|
|
757
|
+
payment_method_id: "gift_card_7794233",
|
|
758
|
+
},
|
|
759
|
+
ctx,
|
|
760
|
+
);
|
|
761
|
+
expect(isError(result)).toBe(false);
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
test("refuses a non-original, non-gift-card method", async () => {
|
|
765
|
+
const ctx = await authedCtx("harper.brown3965@example.com");
|
|
766
|
+
// Paid by PayPal; harper holds no gift card, so the credit card is illegal.
|
|
767
|
+
const result = await returnDeliveredOrderItems.execute(
|
|
768
|
+
{
|
|
769
|
+
order_id: "#W1840144",
|
|
770
|
+
item_ids: ["8590708195"],
|
|
771
|
+
payment_method_id: "credit_card_3240550",
|
|
772
|
+
},
|
|
773
|
+
ctx,
|
|
774
|
+
);
|
|
775
|
+
expect(isError(result) && result.error.toLowerCase()).toContain("original");
|
|
776
|
+
expect(getState(ctx).store.orders["#W1840144"]?.status).toBe("delivered");
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
test("returns both copies of a duplicate item and sorts the recorded list", async () => {
|
|
780
|
+
const ctx = await authedCtx("harper.brown3965@example.com");
|
|
781
|
+
const result = await returnDeliveredOrderItems.execute(
|
|
782
|
+
{
|
|
783
|
+
order_id: "#W1840144",
|
|
784
|
+
item_ids: ["8590708195", "6534134392", "8590708195"],
|
|
785
|
+
payment_method_id: "paypal_2306935",
|
|
786
|
+
},
|
|
787
|
+
ctx,
|
|
788
|
+
);
|
|
789
|
+
expect(isError(result)).toBe(false);
|
|
790
|
+
expect(getState(ctx).store.orders["#W1840144"]?.return_items).toEqual([
|
|
791
|
+
"6534134392",
|
|
792
|
+
"8590708195",
|
|
793
|
+
"8590708195",
|
|
794
|
+
]);
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
test("refuses more copies than the order holds", async () => {
|
|
798
|
+
const ctx = await authedCtx("harper.brown3965@example.com");
|
|
799
|
+
const result = await returnDeliveredOrderItems.execute(
|
|
800
|
+
{
|
|
801
|
+
order_id: "#W1840144",
|
|
802
|
+
item_ids: ["6534134392", "6534134392"],
|
|
803
|
+
payment_method_id: "paypal_2306935",
|
|
804
|
+
},
|
|
805
|
+
ctx,
|
|
806
|
+
);
|
|
807
|
+
expect(isError(result)).toBe(true);
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
test("refuses a pending order and refuses a second return", async () => {
|
|
811
|
+
const ctx = await authedCtx("harper.brown3965@example.com");
|
|
812
|
+
const pending = await returnDeliveredOrderItems.execute(
|
|
813
|
+
{
|
|
814
|
+
order_id: "#W2273069",
|
|
815
|
+
item_ids: ["3909406921"],
|
|
816
|
+
payment_method_id: "credit_card_3240550",
|
|
817
|
+
},
|
|
818
|
+
ctx,
|
|
819
|
+
);
|
|
820
|
+
expect(isError(pending)).toBe(true);
|
|
821
|
+
|
|
822
|
+
await returnDeliveredOrderItems.execute(
|
|
823
|
+
{ order_id: "#W1840144", item_ids: ["8590708195"], payment_method_id: "paypal_2306935" },
|
|
824
|
+
ctx,
|
|
825
|
+
);
|
|
826
|
+
const again = await returnDeliveredOrderItems.execute(
|
|
827
|
+
{ order_id: "#W1840144", item_ids: ["6534134392"], payment_method_id: "paypal_2306935" },
|
|
828
|
+
ctx,
|
|
829
|
+
);
|
|
830
|
+
expect(isError(again) && again.error).toContain("return requested");
|
|
831
|
+
});
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
describe("exchange_delivered_order_items", () => {
|
|
835
|
+
test("records an exchange with a positive price difference", async () => {
|
|
836
|
+
const ctx = await authedCtx("aarav.anderson9752@example.com");
|
|
837
|
+
const result = (await exchangeDeliveredOrderItems.execute(
|
|
838
|
+
{
|
|
839
|
+
order_id: "#W4316152",
|
|
840
|
+
item_ids: ["7292993796"],
|
|
841
|
+
new_item_ids: ["3909406921"],
|
|
842
|
+
payment_method_id: "gift_card_7245904",
|
|
843
|
+
},
|
|
844
|
+
ctx,
|
|
845
|
+
)) as ExchangeResult | ErrorResult;
|
|
846
|
+
if (isError(result)) throw new Error(result.error);
|
|
847
|
+
expect(result.price_difference).toBe(3.45);
|
|
848
|
+
const order = getState(ctx).store.orders["#W4316152"];
|
|
849
|
+
expect(order?.status).toBe("exchange requested");
|
|
850
|
+
expect(order?.exchange_items).toEqual(["7292993796"]);
|
|
851
|
+
expect(order?.exchange_new_items).toEqual(["3909406921"]);
|
|
852
|
+
expect(order?.exchange_price_difference).toBe(3.45);
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
test("does NOT move the gift-card balance — it is a request, not a settlement", async () => {
|
|
856
|
+
const ctx = await authedCtx("aarav.anderson9752@example.com");
|
|
857
|
+
await exchangeDeliveredOrderItems.execute(
|
|
858
|
+
{
|
|
859
|
+
order_id: "#W4316152",
|
|
860
|
+
item_ids: ["7292993796"],
|
|
861
|
+
new_item_ids: ["3909406921"],
|
|
862
|
+
payment_method_id: "gift_card_7245904",
|
|
863
|
+
},
|
|
864
|
+
ctx,
|
|
865
|
+
);
|
|
866
|
+
const card = getState(ctx).store.users.aarav_anderson_8794?.payment_methods.gift_card_7245904;
|
|
867
|
+
expect(card?.source === "gift_card" && card.balance).toBe(17);
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
test("leaves the order's items unchanged until the exchange is fulfilled", async () => {
|
|
871
|
+
const ctx = await authedCtx("aarav.anderson9752@example.com");
|
|
872
|
+
await exchangeDeliveredOrderItems.execute(
|
|
873
|
+
{
|
|
874
|
+
order_id: "#W4316152",
|
|
875
|
+
item_ids: ["7292993796"],
|
|
876
|
+
new_item_ids: ["3909406921"],
|
|
877
|
+
payment_method_id: "gift_card_7245904",
|
|
878
|
+
},
|
|
879
|
+
ctx,
|
|
880
|
+
);
|
|
881
|
+
expect(getState(ctx).store.orders["#W4316152"]?.items.map((i) => i.item_id)).toEqual([
|
|
882
|
+
"7292993796",
|
|
883
|
+
"7292993796",
|
|
884
|
+
]);
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
test("exchanges both copies of a duplicate and doubles the difference", async () => {
|
|
888
|
+
const ctx = await authedCtx("aarav.anderson9752@example.com");
|
|
889
|
+
const result = (await exchangeDeliveredOrderItems.execute(
|
|
890
|
+
{
|
|
891
|
+
order_id: "#W4316152",
|
|
892
|
+
item_ids: ["7292993796", "7292993796"],
|
|
893
|
+
new_item_ids: ["3909406921", "3909406921"],
|
|
894
|
+
payment_method_id: "gift_card_7245904",
|
|
895
|
+
},
|
|
896
|
+
ctx,
|
|
897
|
+
)) as ExchangeResult | ErrorResult;
|
|
898
|
+
expect(isError(result) ? null : result.price_difference).toBe(6.9);
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
test("records a negative difference as a refund direction", async () => {
|
|
902
|
+
const ctx = await authedCtx("olivia.ito5204@example.com");
|
|
903
|
+
const result = (await exchangeDeliveredOrderItems.execute(
|
|
904
|
+
{
|
|
905
|
+
order_id: "#W5866402",
|
|
906
|
+
item_ids: ["6242772310"],
|
|
907
|
+
new_item_ids: ["6200867091"],
|
|
908
|
+
payment_method_id: "gift_card_7794233",
|
|
909
|
+
},
|
|
910
|
+
ctx,
|
|
911
|
+
)) as ExchangeResult | ErrorResult;
|
|
912
|
+
expect(isError(result) ? null : result.price_difference).toBe(-40.86);
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
test("refuses a gift card that cannot cover the difference", async () => {
|
|
916
|
+
const ctx = await authedCtx("aarav.anderson9752@example.com");
|
|
917
|
+
const result = await exchangeDeliveredOrderItems.execute(
|
|
918
|
+
{
|
|
919
|
+
order_id: "#W9311069",
|
|
920
|
+
item_ids: ["1304426904"],
|
|
921
|
+
new_item_ids: ["4725166838"],
|
|
922
|
+
payment_method_id: "gift_card_7245904",
|
|
923
|
+
},
|
|
924
|
+
ctx,
|
|
925
|
+
);
|
|
926
|
+
expect(isError(result) && result.error.toLowerCase()).toContain("balance");
|
|
927
|
+
expect(getState(ctx).store.orders["#W9311069"]?.status).toBe("delivered");
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
test("refuses a cross-product swap", async () => {
|
|
931
|
+
const ctx = await authedCtx("aarav.anderson9752@example.com");
|
|
932
|
+
const result = await exchangeDeliveredOrderItems.execute(
|
|
933
|
+
{
|
|
934
|
+
order_id: "#W4316152",
|
|
935
|
+
item_ids: ["7292993796"],
|
|
936
|
+
new_item_ids: ["4725166838"],
|
|
937
|
+
payment_method_id: "gift_card_7245904",
|
|
938
|
+
},
|
|
939
|
+
ctx,
|
|
940
|
+
);
|
|
941
|
+
expect(isError(result)).toBe(true);
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
test("refuses an unavailable target", async () => {
|
|
945
|
+
const ctx = await authedCtx("aarav.anderson9752@example.com");
|
|
946
|
+
const result = await exchangeDeliveredOrderItems.execute(
|
|
947
|
+
{
|
|
948
|
+
order_id: "#W4316152",
|
|
949
|
+
item_ids: ["7292993796"],
|
|
950
|
+
new_item_ids: ["6454334990"],
|
|
951
|
+
payment_method_id: "gift_card_7245904",
|
|
952
|
+
},
|
|
953
|
+
ctx,
|
|
954
|
+
);
|
|
955
|
+
expect(isError(result) && result.error.toLowerCase()).toContain("not available");
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
test("refuses a pending order and refuses a second exchange", async () => {
|
|
959
|
+
const ctx = await authedCtx("aarav.anderson9752@example.com");
|
|
960
|
+
const pending = await exchangeDeliveredOrderItems.execute(
|
|
961
|
+
{
|
|
962
|
+
order_id: "#W9300146",
|
|
963
|
+
item_ids: ["9190635437"],
|
|
964
|
+
new_item_ids: ["9190635437"],
|
|
965
|
+
payment_method_id: "gift_card_7245904",
|
|
966
|
+
},
|
|
967
|
+
ctx,
|
|
968
|
+
);
|
|
969
|
+
expect(isError(pending)).toBe(true);
|
|
970
|
+
|
|
971
|
+
await exchangeDeliveredOrderItems.execute(
|
|
972
|
+
{
|
|
973
|
+
order_id: "#W4316152",
|
|
974
|
+
item_ids: ["7292993796"],
|
|
975
|
+
new_item_ids: ["3909406921"],
|
|
976
|
+
payment_method_id: "gift_card_7245904",
|
|
977
|
+
},
|
|
978
|
+
ctx,
|
|
979
|
+
);
|
|
980
|
+
const again = await exchangeDeliveredOrderItems.execute(
|
|
981
|
+
{
|
|
982
|
+
order_id: "#W4316152",
|
|
983
|
+
item_ids: ["7292993796"],
|
|
984
|
+
new_item_ids: ["3738831434"],
|
|
985
|
+
payment_method_id: "gift_card_7245904",
|
|
986
|
+
},
|
|
987
|
+
ctx,
|
|
988
|
+
);
|
|
989
|
+
expect(isError(again)).toBe(true);
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
test("a returned order can no longer be exchanged", async () => {
|
|
993
|
+
const ctx = await authedCtx("aarav.anderson9752@example.com");
|
|
994
|
+
await returnDeliveredOrderItems.execute(
|
|
995
|
+
{
|
|
996
|
+
order_id: "#W4316152",
|
|
997
|
+
item_ids: ["7292993796"],
|
|
998
|
+
payment_method_id: "gift_card_7245904",
|
|
999
|
+
},
|
|
1000
|
+
ctx,
|
|
1001
|
+
);
|
|
1002
|
+
const result = await exchangeDeliveredOrderItems.execute(
|
|
1003
|
+
{
|
|
1004
|
+
order_id: "#W4316152",
|
|
1005
|
+
item_ids: ["7292993796"],
|
|
1006
|
+
new_item_ids: ["3909406921"],
|
|
1007
|
+
payment_method_id: "gift_card_7245904",
|
|
1008
|
+
},
|
|
1009
|
+
ctx,
|
|
1010
|
+
);
|
|
1011
|
+
expect(isError(result)).toBe(true);
|
|
1012
|
+
});
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
interface TransferResult {
|
|
1016
|
+
transferred: boolean;
|
|
1017
|
+
summary: string;
|
|
1018
|
+
message: string;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
describe("transfer_to_human_agents", () => {
|
|
1022
|
+
test("works without authentication — the escape hatch cannot be gated", async () => {
|
|
1023
|
+
const result = (await transferToHumanAgents.execute(
|
|
1024
|
+
{ summary: "Caller wants to dispute a charge from 2019." },
|
|
1025
|
+
makeCtx(),
|
|
1026
|
+
)) as TransferResult | ErrorResult;
|
|
1027
|
+
expect(isError(result)).toBe(false);
|
|
1028
|
+
expect(isError(result) ? null : result.transferred).toBe(true);
|
|
1029
|
+
});
|
|
1030
|
+
});
|