@mohasinac/appkit 4.41.10 → 4.41.12
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.
|
@@ -117,12 +117,12 @@ export const authored = {
|
|
|
117
117
|
startPage: "/admin/orders",
|
|
118
118
|
steps: [
|
|
119
119
|
"Sign in as admin@letitrip.in / TempPass123!.",
|
|
120
|
-
"Open /admin/orders and open the row drawer for order-1-
|
|
120
|
+
"Open /admin/orders and open the row drawer for order-1-20251107-11joon.",
|
|
121
121
|
"Write down the item titles, the shipping address, the payment method and the status.",
|
|
122
122
|
"Close the drawer and use that row's Open full page action.",
|
|
123
123
|
"Read the same four things on the full page.",
|
|
124
124
|
],
|
|
125
|
-
inputs: { orderId: "order-1-
|
|
125
|
+
inputs: { orderId: "order-1-20251107-11joon" },
|
|
126
126
|
expectedBehaviour: "The drawer and the full page are two renderings of one record, so they must agree. Where they differ, one of them is reading a field the other is not — and the full page is the bookmarkable one an admin will send to somebody else.",
|
|
127
127
|
expectedUiState: "Item titles, address, payment method and status are identical in both, and the full page is at a real bookmarkable URL rather than a modal over the list.",
|
|
128
128
|
expectedData: { drawerAndPageAgree: true },
|
|
@@ -42,12 +42,12 @@ export const authored = {
|
|
|
42
42
|
startPage: "/user/orders",
|
|
43
43
|
steps: [
|
|
44
44
|
"Sign in as karthik.new@gmail.com / TempPass123!, a seeded buyer who owns no seeded orders.",
|
|
45
|
-
"Navigate directly to /api/user/orders/order-1-
|
|
45
|
+
"Navigate directly to /api/user/orders/order-1-20251107-11joon/invoice — a seeded order belonging to a different persona.",
|
|
46
46
|
"Read the response.",
|
|
47
|
-
"Now navigate to /user/orders/order-1-
|
|
47
|
+
"Now navigate to /user/orders/order-1-20251107-11joon.",
|
|
48
48
|
"Read that page.",
|
|
49
49
|
],
|
|
50
|
-
inputs: { foreignOrderId: "order-1-
|
|
50
|
+
inputs: { foreignOrderId: "order-1-20251107-11joon" },
|
|
51
51
|
expectedBehaviour: "An order id is short and guessable, so ownership is checked on every hit rather than relied upon to be secret. Answering 403 instead of 404 would confirm that an order with that id exists, which is a membership oracle over the id space.",
|
|
52
52
|
expectedUiState: "Neither the invoice nor the order page shows any of that order's contents. No item titles, no address, no amounts.",
|
|
53
53
|
expectedData: { foreignInvoiceServed: false },
|
|
@@ -74,14 +74,14 @@ export const authored = {
|
|
|
74
74
|
startPage: "/user/orders",
|
|
75
75
|
steps: [
|
|
76
76
|
"Sign in as vivaan.kapoor@gmail.com / TempPass123! — this persona owns the seeded DELIVERED orders, and a return can only be requested on one.",
|
|
77
|
-
"Open /user/orders and open order-1-
|
|
77
|
+
"Open /user/orders and open order-1-20251107-11joon, which is delivered.",
|
|
78
78
|
"Use the Return action.",
|
|
79
79
|
"Give the reason: item not as described.",
|
|
80
80
|
"Submit the request.",
|
|
81
81
|
"Reload the order.",
|
|
82
82
|
"Open /user/returns and look for it.",
|
|
83
83
|
],
|
|
84
|
-
inputs: { orderId: "order-1-
|
|
84
|
+
inputs: { orderId: "order-1-20251107-11joon", reason: "item not as described" },
|
|
85
85
|
expectedBehaviour: "A return request moves the order into Return Requested and appears on the returns surface. That status is deliberately an ACTIVE one — it is the seller's next action item, not a closed state — so the order must not disappear from the active list.",
|
|
86
86
|
expectedUiState: "The order's status reads Return Requested, and the same order is listed under /user/returns.",
|
|
87
87
|
expectedData: { statusAfterReturn: "return_requested" },
|
|
@@ -92,12 +92,12 @@ export const authored = {
|
|
|
92
92
|
startPage: "/user/orders",
|
|
93
93
|
steps: [
|
|
94
94
|
"Sign in as rehan.sheikh@gmail.com / TempPass123! — this persona owns the seeded SHIPPED order.",
|
|
95
|
-
"Open /user/orders and open order-1-
|
|
95
|
+
"Open /user/orders and open order-1-20251104-aevnlw.",
|
|
96
96
|
"Use the Track action.",
|
|
97
97
|
"Read every step on the timeline and note which carry a date and which do not.",
|
|
98
98
|
"Compare the dates shown against the order's own placed and shipped dates.",
|
|
99
99
|
],
|
|
100
|
-
inputs: { orderId: "order-1-
|
|
100
|
+
inputs: { orderId: "order-1-20251104-aevnlw" },
|
|
101
101
|
expectedBehaviour: "Each timeline step shows the date actually recorded for it. A step with no recorded date renders an em-dash rather than a guess — an invented or estimated date is worse than a blank, because it cannot be told apart from a real one.",
|
|
102
102
|
expectedUiState: "The placed and shipped steps carry dates matching the order. Steps the order has not reached show no date rather than today's.",
|
|
103
103
|
expectedData: { fabricatedDatesShown: false },
|
|
@@ -108,14 +108,14 @@ export const authored = {
|
|
|
108
108
|
startPage: "/user/orders",
|
|
109
109
|
steps: [
|
|
110
110
|
"Sign in as karthik.new@gmail.com / TempPass123!, a seeded buyer who owns no seeded orders.",
|
|
111
|
-
"Navigate to /user/orders/order-1-
|
|
112
|
-
"Navigate to /user/orders/order-1-
|
|
113
|
-
"Navigate to /user/orders/order-1-
|
|
114
|
-
"Navigate to /user/orders/order-1-
|
|
115
|
-
"Navigate to /user/orders/order-1-
|
|
111
|
+
"Navigate to /user/orders/order-1-20251107-11joon/track.",
|
|
112
|
+
"Navigate to /user/orders/order-1-20251107-11joon/invoice.",
|
|
113
|
+
"Navigate to /user/orders/order-1-20251107-11joon/return.",
|
|
114
|
+
"Navigate to /user/orders/order-1-20251107-11joon/cancel.",
|
|
115
|
+
"Navigate to /user/orders/order-1-20251107-11joon/payment.",
|
|
116
116
|
"Read each page in turn.",
|
|
117
117
|
],
|
|
118
|
-
inputs: { foreignOrderId: "order-1-
|
|
118
|
+
inputs: { foreignOrderId: "order-1-20251107-11joon" },
|
|
119
119
|
expectedBehaviour: "Ownership is enforced on EVERY subroute, not only on the order page that links to them. A subroute that checks nothing is reachable by typing a URL, and these five carry the address, the amounts and the payment details.",
|
|
120
120
|
expectedUiState: "All five show a not-found page. None shows an item title, an address, an amount or a tracking number belonging to that order.",
|
|
121
121
|
expectedData: { subroutesLeakingData: 0 },
|
|
@@ -126,11 +126,11 @@ export const authored = {
|
|
|
126
126
|
startPage: "/user/orders",
|
|
127
127
|
steps: [
|
|
128
128
|
"Open a private window with no session.",
|
|
129
|
-
"Navigate to /user/orders/order-1-
|
|
129
|
+
"Navigate to /user/orders/order-1-20251107-11joon/track.",
|
|
130
130
|
"Read the page.",
|
|
131
131
|
"Navigate to /user/orders and read that page too.",
|
|
132
132
|
],
|
|
133
|
-
inputs: { foreignOrderId: "order-1-
|
|
133
|
+
inputs: { foreignOrderId: "order-1-20251107-11joon" },
|
|
134
134
|
expectedBehaviour: "A signed-out visitor is sent to sign in. Being blocked IS the expected result here — the failure to look for is the page rendering its content for a moment before redirecting, which is enough to read and enough for a crawler to capture.",
|
|
135
135
|
expectedUiState: "The sign-in page, or the orders page in its signed-out state. At no point does any order content appear, not even briefly.",
|
|
136
136
|
expectedData: { orderContentVisibleToGuest: false },
|
|
@@ -40,6 +40,33 @@ import { withOrderSearchTxt } from "./_helpers/search-txt-wrappers";
|
|
|
40
40
|
import { seedPhoto } from "./_helpers/media";
|
|
41
41
|
const NOW = new Date();
|
|
42
42
|
const daysAgo = (n) => new Date(NOW.getTime() - n * 86400000);
|
|
43
|
+
/**
|
|
44
|
+
* The anchor the order IDs are built from — fixed, never `NOW`.
|
|
45
|
+
*
|
|
46
|
+
* 🛑 An ID may not contain a value that moves. `generateOrderId` embeds a
|
|
47
|
+
* `yyyymmdd` derived from its anchor, so with `NOW` every order's id changed on
|
|
48
|
+
* every calendar day: `appkit-seed load` CREATED 50 fresh documents instead of
|
|
49
|
+
* upserting the same 50, and `status`/`delete` computed ids that had never been
|
|
50
|
+
* written.
|
|
51
|
+
*
|
|
52
|
+
* Measured immediately after a successful `load --collections orders` that
|
|
53
|
+
* reported "created 50, errors 0": `appkit-seed status` answered
|
|
54
|
+
* **`orders 50 seed / 0 in db`**. Both numbers were right; they were about
|
|
55
|
+
* different sets of ids.
|
|
56
|
+
*
|
|
57
|
+
* This is exactly the case Recurrent Root Cause #25 names — "`Date.now()` used
|
|
58
|
+
* as an ID component (as opposed to just a display timestamp)". The
|
|
59
|
+
* distinction is the whole fix:
|
|
60
|
+
*
|
|
61
|
+
* - **IDs** anchor to `SEED_EPOCH`, so they are reproducible forever.
|
|
62
|
+
* - **Display timestamps** (`orderDate`, `createdAt`, shipping dates) keep
|
|
63
|
+
* using `daysAgo()` off `NOW`, so a reseeded catalogue still looks recent
|
|
64
|
+
* and time-bound fixtures re-arm, which is what the tester window needs.
|
|
65
|
+
*
|
|
66
|
+
* It also broke every checklist case that names an order by id: they were
|
|
67
|
+
* authored against one day's ids and could never resolve afterwards.
|
|
68
|
+
*/
|
|
69
|
+
const SEED_EPOCH = new Date("2026-01-01T00:00:00.000Z");
|
|
43
70
|
// Deterministic per-product thumbnail — picsum.photos returns a stable image
|
|
44
71
|
// for any seed string, so this doesn't need to match the real product seed's
|
|
45
72
|
// own image, just be present and consistent across reseeds. Backfills the
|
|
@@ -64,7 +91,9 @@ function seededSuffix(seed) {
|
|
|
64
91
|
return (hash >>> 0).toString(36).padStart(6, "0").slice(0, 6);
|
|
65
92
|
}
|
|
66
93
|
const generateOrderId = (itemCount, daysBack, seed) => {
|
|
67
|
-
|
|
94
|
+
// SEED_EPOCH, not NOW — see the block comment on SEED_EPOCH. An id that
|
|
95
|
+
// moves with the calendar is an id that never upserts.
|
|
96
|
+
const date = new Date(SEED_EPOCH.getTime() - daysBack * 86400000);
|
|
68
97
|
const yyyymmdd = date.toISOString().split("T")[0].replace(/-/g, "");
|
|
69
98
|
const rand = seededSuffix(`${itemCount}-${daysBack}-${seed}`);
|
|
70
99
|
return `order-${itemCount}-${yyyymmdd}-${rand}`;
|
|
@@ -419,11 +448,54 @@ const cashOrderVerified = {
|
|
|
419
448
|
createdAt: daysAgo(4),
|
|
420
449
|
updatedAt: daysAgo(2),
|
|
421
450
|
};
|
|
451
|
+
/*
|
|
452
|
+
* 🛑 DERIVES `items[]` FROM THE FLAT FIELDS WHEN A FIXTURE OMITS IT.
|
|
453
|
+
*
|
|
454
|
+
* `items[]` is the canonical shape and every orders surface reads
|
|
455
|
+
* `items[0].productTitle` for its row label (Root Cause #52). The generator
|
|
456
|
+
* below was taught to emit it — and that fix was NARROWER THAN THE DEFECT:
|
|
457
|
+
* measured after it shipped, 14 of the 50 seeded orders still had none,
|
|
458
|
+
* because the hand-written fixtures never go through the generator.
|
|
459
|
+
*
|
|
460
|
+
* That is Root Cause #84's shape — a fix scoped to the population you happened
|
|
461
|
+
* to be looking at. Patching those 14 by hand would leave the 15th to whoever
|
|
462
|
+
* writes it next, so the derivation lives here instead, on the path EVERY
|
|
463
|
+
* fixture already takes.
|
|
464
|
+
*
|
|
465
|
+
* It only ever fills a gap: a fixture that declares its own `items[]` (the
|
|
466
|
+
* multi-line ones, where the flat fields cannot express two products) is
|
|
467
|
+
* passed through untouched.
|
|
468
|
+
*/
|
|
469
|
+
function withOrderItems(order) {
|
|
470
|
+
if (Array.isArray(order.items) && order.items.length > 0)
|
|
471
|
+
return order;
|
|
472
|
+
if (!order.productId)
|
|
473
|
+
return order;
|
|
474
|
+
const quantity = order.quantity ?? 1;
|
|
475
|
+
const unitPrice = order.unitPrice ?? 0;
|
|
476
|
+
return {
|
|
477
|
+
...order,
|
|
478
|
+
items: [
|
|
479
|
+
{
|
|
480
|
+
productId: order.productId,
|
|
481
|
+
productTitle: order.productTitle ?? order.productId,
|
|
482
|
+
listingType: "standard",
|
|
483
|
+
quantity,
|
|
484
|
+
unitPrice,
|
|
485
|
+
// Prefer the order's own total: on a single-line order it is the
|
|
486
|
+
// authoritative figure, and re-deriving it would silently drop any
|
|
487
|
+
// fixture that deliberately encodes a locked or negotiated price.
|
|
488
|
+
totalPrice: order.totalPrice ?? unitPrice * quantity,
|
|
489
|
+
},
|
|
490
|
+
],
|
|
491
|
+
};
|
|
492
|
+
}
|
|
422
493
|
// Backfills image/imageUrls (see orderItemImage() above) on every fixture
|
|
423
494
|
// regardless of which shape it uses (multi-item `items[]` vs the legacy
|
|
424
495
|
// single-item top-level fields), so no seeded order silently renders without
|
|
425
496
|
// a thumbnail the way real pre-2026-08-20 orders do.
|
|
426
|
-
function withOrderImages(
|
|
497
|
+
function withOrderImages(input) {
|
|
498
|
+
const order = withOrderItems(input);
|
|
427
499
|
return {
|
|
428
500
|
...order,
|
|
429
501
|
imageUrls: order.imageUrls ?? (order.productId ? [orderItemImage(order.productId)] : undefined),
|