@mohasinac/appkit 4.33.1 → 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.
Files changed (104) hide show
  1. package/dist/_internal/server/features/checkout/actions.js +9 -1
  2. package/dist/_internal/server/features/checkout/expand-order-items.js +9 -0
  3. package/dist/_internal/server/features/media/contextGuards.d.ts +3 -3
  4. package/dist/_internal/server/features/media/contextGuards.js +19 -5
  5. package/dist/_internal/server/features/orders/actions.d.ts +9 -0
  6. package/dist/_internal/server/features/orders/actions.js +47 -4
  7. package/dist/_internal/server/features/orders/adapters.js +10 -0
  8. package/dist/_internal/server/features/refunds/actions.d.ts +9 -1
  9. package/dist/_internal/server/features/refunds/actions.js +36 -3
  10. package/dist/_internal/server/features/seo/sitemap.js +2 -2
  11. package/dist/_internal/server/features/site-settings/adapters.js +1 -1
  12. package/dist/_internal/server/functions/scheduled.d.ts +1 -2
  13. package/dist/_internal/server/functions/scheduled.js +8 -9
  14. package/dist/_internal/server/jobs/core/testerSandboxCleanup.d.ts +15 -0
  15. package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +15 -6
  16. package/dist/_internal/server/jobs/handlers/index.d.ts +0 -1
  17. package/dist/_internal/server/jobs/handlers/index.js +0 -1
  18. package/dist/_internal/shared/features/orders/refund-copy.d.ts +12 -0
  19. package/dist/_internal/shared/features/orders/refund-copy.js +12 -0
  20. package/dist/_internal/shared/features/orders/return-reasons.d.ts +59 -0
  21. package/dist/_internal/shared/features/orders/return-reasons.js +96 -0
  22. package/dist/_internal/shared/features/orders/schema.d.ts +30 -0
  23. package/dist/_internal/shared/features/orders/schema.js +20 -0
  24. package/dist/_internal/shared/features/products/schema.d.ts +12 -0
  25. package/dist/_internal/shared/features/products/schema.js +4 -1
  26. package/dist/_internal/shared/media/limits.d.ts +14 -0
  27. package/dist/_internal/shared/media/limits.js +29 -0
  28. package/dist/client.d.ts +9 -1
  29. package/dist/client.js +19 -1
  30. package/dist/features/admin/components/AdminProductEditorView.js +6 -1
  31. package/dist/features/admin/components/AdminSiteSettingsView.js +2 -6
  32. package/dist/features/admin/components/index.d.ts +0 -2
  33. package/dist/features/admin/components/index.js +0 -1
  34. package/dist/features/admin/schemas/admin-editor-forms.js +1 -1
  35. package/dist/features/admin/schemas/firestore.d.ts +0 -2
  36. package/dist/features/auctions/components/AuctionDetailPageView.js +4 -3
  37. package/dist/features/auth/schemas/firestore.d.ts +1 -0
  38. package/dist/features/contact/components/ContactForm.js +46 -31
  39. package/dist/features/media/upload/MediaUploadList.d.ts +16 -1
  40. package/dist/features/media/upload/MediaUploadList.js +58 -8
  41. package/dist/features/orders/components/RefundRequestView.d.ts +26 -10
  42. package/dist/features/orders/components/RefundRequestView.js +108 -24
  43. package/dist/features/orders/schemas/firestore.d.ts +42 -0
  44. package/dist/features/orders/types/index.d.ts +24 -0
  45. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +5 -3
  46. package/dist/features/products/components/ProductDetailPageView.js +14 -2
  47. package/dist/features/products/components/ProductFeatureBadges.d.ts +9 -1
  48. package/dist/features/products/components/ProductFeatureBadges.js +17 -1
  49. package/dist/features/products/components/ProductForm.js +46 -10
  50. package/dist/features/products/components/ProductGrid.js +10 -1
  51. package/dist/features/products/constants/final-sale.d.ts +40 -0
  52. package/dist/features/products/constants/final-sale.js +40 -0
  53. package/dist/features/products/index.d.ts +1 -0
  54. package/dist/features/products/index.js +1 -0
  55. package/dist/features/products/schemas/firestore.d.ts +19 -2
  56. package/dist/features/products/schemas/firestore.js +4 -0
  57. package/dist/features/products/types/index.d.ts +2 -0
  58. package/dist/features/seller/components/SellerProductShell.d.ts +7 -0
  59. package/dist/features/seller/components/SellerProductShell.js +82 -11
  60. package/dist/features/seller/index.d.ts +1 -0
  61. package/dist/features/seller/index.js +1 -0
  62. package/dist/features/seller/schemas/product-form.d.ts +304 -0
  63. package/dist/features/seller/schemas/product-form.js +132 -0
  64. package/dist/features/seller/utils/product-draft-mapping.d.ts +117 -0
  65. package/dist/features/seller/utils/product-draft-mapping.js +147 -0
  66. package/dist/features/shell/SectionForm.js +62 -7
  67. package/dist/features/shell/form-sections.js +4 -0
  68. package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +16 -1
  69. package/dist/features/tester/repository/tester-checklist-item.repository.js +32 -6
  70. package/dist/features/tester/seed-data/events-tester-seed-data.js +77 -5
  71. package/dist/features/tester/seed-data/offers-tester-seed-data.js +23 -9
  72. package/dist/features/tester/seed-data/products-tester-seed-data.js +65 -49
  73. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +182 -0
  74. package/dist/features/tester/seed-data/tester-window.d.ts +25 -0
  75. package/dist/features/tester/seed-data/tester-window.js +66 -0
  76. package/dist/index.d.ts +6 -3
  77. package/dist/index.js +12 -3
  78. package/dist/next/routing/route-map.d.ts +2 -0
  79. package/dist/next/routing/route-map.js +1 -0
  80. package/dist/schemas/registry.d.ts +271 -0
  81. package/dist/schemas/registry.js +9 -0
  82. package/dist/seed/_helpers/final-sale-fixtures.d.ts +35 -0
  83. package/dist/seed/_helpers/final-sale-fixtures.js +61 -0
  84. package/dist/seed/products-art-seed-data.js +9 -1
  85. package/dist/seed/products-auctions-seed-data.js +2 -1
  86. package/dist/seed/products-classifieds-seed-data.js +2 -1
  87. package/dist/seed/products-digital-codes-seed-data.js +2 -1
  88. package/dist/seed/products-live-items-seed-data.js +2 -1
  89. package/dist/seed/products-preorders-seed-data.d.ts +2 -138
  90. package/dist/seed/products-preorders-seed-data.js +2 -1
  91. package/dist/seed/products-prize-draws-seed-data.js +2 -1
  92. package/dist/seed/products-standard-seed-data.js +2 -1
  93. package/dist/seed/products-stickers-seed-data.js +9 -1
  94. package/dist/seed/users-seed-data.js +107 -4
  95. package/dist/server.d.ts +6 -2
  96. package/dist/server.js +15 -2
  97. package/dist/styles.css +1 -1
  98. package/dist/tailwind-utilities.css +1 -1
  99. package/dist/tokens/motion.d.ts +12 -0
  100. package/dist/tokens/motion.js +12 -0
  101. package/dist/ui/components/CollapsibleSection.d.ts +18 -1
  102. package/dist/ui/components/CollapsibleSection.js +20 -3
  103. package/dist/ui/components/Motion.js +17 -1
  104. package/package.json +1 -1
