@mohasinac/appkit 4.9.4 → 4.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/dist/_internal/client/features/layout/DashboardLayoutClient.js +7 -2
  2. package/dist/_internal/client/features/layout/SidebarCollapseToggle.d.ts +5 -1
  3. package/dist/_internal/client/features/layout/SidebarCollapseToggle.js +8 -2
  4. package/dist/_internal/client/features/layout/sidebarPositionClasses.d.ts +20 -0
  5. package/dist/_internal/client/features/layout/sidebarPositionClasses.js +20 -0
  6. package/dist/_internal/client/hand-mode/HandModeProvider.d.ts +21 -0
  7. package/dist/_internal/client/hand-mode/HandModeProvider.js +81 -0
  8. package/dist/_internal/client/hand-mode/index.d.ts +2 -0
  9. package/dist/_internal/client/hand-mode/index.js +1 -0
  10. package/dist/_internal/server/features/auctions/actions.js +3 -2
  11. package/dist/_internal/server/features/auctions/service.d.ts +4 -3
  12. package/dist/_internal/server/features/auctions/service.js +6 -6
  13. package/dist/_internal/server/features/bundles/data.d.ts +9 -0
  14. package/dist/_internal/server/features/bundles/data.js +14 -0
  15. package/dist/_internal/server/features/bundles/index.d.ts +1 -1
  16. package/dist/_internal/server/features/bundles/index.js +1 -1
  17. package/dist/_internal/server/features/catalogue/og.d.ts +20 -0
  18. package/dist/_internal/server/features/catalogue/og.js +27 -0
  19. package/dist/_internal/server/features/checkout/actions.d.ts +43 -0
  20. package/dist/_internal/server/features/checkout/actions.js +172 -81
  21. package/dist/_internal/server/features/checkout/index.d.ts +1 -1
  22. package/dist/_internal/server/features/checkout/index.js +1 -1
  23. package/dist/_internal/server/features/grouped/data.d.ts +12 -0
  24. package/dist/_internal/server/features/grouped/data.js +26 -1
  25. package/dist/_internal/server/features/grouped/index.d.ts +1 -1
  26. package/dist/_internal/server/features/grouped/index.js +1 -1
  27. package/dist/_internal/server/features/products/data.d.ts +23 -0
  28. package/dist/_internal/server/features/products/data.js +159 -0
  29. package/dist/_internal/server/features/products/index.d.ts +1 -1
  30. package/dist/_internal/server/features/products/index.js +1 -1
  31. package/dist/_internal/server/features/seo/robots.js +3 -2
  32. package/dist/_internal/server/features/seo/sitemap.js +70 -5
  33. package/dist/_internal/shared/config/schema.d.ts +1 -0
  34. package/dist/_internal/shared/features/auctions/config.d.ts +20 -0
  35. package/dist/_internal/shared/features/auctions/config.js +31 -1
  36. package/dist/_internal/shared/listing-types/art/config.d.ts +1 -1
  37. package/dist/_internal/shared/listing-types/art/config.js +1 -1
  38. package/dist/_internal/shared/listing-types/classified/config.d.ts +1 -1
  39. package/dist/_internal/shared/listing-types/classified/config.js +2 -2
  40. package/dist/_internal/shared/listing-types/digital-code/config.d.ts +1 -1
  41. package/dist/_internal/shared/listing-types/digital-code/config.js +2 -2
  42. package/dist/_internal/shared/listing-types/live/config.d.ts +1 -1
  43. package/dist/_internal/shared/listing-types/live/config.js +2 -2
  44. package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +1 -1
  45. package/dist/_internal/shared/listing-types/prize-draw/config.js +1 -1
  46. package/dist/_internal/shared/listing-types/stickers/config.d.ts +1 -1
  47. package/dist/_internal/shared/listing-types/stickers/config.js +1 -1
  48. package/dist/client.d.ts +4 -6
  49. package/dist/client.js +10 -4
  50. package/dist/constants/api-endpoints.d.ts +6 -0
  51. package/dist/constants/api-endpoints.js +2 -0
  52. package/dist/features/about/components/AboutView.js +2 -2
  53. package/dist/features/about/components/DeveloperView.js +2 -2
  54. package/dist/features/about/schemas/firestore.d.ts +1 -0
  55. package/dist/features/account/components/UserSidebar.js +8 -2
  56. package/dist/features/account/hooks/useProfile.d.ts +2 -0
  57. package/dist/features/admin/components/AdminSidebar.js +8 -2
  58. package/dist/features/admin/components/AdminSiteSettingsView.js +12 -4
  59. package/dist/features/admin/schemas/firestore.d.ts +9 -0
  60. package/dist/features/admin/schemas/firestore.js +6 -0
  61. package/dist/features/auctions/actions/bid-actions.js +4 -1
  62. package/dist/features/auctions/components/AuctionBottomActions.d.ts +2 -1
  63. package/dist/features/auctions/components/AuctionBottomActions.js +14 -1
  64. package/dist/features/auctions/components/AuctionDetailPageView.js +21 -9
  65. package/dist/features/auctions/components/LiveMinIncrement.d.ts +18 -0
  66. package/dist/features/auctions/components/LiveMinIncrement.js +17 -0
  67. package/dist/features/auctions/components/PlaceBidFormClient.d.ts +5 -2
  68. package/dist/features/auctions/components/PlaceBidFormClient.js +7 -3
  69. package/dist/features/auth/schemas/firestore.d.ts +3 -0
  70. package/dist/features/cart/components/CartDrawer.js +4 -1
  71. package/dist/features/catalogue/components/PublicCatalogueItemDetailView.d.ts +11 -0
  72. package/dist/features/catalogue/components/PublicCatalogueItemDetailView.js +33 -0
  73. package/dist/features/catalogue/components/PublicCatalogueView.js +2 -1
  74. package/dist/features/catalogue/index.d.ts +2 -0
  75. package/dist/features/catalogue/index.js +1 -0
  76. package/dist/features/categories/components/BundleDetailView.d.ts +3 -1
  77. package/dist/features/categories/components/BundleDetailView.js +3 -2
  78. package/dist/features/classified/components/ClassifiedContactSellerPanel.d.ts +12 -0
  79. package/dist/features/classified/components/ClassifiedContactSellerPanel.js +49 -0
  80. package/dist/features/classified/components/ClassifiedDetailPageView.d.ts +7 -0
  81. package/dist/features/classified/components/ClassifiedDetailPageView.js +69 -0
  82. package/dist/features/contact/components/ContactPageView.js +8 -2
  83. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.d.ts +19 -0
  84. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +79 -0
  85. package/dist/features/grouped/components/GroupedListingsCarousel.d.ts +13 -0
  86. package/dist/features/grouped/components/GroupedListingsCarousel.js +31 -0
  87. package/dist/features/grouped/repository/grouped-listings.repository.d.ts +8 -0
  88. package/dist/features/grouped/repository/grouped-listings.repository.js +16 -0
  89. package/dist/features/homepage/components/BrandsSection.js +3 -1
  90. package/dist/features/homepage/components/FeaturedBundlesSection.js +12 -1
  91. package/dist/features/homepage/components/GoogleReviewsSection.js +2 -2
  92. package/dist/features/homepage/components/HeroCarousel.js +3 -1
  93. package/dist/features/homepage/components/SectionCarousel.js +5 -2
  94. package/dist/features/homepage/components/SocialFeedSection.js +2 -2
  95. package/dist/features/homepage/components/WhatsAppCommunitySection.js +3 -1
  96. package/dist/features/layout/BackToTop.js +3 -1
  97. package/dist/features/layout/BottomActions.js +4 -3
  98. package/dist/features/layout/BottomActionsContext.d.ts +10 -0
  99. package/dist/features/layout/BottomActionsContext.js +5 -1
  100. package/dist/features/layout/SidebarLayout.js +7 -1
  101. package/dist/features/layout/hooks/useBottomActions.d.ts +2 -0
  102. package/dist/features/layout/hooks/useBottomActions.js +4 -1
  103. package/dist/features/live/components/LiveItemDetailPageView.d.ts +9 -0
  104. package/dist/features/live/components/LiveItemDetailPageView.js +74 -0
  105. package/dist/features/media/MediaVideo.js +17 -1
  106. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +9 -1
  107. package/dist/features/products/components/PrizeDrawDetailPageView.js +11 -1
  108. package/dist/features/products/components/ProductDetailPageView.js +11 -116
  109. package/dist/features/products/components/RelatedItemsSection.d.ts +16 -0
  110. package/dist/features/products/components/RelatedItemsSection.js +17 -0
  111. package/dist/features/seller/components/SellerProductShell.js +1 -1
  112. package/dist/features/seller/components/SellerSidebar.js +8 -2
  113. package/dist/features/shell/QuickFormDrawer.js +3 -1
  114. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +114 -3
  115. package/dist/hand-mode/index.d.ts +12 -0
  116. package/dist/hand-mode/index.js +11 -0
  117. package/dist/index.d.ts +2 -0
  118. package/dist/index.js +1 -0
  119. package/dist/next/routing/route-map.d.ts +2 -0
  120. package/dist/next/routing/route-map.js +7 -0
  121. package/dist/react/contexts/SessionContext.d.ts +5 -0
  122. package/dist/react/contexts/SessionContext.js +2 -0
  123. package/dist/seed/bids-seed-data.js +48 -3
  124. package/dist/seed/grouped-listings-seed-data.js +167 -0
  125. package/dist/seed/payouts-seed-data.js +4 -4
  126. package/dist/seed/products-art-seed-data.js +188 -5
  127. package/dist/seed/products-auctions-seed-data.js +215 -6
  128. package/dist/seed/products-classifieds-seed-data.js +255 -8
  129. package/dist/seed/products-digital-codes-seed-data.js +258 -19
  130. package/dist/seed/products-live-items-seed-data.js +143 -10
  131. package/dist/seed/products-preorders-seed-data.js +213 -8
  132. package/dist/seed/products-prize-draws-seed-data.js +198 -0
  133. package/dist/seed/products-standard-seed-data.js +141 -0
  134. package/dist/seed/products-stickers-seed-data.js +178 -5
  135. package/dist/seed/site-settings-seed-data.js +13 -31
  136. package/dist/seed/store-extensions-seed-data.js +1 -1
  137. package/dist/server-entry.d.ts +5 -2
  138. package/dist/server-entry.js +5 -2
  139. package/dist/server.d.ts +4 -2
  140. package/dist/server.js +4 -2
  141. package/dist/styles.css +30 -1
  142. package/dist/tailwind-utilities.css +1 -1
  143. package/dist/ui/components/Drawer.js +7 -4
  144. package/dist/ui/components/FilterDrawer.js +1 -1
  145. package/dist/ui/components/HandModeRow.d.ts +12 -0
  146. package/dist/ui/components/HandModeRow.js +18 -0
  147. package/dist/ui/components/ImageLightbox.js +37 -3
  148. package/dist/ui/components/ImageLightbox.style.css +28 -0
  149. package/dist/ui/components/Layout.d.ts +5 -1
  150. package/dist/ui/components/Layout.js +2 -1
  151. package/dist/ui/components/Layout.style.css +1 -0
  152. package/dist/ui/components/ListingFilterDrawer.js +3 -1
  153. package/dist/ui/components/Modal.js +3 -1
  154. package/dist/ui/components/SideDrawer.js +8 -5
  155. package/dist/ui/components/SideModal.js +7 -4
  156. package/dist/ui/index.d.ts +1 -0
  157. package/dist/ui/index.js +1 -0
  158. package/dist/utils/media-url.d.ts +9 -0
  159. package/dist/utils/media-url.js +39 -0
  160. package/package.json +1 -1
