@mohasinac/appkit 4.34.0 → 4.34.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/dist/_internal/server/features/seo/sitemap.js +2 -2
- package/dist/_internal/server/functions/scheduled.d.ts +1 -2
- package/dist/_internal/server/functions/scheduled.js +8 -9
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.d.ts +15 -0
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +15 -6
- package/dist/_internal/server/jobs/handlers/index.d.ts +0 -1
- package/dist/_internal/server/jobs/handlers/index.js +0 -1
- package/dist/features/auth/schemas/firestore.d.ts +1 -0
- package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +16 -1
- package/dist/features/tester/repository/tester-checklist-item.repository.js +32 -6
- package/dist/features/tester/seed-data/events-tester-seed-data.js +77 -5
- package/dist/features/tester/seed-data/offers-tester-seed-data.js +23 -9
- package/dist/features/tester/seed-data/products-tester-seed-data.js +65 -49
- package/dist/features/tester/seed-data/tester-window.d.ts +25 -0
- package/dist/features/tester/seed-data/tester-window.js +66 -0
- package/dist/seed/users-seed-data.js +107 -4
- package/package.json +1 -1
|
@@ -16,8 +16,8 @@ const PRODUCT_SLUG = "slug";
|
|
|
16
16
|
const PRODUCT_UPDATED_AT = "updatedAt";
|
|
17
17
|
/**
|
|
18
18
|
* Tester-sandbox fixtures must never reach the public sitemap. They are created
|
|
19
|
-
* and destroyed on a cycle by `testerSandboxCleanup`
|
|
20
|
-
* so anything Google indexes from them 404s shortly after. 34 of 182 sitemap
|
|
19
|
+
* and destroyed on a cycle by `testerSandboxCleanup` and by each tester run's
|
|
20
|
+
* wipe/seed, so anything Google indexes from them 404s shortly after. 34 of 182 sitemap
|
|
21
21
|
* URLs were disposable test fixtures before this filter existed.
|
|
22
22
|
*
|
|
23
23
|
* 🛑 This MUST be an in-memory filter, never a Firestore inequality on the
|
|
@@ -22,9 +22,8 @@ export declare const cleanupRtdbEvents: import("./types").ScheduledFunctionDefin
|
|
|
22
22
|
export declare const mediaTmpCleanup: import("./types").ScheduledFunctionDefinition;
|
|
23
23
|
export declare const draftPrune: import("./types").ScheduledFunctionDefinition;
|
|
24
24
|
export declare const testerSandboxCleanup: import("./types").ScheduledFunctionDefinition;
|
|
25
|
-
export declare const testerSandboxRefresh: import("./types").ScheduledFunctionDefinition;
|
|
26
25
|
export declare const prizeDrawExpiryReveal: import("./types").ScheduledFunctionDefinition;
|
|
27
26
|
export declare const bundleStockSync: import("./types").ScheduledFunctionDefinition;
|
|
28
27
|
export declare const emiInstallmentReminder: import("./types").ScheduledFunctionDefinition;
|
|
29
28
|
export declare const catalogueImageStalenessReminder: import("./types").ScheduledFunctionDefinition;
|
|
30
|
-
export declare const SCHEDULED_FUNCTIONS: readonly [import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition
|
|
29
|
+
export declare const SCHEDULED_FUNCTIONS: readonly [import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Scheduled function definitions (Cloud Scheduler triggers).
|
|
2
2
|
// Cron syntax accepts both Firebase shorthand and standard cron expressions.
|
|
3
|
-
import { auctionSettlementHandler, autoPayoutEligibilityHandler, bundleStockSyncHandler, cartPruneHandler, cleanupRtdbEventsHandler, countersReconcileHandler, couponExpiryHandler, dailyDataCleanupHandler, draftPruneHandler, emiInstallmentReminderHandler, catalogueImageStalenessReminderHandler, mediaTmpCleanupHandler, notificationPruneHandler, offerExpiryHandler, payoutBatchHandler, paymentWindowTimeoutHandler, hardBanReinstatementHandler, paymentReviewAutoApproveHandler, pendingOrderTimeoutHandler, positionsReconcileHandler, prizeDrawExpiryRevealHandler, productStatsSyncHandler, revenueRollupHandler, pageViewRollupHandler, pageViewPruneHandler, dailyStatusDigestHandler, weeklyPayoutEligibilityHandler, testerSandboxCleanupHandler,
|
|
3
|
+
import { auctionSettlementHandler, autoPayoutEligibilityHandler, bundleStockSyncHandler, cartPruneHandler, cleanupRtdbEventsHandler, countersReconcileHandler, couponExpiryHandler, dailyDataCleanupHandler, draftPruneHandler, emiInstallmentReminderHandler, catalogueImageStalenessReminderHandler, mediaTmpCleanupHandler, notificationPruneHandler, offerExpiryHandler, payoutBatchHandler, paymentWindowTimeoutHandler, hardBanReinstatementHandler, paymentReviewAutoApproveHandler, pendingOrderTimeoutHandler, positionsReconcileHandler, prizeDrawExpiryRevealHandler, productStatsSyncHandler, revenueRollupHandler, pageViewRollupHandler, pageViewPruneHandler, dailyStatusDigestHandler, weeklyPayoutEligibilityHandler, testerSandboxCleanupHandler, } from "../jobs/handlers";
|
|
4
4
|
import { defineFunction } from "./define";
|
|
5
5
|
const REGION = "asia-south1";
|
|
6
6
|
const EVERY_5_MIN = "every 5 minutes";
|
|
@@ -182,13 +182,13 @@ export const testerSandboxCleanup = defineFunction({
|
|
|
182
182
|
handler: testerSandboxCleanupHandler,
|
|
183
183
|
options: { region: REGION, timeoutSeconds: 300, memory: "256MiB", maxInstances: 1 },
|
|
184
184
|
});
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
185
|
+
// testerSandboxRefresh (every 4h: revert fixtures to seed shape, prune extras) was
|
|
186
|
+
// REMOVED. The tester runner now owns fixture state — it seeds a known catalog at the
|
|
187
|
+
// start of a run and wipes at the end — so a background job independently rewriting
|
|
188
|
+
// those same documents was redundant and, worse, raced a run in progress: it reset
|
|
189
|
+
// currentBid and recomputed auctionEndDate underneath a test that was mid-assertion.
|
|
190
|
+
// Do not reinstate it without also deciding who owns fixture state; two owners is the
|
|
191
|
+
// bug it caused. See tester/scripts/lifecycle.mjs.
|
|
192
192
|
export const prizeDrawExpiryReveal = defineFunction({
|
|
193
193
|
name: "prizeDrawExpiryReveal",
|
|
194
194
|
description: "Assign winners for outstanding paid orders on scheduled-mode prize draws past their expiry, then close them (every 5 minutes).",
|
|
@@ -242,7 +242,6 @@ export const SCHEDULED_FUNCTIONS = [
|
|
|
242
242
|
mediaTmpCleanup,
|
|
243
243
|
draftPrune,
|
|
244
244
|
testerSandboxCleanup,
|
|
245
|
-
testerSandboxRefresh,
|
|
246
245
|
prizeDrawExpiryReveal,
|
|
247
246
|
bundleStockSync,
|
|
248
247
|
emiInstallmentReminder,
|
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import type { JobContext } from "../runtime/types";
|
|
2
|
+
/**
|
|
3
|
+
* 🛑 THE definition of "sandbox scope" — six collections, and nothing else.
|
|
4
|
+
*
|
|
5
|
+
* Everything OUTSIDE this list is permanent: neither this job nor anything else
|
|
6
|
+
* reclaims it. That is why `users`, `addresses`, `sessions` and `siteSettings`
|
|
7
|
+
* survive a tester run untouched, and why the tester lifecycle's cascade delete
|
|
8
|
+
* (tester/scripts/lib/collections.mjs) mirrors this list rather than inventing
|
|
9
|
+
* its own — a second definition is how the two silently disagree.
|
|
10
|
+
*
|
|
11
|
+
* Adding a collection here makes it disposable. Do not add one without also
|
|
12
|
+
* adding a seed fixture for it, or a run will delete rows nothing recreates.
|
|
13
|
+
*
|
|
14
|
+
* (This used to be shared with testerSandboxRefresh.ts, which was removed when
|
|
15
|
+
* the tester runner took over fixture state — see scheduled.ts.)
|
|
16
|
+
*/
|
|
2
17
|
export declare const SANDBOX_COLLECTIONS: readonly ["categories", "stores", "products", "blogPosts", "events", "offers"];
|
|
3
18
|
export interface TesterSandboxCleanupOptions {
|
|
4
19
|
/** true = wipe every isTestData:true doc regardless of testDataExpiresAt (manual force-purge). */
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import { batchDelete, getTestDataRefs } from "../handlers/_helpers";
|
|
2
2
|
import { BID_FIELDS } from "../../../../constants/field-names";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
/**
|
|
4
|
+
* 🛑 THE definition of "sandbox scope" — six collections, and nothing else.
|
|
5
|
+
*
|
|
6
|
+
* Everything OUTSIDE this list is permanent: neither this job nor anything else
|
|
7
|
+
* reclaims it. That is why `users`, `addresses`, `sessions` and `siteSettings`
|
|
8
|
+
* survive a tester run untouched, and why the tester lifecycle's cascade delete
|
|
9
|
+
* (tester/scripts/lib/collections.mjs) mirrors this list rather than inventing
|
|
10
|
+
* its own — a second definition is how the two silently disagree.
|
|
11
|
+
*
|
|
12
|
+
* Adding a collection here makes it disposable. Do not add one without also
|
|
13
|
+
* adding a seed fixture for it, or a run will delete rows nothing recreates.
|
|
14
|
+
*
|
|
15
|
+
* (This used to be shared with testerSandboxRefresh.ts, which was removed when
|
|
16
|
+
* the tester runner took over fixture state — see scheduled.ts.)
|
|
17
|
+
*/
|
|
9
18
|
export const SANDBOX_COLLECTIONS = ["categories", "stores", "products", "blogPosts", "events", "offers"];
|
|
10
19
|
const BID_CHUNK_SIZE = 30; // Firestore `in` query cap
|
|
11
20
|
export async function runTesterSandboxCleanup(ctx, opts = {}) {
|
|
@@ -54,7 +54,6 @@ export { onScamReportCreateHandler } from "./onScamReportCreate";
|
|
|
54
54
|
export { onScamReportUpdateHandler } from "./onScamReportUpdate";
|
|
55
55
|
export { draftPruneHandler } from "./draftPrune";
|
|
56
56
|
export { testerSandboxCleanupHandler } from "./testerSandboxCleanup";
|
|
57
|
-
export { testerSandboxRefreshHandler } from "./testerSandboxRefresh";
|
|
58
57
|
export { emiInstallmentReminderHandler } from "./emiInstallmentReminder";
|
|
59
58
|
export { onShipmentItemWriteHandler } from "./onShipmentItemWrite";
|
|
60
59
|
export { onShipmentLotWriteHandler, onShipmentHeaderWriteHandler } from "./onShipmentAllocationSync";
|
|
@@ -67,7 +67,6 @@ export { draftPruneHandler } from "./draftPrune";
|
|
|
67
67
|
// Tester program — 7-day auto-expiry sweep for the shared admin-seeded test sandbox
|
|
68
68
|
export { testerSandboxCleanupHandler } from "./testerSandboxCleanup";
|
|
69
69
|
// Tester program — every-4h revert-live-edits + prune-extras refresh (faster than the 7-day sweep above)
|
|
70
|
-
export { testerSandboxRefreshHandler } from "./testerSandboxRefresh";
|
|
71
70
|
// EMI — installment reminder + overdue-flagging sweep
|
|
72
71
|
export { emiInstallmentReminderHandler } from "./emiInstallmentReminder";
|
|
73
72
|
// Procurement Shipments — 3-function recompute cascade (item→lot, lot/header→shipment allocation, cascade-delete)
|
|
@@ -15,9 +15,24 @@ export declare class TesterChecklistItemRepository extends BaseRepository<Tester
|
|
|
15
15
|
/** Reopens a fixed, bug-confirmed case as a new, active version for retest.
|
|
16
16
|
* The old item stays disabled forever with its bug-hunter credit intact. */
|
|
17
17
|
reopenAsNewVersion(oldItemId: string): Promise<TesterChecklistItemDocument>;
|
|
18
|
+
/**
|
|
19
|
+
* Ids of automated (non-human) accounts, excluded from the PUBLIC leaderboard.
|
|
20
|
+
*
|
|
21
|
+
* Read as a bare collection query rather than through `userRepository` on purpose:
|
|
22
|
+
* no repository in this codebase imports another, and this needs a key set, not
|
|
23
|
+
* user documents. `.select()` with no fields returns refs only, so the read is as
|
|
24
|
+
* cheap as Firestore allows, and `isBot == true` is a single-field equality served
|
|
25
|
+
* by the automatic index — no composite index to declare.
|
|
26
|
+
*/
|
|
27
|
+
private botHunterIds;
|
|
18
28
|
/** Single-query, in-memory aggregation of bug credits per hunter — mirrors
|
|
19
29
|
* EventEntryRepository.getLeaderboard()'s shape. Includes old/disabled/
|
|
20
|
-
* superseded items, since bug credit is permanent.
|
|
30
|
+
* superseded items, since bug credit is permanent.
|
|
31
|
+
*
|
|
32
|
+
* Bot accounts are aggregated out (not merely ranked last): the board exists to
|
|
33
|
+
* credit people, and a runner working all 943 cases would otherwise dominate it.
|
|
34
|
+
* The credit still lives on the item, so admin triage and the item's own
|
|
35
|
+
* `bugHunterName` are unaffected. */
|
|
21
36
|
getBugHunterLeaderboard(limit?: number): Promise<BugHunterLeaderboardEntry[]>;
|
|
22
37
|
}
|
|
23
38
|
export declare const testerChecklistItemRepository: TesterChecklistItemRepository;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BaseRepository, parseSieveDateValue } from "../../../providers/db-firebase";
|
|
2
2
|
import { buildSearchTxt } from "../../../utils/search-txt";
|
|
3
3
|
import { DatabaseError } from "../../../errors";
|
|
4
|
+
import { USER_COLLECTION } from "../../auth/schemas/firestore";
|
|
4
5
|
import { TESTER_CHECKLIST_ITEM_COLLECTION, TESTER_CHECKLIST_ITEM_FIELDS, createChecklistItemId, } from "../schemas/firestore";
|
|
5
6
|
export class TesterChecklistItemRepository extends BaseRepository {
|
|
6
7
|
constructor() {
|
|
@@ -86,18 +87,43 @@ export class TesterChecklistItemRepository extends BaseRepository {
|
|
|
86
87
|
await this.update(old.id, { supersededByItemId: newId });
|
|
87
88
|
return newItem;
|
|
88
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Ids of automated (non-human) accounts, excluded from the PUBLIC leaderboard.
|
|
92
|
+
*
|
|
93
|
+
* Read as a bare collection query rather than through `userRepository` on purpose:
|
|
94
|
+
* no repository in this codebase imports another, and this needs a key set, not
|
|
95
|
+
* user documents. `.select()` with no fields returns refs only, so the read is as
|
|
96
|
+
* cheap as Firestore allows, and `isBot == true` is a single-field equality served
|
|
97
|
+
* by the automatic index — no composite index to declare.
|
|
98
|
+
*/
|
|
99
|
+
async botHunterIds() {
|
|
100
|
+
const snapshot = await this.db
|
|
101
|
+
.collection(USER_COLLECTION)
|
|
102
|
+
.where("isBot", "==", true)
|
|
103
|
+
.select()
|
|
104
|
+
.get();
|
|
105
|
+
return new Set(snapshot.docs.map((d) => d.id));
|
|
106
|
+
}
|
|
89
107
|
/** Single-query, in-memory aggregation of bug credits per hunter — mirrors
|
|
90
108
|
* EventEntryRepository.getLeaderboard()'s shape. Includes old/disabled/
|
|
91
|
-
* superseded items, since bug credit is permanent.
|
|
109
|
+
* superseded items, since bug credit is permanent.
|
|
110
|
+
*
|
|
111
|
+
* Bot accounts are aggregated out (not merely ranked last): the board exists to
|
|
112
|
+
* credit people, and a runner working all 943 cases would otherwise dominate it.
|
|
113
|
+
* The credit still lives on the item, so admin triage and the item's own
|
|
114
|
+
* `bugHunterName` are unaffected. */
|
|
92
115
|
async getBugHunterLeaderboard(limit = 50) {
|
|
93
|
-
const snapshot = await
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
116
|
+
const [snapshot, botIds] = await Promise.all([
|
|
117
|
+
this.db
|
|
118
|
+
.collection(this.collection)
|
|
119
|
+
.where(TESTER_CHECKLIST_ITEM_FIELDS.BUG_CONFIRMED, "==", true)
|
|
120
|
+
.get(),
|
|
121
|
+
this.botHunterIds(),
|
|
122
|
+
]);
|
|
97
123
|
const byHunter = new Map();
|
|
98
124
|
for (const doc of snapshot.docs) {
|
|
99
125
|
const item = this.mapDoc(doc);
|
|
100
|
-
if (!item.bugHunterId)
|
|
126
|
+
if (!item.bugHunterId || botIds.has(item.bugHunterId))
|
|
101
127
|
continue;
|
|
102
128
|
const entry = byHunter.get(item.bugHunterId) ?? {
|
|
103
129
|
name: item.bugHunterName ?? "Unknown tester",
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* WHY: Shared tester sandbox —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* WHAT: Exports eventsTesterSeedData —
|
|
2
|
+
* WHY: Shared tester sandbox — disposable test events covering spin-wheel, both raffle
|
|
3
|
+
* variants, a coupon offer, and a lottery with a pre-booked slot, so testers can
|
|
4
|
+
* exercise each flow end to end.
|
|
5
|
+
* WHAT: Exports eventsTesterSeedData — 5 Partial<EventDocument> tagged isTestData:true.
|
|
6
|
+
*
|
|
7
|
+
* The lottery's draw window is expressed via tester-window.ts rather than a fixed
|
|
8
|
+
* duration, so an automated run can shorten it and still watch the window close.
|
|
9
|
+
* The other four stay long-lived on purpose: they are meant to read as ACTIVE
|
|
10
|
+
* events for a human tester who seeds today and returns tomorrow. A fixture that
|
|
11
|
+
* must expire mid-session should be added as its own row, not by re-timing these.
|
|
6
12
|
*
|
|
7
13
|
* EXPORTS:
|
|
8
|
-
* eventsTesterSeedData — Array of
|
|
14
|
+
* eventsTesterSeedData — Array of 5 Partial<EventDocument> for the seed runner
|
|
9
15
|
*
|
|
10
16
|
* @tag domain:events,tester
|
|
11
17
|
* @tag layer:seed
|
|
@@ -17,6 +23,7 @@
|
|
|
17
23
|
import { EVENT_FIELDS } from "../../../constants/field-names";
|
|
18
24
|
import { seedPhoto } from "../../../seed/_helpers/media";
|
|
19
25
|
import { testDataExpiresAt } from "./tester-ttl";
|
|
26
|
+
import { windowMinutes, windowOffset } from "./tester-window";
|
|
20
27
|
const TEST_EVENT_COVER_ALT = "Test event cover";
|
|
21
28
|
const TEST_RAFFLE_PRIZE = "Test prize — not a real reward";
|
|
22
29
|
export const eventsTesterSeedData = [
|
|
@@ -118,4 +125,69 @@ export const eventsTesterSeedData = [
|
|
|
118
125
|
createdAt: new Date(),
|
|
119
126
|
updatedAt: new Date(),
|
|
120
127
|
},
|
|
128
|
+
// ── Lottery, with one slot ALREADY BOOKED ───────────────────────────────────
|
|
129
|
+
// This fixture exists for exactly two checklist cases, both marked 🛑:
|
|
130
|
+
// page-wiring/data-loss/lottery-edit-preserves-bookings
|
|
131
|
+
// page-wiring/data-loss/lottery-booked-slot-cannot-be-deleted
|
|
132
|
+
// Neither could be tested before, because no lottery fixture existed anywhere —
|
|
133
|
+
// lottery events could previously only come from a hand-run seed.
|
|
134
|
+
//
|
|
135
|
+
// 🛑 Slot 3's booking IS the fixture. A booked slot carries a buyer id, a display
|
|
136
|
+
// name and a lottery number, and the bug being guarded silently cleared all three
|
|
137
|
+
// while returning a success message. Do not "tidy" this into an all-unbooked
|
|
138
|
+
// lottery — an unbooked slot cannot demonstrate the loss, and the case would then
|
|
139
|
+
// pass against the very bug it exists to catch.
|
|
140
|
+
//
|
|
141
|
+
// price/weight are server-only (toClientLotterySlot strips them). Uniform pricing
|
|
142
|
+
// means every slot is weight 50 by definition — see computeWeight().
|
|
143
|
+
{
|
|
144
|
+
id: "event-tester-sandbox-lottery",
|
|
145
|
+
slug: "tester-sandbox-lottery",
|
|
146
|
+
type: EVENT_FIELDS.TYPE_VALUES.LOTTERY,
|
|
147
|
+
title: "Tester Sandbox — Lottery",
|
|
148
|
+
description: "Disposable test lottery for the tester QA program. Slot 3 is pre-booked: edit an unrelated slot, save, reopen, and confirm slot 3 is STILL booked to the same buyer with the same lottery number.",
|
|
149
|
+
status: EVENT_FIELDS.STATUS_VALUES.ACTIVE,
|
|
150
|
+
startsAt: new Date(),
|
|
151
|
+
endsAt: windowOffset(1),
|
|
152
|
+
coverImage: {
|
|
153
|
+
type: "image",
|
|
154
|
+
url: seedPhoto("event-cover-tester-sandbox-lottery-20260101", 1200, 600),
|
|
155
|
+
alt: TEST_EVENT_COVER_ALT,
|
|
156
|
+
},
|
|
157
|
+
tags: ["test"],
|
|
158
|
+
lotteryConfig: {
|
|
159
|
+
totalSlots: 6,
|
|
160
|
+
pricingMode: "uniform",
|
|
161
|
+
uniformPrice: 100,
|
|
162
|
+
// Must outlast the run, or late batches meet a closed lottery. windowOffset(1)
|
|
163
|
+
// is the run's end, so the draw window is expressed in the same units.
|
|
164
|
+
drawWindowDurationMinutes: Math.ceil(windowMinutes()),
|
|
165
|
+
maxPullsPerTransaction: 1,
|
|
166
|
+
maxPullsPerUser: 1,
|
|
167
|
+
slots: [
|
|
168
|
+
{ slotNumber: 1, name: "Test Prize — Dragoon Storm", price: 100, weight: 50, isBooked: false, image: seedPhoto("lottery-slot-tester-1-20260101", 600, 600) },
|
|
169
|
+
{ slotNumber: 2, name: "Test Prize — Dranzer S", price: 100, weight: 50, isBooked: false, image: seedPhoto("lottery-slot-tester-2-20260101", 600, 600) },
|
|
170
|
+
{
|
|
171
|
+
slotNumber: 3,
|
|
172
|
+
name: "Test Prize — Draciel Shield",
|
|
173
|
+
price: 100,
|
|
174
|
+
weight: 50,
|
|
175
|
+
isBooked: true,
|
|
176
|
+
bookedByUserId: "user-ash-trainer",
|
|
177
|
+
bookedByDisplayName: "Mock User 4",
|
|
178
|
+
bookedByUserLotteryNumber: 1,
|
|
179
|
+
image: seedPhoto("lottery-slot-tester-3-20260101", 600, 600),
|
|
180
|
+
},
|
|
181
|
+
{ slotNumber: 4, name: "Test Prize — Driger F", price: 100, weight: 50, isBooked: false },
|
|
182
|
+
{ slotNumber: 5, name: "Test Prize — Wolborg", price: 100, weight: 50, isBooked: false },
|
|
183
|
+
{ slotNumber: 6, name: "Test Prize — Trygle", price: 100, weight: 50, isBooked: false },
|
|
184
|
+
],
|
|
185
|
+
},
|
|
186
|
+
stats: { totalEntries: 1, approvedEntries: 1, flaggedEntries: 0 },
|
|
187
|
+
createdBy: "user-admin-letitrip",
|
|
188
|
+
isTestData: true,
|
|
189
|
+
testDataExpiresAt: testDataExpiresAt(),
|
|
190
|
+
createdAt: new Date(),
|
|
191
|
+
updatedAt: new Date(),
|
|
192
|
+
},
|
|
121
193
|
];
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* drive it straight through the Offers checkout lane.
|
|
12
12
|
*
|
|
13
13
|
* Every date is Date.now()-relative (per CLAUDE.md's tester-fixture standards)
|
|
14
|
-
* so
|
|
14
|
+
* so each seed run re-arms it instead of letting it go stale.
|
|
15
15
|
*
|
|
16
16
|
* EXPORTS:
|
|
17
17
|
* offersTesterSeedData — Array of 1 Partial<OfferDocument> for the seed runner
|
|
@@ -25,9 +25,23 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import { OfferStatusValues } from "../../seller/schemas/firestore";
|
|
27
27
|
import { testDataExpiresAt } from "./tester-ttl";
|
|
28
|
+
import { windowOffset } from "./tester-window";
|
|
28
29
|
const NOW = new Date();
|
|
29
30
|
const hoursAgo = (n) => new Date(NOW.getTime() - n * 3600000);
|
|
30
|
-
|
|
31
|
+
/*
|
|
32
|
+
* Deadlines expressed as fractions of the testing window rather than fixed hours.
|
|
33
|
+
*
|
|
34
|
+
* "An accepted offer past its checkoutDeadline cannot be checked out" is a real
|
|
35
|
+
* checklist case, and a fixed 36-hour deadline makes it untestable in any session
|
|
36
|
+
* shorter than a day and a half. Fractions scale with TESTER_WINDOW_MINUTES, so the
|
|
37
|
+
* lapse actually happens while someone is watching.
|
|
38
|
+
*
|
|
39
|
+
* OUTLASTS_RUN is deliberately greater than 1: the "still live" offers must NOT
|
|
40
|
+
* lapse mid-run, or every other offer case starts failing for the wrong reason.
|
|
41
|
+
*/
|
|
42
|
+
const OUTLASTS_RUN = 2;
|
|
43
|
+
const LAPSES_MID_RUN = 1 / 3;
|
|
44
|
+
const CHECKOUT_LAPSES_MID_RUN = 1 / 6;
|
|
31
45
|
export const offersTesterSeedData = [
|
|
32
46
|
{
|
|
33
47
|
id: "offer-tester-sandbox-accepted",
|
|
@@ -52,11 +66,11 @@ export const offersTesterSeedData = [
|
|
|
52
66
|
status: OfferStatusValues.ACCEPTED,
|
|
53
67
|
buyerNote: "Sandbox offer — already accepted so testers can drive checkout.",
|
|
54
68
|
sellerNote: "Accepted for QA sandbox testing.",
|
|
55
|
-
expiresAt:
|
|
69
|
+
expiresAt: windowOffset(OUTLASTS_RUN),
|
|
56
70
|
acceptedAt: hoursAgo(1),
|
|
57
71
|
respondedAt: hoursAgo(1),
|
|
58
72
|
// Well inside the 48h window so the lane is live for a whole test session.
|
|
59
|
-
checkoutDeadline:
|
|
73
|
+
checkoutDeadline: windowOffset(OUTLASTS_RUN),
|
|
60
74
|
createdAt: hoursAgo(2),
|
|
61
75
|
updatedAt: hoursAgo(1),
|
|
62
76
|
isTestData: true,
|
|
@@ -83,7 +97,7 @@ export const offersTesterSeedData = [
|
|
|
83
97
|
status: OfferStatusValues.COUNTERED,
|
|
84
98
|
buyerNote: "Sandbox offer — seller has countered, accept it to test the counter path.",
|
|
85
99
|
sellerNote: "Counter: ₹1,150.",
|
|
86
|
-
expiresAt:
|
|
100
|
+
expiresAt: windowOffset(OUTLASTS_RUN),
|
|
87
101
|
respondedAt: hoursAgo(1),
|
|
88
102
|
createdAt: hoursAgo(2),
|
|
89
103
|
updatedAt: hoursAgo(1),
|
|
@@ -112,10 +126,10 @@ export const offersTesterSeedData = [
|
|
|
112
126
|
status: OfferStatusValues.ACCEPTED,
|
|
113
127
|
buyerNote: "Sandbox offer — accepted with a short checkout window.",
|
|
114
128
|
sellerNote: "Accepted. Pay soon!",
|
|
115
|
-
expiresAt:
|
|
129
|
+
expiresAt: windowOffset(LAPSES_MID_RUN),
|
|
116
130
|
acceptedAt: hoursAgo(0),
|
|
117
131
|
respondedAt: hoursAgo(0),
|
|
118
|
-
checkoutDeadline:
|
|
132
|
+
checkoutDeadline: windowOffset(CHECKOUT_LAPSES_MID_RUN),
|
|
119
133
|
createdAt: hoursAgo(1),
|
|
120
134
|
updatedAt: hoursAgo(0),
|
|
121
135
|
isTestData: true,
|
|
@@ -141,7 +155,7 @@ export const offersTesterSeedData = [
|
|
|
141
155
|
currency: "INR",
|
|
142
156
|
status: OfferStatusValues.PENDING,
|
|
143
157
|
buyerNote: "Sandbox offer — still pending, withdraw it to test that path.",
|
|
144
|
-
expiresAt:
|
|
158
|
+
expiresAt: windowOffset(OUTLASTS_RUN),
|
|
145
159
|
createdAt: hoursAgo(1),
|
|
146
160
|
updatedAt: hoursAgo(1),
|
|
147
161
|
isTestData: true,
|
|
@@ -168,7 +182,7 @@ export const offersTesterSeedData = [
|
|
|
168
182
|
currency: "INR",
|
|
169
183
|
status: OfferStatusValues.PENDING,
|
|
170
184
|
buyerNote: "Sandbox inbound offer — accept, counter, or decline it from your store dashboard.",
|
|
171
|
-
expiresAt:
|
|
185
|
+
expiresAt: windowOffset(OUTLASTS_RUN),
|
|
172
186
|
createdAt: hoursAgo(2),
|
|
173
187
|
updatedAt: hoursAgo(2),
|
|
174
188
|
isTestData: true,
|
|
@@ -5,12 +5,16 @@
|
|
|
5
5
|
* pre-ordering against real code paths — including navigating every entry of the store
|
|
6
6
|
* detail page's listing-type dropdown (see StoreNavTabs). Auto-expires after 7 days
|
|
7
7
|
* (testerSandboxCleanup cascades into any bids on the auction).
|
|
8
|
-
* WHAT: Exports productsTesterSeedData — 2 standard + 4 auction (3 staggered
|
|
9
|
-
*
|
|
8
|
+
* WHAT: Exports productsTesterSeedData — 2 standard + 4 auction (3 staggered "still open"
|
|
9
|
+
* cycling fixtures + 1 already-ended "won" fixture) + 1 pre-order + 1 prize-draw +
|
|
10
10
|
* 1 classified + 1 digital-code + 1 live + 1 art + 1 stickers product + 1 product-group
|
|
11
11
|
* ("Set") parent (product-tester-standard-1/2 are its children, linked via groupId).
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
*
|
|
13
|
+
* Every auction end date is recomputed on each seed run from tester-window.ts, as a
|
|
14
|
+
* FRACTION of the testing window rather than a fixed duration. At the default
|
|
15
|
+
* 180-minute window the staggered three land on the historical 1h/2h/3h; a shorter
|
|
16
|
+
* TESTER_WINDOW_MINUTES pulls all of them in together, which is what lets a run
|
|
17
|
+
* actually watch an auction cross its end time.
|
|
14
18
|
*
|
|
15
19
|
* EXPORTS:
|
|
16
20
|
* productsTesterSeedData — Array of Partial<ProductDocument> for the seed runner
|
|
@@ -26,6 +30,7 @@ import { PRODUCT_FIELDS } from "../../../constants/field-names";
|
|
|
26
30
|
import { buildSearchTxt } from "../../../utils/search-txt";
|
|
27
31
|
import { seedPhoto } from "../../../seed/_helpers/media";
|
|
28
32
|
import { testDataExpiresAt } from "./tester-ttl";
|
|
33
|
+
import { windowOffset } from "./tester-window";
|
|
29
34
|
const TESTER_STORE_ID = "store-tester-sandbox";
|
|
30
35
|
const TESTER_STORE_NAME = "Tester Sandbox Store";
|
|
31
36
|
const TESTER_GROUP_TITLE = "Tester Sandbox Bundle";
|
|
@@ -45,44 +50,56 @@ function withTokens(p) {
|
|
|
45
50
|
searchTxt: buildSearchTxt([p.title, p.description, p.brand, p.brandSlug, p.categoryNames, p.tags]),
|
|
46
51
|
};
|
|
47
52
|
}
|
|
48
|
-
// Staggered, incrementing-id auction fixtures so
|
|
49
|
-
//
|
|
50
|
-
// (
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
})
|
|
53
|
+
// Staggered, incrementing-id auction fixtures so a tester can watch a live auction
|
|
54
|
+
// actually cross its end time inside one session, instead of the old single fixture
|
|
55
|
+
// that always sat 5 days out. A loop (not hand-typed ids) avoids id collisions if
|
|
56
|
+
// the stagger list is ever extended.
|
|
57
|
+
//
|
|
58
|
+
// Expressed as FRACTIONS of the testing window rather than fixed hours: with the
|
|
59
|
+
// default 180-minute window these resolve to the same 1h/2h/3h this fixture has
|
|
60
|
+
// always used, and an automated run shortens all three together by setting
|
|
61
|
+
// TESTER_WINDOW_MINUTES. Hard-coded hours cannot be shortened, which is what made
|
|
62
|
+
// "watch an auction end" untestable in any run shorter than three hours.
|
|
63
|
+
const AUCTION_CYCLE_STAGGER_FRACTIONS = [1 / 3, 2 / 3, 1];
|
|
64
|
+
/** Deliberately past the end of the run — "plenty of clock left" fixtures must NOT expire mid-test. */
|
|
65
|
+
const OUTLASTS_RUN = 2;
|
|
66
|
+
/** ~1/9 of the window: 20 minutes at the default 180, 10 at a 90-minute run. Scales with the window instead of pinning 20 minutes that a short run can never reach. */
|
|
67
|
+
const EXPIRES_MID_RUN = 1 / 9;
|
|
68
|
+
const stagedAuctions = AUCTION_CYCLE_STAGGER_FRACTIONS.map((fraction, i) => {
|
|
69
|
+
const endsAt = windowOffset(fraction);
|
|
70
|
+
const endsInMinutes = Math.max(1, Math.round((endsAt.getTime() - Date.now()) / 60000));
|
|
71
|
+
return withTokens({
|
|
72
|
+
id: `auction-tester-sandbox-cycle-${i + 1}`,
|
|
73
|
+
slug: `auction-tester-sandbox-cycle-${i + 1}`,
|
|
74
|
+
title: `Test Auction — Ends in ~${endsInMinutes}m`,
|
|
75
|
+
description: `Disposable test auction for the tester QA program, staggered to end roughly ${endsInMinutes} minute(s) after seeding so testers can watch the end-of-auction flow live. Place a bid — it auto-expires in 7 days, cascading to any bids.`,
|
|
76
|
+
categorySlugs: COLLECTIBLES_CATEGORY_SLUGS,
|
|
77
|
+
categoryNames: COLLECTIBLES_CATEGORY_NAMES,
|
|
78
|
+
brandSlug: "brand-tester-sandbox",
|
|
79
|
+
brand: "TestBrand",
|
|
80
|
+
startingBid: 15000,
|
|
81
|
+
currentBid: 15000,
|
|
82
|
+
currency: "INR",
|
|
83
|
+
price: 150,
|
|
84
|
+
stockQuantity: 1,
|
|
85
|
+
availableQuantity: 1,
|
|
86
|
+
auctionEndDate: endsAt,
|
|
87
|
+
bidCount: 0,
|
|
88
|
+
bidsHaveStarted: false,
|
|
89
|
+
isSold: false,
|
|
90
|
+
mainImage: seedPhoto(`auction-image-tester-sandbox-cycle-${i + 1}-20260101`, 900, 900),
|
|
91
|
+
images: [seedPhoto(`auction-image-tester-sandbox-cycle-${i + 1}-20260101`, 900, 900)],
|
|
92
|
+
status: PRODUCT_FIELDS.STATUS_VALUES.PUBLISHED,
|
|
93
|
+
condition: PRODUCT_FIELDS.CONDITION_VALUES.NEW,
|
|
94
|
+
listingType: "auction",
|
|
95
|
+
customFields: [],
|
|
96
|
+
customSections: [],
|
|
97
|
+
isPromoted: false,
|
|
98
|
+
isOnSale: false,
|
|
99
|
+
createdAt: new Date(),
|
|
100
|
+
updatedAt: new Date(),
|
|
101
|
+
});
|
|
102
|
+
});
|
|
86
103
|
/**
|
|
87
104
|
* Buy-Now (buyout) fixtures. Four states, because the 2026-08-24 rework made
|
|
88
105
|
* buyout availability depend on `buyNowPrice > currentBid` rather than on
|
|
@@ -90,7 +107,7 @@ const stagedAuctions = AUCTION_CYCLE_STAGGER_HOURS.map((hours, i) => withTokens(
|
|
|
90
107
|
* three of them were previously unreachable as test data.
|
|
91
108
|
*
|
|
92
109
|
* Every date is Date.now()-relative at module import, per CLAUDE.md's tester
|
|
93
|
-
* fixture standard, so
|
|
110
|
+
* fixture standard, so each seed run re-arms them; durations come from tester-window.ts.
|
|
94
111
|
*/
|
|
95
112
|
const buyoutAuctions = [
|
|
96
113
|
{
|
|
@@ -101,7 +118,7 @@ const buyoutAuctions = [
|
|
|
101
118
|
buyNowPrice: 12000,
|
|
102
119
|
bidCount: 0,
|
|
103
120
|
bidsHaveStarted: false,
|
|
104
|
-
|
|
121
|
+
endsAtFraction: OUTLASTS_RUN,
|
|
105
122
|
},
|
|
106
123
|
{
|
|
107
124
|
key: "buyout-contested",
|
|
@@ -111,7 +128,7 @@ const buyoutAuctions = [
|
|
|
111
128
|
buyNowPrice: 12000,
|
|
112
129
|
bidCount: 2,
|
|
113
130
|
bidsHaveStarted: true,
|
|
114
|
-
|
|
131
|
+
endsAtFraction: OUTLASTS_RUN,
|
|
115
132
|
},
|
|
116
133
|
{
|
|
117
134
|
key: "buyout-passed",
|
|
@@ -121,7 +138,7 @@ const buyoutAuctions = [
|
|
|
121
138
|
buyNowPrice: 9000,
|
|
122
139
|
bidCount: 5,
|
|
123
140
|
bidsHaveStarted: true,
|
|
124
|
-
|
|
141
|
+
endsAtFraction: OUTLASTS_RUN,
|
|
125
142
|
},
|
|
126
143
|
{
|
|
127
144
|
key: "buyout-expiring",
|
|
@@ -131,7 +148,7 @@ const buyoutAuctions = [
|
|
|
131
148
|
buyNowPrice: 5000,
|
|
132
149
|
bidCount: 1,
|
|
133
150
|
bidsHaveStarted: true,
|
|
134
|
-
|
|
151
|
+
endsAtFraction: EXPIRES_MID_RUN,
|
|
135
152
|
},
|
|
136
153
|
].map((f) => withTokens({
|
|
137
154
|
id: `auction-tester-sandbox-${f.key}`,
|
|
@@ -149,8 +166,7 @@ const buyoutAuctions = [
|
|
|
149
166
|
price: f.currentBid,
|
|
150
167
|
stockQuantity: 1,
|
|
151
168
|
availableQuantity: 1,
|
|
152
|
-
auctionEndDate:
|
|
153
|
-
(f.endsInMinutes ? f.endsInMinutes * 60 * 1000 : (f.endsInHours ?? 6) * 60 * 60 * 1000)),
|
|
169
|
+
auctionEndDate: windowOffset(f.endsAtFraction),
|
|
154
170
|
bidCount: f.bidCount,
|
|
155
171
|
bidsHaveStarted: f.bidsHaveStarted,
|
|
156
172
|
isSold: false,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Reproduces the historical 1h/2h/3h auction stagger when nothing overrides it. */
|
|
2
|
+
export declare const DEFAULT_TESTER_WINDOW_MINUTES = 180;
|
|
3
|
+
/** Below this nothing is observable — a page load alone can outlast it. */
|
|
4
|
+
export declare const MIN_TESTER_WINDOW_MINUTES = 5;
|
|
5
|
+
/** The sandbox TTL is 7 days; a window longer than that outlives its own fixtures. */
|
|
6
|
+
export declare const MAX_TESTER_WINDOW_MINUTES: number;
|
|
7
|
+
/**
|
|
8
|
+
* Length of the testing window, in minutes.
|
|
9
|
+
*
|
|
10
|
+
* Read at call time rather than captured at module scope so a caller that sets the
|
|
11
|
+
* env var before importing a seed file still wins. Anything unparseable, non-finite
|
|
12
|
+
* or out of range falls back to the default rather than throwing: a malformed env
|
|
13
|
+
* var must not be able to brick `appkit-seed load`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function windowMinutes(): number;
|
|
16
|
+
/**
|
|
17
|
+
* A point `fraction` of the way through the window, measured from now.
|
|
18
|
+
*
|
|
19
|
+
* `windowOffset(0)` is now, `windowOffset(1)` is the window's end. Fractions above 1
|
|
20
|
+
* are allowed and mean "after this run" — that is how a fixture says "still open when
|
|
21
|
+
* testing finishes" without hard-coding a duration.
|
|
22
|
+
*/
|
|
23
|
+
export declare function windowOffset(fraction: number): Date;
|
|
24
|
+
/** The mirror of windowOffset, into the past — for fixtures that must already be closed. */
|
|
25
|
+
export declare function windowAgo(fraction: number): Date;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: Every time-bound tester fixture — auction end dates, coupon validity, event
|
|
3
|
+
* start/end, prize-draw reveal windows, offer expiry, lottery draw windows —
|
|
4
|
+
* must open and close INSIDE one testing session. Hand-written offsets like
|
|
5
|
+
* `Date.now() + 2 * 60 * 60 * 1000` cannot do that: they bake in a duration
|
|
6
|
+
* nobody can shorten, so a case reading "watch the auction actually end" is
|
|
7
|
+
* untestable without sitting there for two hours.
|
|
8
|
+
*
|
|
9
|
+
* This file is the one knob. Fixtures express WHEN as a fraction of the run
|
|
10
|
+
* window; the window's real length comes from TESTER_WINDOW_MINUTES.
|
|
11
|
+
*
|
|
12
|
+
* WHAT: windowMinutes() / windowOffset(fraction) / windowAgo(fraction).
|
|
13
|
+
*
|
|
14
|
+
* 🛑 The default is 180 minutes ON PURPOSE — it reproduces the pre-existing 1h/2h/3h
|
|
15
|
+
* auction stagger exactly (fractions 1/3, 2/3, 1), so an ordinary `appkit-seed load`
|
|
16
|
+
* behaves as it always has. Only the automated runner shortens it. Changing this
|
|
17
|
+
* default silently re-times every fixture for human testers too.
|
|
18
|
+
*
|
|
19
|
+
* @tag domain:tester
|
|
20
|
+
* @tag layer:seed
|
|
21
|
+
* @tag pattern:none
|
|
22
|
+
* @tag access:server-only
|
|
23
|
+
* @tag consumers:seed-data/*-tester-seed-data.ts
|
|
24
|
+
* @tag sideEffects:none
|
|
25
|
+
*/
|
|
26
|
+
/** Reproduces the historical 1h/2h/3h auction stagger when nothing overrides it. */
|
|
27
|
+
export const DEFAULT_TESTER_WINDOW_MINUTES = 180;
|
|
28
|
+
/** Below this nothing is observable — a page load alone can outlast it. */
|
|
29
|
+
export const MIN_TESTER_WINDOW_MINUTES = 5;
|
|
30
|
+
/** The sandbox TTL is 7 days; a window longer than that outlives its own fixtures. */
|
|
31
|
+
export const MAX_TESTER_WINDOW_MINUTES = 7 * 24 * 60;
|
|
32
|
+
/**
|
|
33
|
+
* Length of the testing window, in minutes.
|
|
34
|
+
*
|
|
35
|
+
* Read at call time rather than captured at module scope so a caller that sets the
|
|
36
|
+
* env var before importing a seed file still wins. Anything unparseable, non-finite
|
|
37
|
+
* or out of range falls back to the default rather than throwing: a malformed env
|
|
38
|
+
* var must not be able to brick `appkit-seed load`.
|
|
39
|
+
*/
|
|
40
|
+
export function windowMinutes() {
|
|
41
|
+
const raw = process.env.TESTER_WINDOW_MINUTES;
|
|
42
|
+
if (!raw)
|
|
43
|
+
return DEFAULT_TESTER_WINDOW_MINUTES;
|
|
44
|
+
const parsed = Number(raw);
|
|
45
|
+
if (!Number.isFinite(parsed))
|
|
46
|
+
return DEFAULT_TESTER_WINDOW_MINUTES;
|
|
47
|
+
if (parsed < MIN_TESTER_WINDOW_MINUTES)
|
|
48
|
+
return MIN_TESTER_WINDOW_MINUTES;
|
|
49
|
+
if (parsed > MAX_TESTER_WINDOW_MINUTES)
|
|
50
|
+
return MAX_TESTER_WINDOW_MINUTES;
|
|
51
|
+
return parsed;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A point `fraction` of the way through the window, measured from now.
|
|
55
|
+
*
|
|
56
|
+
* `windowOffset(0)` is now, `windowOffset(1)` is the window's end. Fractions above 1
|
|
57
|
+
* are allowed and mean "after this run" — that is how a fixture says "still open when
|
|
58
|
+
* testing finishes" without hard-coding a duration.
|
|
59
|
+
*/
|
|
60
|
+
export function windowOffset(fraction) {
|
|
61
|
+
return new Date(Date.now() + fraction * windowMinutes() * 60000);
|
|
62
|
+
}
|
|
63
|
+
/** The mirror of windowOffset, into the past — for fixtures that must already be closed. */
|
|
64
|
+
export function windowAgo(fraction) {
|
|
65
|
+
return windowOffset(-fraction);
|
|
66
|
+
}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* WHY: Provides
|
|
3
|
-
* WHAT: Exports
|
|
4
|
-
* is also the dedicated isTester QA account), 15 buyers
|
|
2
|
+
* WHY: Provides 21 seed user accounts covering all marketplace roles and demographics.
|
|
3
|
+
* WHAT: Exports 21 UserDocument partials — 1 admin, 2 sellers (store owners, one of which
|
|
4
|
+
* is also the dedicated isTester QA account), 15 buyers, 1 automated tester
|
|
5
|
+
* (`user-claude-tester`, isBot), and 2 employees on contrasting permission presets.
|
|
5
6
|
* All passwords are TempPass123! in the Firebase Auth emulator. UIDs match slugs.
|
|
6
7
|
*
|
|
8
|
+
* 🛑 seed-cli sets that password ONLY when it creates the Auth record. A re-seed
|
|
9
|
+
* never resets an existing password, so an account whose password was changed
|
|
10
|
+
* stays changed and must be reset explicitly.
|
|
11
|
+
*
|
|
7
12
|
* EXPORTS:
|
|
8
|
-
* usersSeedData — array of
|
|
13
|
+
* usersSeedData — array of 21 Partial<UserDocument> objects for the seed runner
|
|
9
14
|
*
|
|
10
15
|
* @tag domain:users,auth
|
|
11
16
|
* @tag layer:seed
|
|
@@ -15,6 +20,7 @@
|
|
|
15
20
|
* @tag sideEffects:none
|
|
16
21
|
*/
|
|
17
22
|
import { getDefaultPhonePrefix } from "./seed-market-config";
|
|
23
|
+
import { PERMISSION_GROUPS } from "../features/auth/permissions/constants";
|
|
18
24
|
import { USER_FIELDS } from "../constants/field-names";
|
|
19
25
|
import { seedPhoto } from "./_helpers/media";
|
|
20
26
|
const _ph = getDefaultPhonePrefix();
|
|
@@ -522,4 +528,101 @@ export const usersSeedData = [
|
|
|
522
528
|
createdAt: daysAgo(1),
|
|
523
529
|
updatedAt: daysAgo(1),
|
|
524
530
|
},
|
|
531
|
+
// ── Automated tester (the Claude-driven checklist runner answers as this account) ──
|
|
532
|
+
// `canTestAdmin` is REQUIRED, not decorative: 116 checklist cases are adminOnly and
|
|
533
|
+
// GET/PUT /api/user/tester-checklist 404s them for a caller without it, so the runner
|
|
534
|
+
// would silently see a shorter catalog than a human tester does.
|
|
535
|
+
// `isBot` keeps it off the public Bug Hunters leaderboard — see getBugHunterLeaderboard().
|
|
536
|
+
{
|
|
537
|
+
uid: "user-claude-tester",
|
|
538
|
+
email: "claude-tester@letitrip.in",
|
|
539
|
+
phoneNumber: `${_ph}9999900019`,
|
|
540
|
+
phoneVerified: true,
|
|
541
|
+
displayName: "Claude (automated)",
|
|
542
|
+
photoURL: seedPhoto("user-avatar-claude-tester-20260101", 400, 400),
|
|
543
|
+
role: USER_FIELDS.ROLE_VALUES.SELLER,
|
|
544
|
+
isTester: true,
|
|
545
|
+
canTestAdmin: true,
|
|
546
|
+
isBot: true,
|
|
547
|
+
emailVerified: true,
|
|
548
|
+
disabled: false,
|
|
549
|
+
storeId: "store-tester-qa-seller",
|
|
550
|
+
storeSlug: "store-tester-qa-seller",
|
|
551
|
+
storeStatus: "approved",
|
|
552
|
+
publicProfile: {
|
|
553
|
+
isPublic: false,
|
|
554
|
+
showEmail: false,
|
|
555
|
+
showPhone: false,
|
|
556
|
+
showOrders: false,
|
|
557
|
+
showWishlist: false,
|
|
558
|
+
bio: "Automated tester. Works the tester checklist against a freshly seeded catalog and records a verdict per case.",
|
|
559
|
+
location: "Bengaluru, Karnataka",
|
|
560
|
+
},
|
|
561
|
+
stats: { totalOrders: 0, auctionsWon: 0, itemsSold: 0, reviewsCount: 0 },
|
|
562
|
+
metadata: { lastSignInTime: daysAgo(1), creationTime: daysAgo(1).toISOString(), loginCount: 1 },
|
|
563
|
+
createdAt: daysAgo(1),
|
|
564
|
+
updatedAt: daysAgo(1),
|
|
565
|
+
},
|
|
566
|
+
// ── Employees: two contrasting permission presets ───────────────────────────
|
|
567
|
+
// These exist so the sidebar-scoping checklist cases can be tested AT ALL. Before
|
|
568
|
+
// this the seed had no `role: "employee"` account anywhere, so the case the
|
|
569
|
+
// checklist itself marks "🛑 THE BIG ONE" — an employee must see only what their
|
|
570
|
+
// permissions allow, not all 91 admin entries — had nothing to sign in as.
|
|
571
|
+
//
|
|
572
|
+
// `permissions` is the load-bearing field (it is what the RBAC checks read);
|
|
573
|
+
// `permissionGroup` is display-only. Both are set from the same preset so they
|
|
574
|
+
// cannot disagree. Two presets on purpose: one narrow, one broad — a scoping bug
|
|
575
|
+
// that happens to produce the right count for one group is caught by the other.
|
|
576
|
+
{
|
|
577
|
+
uid: "user-employee-blog",
|
|
578
|
+
email: "employee-blog@letitrip.in",
|
|
579
|
+
phoneNumber: `${_ph}9999900020`,
|
|
580
|
+
phoneVerified: true,
|
|
581
|
+
displayName: "Mock Employee 1",
|
|
582
|
+
photoURL: seedPhoto("user-avatar-employee-blog-20260101", 400, 400),
|
|
583
|
+
role: USER_FIELDS.ROLE_VALUES.EMPLOYEE,
|
|
584
|
+
permissionGroup: "blog_poster",
|
|
585
|
+
permissions: [...PERMISSION_GROUPS.blog_poster],
|
|
586
|
+
emailVerified: true,
|
|
587
|
+
disabled: false,
|
|
588
|
+
publicProfile: {
|
|
589
|
+
isPublic: false,
|
|
590
|
+
showEmail: false,
|
|
591
|
+
showPhone: false,
|
|
592
|
+
showOrders: false,
|
|
593
|
+
showWishlist: false,
|
|
594
|
+
bio: "Content employee — blog and media only.",
|
|
595
|
+
location: "Pune, Maharashtra",
|
|
596
|
+
},
|
|
597
|
+
stats: { totalOrders: 0, auctionsWon: 0, itemsSold: 0, reviewsCount: 0 },
|
|
598
|
+
metadata: { lastSignInTime: daysAgo(2), creationTime: daysAgo(30).toISOString(), loginCount: 12 },
|
|
599
|
+
createdAt: daysAgo(30),
|
|
600
|
+
updatedAt: daysAgo(2),
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
uid: "user-employee-trust",
|
|
604
|
+
email: "employee-trust@letitrip.in",
|
|
605
|
+
phoneNumber: `${_ph}9999900021`,
|
|
606
|
+
phoneVerified: true,
|
|
607
|
+
displayName: "Mock Employee 2",
|
|
608
|
+
photoURL: seedPhoto("user-avatar-employee-trust-20260101", 400, 400),
|
|
609
|
+
role: USER_FIELDS.ROLE_VALUES.EMPLOYEE,
|
|
610
|
+
permissionGroup: "trust_and_safety",
|
|
611
|
+
permissions: [...PERMISSION_GROUPS.trust_and_safety],
|
|
612
|
+
emailVerified: true,
|
|
613
|
+
disabled: false,
|
|
614
|
+
publicProfile: {
|
|
615
|
+
isPublic: false,
|
|
616
|
+
showEmail: false,
|
|
617
|
+
showPhone: false,
|
|
618
|
+
showOrders: false,
|
|
619
|
+
showWishlist: false,
|
|
620
|
+
bio: "Trust & Safety employee — users, bans, reviews and reports.",
|
|
621
|
+
location: "Chennai, Tamil Nadu",
|
|
622
|
+
},
|
|
623
|
+
stats: { totalOrders: 0, auctionsWon: 0, itemsSold: 0, reviewsCount: 0 },
|
|
624
|
+
metadata: { lastSignInTime: daysAgo(2), creationTime: daysAgo(60).toISOString(), loginCount: 40 },
|
|
625
|
+
createdAt: daysAgo(60),
|
|
626
|
+
updatedAt: daysAgo(2),
|
|
627
|
+
},
|
|
525
628
|
];
|