@@ -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 1h/2h/3h "still
9
- * open" cycling fixtures + 1 already-ended "won" fixture) + 1 pre-order + 1 prize-draw +
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
- * The 3 staggered auction ids/end-dates are recomputed on every seed run (including the
13
- * testerSandboxRefresh job, every 4h) so testers can watch one actually end mid-session.
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 testers can watch a live
49
- // auction actually cross its end time within a single sandbox refresh cycle
50
- // (testerSandboxRefresh re-imports this module every 4 hours, recomputing
51
- // these Date.now()-relative offsets fresh each time) instead of the old
52
- // single fixture that always sat 5 days out. A loop (not hand-typed ids)
53
- // avoids id collisions if the stagger list is ever extended.
54
- const AUCTION_CYCLE_STAGGER_HOURS = [1, 2, 3];
55
- const stagedAuctions = AUCTION_CYCLE_STAGGER_HOURS.map((hours, i) => withTokens({
56
- id: `auction-tester-sandbox-cycle-${i + 1}`,
57
- slug: `auction-tester-sandbox-cycle-${i + 1}`,
58
- title: `Test Auction — Ends in ~${hours}h`,
59
- description: `Disposable test auction for the tester QA program, staggered to end roughly ${hours} hour(s) after the last sandbox refresh so testers can watch the end-of-auction flow live. Place a bid — it auto-expires in 7 days, cascading to any bids.`,
60
- categorySlugs: COLLECTIBLES_CATEGORY_SLUGS,
61
- categoryNames: COLLECTIBLES_CATEGORY_NAMES,
62
- brandSlug: "brand-tester-sandbox",
63
- brand: "TestBrand",
64
- startingBid: 15000,
65
- currentBid: 15000,
66
- currency: "INR",
67
- price: 150,
68
- stockQuantity: 1,
69
- availableQuantity: 1,
70
- auctionEndDate: new Date(Date.now() + hours * 60 * 60 * 1000),
71
- bidCount: 0,
72
- bidsHaveStarted: false,
73
- isSold: false,
74
- mainImage: seedPhoto(`auction-image-tester-sandbox-cycle-${i + 1}-20260101`, 900, 900),
75
- images: [seedPhoto(`auction-image-tester-sandbox-cycle-${i + 1}-20260101`, 900, 900)],
76
- status: PRODUCT_FIELDS.STATUS_VALUES.PUBLISHED,
77
- condition: PRODUCT_FIELDS.CONDITION_VALUES.NEW,
78
- listingType: "auction",
79
- customFields: [],
80
- customSections: [],
81
- isPromoted: false,
82
- isOnSale: false,
83
- createdAt: new Date(),
84
- updatedAt: new Date(),
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 the 4-hour testerSandboxRefresh re-arms them.
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
- endsInHours: 6,
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
- endsInHours: 6,
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
- endsInHours: 6,
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
- endsInMinutes: 20,
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: new Date(Date.now() +
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,
@@ -1927,6 +1927,48 @@ const rawTesterChecklistItems = [
1927
1927
  },
1928
1928
  ],
1929
1929
  },
1930
+ {
1931
+ pageKey: "return-request",
1932
+ pageLabel: "Requesting a return \u2014 and what final sale actually blocks",
1933
+ href: "/user/orders",
1934
+ cases: [
1935
+ {
1936
+ key: "return-cta-only-when-returnable",
1937
+ label: "The \"Request Return\" button appears only on a DELIVERED order inside the 7-day window",
1938
+ description: "Added 2026-09-04. BEFORE there was no buyer-facing return flow at all: the action existed, the CTA was registered in ACTIONS.USER, and nothing anywhere rendered it. AFTER: open a delivered order's detail page and the button is there beside Track Shipment. Open a pending or shipped one and it is not. Open a delivered order older than 7 days and the page explains the window has closed rather than offering a form that would be refused.",
1939
+ },
1940
+ {
1941
+ key: "return-not-received-accepted-on-final-sale",
1942
+ label: "\"It never arrived\" is accepted on a FINAL SALE order",
1943
+ description: "This is the whole rule. Final sale refuses a change of mind; it can never refuse a claim that the buyer did not get what they paid for. On a final-sale order, choose \"It never arrived\", tick the three acknowledgements, submit \u2014 it must go through and the order must move to Return Requested.",
1944
+ },
1945
+ {
1946
+ key: "return-change-of-mind-refused-by-name",
1947
+ label: "On a final-sale order the change-of-mind reasons are NOT OFFERED, and the panel says why",
1948
+ description: "Open the return form for a final-sale order. A warning panel names the situation, and the reason picker lists only the five fault reasons \u2014 never-arrived, not-as-described, damaged, wrong item, counterfeit. \"I changed my mind\" must not be selectable. On a returnable order all ten reasons appear. A generic \"request refused\" with no explanation is the failure this replaced.",
1949
+ },
1950
+ {
1951
+ key: "return-reason-is-persisted",
1952
+ label: "The reason the buyer picked is actually STORED and visible to staff",
1953
+ description: "BEFORE: `requestReturnAction` parsed a free-text reason and then threw it away before writing \u2014 an order reached Return Requested with nothing recorded about why, and the seller had nothing to act on. AFTER: submit a return, then open the same order in /admin/return-requests and confirm the coded reason and any note are there.",
1954
+ },
1955
+ {
1956
+ key: "return-partial-gates-selected-lines-only",
1957
+ label: "On a mixed order, a final-sale line does not block a return of the returnable line",
1958
+ description: "Final sale is snapshotted PER LINE at purchase, not per order. On an order containing one final-sale item and one returnable item, a change-of-mind return scoped to the returnable line must be accepted; the same reason scoped to the final-sale line must not.",
1959
+ },
1960
+ {
1961
+ key: "return-prize-draw-still-refused-entirely",
1962
+ label: "A prize-draw order is still refused for EVERY reason \u2014 a different, earlier gate",
1963
+ description: "`isNonRefundable` is a property of the listing TYPE and admits no exception, including a seller-fault one. Open a prize-draw order's refund surface: it must refuse outright and say so, without offering a reason picker at all. Confusing this with final sale in either direction is the failure.",
1964
+ },
1965
+ {
1966
+ key: "return-terms-snapshotted-at-purchase",
1967
+ label: "Changing a listing's return terms does NOT change an order already placed",
1968
+ description: "Buy a returnable listing. Then, as the seller, switch that listing to final sale. Go back to the order and request a change-of-mind return \u2014 it must still be ACCEPTED, because the buyer agreed to the terms shown at checkout. The order carries its own copy; a seller must not be able to withdraw a right after the sale.",
1969
+ },
1970
+ ],
1971
+ },
1930
1972
  ]),