@@ -11,7 +11,7 @@ import { roundRupees } from "../../../../utils/number.formatter";
11
11
  import { ApiError, ValidationError, NotFoundError, ERROR_MESSAGES } from "../../../../errors";
12
12
  import { ORDER_FIELDS } from "../../../../constants/field-names";
13
13
  import { serverLogger } from "../../../../monitoring";
14
- import { unitOfWork, siteSettingsRepository, userRepository, storeRepository, couponsRepository, claimedCouponsRepository, addressesRepository, } from "../../../../repositories";
14
+ import { unitOfWork, siteSettingsRepository, userRepository, storeRepository, couponsRepository, claimedCouponsRepository, addressesRepository, productRepository, } from "../../../../repositories";
15
15
  import { calculateGst } from "../../../shared/fees/calculator";
16
16
  import { computePreOrderDepositAmount } from "../../../shared/checkout/order-math";
17
17
  import { failedCheckoutRepository } from "../../../../features/checkout/repository/failed-checkout.repository";
@@ -151,7 +151,13 @@ function accumulateCouponUsage(accumulator, couponCode, couponId, discountAmount
151
151
  entry.totalDiscount += discountAmount;
152
152
  accumulator.set(couponCode, entry);
153
153
  }
154
- async function resolveShippingCost(storeId) {
154
+ /**
155
+ * Exported so `/api/payment/create-order` (pre-charge amount) and the
156
+ * read-only `previewCheckoutPricing` (Order Summary display) can compute the
157
+ * exact same per-seller shipping fee that order creation charges/records —
158
+ * a single source of truth instead of a third re-implementation.
159
+ */
160
+ export async function resolveShippingCost(storeId) {
155
161
  if (!storeId)
156
162
  return { shippingFee: 0, storeOwnerId: undefined, storeEmiEnabled: false, storeState: undefined };
157
163
  const store = await storeRepository.findById(storeId);
@@ -172,6 +178,55 @@ async function resolveStoreState(storeId) {
172
178
  const pickup = addresses.find((a) => a.isDefault) ?? addresses[0];
173
179
  return pickup?.state;
174
180
  }
181
+ /**
182
+ * Prorates every buyer-applied coupon against one seller-group's total. Pure
183
+ * — no side effects (usage-accumulator bookkeeping is layered on by callers
184
+ * that actually place an order). Shared by createOrderForGroup,
185
+ * createRazorpayGroupOrder, and the read-only previewCheckoutPricing so the
186
+ * three can never drift on how a coupon splits across sellers (was three
187
+ * near-identical copies before this extraction — Duplication Decision
188
+ * Framework's Rule of Three).
189
+ */
190
+ function computeGroupCouponDiscount(appliedCoupons, groupLines, groupTotal, cartSubtotal, storeId) {
191
+ let couponDiscount = 0;
192
+ const appliedDiscounts = [];
193
+ for (const coupon of appliedCoupons) {
194
+ let couponGroupDiscount = 0;
195
+ const isSellerScoped = coupon.scope === "seller" && coupon.storeId;
196
+ if (isSellerScoped) {
197
+ if (coupon.storeId !== storeId)
198
+ continue;
199
+ if (coupon.applicableItemIds?.length) {
200
+ const eligibleTotal = groupLines
201
+ .filter((l) => coupon.applicableItemIds.includes(l.itemId))
202
+ .reduce((s, l) => s + l.lineTotal, 0);
203
+ couponGroupDiscount =
204
+ eligibleTotal > 0
205
+ ? Math.min(roundRupees((eligibleTotal / groupTotal) * coupon.discountAmount), eligibleTotal)
206
+ : 0;
207
+ }
208
+ else {
209
+ couponGroupDiscount = Math.min(coupon.discountAmount, groupTotal);
210
+ }
211
+ }
212
+ else if (cartSubtotal > 0) {
213
+ couponGroupDiscount = Math.min(roundRupees((groupTotal / cartSubtotal) * coupon.discountAmount), groupTotal);
214
+ }
215
+ if (couponGroupDiscount > 0) {
216
+ couponDiscount += couponGroupDiscount;
217
+ appliedDiscounts.push({
218
+ code: coupon.code,
219
+ couponId: coupon.couponId,
220
+ type: "coupon",
221
+ discountAmount: couponGroupDiscount,
222
+ scope: coupon.scope,
223
+ storeId: coupon.storeId,
224
+ });
225
+ }
226
+ }
227
+ couponDiscount = Math.min(couponDiscount, groupTotal);
228
+ return { couponDiscount, appliedDiscounts };
229
+ }
175
230
  function buildStockUpdatePayload(product, qtyDelta) {
176
231
  const lt = (product.listingType ?? "standard");
177
232
  const rule = getListingRule(lt);
@@ -313,50 +368,15 @@ async function createOrderForGroup(group, orderType, ctx) {
313
368
  const whatsappNotifyFee = computeWhatsAppNotifyFee(whatsappNotifyAddon, commissions);
314
369
  const giftWrapFee = computeGiftWrapFee(giftWrapAddon, commissions);
315
370
  const shipmentProtectionFee = computeShipmentProtectionFee(groupTotal, shipmentProtectionAddon, commissions);
316
- let couponDiscount = 0;
317
- const appliedDiscounts = [];
318
371
  const groupCouponCodes = new Set();
319
- for (const coupon of appliedCoupons) {
320
- let couponGroupDiscount = 0;
321
- const isSellerScoped = coupon.scope === "seller" && coupon.storeId;
322
- if (isSellerScoped) {
323
- if (coupon.storeId !== firstItem.storeId)
324
- continue;
325
- if (coupon.applicableItemIds?.length) {
326
- const eligibleTotal = group
327
- .filter(({ item }) => coupon.applicableItemIds.includes(item.itemId))
328
- .reduce((s, { item }) => s + item.price * item.quantity, 0);
329
- couponGroupDiscount =
330
- eligibleTotal > 0
331
- ? Math.min(roundRupees((eligibleTotal / groupTotal) * coupon.discountAmount), eligibleTotal)
332
- : 0;
333
- }
334
- else {
335
- couponGroupDiscount = Math.min(coupon.discountAmount, groupTotal);
336
- }
337
- }
338
- else {
339
- if (cartSubtotal > 0) {
340
- couponGroupDiscount = Math.min(roundRupees((groupTotal / cartSubtotal) * coupon.discountAmount), groupTotal);
341
- }
342
- }
343
- if (couponGroupDiscount > 0) {
344
- couponDiscount += couponGroupDiscount;
345
- appliedDiscounts.push({
346
- code: coupon.code,
347
- couponId: coupon.couponId,
348
- type: "coupon",
349
- discountAmount: couponGroupDiscount,
350
- scope: coupon.scope,
351
- storeId: coupon.storeId,
352
- });
353
- if (coupon.couponId) {
354
- accumulateCouponUsage(couponUsageAccumulator, coupon.code, coupon.couponId, couponGroupDiscount);
355
- groupCouponCodes.add(coupon.code);
356
- }
372
+ const groupLines = group.map(({ item }) => ({ itemId: item.itemId, lineTotal: item.price * item.quantity }));
373
+ const { couponDiscount, appliedDiscounts } = computeGroupCouponDiscount(appliedCoupons, groupLines, groupTotal, cartSubtotal, firstItem.storeId);
374
+ for (const discount of appliedDiscounts) {
375
+ if (discount.couponId) {
376
+ accumulateCouponUsage(couponUsageAccumulator, discount.code, discount.couponId, discount.discountAmount);
377
+ groupCouponCodes.add(discount.code);
357
378
  }
358
379
  }
359
- couponDiscount = Math.min(couponDiscount, groupTotal);
360
380
  const orderTotal = Math.max(0, groupTotal - couponDiscount) + shippingFee + codHandlingFee + whatsappNotifyFee + giftWrapFee + shipmentProtectionFee + (emiSchedule?.surchargeAmount ?? 0) + (gstBreakdown?.gstAmount ?? 0);
361
381
  const imageUrls = [
362
382
  ...new Set(group
@@ -733,6 +753,105 @@ export async function createCheckoutOrderAction(input) {
733
753
  ...(unavailable.length > 0 ? { unavailableItems: unavailable } : {}),
734
754
  };
735
755
  }
756
+ const EMPTY_PRICING_PREVIEW = {
757
+ subtotal: 0,
758
+ shippingFee: 0,
759
+ codHandlingFee: 0,
760
+ whatsappNotifyFee: 0,
761
+ giftWrapFee: 0,
762
+ shipmentProtectionFee: 0,
763
+ gstAmount: 0,
764
+ couponDiscount: 0,
765
+ total: 0,
766
+ };
767
+ /**
768
+ * Read-only "what will I actually be charged" preview for the checkout
769
+ * Order Summary. Reuses the exact same building blocks
770
+ * createCheckoutOrderAction / verifyAndPlaceRazorpayOrderAction use when an
771
+ * order is actually placed — resolveShippingCost, the compute*Fee helpers,
772
+ * computeGroupCouponDiscount, and the GST calc — so the number shown to the
773
+ * buyer can never diverge from what gets charged/recorded. Never decrements
774
+ * stock, creates an order, or writes coupon usage.
775
+ */
776
+ export async function previewCheckoutPricing(input) {
777
+ const { userId: uid, addressId, paymentMethod, excludedProductIds = [], whatsappNotifyAddon = false, giftWrapAddon = false, shipmentProtectionAddon = false, } = input;
778
+ const siteSettings = await siteSettingsRepository.getSingleton();
779
+ const commissions = siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS;
780
+ const cart = await unitOfWork.carts.getOrCreate(uid);
781
+ const excludedSet = new Set(excludedProductIds);
782
+ const selectedSet = cart.selectedItemIds?.length ? new Set(cart.selectedItemIds) : null;
783
+ const cartItems = (cart.items ?? []).filter((item) => !excludedSet.has(item.productId) && (!selectedSet || selectedSet.has(item.itemId)));
784
+ if (cartItems.length === 0)
785
+ return EMPTY_PRICING_PREVIEW;
786
+ // Best-effort product lookup — a preview never throws on a since-unpublished
787
+ // product, it just falls back to the item's cart-snapshot price for that
788
+ // line (same fallback cartRepository.getSubtotal() implicitly relies on).
789
+ const uniqueProductIds = [...new Set(cartItems.map((item) => item.productId))];
790
+ const productDocs = await Promise.all(uniqueProductIds.map((pid) => productRepository.findById(pid)));
791
+ const productById = new Map(uniqueProductIds.map((pid, i) => [pid, productDocs[i]]));
792
+ let resolvedAddress = null;
793
+ if (addressId) {
794
+ const addressDoc = await unitOfWork.addresses.findById(addressId);
795
+ resolvedAddress =
796
+ addressDoc && addressDoc.ownerType === "user" && addressDoc.ownerId === uid ? addressDoc : null;
797
+ }
798
+ const unitPriceFor = (item, product) => item.bundleCategorySlug && item.bundleProductIds?.length ? item.price : (product?.price ?? item.price);
799
+ const checks = cartItems.map((item) => ({ item, product: productById.get(item.productId) ?? null }));
800
+ const orderGroups = splitCartIntoOrderGroups(checks);
801
+ const cartSubtotal = orderGroups.reduce((s, { items: g }) => s + g.reduce((gs, { item, product }) => gs + unitPriceFor(item, product) * item.quantity, 0), 0);
802
+ const appliedCoupons = cart.appliedCoupons ?? [];
803
+ let shippingFee = 0;
804
+ let codHandlingFee = 0;
805
+ let whatsappNotifyFeeTotal = 0;
806
+ let giftWrapFeeTotal = 0;
807
+ let shipmentProtectionFeeTotal = 0;
808
+ let gstAmount = 0;
809
+ let couponDiscount = 0;
810
+ for (const { items: group } of orderGroups) {
811
+ const firstItem = group[0].item;
812
+ const groupTotal = group.reduce((sum, { item, product }) => sum + unitPriceFor(item, product) * item.quantity, 0);
813
+ const { shippingFee: groupShippingFee, storeState } = await resolveShippingCost(firstItem.storeId);
814
+ shippingFee += groupShippingFee;
815
+ // Mirrors createOrderForGroup exactly — the COD handling fee is only
816
+ // ever charged for literal "cod" (pay the courier), never upi_manual or
817
+ // cash (both pre-paid via UPI/bank transfer before dispatch).
818
+ if (paymentMethod === "cod")
819
+ codHandlingFee += computeCodHandlingFee(groupTotal, commissions);
820
+ whatsappNotifyFeeTotal += computeWhatsAppNotifyFee(whatsappNotifyAddon, commissions);
821
+ giftWrapFeeTotal += computeGiftWrapFee(giftWrapAddon, commissions);
822
+ shipmentProtectionFeeTotal += computeShipmentProtectionFee(groupTotal, shipmentProtectionAddon, commissions);
823
+ if (siteSettings?.gst?.enabled && resolvedAddress?.state) {
824
+ const intraState = !!storeState && storeState === resolvedAddress.state;
825
+ for (const { item, product } of group) {
826
+ const lineTotal = unitPriceFor(item, product) * item.quantity;
827
+ const rate = product?.gstRate ?? 0;
828
+ if (rate > 0)
829
+ gstAmount += calculateGst(rate, intraState, lineTotal).gstAmount;
830
+ }
831
+ }
832
+ const groupLines = group.map(({ item, product }) => ({ itemId: item.itemId, lineTotal: unitPriceFor(item, product) * item.quantity }));
833
+ const { couponDiscount: groupCouponDiscount } = computeGroupCouponDiscount(appliedCoupons, groupLines, groupTotal, cartSubtotal, firstItem.storeId);
834
+ couponDiscount += groupCouponDiscount;
835
+ }
836
+ const total = Math.max(0, cartSubtotal - couponDiscount) +
837
+ shippingFee +
838
+ codHandlingFee +
839
+ whatsappNotifyFeeTotal +
840
+ giftWrapFeeTotal +
841
+ shipmentProtectionFeeTotal +
842
+ gstAmount;
843
+ return {
844
+ subtotal: cartSubtotal,
845
+ shippingFee,
846
+ codHandlingFee,
847
+ whatsappNotifyFee: whatsappNotifyFeeTotal,
848
+ giftWrapFee: giftWrapFeeTotal,
849
+ shipmentProtectionFee: shipmentProtectionFeeTotal,
850
+ gstAmount,
851
+ couponDiscount,
852
+ total,
853
+ };
854
+ }
736
855
  /**
737
856
  * Mark an order as paid after a payment provider verifies the transaction.
738
857
  * Consumers call this from /api/payment/verify or a payment webhook.
@@ -864,43 +983,8 @@ async function createRazorpayGroupOrder(group, orderType, ctx) {
864
983
  // of re-implementing it inline — was two sequential findById calls per
865
984
  // seller group here, duplicated from resolveShippingCost.
866
985
  const { shippingFee, storeOwnerId } = await resolveShippingCost(firstItem.storeId);
867
- let couponDiscount = 0;
868
- const appliedDiscounts = [];
869
- for (const coupon of appliedCoupons) {
870
- let couponGroupDiscount = 0;
871
- const isSellerScoped = coupon.scope === "seller" && coupon.storeId;
872
- if (isSellerScoped) {
873
- if (coupon.storeId !== firstItem.storeId)
874
- continue;
875
- if (coupon.applicableItemIds?.length) {
876
- const eligibleTotal = group
877
- .filter(({ item }) => coupon.applicableItemIds.includes(item.itemId))
878
- .reduce((s, { item, product }) => s + unitPriceFor(item, product) * item.quantity, 0);
879
- couponGroupDiscount =
880
- eligibleTotal > 0
881
- ? Math.min(roundRupees((eligibleTotal / groupTotal) * coupon.discountAmount), eligibleTotal)
882
- : 0;
883
- }
884
- else {
885
- couponGroupDiscount = Math.min(coupon.discountAmount, groupTotal);
886
- }
887
- }
888
- else if (cartSubtotal > 0) {
889
- couponGroupDiscount = Math.min(roundRupees((groupTotal / cartSubtotal) * coupon.discountAmount), groupTotal);
890
- }
891
- if (couponGroupDiscount > 0) {
892
- couponDiscount += couponGroupDiscount;
893
- appliedDiscounts.push({
894
- code: coupon.code,
895
- couponId: coupon.couponId,
896
- type: "coupon",
897
- discountAmount: couponGroupDiscount,
898
- scope: coupon.scope,
899
- storeId: coupon.storeId,
900
- });
901
- }
902
- }
903
- couponDiscount = Math.min(couponDiscount, groupTotal);
986
+ const groupLines = group.map(({ item, product }) => ({ itemId: item.itemId, lineTotal: unitPriceFor(item, product) * item.quantity }));
987
+ const { couponDiscount, appliedDiscounts } = computeGroupCouponDiscount(appliedCoupons, groupLines, groupTotal, cartSubtotal, firstItem.storeId);
904
988
  const rawPlatformFee = roundRupees(groupTotal * (platformFeePercent / 100));
905
989
  const gstOnFee = roundRupees(rawPlatformFee * (gstPercent / 100));
906
990
  const platformFee = rawPlatformFee + gstOnFee;
@@ -1165,7 +1249,14 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
1165
1249
  }, 0);
1166
1250
  const expectedPlatformFee = roundRupees(cartSubtotalRs * (platformFeePercent / 100));
1167
1251
  const expectedGstOnFee = roundRupees(expectedPlatformFee * (gstPercent / 100));
1168
- const expectedPaymentAmountRs = cartSubtotalRs + expectedPlatformFee + expectedGstOnFee;
1252
+ // Same per-seller-group shipping sum /api/payment/create-order charged
1253
+ // upfront (and the same resolveShippingCost createRazorpayGroupOrder
1254
+ // below will use to build each order's recorded totalPrice) — keeps this
1255
+ // floor check from passing an amount that's short by the shipping fee.
1256
+ const expectedShippingGroups = splitCartIntoOrderGroups(bucketedPaid.available);
1257
+ const expectedShippingFees = await Promise.all(expectedShippingGroups.map((g) => resolveShippingCost(g.items[0].item.storeId)));
1258
+ const expectedShippingFee = expectedShippingFees.reduce((sum, r) => sum + r.shippingFee, 0);
1259
+ const expectedPaymentAmountRs = cartSubtotalRs + expectedPlatformFee + expectedGstOnFee + expectedShippingFee;
1169
1260
  const rzpOrderRecord = await fetchRazorpayOrder(razorpay_order_id);
1170
1261
  const paidAmountRs = paiseToRupees(rzpOrderRecord.amount);
1171
1262
  if (paidAmountRs < expectedPaymentAmountRs - 1) {
@@ -1,3 +1,3 @@
1
- export { createCheckoutOrderAction, attachPaymentAction, verifyAndPlaceRazorpayOrderAction, type CreateCheckoutOrderInput, type VerifyAndPlaceRazorpayOrderInput, } from "./actions";
1
+ export { createCheckoutOrderAction, attachPaymentAction, verifyAndPlaceRazorpayOrderAction, previewCheckoutPricing, resolveShippingCost, type CreateCheckoutOrderInput, type VerifyAndPlaceRazorpayOrderInput, type CheckoutPricingPreviewInput, type CheckoutPricingPreview, } from "./actions";
2
2
  export { formatShippingAddress, type CheckoutOrderResult, } from "./data";
3
3
  export { CHECKOUT_DEFAULT_COMMISSIONS, CHECKOUT_PAYMENT_METHODS, type CheckoutPaymentMethod, } from "../../../shared/features/checkout/config";
@@ -1,3 +1,3 @@
1
- export { createCheckoutOrderAction, attachPaymentAction, verifyAndPlaceRazorpayOrderAction, } from "./actions";
1
+ export { createCheckoutOrderAction, attachPaymentAction, verifyAndPlaceRazorpayOrderAction, previewCheckoutPricing, resolveShippingCost, } from "./actions";
2
2
  export { formatShippingAddress, } from "./data";
3
3
  export { CHECKOUT_DEFAULT_COMMISSIONS, CHECKOUT_PAYMENT_METHODS, } from "../../../shared/features/checkout/config";
@@ -18,6 +18,18 @@ export interface ListGroupedListingsParams {
18
18
  export declare function listGroupedListings(params?: ListGroupedListingsParams): Promise<GroupedListingDocument[]>;
19
19
  /** Featured grouped listings for homepage strips. */
20
20
  export declare const listFeaturedGroupedListings: (limit?: number) => Promise<GroupedListingDocument[]>;
21
+ /**
22
+ * Groups a product belongs to that are eligible to render publicly —
23
+ * powers the "You might also like" / "Same character" / etc. carousel
24
+ * on product-based listing detail pages. Deduped per request.
25
+ */
26
+ export declare const getGroupsForProduct: (productId: string) => Promise<GroupedListingDocument[]>;
27
+ /**
28
+ * Groups a product belongs to, each hydrated with its OTHER member products
29
+ * (the current product is excluded from its own group's item list) — ready
30
+ * to render directly as a public GroupedListingsCarousel section.
31
+ */
32
+ export declare const getGroupsWithItemsForProduct: (productId: string) => Promise<GroupedListingWithItems[]>;
21
33
  export interface SitemapGroupedListing {
22
34
  slug: string;
23
35
  updatedAt: Date;
@@ -3,7 +3,7 @@ import { getAdminDb } from "../../../../providers/db-firebase";
3
3
  import { normalizeError } from "../../../../errors/normalize";
4
4
  import { serverLogger } from "../../../../monitoring/server-logger";
5
5
  import { GROUPED_LISTINGS_COLLECTION, } from "../../../../features/grouped/schemas/firestore";
6
- import { productRepository } from "../../../../repositories";
6
+ import { productRepository, groupedListingsRepository } from "../../../../repositories";
7
7
  import { PRODUCT_FIELDS, CATEGORY_FIELDS } from "../../../../constants/field-names";
8
8
  import { GROUPED_LISTINGS_FEATURED_LIMIT, GROUPED_LISTINGS_PAGE_SIZE, GROUPED_LISTINGS_SITEMAP_LIMIT, } from "../../../shared/features/grouped/config";
9
9
  function mapDoc(doc) {
@@ -107,6 +107,31 @@ export async function listGroupedListings(params = {}) {
107
107
  export const listFeaturedGroupedListings = cache(async (limit = GROUPED_LISTINGS_FEATURED_LIMIT) => {
108
108
  return listGroupedListings({ featuredOnly: true, limit });
109
109
  });
110
+ /**
111
+ * Groups a product belongs to that are eligible to render publicly —
112
+ * powers the "You might also like" / "Same character" / etc. carousel
113
+ * on product-based listing detail pages. Deduped per request.
114
+ */
115
+ export const getGroupsForProduct = cache(async (productId) => {
116
+ return groupedListingsRepository.findByProductId(productId).catch((err) => {
117
+ void normalizeError(err);
118
+ serverLogger.warn("grouped-data: getGroupsForProduct failed — returning empty", { productId, error: err instanceof Error ? err.message : String(err) });
119
+ return [];
120
+ });
121
+ });
122
+ /**
123
+ * Groups a product belongs to, each hydrated with its OTHER member products
124
+ * (the current product is excluded from its own group's item list) — ready
125
+ * to render directly as a public GroupedListingsCarousel section.
126
+ */
127
+ export const getGroupsWithItemsForProduct = cache(async (productId) => {
128
+ const groups = await getGroupsForProduct(productId);
129
+ return Promise.all(groups.map(async (group) => {
130
+ const otherIds = group.productIds.filter((id) => id !== productId);
131
+ const items = await Promise.all(otherIds.map((id) => productRepository.findByIdOrSlug(id).catch(() => null)));
132
+ return { ...group, items: items.filter((p) => p !== null) };
133
+ }));
134
+ });
110
135
  export async function listSitemapGroupedListings() {
111
136
  try {
112
137
  const db = getAdminDb();
@@ -1,2 +1,2 @@
1
- export { getGroupedListingForDetail, getGroupedListingWithItems, listGroupedListings, listFeaturedGroupedListings, listSitemapGroupedListings, type GroupedListingWithItems, type ListGroupedListingsParams, type SitemapGroupedListing, } from "./data";
1
+ export { getGroupedListingForDetail, getGroupedListingWithItems, listGroupedListings, listFeaturedGroupedListings, listSitemapGroupedListings, getGroupsForProduct, getGroupsWithItemsForProduct, type GroupedListingWithItems, type ListGroupedListingsParams, type SitemapGroupedListing, } from "./data";
2
2
  export { GROUPED_LISTINGS_PAGE_SIZE, GROUPED_LISTINGS_FEATURED_LIMIT, GROUPED_LISTINGS_SITEMAP_LIMIT, } from "../../../shared/features/grouped/config";
@@ -1,2 +1,2 @@
1
- export { getGroupedListingForDetail, getGroupedListingWithItems, listGroupedListings, listFeaturedGroupedListings, listSitemapGroupedListings, } from "./data";
1
+ export { getGroupedListingForDetail, getGroupedListingWithItems, listGroupedListings, listFeaturedGroupedListings, listSitemapGroupedListings, getGroupsForProduct, getGroupsWithItemsForProduct, } from "./data";
2
2
  export { GROUPED_LISTINGS_PAGE_SIZE, GROUPED_LISTINGS_FEATURED_LIMIT, GROUPED_LISTINGS_SITEMAP_LIMIT, } from "../../../shared/features/grouped/config";
@@ -1,9 +1,16 @@
1
1
  import type { ProductDocument } from "../../../../features/products/schemas/firestore";
2
2
  import type { ListingType } from "../../../../features/products/types";
3
+ import type { ProductItem } from "../../../../features/products/types";
4
+ import type { FirestoreDocument } from "../../../../schemas/types";
5
+ import type { Review } from "../../../../features/reviews/types";
3
6
  /** Fetch a single product by slug or id, deduped per request. */
4
7
  export declare const getProductForDetail: (slugOrId: string) => Promise<ProductDocument | null>;
5
8
  /** Fetch the first page of approved reviews for a product, deduped per request. */
6
9
  export declare const getReviewsForProduct: (productId: string) => Promise<unknown[]>;
10
+ /** Firestore review doc → client Review shape. Shared by every listing-type detail page's reviews tab. */
11
+ export declare function toReview(doc: FirestoreDocument): Review;
12
+ /** Fetch + map approved reviews for a product straight to the client Review shape, deduped per request. */
13
+ export declare const getReviewItemsForProduct: (productId: string) => Promise<Review[]>;
7
14
  /** Minimal product fields needed for sitemap generation. */
8
15
  export interface SitemapProduct {
9
16
  slugOrId: string;
@@ -13,3 +20,19 @@ export interface SitemapProduct {
13
20
  }
14
21
  /** List all published products for sitemap generation. */
15
22
  export declare function listSitemapProducts(): Promise<SitemapProduct[]>;
23
+ export interface RelatedItemsResult {
24
+ relatedItems: ProductItem[];
25
+ relatedByBrand: ProductItem[];
26
+ relatedByTags: ProductItem[];
27
+ relatedByStore: ProductItem[];
28
+ }
29
+ /** Firestore doc → card-grid shape. Shared by every related/similar-items carousel. */
30
+ export declare function toProductItem(doc: FirestoreDocument): ProductItem;
31
+ /**
32
+ * Same 4-signal related-items computation used across every listing-type
33
+ * detail page (category / brand / tag-overlap / same-store), each excluding
34
+ * invalid states for that item's own listing type. Extracted from
35
+ * ProductDetailPageView so pre-order/prize-draw/classified/digital-code/live
36
+ * pages can reuse it instead of having no related section at all.
37
+ */
38
+ export declare const computeRelatedItems: (product: ProductDocument) => Promise<RelatedItemsResult>;
@@ -7,6 +7,7 @@ import { serverLogger } from "../../../../monitoring/server-logger";
7
7
  import { PRODUCT_COLLECTION } from "../../../../features/products/schemas/firestore";
8
8
  import { PRODUCTS_SITEMAP_LIMIT } from "../../../shared/features/products/config";
9
9
  import { PRODUCT_FIELDS } from "../../../../constants/field-names";
10
+ import { SIEVE_OP, sieveAnd, sieveFilter, sortBy } from "../../../../utils/sieve-builder";
10
11
  // ---------------------------------------------------------------------------
11
12
  // Request-scoped cache — each function is deduplicated per RSC render tree.
12
13
  // Both generateMetadata() and the page component get the same promise.
@@ -23,6 +24,38 @@ export const getProductForDetail = cache(async (slugOrId) => {
23
24
  export const getReviewsForProduct = cache(async (productId) => {
24
25
  return reviewRepository.findApprovedByProduct(productId).catch(() => []);
25
26
  });
27
+ /** Firestore review doc → client Review shape. Shared by every listing-type detail page's reviews tab. */
28
+ export function toReview(doc) {
29
+ const images = Array.isArray(doc.images) ? doc.images : undefined;
30
+ const createdAt = doc.createdAt instanceof Date
31
+ ? doc.createdAt.toISOString()
32
+ : typeof doc.createdAt === "string"
33
+ ? doc.createdAt
34
+ : undefined;
35
+ const rawRating = typeof doc.rating === "number" ? doc.rating : 3;
36
+ const rating = Math.min(5, Math.max(1, Math.round(rawRating)));
37
+ return {
38
+ id: String(doc.id ?? ""),
39
+ productId: String(doc.productId ?? ""),
40
+ userId: String(doc.userId ?? ""),
41
+ userName: String(doc.userName ?? "Anonymous"),
42
+ userAvatar: typeof doc.userAvatar === "string" ? doc.userAvatar : undefined,
43
+ rating,
44
+ title: typeof doc.title === "string" ? doc.title : undefined,
45
+ comment: typeof doc.comment === "string" ? doc.comment : undefined,
46
+ images,
47
+ status: doc.status ?? "approved",
48
+ helpfulCount: typeof doc.helpfulCount === "number" ? doc.helpfulCount : undefined,
49
+ verified: doc.verified === true,
50
+ featured: doc.featured === true,
51
+ createdAt,
52
+ };
53
+ }
54
+ /** Fetch + map approved reviews for a product straight to the client Review shape, deduped per request. */
55
+ export const getReviewItemsForProduct = cache(async (productId) => {
56
+ const docs = await reviewRepository.findApprovedByProduct(productId).catch(() => []);
57
+ return docs.map(toReview);
58
+ });
26
59
  /** List all published products for sitemap generation. */
27
60
  export async function listSitemapProducts() {
28
61
  try {
@@ -54,3 +87,129 @@ export async function listSitemapProducts() {
54
87
  return [];
55
88
  }
56
89
  }
90
+ // ---------------------------------------------------------------------------
91
+ // Related items — same 4-signal computation shared by every listing-type
92
+ // detail page (category / brand / tag-overlap / same-store).
93
+ // ---------------------------------------------------------------------------
94
+ const RELATED_QUERY_PAGE_SIZE = 9;
95
+ const RELATED_DISPLAY_LIMIT = 8;
96
+ /** Firestore doc → card-grid shape. Shared by every related/similar-items carousel. */
97
+ export function toProductItem(doc) {
98
+ return {
99
+ id: String(doc.id ?? ""),
100
+ title: String(doc.title ?? doc.name ?? ""),
101
+ price: typeof doc.price === "number" ? doc.price : 0,
102
+ originalPrice: typeof doc.originalPrice === "number" ? doc.originalPrice : undefined,
103
+ mainImage: Array.isArray(doc.images)
104
+ ? doc.images[0]
105
+ : typeof doc.mainImage === "string"
106
+ ? doc.mainImage
107
+ : undefined,
108
+ status: doc.status ?? "published",
109
+ slug: typeof doc.slug === "string" ? doc.slug : undefined,
110
+ storeName: typeof doc.storeName === "string" ? doc.storeName : undefined,
111
+ rating: typeof doc.rating === "number" ? doc.rating : undefined,
112
+ reviewCount: typeof doc.reviewCount === "number" ? doc.reviewCount : undefined,
113
+ // Without this, every related-item card silently linked to the standard
114
+ // PDP regardless of its real type (pluginFor() defaults to "standard"
115
+ // when listingType is missing) — same bug class as the Phase 1 routing
116
+ // fix, one hop further downstream in the related-items card link path.
117
+ listingType: doc.listingType ?? undefined,
118
+ };
119
+ }
120
+ /**
121
+ * Excludes the current product plus any listing-type-specific "invalid to
122
+ * surface as related" state: sold/archived/out-of-stock/draft, ended
123
+ * auctions, closed prize-draws, sold-out pre-orders, depleted digital-code
124
+ * pools.
125
+ */
126
+ function isValidRelatedItem(r, currentProductId, now) {
127
+ if (r.id === currentProductId)
128
+ return false;
129
+ const s = r.status;
130
+ if (s && ["sold", "out_of_stock", "archived", "discontinued", "draft"].includes(s))
131
+ return false;
132
+ if (r.isSold === true)
133
+ return false;
134
+ if (r.availableQuantity === 0)
135
+ return false;
136
+ if (r.listingType === "auction" && r.auctionEndDate) {
137
+ const end = r.auctionEndDate;
138
+ const endDate = typeof end.toDate === "function"
139
+ ? end.toDate()
140
+ : end instanceof Date
141
+ ? end
142
+ : new Date(String(end));
143
+ if (endDate <= now)
144
+ return false;
145
+ }
146
+ if (r.listingType === "prize-draw" && r.prizeRevealStatus === "closed")
147
+ return false;
148
+ if (r.listingType === "pre-order" &&
149
+ typeof r.preOrderMaxQuantity === "number" &&
150
+ typeof r.preOrderCurrentCount === "number" &&
151
+ r.preOrderCurrentCount >= r.preOrderMaxQuantity) {
152
+ return false;
153
+ }
154
+ if (r.listingType === "digital-code" && r.codesAvailable === 0)
155
+ return false;
156
+ return true;
157
+ }
158
+ function toRelatedItems(docs, currentProductId) {
159
+ const now = new Date();
160
+ return docs
161
+ .filter((r) => isValidRelatedItem(r, currentProductId, now))
162
+ .slice(0, RELATED_DISPLAY_LIMIT)
163
+ .map(toProductItem);
164
+ }
165
+ /**
166
+ * Same 4-signal related-items computation used across every listing-type
167
+ * detail page (category / brand / tag-overlap / same-store), each excluding
168
+ * invalid states for that item's own listing type. Extracted from
169
+ * ProductDetailPageView so pre-order/prize-draw/classified/digital-code/live
170
+ * pages can reuse it instead of having no related section at all.
171
+ */
172
+ export const computeRelatedItems = cache(async (product) => {
173
+ const p = product;
174
+ const primaryCategorySlug = Array.isArray(p.categorySlugs) ? p.categorySlugs[0] : undefined;
175
+ const brand = typeof p.brand === "string" ? p.brand : undefined;
176
+ const tags = Array.isArray(p.tags) ? p.tags : [];
177
+ const storeId = typeof p.storeId === "string" ? p.storeId : undefined;
178
+ const currentProductId = product.id;
179
+ const [sameCategoryDocs, sameBrandDocs, tagOverlapDocs, sameStoreDocs] = await Promise.all([
180
+ primaryCategorySlug
181
+ ? productRepository.findByCategory(primaryCategorySlug).catch(() => [])
182
+ : Promise.resolve([]),
183
+ brand
184
+ ? productRepository
185
+ .list({
186
+ filters: sieveAnd(sieveFilter("status", SIEVE_OP.EQ, "published"), sieveFilter("brand", SIEVE_OP.EQ, brand)),
187
+ sorts: sortBy("createdAt", "DESC"),
188
+ page: 1,
189
+ pageSize: RELATED_QUERY_PAGE_SIZE,
190
+ })
191
+ .then((r) => r.items)
192
+ .catch(() => [])
193
+ : Promise.resolve([]),
194
+ tags.length > 0
195
+ ? productRepository.findByTagsOverlap(tags, RELATED_QUERY_PAGE_SIZE).catch(() => [])
196
+ : Promise.resolve([]),
197
+ storeId
198
+ ? productRepository
199
+ .list({
200
+ filters: sieveAnd(sieveFilter("status", SIEVE_OP.EQ, "published"), sieveFilter("storeId", SIEVE_OP.EQ, storeId)),
201
+ sorts: sortBy("createdAt", "DESC"),
202
+ page: 1,
203
+ pageSize: RELATED_QUERY_PAGE_SIZE,
204
+ })
205
+ .then((r) => r.items)
206
+ .catch(() => [])
207
+ : Promise.resolve([]),
208
+ ]);
209
+ return {
210
+ relatedItems: toRelatedItems(sameCategoryDocs, currentProductId),
211
+ relatedByBrand: toRelatedItems(sameBrandDocs, currentProductId),
212
+ relatedByTags: toRelatedItems(tagOverlapDocs, currentProductId),
213
+ relatedByStore: toRelatedItems(sameStoreDocs, currentProductId),
214
+ };
215
+ });
@@ -1,4 +1,4 @@
1
- export { getProductForDetail, getReviewsForProduct, listSitemapProducts, type SitemapProduct, } from "./data";
1
+ export { getProductForDetail, getReviewsForProduct, listSitemapProducts, type SitemapProduct, computeRelatedItems, toProductItem, toReview, getReviewItemsForProduct, type RelatedItemsResult, } from "./data";
2
2
  export { assertProductOwnership, assertStatusTransition, assertInStock, effectivePrice, isAvailableForPurchase, } from "./service";
3
3
  export { createProductAction, createAuctionAction, createPreOrderAction, updateProductAction, deleteProductAction, setProductStatusAction, setProductFeaturedAction, } from "./actions";
4
4
  export { PRODUCTS_PAGE_SIZE, PRODUCTS_FEATURED_LIMIT, PRODUCTS_RELATED_LIMIT, PRODUCTS_SITEMAP_LIMIT } from "../../../shared/features/products/config";
@@ -1,4 +1,4 @@
1
- export { getProductForDetail, getReviewsForProduct, listSitemapProducts, } from "./data";
1
+ export { getProductForDetail, getReviewsForProduct, listSitemapProducts, computeRelatedItems, toProductItem, toReview, getReviewItemsForProduct, } from "./data";
2
2
  export { assertProductOwnership, assertStatusTransition, assertInStock, effectivePrice, isAvailableForPurchase, } from "./service";
3
3
  export { createProductAction, createAuctionAction, createPreOrderAction, updateProductAction, deleteProductAction, setProductStatusAction, setProductFeaturedAction, } from "./actions";
4
4
  export { PRODUCTS_PAGE_SIZE, PRODUCTS_FEATURED_LIMIT, PRODUCTS_RELATED_LIMIT, PRODUCTS_SITEMAP_LIMIT } from "../../../shared/features/products/config";
@@ -7,12 +7,13 @@ export function buildRobots({ siteUrl }) {
7
7
  disallow: [
8
8
  "/admin/",
9
9
  "/api/", // audit-hardcoded-api-routes-ok: robots.txt disallow prefix, not an endpoint reference
10
- "/seller/",
10
+ "/store/",
11
11
  "/user/",
12
12
  "/auth/",
13
13
  "/checkout/",
14
14
  "/cart/",
15
- "/demo/",
15
+ "/wishlist",
16
+ "/preview/",
16
17
  "/track/",
17
18
  "/unauthorized/",
18
19
  "/_next/",