1931
1973
  ...group("selling", "Selling", [
1932
1974
  {
@@ -2162,6 +2204,30 @@ const rawTesterChecklistItems = [
2162
2204
  description: "BEFORE: an invalid field produced a banner listing the problem with no way to reach it, and on the site-settings page the summary was mounted outside every form context and so displayed NOTHING at all. AFTER: submit a form with a bad value (a negative refund on an order, a non-https third-party ad URL, an AdSense id that is not ca-pub-…) and the summary must list it, tag it with its section, collapse open that section when clicked, and show a count badge on the section header.",
2163
2205
  href: "/admin/ads",
2164
2206
  },
2207
+ {
2208
+ key: "required-section-has-no-dead-chevron",
2209
+ label: "A REQUIRED section renders as a plain heading \u2014 no chevron, nothing to click",
2210
+ description: "Fixed 2026-09-04. BEFORE: SectionDef documented a required section as rendering \"without a collapse control so it can't be hidden\", but CollapsibleSection had no way to say that, so SectionForm passed a no-op handler and the header still drew a full button with a rotating chevron. Clicking it did nothing, on ~73 required sections across the app \u2014 which reads as a broken form. AFTER: on any sectionised form (admin product, admin user, store product), the first section's header has no chevron and no hover state, while every optional section below it still expands and collapses normally.",
2211
+ href: "/admin/products/new",
2212
+ },
2213
+ {
2214
+ key: "open-section-does-not-clip-dropdowns",
2215
+ label: "A dropdown opened inside an expanded section is not clipped at the panel edge",
2216
+ description: "BEFORE: the collapse animation kept `overflow: hidden` in its SETTLED open state, not just during the transition, so anything escaping the panel box was cut off. AFTER: expand a section containing a select or picker near its bottom edge, open it, and the list must render in full rather than being sliced by the section border.",
2217
+ href: "/admin/products/new",
2218
+ },
2219
+ {
2220
+ key: "error-jump-lands-on-the-field",
2221
+ label: "Jumping to an error scrolls to the section AND lands focus on the offending field",
2222
+ description: "BEFORE: the scroll fired one animation frame after expanding, while the panel was still ~0px tall inside overflow:hidden \u2014 so focus landed against a box that was about to grow and the field could end up off-screen. AFTER: submit a form with an error inside a COLLAPSED section. It expands, scrolls, and the invalid input is both visible and focused once the animation finishes.",
2223
+ href: "/admin/products/new",
2224
+ },
2225
+ {
2226
+ key: "long-form-typing-is-smooth",
2227
+ label: "Typing in a long form does not stutter",
2228
+ description: "BEFORE: the section list was rebuilt on every keystroke, which re-fired the validation effect on top of the validation the keystroke already triggered \u2014 two full-tree schema parses per character on a 40-field listing form. AFTER: type a long description into the seller product form. It should keep up. This is a feel test, not a pass/fail assertion; report it if it lags.",
2229
+ href: "/store/products/new",
2230
+ },
2165
2231
  {
2166
2232
  key: "form-mobile-action-bar",
2167
2233
  label: "Save and Cancel appear in the pinned mobile bar, once",
@@ -2410,6 +2476,122 @@ const rawTesterChecklistItems = [
2410
2476
  },
2411
2477
  ],
2412
2478
  },
2479
+ {
2480
+ pageKey: "final-sale-authoring",
2481
+ pageLabel: "Final sale \u2014 the seller switch",
2482
+ href: "/store/products/new",
2483
+ cases: [
2484
+ {
2485
+ key: "final-sale-defaults-on",
2486
+ label: "A brand-new listing is FINAL SALE by default \u2014 the Returns section shows \"Accept change-of-mind returns\" switched OFF",
2487
+ description: "Added 2026-09-04. Open the Returns section on a new listing of any type. The toggle is off and the panel explains that not-received / damaged / wrong-item / not-as-described / counterfeit claims are still accepted. Absent means final sale, so a listing saved without ever opening this section is also final sale \u2014 which is the point.",
2488
+ href: "/store/products/new",
2489
+ },
2490
+ {
2491
+ key: "final-sale-opt-out-survives-reload",
2492
+ label: "Turning returns ON, saving, and RELOADING shows it still on",
2493
+ description: "This is the case that matters, and a reload is the only way to see it. `productBaseSchema` is a plain z.object() with no .passthrough(), so it strips every key it does not name \u2014 a field missing from it saves with a success toast and is gone. BEFORE such a field existed at all. AFTER: toggle Accept returns ON, Save, reload the edit page, and the toggle must still be ON. If it reverts to off, `finalSale` is being stripped on write.",
2494
+ href: "/store/products",
2495
+ },
2496
+ {
2497
+ key: "return-policy-authorable-by-seller",
2498
+ label: "A seller can write their own Return policy text \u2014 for the first time",
2499
+ description: "BEFORE: `returnPolicy` existed on the document and the ONLY editor for it was the admin product form, so a seller could never state their own terms. AFTER: the Returns section on the seller form has a Return policy textarea. Type into it, save, reload, and confirm it persists and appears on the public listing under Delivery & Returns.",
2500
+ href: "/store/products",
2501
+ },
2502
+ {
2503
+ key: "final-sale-admin-override",
2504
+ label: "An admin can flip the same switch on any listing, and it is the SAME field",
2505
+ description: "Open a listing in the admin product editor. The \"Accept change-of-mind returns\" toggle sits directly above the Return policy box and reflects whatever the seller chose. Change it, save, then reopen the SELLER edit form for that listing \u2014 it must show the admin's value. Two independent flags would be the failure.",
2506
+ href: "/admin/products",
2507
+ },
2508
+ {
2509
+ key: "final-sale-badges-render",
2510
+ label: "A final-sale listing shows the pill on its card AND the line on its detail page",
2511
+ description: "Seed data opts one listing per type OUT of final sale, so both states are visible. On /products, a final-sale listing carries a dark \"Final Sale\" pill; the opted-out one does not. On the detail page, the Delivery & Returns block always renders a returns line \u2014 even when the seller wrote no shipping or policy text at all, which previously hid the block entirely and with it the most consequential term of the sale.",
2512
+ href: "/products",
2513
+ },
2514
+ ],
2515
+ },
2516
+ {
2517
+ pageKey: "media-limits",
2518
+ pageLabel: "Media \u2014 ten images and one video, on every listing type",
2519
+ href: "/store/products/new",
2520
+ cases: [
2521
+ {
2522
+ key: "media-gallery-accepts-images-and-video",
2523
+ label: "The gallery takes BOTH images and a video, and shows a live per-type count",
2524
+ description: "Changed 2026-09-04. BEFORE the single-media control on the listing form was video-only (`kind=\"video\"`), so the one \"add media\" affordance a seller saw refused photos. AFTER: the gallery accepts image files and a video file in the same control, and a counter under it reads \"N/10 images \u00b7 N/1 video\" as you add them.",
2525
+ href: "/store/products/new",
2526
+ },
2527
+ {
2528
+ key: "media-tenth-image-uploads",
2529
+ label: "The 10th gallery image uploads \u2014 the 5th used to fail with a 400",
2530
+ description: "BEFORE: the gallery was capped at 5 and sent its Nth image as index N+1, while the server's signed-URL guard rejected any index above 5 \u2014 so the LAST slot of every product form returned a 400 that read as a random upload failure. AFTER: add ten images one at a time. All ten must succeed, including the tenth.",
2531
+ href: "/store/products/new",
2532
+ },
2533
+ {
2534
+ key: "media-eleventh-image-refused-client-side",
2535
+ label: "The 11th image is refused by the FORM with a clear message, not by the server",
2536
+ description: "Try to add an 11th image. The control must refuse it before uploading, naming the limit (\"You can upload up to 10 images.\"). A server-side 400 arriving after the bytes have gone up is the wrong behaviour \u2014 the user has already waited.",
2537
+ href: "/store/products/new",
2538
+ },
2539
+ {
2540
+ key: "media-second-video-refused-first-kept",
2541
+ label: "A second video is refused and the FIRST one is kept",
2542
+ description: "With one video already in the gallery, try to add another. The message must say only one video is allowed and tell you to remove the current one \u2014 and crucially the existing video must still be there afterwards. Silently replacing it would lose work.",
2543
+ href: "/store/products/new",
2544
+ },
2545
+ {
2546
+ key: "media-caps-flat-across-types",
2547
+ label: "The same 10 + 1 applies to an auction and a pre-order \u2014 there is no per-type table",
2548
+ description: "Repeat the ten-image check on /store/auctions/new and /store/pre-orders/new. Both must allow ten. BEFORE the server carried separate AUCTION_IMAGE_MAX and PREORDER_IMAGE_MAX constants, both 5, disagreeing with everything else.",
2549
+ href: "/store/auctions/new",
2550
+ },
2551
+ {
2552
+ key: "media-live-listing-still-requires-video",
2553
+ label: "A live listing STILL refuses to publish without a video",
2554
+ description: "Unchanged behaviour, re-verified because the surrounding control changed. Create a live listing with photos but no video and press Publish. It must be refused, and the Video field must be marked required. That is a requiredness rule, not a count rule, and the media-limit work must not have removed it.",
2555
+ href: "/store/live/new",
2556
+ },
2557
+ ],
2558
+ },
2559
+ {
2560
+ pageKey: "listing-type-fields-roundtrip",
2561
+ pageLabel: "Per-type fields survive a save \u2014 all nine listing types",
2562
+ href: "/store/products",
2563
+ cases: [
2564
+ {
2565
+ key: "roundtrip-classified-meetup",
2566
+ label: "A classified's meetup city, locality and pincode come back after a save",
2567
+ description: "Fixed 2026-09-04, and this was live data loss. BEFORE: the form held `classifiedCity` while the write schema expected `classified.meetupArea.city`, nothing translated between them, and the schema strips unnamed keys \u2014 so the seller pressed Publish, got a success, and the listing saved with none of the fields that made it a classified. AFTER: fill in the meetup details, save, reopen the edit form, and every value must be there.",
2568
+ href: "/store/classified/new",
2569
+ },
2570
+ {
2571
+ key: "roundtrip-live-species-jurisdictions",
2572
+ label: "A live listing's species, transport method and permitted jurisdictions survive",
2573
+ description: "Same bug, and the most serious instance of it: `jurisdictionAllowed` is the field that prevents shipping a live animal somewhere that forbids it, and it was being dropped. Create a live listing with species, age, sex, care info, transport method, handling fee and at least one jurisdiction. Save, reopen, confirm all of it returned.",
2574
+ href: "/store/live/new",
2575
+ },
2576
+ {
2577
+ key: "roundtrip-digital-code-delivery",
2578
+ label: "A digital code's delivery method, pool size and redemption instructions survive",
2579
+ href: "/store/digital-codes/new",
2580
+ },
2581
+ {
2582
+ key: "roundtrip-print-meta",
2583
+ label: "An art print's size, material, finish and edition size survive \u2014 and so do a sticker sheet's",
2584
+ description: "Art and stickers share one `printMeta` block, so check both: create one of each with all four fields filled, save, reopen.",
2585
+ href: "/store/art/new",
2586
+ },
2587
+ {
2588
+ key: "roundtrip-edit-form-opens-populated",
2589
+ label: "Opening the edit form for an EXISTING listing shows its per-type fields already filled",
2590
+ description: "The inverse half of the same bug. BEFORE: the edit pages hand-built their initial values and listed none of the per-type keys, so the form opened blank for them even when the document held real data \u2014 which read as \"my data was deleted\". AFTER: open the edit page for any seeded classified, live, digital-code, art or sticker listing and its per-type fields are populated on arrival.",
2591
+ href: "/store/classified",
2592
+ },
2593
+ ],
2594
+ },
2413
2595
  ]),
2414
2596
  ...group("content-discovery", "Content & Discovery", [
2415
2597
  {
@@ -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
+ }
package/dist/index.d.ts CHANGED
@@ -1095,7 +1095,6 @@ export { AdminReviewsView } from "./features/admin/index";
1095
1095
  export { AdminSectionsView } from "./features/admin/index";
1096
1096
  export { AdminSessionsManager } from "./features/admin/index";
1097
1097
  export { AdminSidebar } from "./features/admin/index";
1098
- export { AdminSiteView } from "./features/admin/index";
1099
1098
  export { AdminSublistingCategoriesView } from "./features/admin/index";
1100
1099
  export { AdminSublistingCategoryEditorView } from "./features/admin/index";
1101
1100
  export type { AdminSublistingCategoryEditorViewProps } from "./features/admin/index";
@@ -1198,7 +1197,6 @@ export type { AdminReviewsViewProps } from "./features/admin/index";
1198
1197
  export type { AdminSectionsViewProps } from "./features/admin/index";
1199
1198
  export type { AdminSessionsManagerProps } from "./features/admin/index";
1200
1199
  export type { AdminSidebarProps } from "./features/admin/index";
1201
- export type { AdminSiteViewProps } from "./features/admin/index";
1202
1200
  export type { AdminSiteSettingsViewProps } from "./features/admin/index";
1203
1201
  export type { AdminStatCardProps } from "./features/admin/index";
1204
1202
  export type { AdminStoresViewProps } from "./features/admin/index";
@@ -2175,6 +2173,7 @@ export { RefundHistoryTable } from "./features/orders/index";
2175
2173
  export type { RefundHistoryTableProps } from "./features/orders/index";
2176
2174
  export { RefundRequestView } from "./features/orders/index";
2177
2175
  export type { RefundRequestViewProps } from "./features/orders/index";
2176
+ export type { RefundRequestSubmission } from "./features/orders/components/RefundRequestView";
2178
2177
  export { PaymentGatewayValues } from "./features/orders/index";
2179
2178
  export { PaymentMethodValues } from "./features/orders/index";
2180
2179
  export { OutOfStockPolicyValues } from "./features/orders/index";
@@ -2386,6 +2385,7 @@ export { getProductFilterKeys } from "./features/products/index";
2386
2385
  export { getProductSortOptions } from "./features/products/index";
2387
2386
  export { getProductTableColumns } from "./features/products/index";
2388
2387
  export { normalizeListingType, isAuctionListing, isPreOrderListing, isStandardListing, isPrizeDrawListing, isClassifiedListing, isDigitalCodeListing, isLiveListing, isArtListing, isStickersListing, } from "./features/products/index";
2388
+ export { isFinalSale, acceptsChangeOfMindReturns, } from "./features/products/constants/final-sale";
2389
2389
  export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabledCategoryTypes, ALL_LISTING_TYPES, } from "./_internal/shared/listing-types/feature-flags";
2390
2390
  export { actionTracker, setActionTrackerSink, resetActionTrackerSink, type ActionEvent, type ActionTrackerSink, } from "./_internal/shared/listing-types/action-tracker";
2391
2391
  export { cartRequiresShipping, cartIsDigitalOnly, cartIsChatOnly, } from "./_internal/shared/listing-types/cart-shipping";
@@ -3072,6 +3072,8 @@ export type { BundleQueryRule } from "./features/categories/schemas/firestore";
3072
3072
  export { BUNDLE_MIN_ITEMS, BUNDLE_MAX_ITEMS, BUNDLE_MAX_PER_USER_DEFAULT, BUNDLES_PAGE_SIZE, BUNDLES_FEATURED_LIMIT, } from "./_internal/shared/features/categories/bundle-config";
3073
3073
  export { BUNDLE_COPY, BUNDLE_STOCK_VARIANT, } from "./_internal/shared/features/categories/bundle-copy";
3074
3074
  export { REFUND_COPY } from "./_internal/shared/features/orders/refund-copy";
3075
+ export { RETURN_REASON, FINAL_SALE_EXEMPT_REASONS, RETURN_REASON_LABEL, RETURN_REASON_GROUP_LABEL, isFinalSaleExempt, isReturnReason, selectableReturnReasons, } from "./_internal/shared/features/orders/return-reasons";
3076
+ export type { ReturnReason } from "./_internal/shared/features/orders/return-reasons";
3075
3077
  export { bundleCreateSchema, bundleUpdateSchema, bundleQueryRuleSchema, bundleDisplaySchema, type BundleCreateBody, type BundleUpdateBody, } from "./_internal/shared/features/categories/bundle-schemas";
3076
3078
  export type { GroupedListingDocument, GroupTheme, GroupVisibility } from "./features/grouped/schemas/firestore";
3077
3079
  export { navItemFormSchema, navItemCreateSchema, navItemUpdateSchema, type NavItemFormValues, } from "./features/admin/schemas/nav-item-form";
@@ -3144,7 +3146,7 @@ export { LISTING_TYPE_REGISTRY, pluginFor, detectListingTypeFromSlug } from "./_
3144
3146
  export type { ListingTypePlugin } from "./_internal/shared/listing-types/_registry";
3145
3147
  export { getListingRule, getCategoryRule, pickOrderType, getSplitKey, runSyncPreflight, CHECKOUT_RULES, CATEGORY_CHECKOUT_RULES, CHECKOUT_MAX_ORDERS_PER_TX, PRIZE_DRAW_MAX_REVEALS_PER_ORDER, BUNDLE_MAX_QTY_PER_TX, STANDARD_MAX_QTY_PER_LINE, } from "./_internal/shared/checkout/rules";
3146
3148
  export type { ListingCheckoutRule, CategoryCheckoutRule, CartItemProductPair, OrderItemInput, RefundPolicy, } from "./_internal/shared/checkout/rules";
3147
- export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, } from "./_internal/shared/media/limits";
3149
+ export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits";
3148
3150
  export type { MediaKind, AllowedImageMime, AllowedVideoMime, AllowedDocMime, AllowedMime, } from "./_internal/shared/media/limits";
3149
3151
  export { PRODUCT_FIELDS, PRODUCT_STATUS_TRANSITIONS, ORDER_FIELDS, REVIEW_FIELDS, BID_FIELDS, AD_FIELDS, AD_TRACKED_FIELDS, BLOG_FIELDS, BRAND_FIELDS, WISHLIST_FIELDS, HISTORY_FIELDS, COUPON_USAGE_FIELDS, CAROUSEL_FIELDS, FAQ_FIELDS, HOMEPAGE_SECTION_FIELDS, SITE_SETTINGS_FIELDS, COMMON_FIELDS, OAUTH_STATE_VALUES, SCHEMA_DEFAULTS, } from "./constants/field-names";
3150
3152
  export { SORT_DIR, sortBy } from "./constants/sort";
@@ -3226,3 +3228,4 @@ export { ROLES_TABS, isRolesTabId, ADMIN_SETTINGS_TABS, isAdminSettingsTabId, }
3226
3228
  export type { AdminPageTab, RolesTabId, AdminSettingsTabId, } from "./features/admin/constants/admin-page-tabs";
3227
3229
  export { validateActionIndexEntry, normaliseRoute, } from "./features/search/action-index/validate-entry";
3228
3230
  export type { ActionIndexValidationContext, ActionIndexValidationIssue, } from "./features/search/action-index/validate-entry";
3231
+ export { draftToProductInput, productToDraft, } from "./features/seller/utils/product-draft-mapping";
package/dist/index.js CHANGED
@@ -2126,8 +2126,6 @@ export { AdminSessionsManager } from "./features/admin/index";
2126
2126
  // AdminSidebar - Shared export for admin sidebar.
2127
2127
  export { AdminSidebar } from "./features/admin/index";
2128
2128
  // [CLIENT-SSR]-Runs in both SSR and browser â€" React component or hook that does not depend on browser-only APIs.
2129
- // AdminSiteView - Component for admin site view.
2130
- export { AdminSiteView } from "./features/admin/index";
2131
2129
  // AdminSublistingCategoriesView - Admin list view for sublisting categories (SC1).
2132
2130
  export { AdminSublistingCategoriesView } from "./features/admin/index";
2133
2131
  // AdminSublistingCategoryEditorView - Admin create/edit form for sublisting categories (SC1).
@@ -4367,6 +4365,9 @@ export { getProductTableColumns } from "./features/products/index";
4367
4365
  // normalizeListingType + predicate helpers â€" canonical SB1-G accessors.
4368
4366
  // SB-UNI-F 2026-05-13 â€" Phase 2 predicates added (classified/digital-code/live).
4369
4367
  export { normalizeListingType, isAuctionListing, isPreOrderListing, isStandardListing, isPrizeDrawListing, isClassifiedListing, isDigitalCodeListing, isLiveListing, isArtListing, isStickersListing, } from "./features/products/index";
4368
+ // Final sale — ABSENT MEANS TRUE, so these are the only legal reads of the
4369
+ // `finalSale` field. See the module docstring for why `!p.finalSale` is wrong.
4370
+ export { isFinalSale, acceptsChangeOfMindReturns, } from "./features/products/constants/final-sale";
4370
4371
  // SB-UNI-X4 2026-05-13 â€" per-type feature-flag helpers.
4371
4372
  export { isListingTypeEnabled, isCategoryTypeEnabled, enabledListingTypes, enabledCategoryTypes, ALL_LISTING_TYPES, } from "./_internal/shared/listing-types/feature-flags";
4372
4373
  // SB-UNI-X5 2026-05-13 â€" action telemetry sink.
@@ -5454,6 +5455,10 @@ export { BUNDLE_MIN_ITEMS, BUNDLE_MAX_ITEMS, BUNDLE_MAX_PER_USER_DEFAULT, BUNDLE
5454
5455
  export { BUNDLE_COPY, BUNDLE_STOCK_VARIANT, } from "./_internal/shared/features/categories/bundle-copy";
5455
5456
  // S-SBUNI-RULES 2026-05-13 â€" shared copy for refund / shipping / sibling-payment UI.
5456
5457
  export { REFUND_COPY } from "./_internal/shared/features/orders/refund-copy";
5458
+ // Return / refund reasons — the closed enum the final-sale gate branches on.
5459
+ // See the module docstring for why the exempt set is "seller or carrier fault"
5460
+ // rather than "serious reasons".
5461
+ export { RETURN_REASON, FINAL_SALE_EXEMPT_REASONS, RETURN_REASON_LABEL, RETURN_REASON_GROUP_LABEL, isFinalSaleExempt, isReturnReason, selectableReturnReasons, } from "./_internal/shared/features/orders/return-reasons";
5457
5462
  // S-SBUNI-4 follow-up â€" shared admin bundle zod schemas.
5458
5463
  export { bundleCreateSchema, bundleUpdateSchema, bundleQueryRuleSchema, bundleDisplaySchema, } from "./_internal/shared/features/categories/bundle-schemas";
5459
5464
  export { navItemFormSchema, navItemCreateSchema, navItemUpdateSchema, } from "./features/admin/schemas/nav-item-form";
@@ -5520,7 +5525,7 @@ export { LISTING_TYPE_REGISTRY, pluginFor, detectListingTypeFromSlug } from "./_
5520
5525
  // Checkout rule registry â€" pure TS, safe in both client and server bundles.
5521
5526
  export { getListingRule, getCategoryRule, pickOrderType, getSplitKey, runSyncPreflight, CHECKOUT_RULES, CATEGORY_CHECKOUT_RULES, CHECKOUT_MAX_ORDERS_PER_TX, PRIZE_DRAW_MAX_REVEALS_PER_ORDER, BUNDLE_MAX_QTY_PER_TX, STANDARD_MAX_QTY_PER_LINE, } from "./_internal/shared/checkout/rules";
5522
5527
  // Media upload limits â€" pure constants, safe in both client and server bundles.
5523
- export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, } from "./_internal/shared/media/limits";
5528
+ export { MEGABYTE, MAX_IMAGE_BYTES, MAX_PDF_BYTES, MAX_VIDEO_BYTES, MAX_LABEL, MAX_BYTES, ALLOWED_IMAGE_MIMES, ALLOWED_VIDEO_MIMES, ALLOWED_DOC_MIMES, ALLOWED_MIMES, ALLOWED_TYPES_LABEL, MIME_TO_EXT, PDF_MAGIC, VIDEO_CONVERSION_HINTS, classifyMime, isAllowedMime, maxBytesFor, getConversionHint, PRODUCT_MAX_IMAGES, PRODUCT_MAX_VIDEOS, PRODUCT_IMAGE_INDEX_MAX, } from "./_internal/shared/media/limits";
5524
5529
  // ── Field name constants — canonical source of truth for all Firestore field names ──
5525
5530
  // NOTE: STORE_FIELDS, CATEGORY_FIELDS, EVENT_FIELDS, EVENT_ENTRY_FIELDS, PAYOUT_FIELDS,
5526
5531
  // COUPON_FIELDS, CART_FIELDS, USER_FIELDS, ADDRESS_FIELDS, NOTIFICATION_FIELDS,
@@ -5629,3 +5634,7 @@ export { SITE_SETTINGS_TABS, SITE_SETTINGS_TAB_IDS, isSiteSettingsTabId, } from
5629
5634
  export { PAYOUTS_TABS, isPayoutsTabId, SHIPPING_TABS, isShippingTabId, FULFILLMENT_TABS, isFulfillmentTabId, STOREFRONT_TABS, isStorefrontTabId, } from "./features/seller/constants/store-tabs";
5630
5635
  export { ROLES_TABS, isRolesTabId, ADMIN_SETTINGS_TABS, isAdminSettingsTabId, } from "./features/admin/constants/admin-page-tabs";
5631
5636
  export { validateActionIndexEntry, normaliseRoute, } from "./features/search/action-index/validate-entry";
5637
+ // Seller listing form draft <-> product write payload. The flat form keys and
5638
+ // the nested document blocks are different shapes, and nothing translated
5639
+ // between them — see the module docstring.
5640
+ export { draftToProductInput, productToDraft, } from "./features/seller/utils/product-draft-mapping";
@@ -146,6 +146,7 @@ export declare const DEFAULT_ROUTE_MAP: {
146
146
  readonly ORDER_DETAIL: (id: string) => string;
147
147
  readonly ORDER_CANCEL: (id: string) => string;
148
148
  readonly ORDER_TRACK: (id: string) => string;
149
+ readonly ORDER_RETURN: (id: string) => string;
149
150
  readonly ORDER_INVOICE: (id: string) => string;
150
151
  readonly ORDER_PAYMENT: (id: string) => string;
151
152
  readonly NOTIFICATIONS: "/user/notifications";
@@ -549,6 +550,7 @@ export declare const ROUTES: {
549
550
  readonly ORDER_DETAIL: (id: string) => string;
550
551
  readonly ORDER_CANCEL: (id: string) => string;
551
552
  readonly ORDER_TRACK: (id: string) => string;
553
+ readonly ORDER_RETURN: (id: string) => string;
552
554
  readonly ORDER_INVOICE: (id: string) => string;
553
555
  readonly ORDER_PAYMENT: (id: string) => string;
554
556
  readonly NOTIFICATIONS: "/user/notifications";
@@ -150,6 +150,7 @@ export const DEFAULT_ROUTE_MAP = {
150
150
  ORDER_DETAIL: (id) => `/user/orders/view/${id}`,
151
151
  ORDER_CANCEL: (id) => `/user/orders/${id}/cancel`,
152
152
  ORDER_TRACK: (id) => `/user/orders/${id}/track`,
153
+ ORDER_RETURN: (id) => `/user/orders/${id}/return`,
153
154
  ORDER_INVOICE: (id) => `/user/orders/${id}/invoice`,
154
155
  ORDER_PAYMENT: (id) => `/user/orders/${id}/payment`,
155
156
  NOTIFICATIONS: "/user/notifications",