@mohasinac/appkit 2.7.16 → 2.7.18

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 (105) hide show
  1. package/dist/_internal/server/features/auctions/actions.js +1 -1
  2. package/dist/_internal/server/features/blog/actions.js +2 -2
  3. package/dist/_internal/server/features/brands/actions.js +2 -2
  4. package/dist/_internal/server/features/bundles/actions.d.ts +15 -0
  5. package/dist/_internal/server/features/bundles/actions.js +48 -0
  6. package/dist/_internal/server/features/bundles/index.d.ts +1 -0
  7. package/dist/_internal/server/features/bundles/index.js +1 -0
  8. package/dist/_internal/server/features/cart/actions.js +3 -3
  9. package/dist/_internal/server/features/checkout/actions.d.ts +4 -0
  10. package/dist/_internal/server/features/checkout/actions.js +23 -12
  11. package/dist/_internal/server/features/events/actions.js +3 -3
  12. package/dist/_internal/server/features/orders/actions.js +4 -4
  13. package/dist/_internal/server/features/payments/data.d.ts +5 -3
  14. package/dist/_internal/server/features/payments/data.js +13 -8
  15. package/dist/_internal/server/features/payments/index.d.ts +1 -1
  16. package/dist/_internal/server/features/payments/index.js +1 -1
  17. package/dist/_internal/server/features/pre-orders/actions.js +1 -1
  18. package/dist/_internal/server/features/products/actions.js +6 -6
  19. package/dist/_internal/server/features/promotions/actions.js +3 -3
  20. package/dist/_internal/shared/features/categories/bundle-config.d.ts +24 -0
  21. package/dist/_internal/shared/features/categories/bundle-config.js +28 -0
  22. package/dist/_internal/shared/features/categories/bundle-copy.d.ts +7 -0
  23. package/dist/_internal/shared/features/categories/bundle-copy.js +9 -0
  24. package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +69 -19
  25. package/dist/_internal/shared/features/categories/bundle-schemas.js +20 -2
  26. package/dist/_internal/shared/features/checkout/config.d.ts +1 -1
  27. package/dist/_internal/shared/features/checkout/config.js +1 -1
  28. package/dist/_internal/shared/features/payments/config.d.ts +2 -1
  29. package/dist/_internal/shared/features/payments/config.js +2 -1
  30. package/dist/_internal/shared/features/products/schema.d.ts +8 -8
  31. package/dist/constants/field-names.d.ts +8 -1
  32. package/dist/constants/field-names.js +9 -2
  33. package/dist/constants/table-keys.d.ts +3 -0
  34. package/dist/constants/table-keys.js +3 -0
  35. package/dist/features/admin/components/AdminSiteSettingsView.js +17 -15
  36. package/dist/features/admin/components/DashboardStats.js +20 -12
  37. package/dist/features/admin/components/analytics/AdminAnalyticsCharts.js +18 -2
  38. package/dist/features/admin/components/analytics/AdminStatCard.d.ts +7 -1
  39. package/dist/features/admin/components/analytics/AdminStatCard.js +13 -2
  40. package/dist/features/admin/schemas/firestore.d.ts +18 -4
  41. package/dist/features/admin/schemas/firestore.js +11 -1
  42. package/dist/features/categories/components/BundleBuyNowCta.d.ts +3 -1
  43. package/dist/features/categories/components/BundleBuyNowCta.js +3 -3
  44. package/dist/features/categories/components/BundleDynamicRuleEditor.d.ts +1 -1
  45. package/dist/features/categories/components/BundleDynamicRuleEditor.js +2 -1
  46. package/dist/features/categories/components/BundlesListView.d.ts +4 -1
  47. package/dist/features/categories/components/BundlesListView.js +2 -2
  48. package/dist/features/categories/components/CategoryBundlesListing.d.ts +4 -1
  49. package/dist/features/categories/components/CategoryBundlesListing.js +9 -8
  50. package/dist/features/categories/schemas/firestore.d.ts +29 -1
  51. package/dist/features/checkout/actions/checkout-actions.d.ts +11 -0
  52. package/dist/features/checkout/actions/checkout-actions.js +26 -0
  53. package/dist/features/filters/FilterFacetSection.d.ts +6 -1
  54. package/dist/features/filters/FilterFacetSection.js +25 -2
  55. package/dist/features/homepage/components/FeaturedBundlesSection.d.ts +4 -1
  56. package/dist/features/homepage/components/FeaturedBundlesSection.js +9 -8
  57. package/dist/features/orders/actions/refund-actions.js +10 -7
  58. package/dist/features/orders/schemas/firestore.d.ts +16 -13
  59. package/dist/features/orders/schemas/firestore.js +6 -5
  60. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +1 -1
  61. package/dist/features/pre-orders/components/PreOrdersIndexListing.js +2 -1
  62. package/dist/features/products/components/AuctionsIndexListing.js +2 -1
  63. package/dist/features/products/components/ProductDetailPageView.js +3 -1
  64. package/dist/features/products/components/ProductDetailView.d.ts +3 -1
  65. package/dist/features/products/components/ProductDetailView.js +2 -2
  66. package/dist/features/products/components/ProductFilters.d.ts +10 -4
  67. package/dist/features/products/components/ProductFilters.js +20 -16
  68. package/dist/features/products/components/ProductGrid.js +3 -3
  69. package/dist/features/products/components/ProductsIndexListing.js +2 -1
  70. package/dist/features/products/constants/product-features.constants.js +5 -1
  71. package/dist/features/products/schemas/index.d.ts +2 -2
  72. package/dist/features/products/schemas/product-features.d.ts +1 -1
  73. package/dist/features/products/schemas/product-features.validators.d.ts +10 -10
  74. package/dist/features/products/schemas/product-features.validators.js +4 -0
  75. package/dist/features/seller/components/SellerCouponEditorView.js +2 -2
  76. package/dist/features/seller/components/analytics/SellerAnalyticsStats.js +10 -4
  77. package/dist/features/shell/FormShell.js +1 -1
  78. package/dist/features/stores/components/StoreBundlesPageView.d.ts +4 -1
  79. package/dist/features/stores/components/StoreBundlesPageView.js +2 -2
  80. package/dist/features/stores/components/StoresIndexListing.js +2 -1
  81. package/dist/features/wishlist/hooks/useWishlistCount.d.ts +2 -2
  82. package/dist/features/wishlist/hooks/useWishlistCount.js +5 -8
  83. package/dist/providers/db-firebase/admin-app-lite.js +8 -2
  84. package/dist/providers/db-firebase/admin.js +8 -2
  85. package/dist/seed/addresses-seed-data.js +24 -7
  86. package/dist/seed/cart-seed-data.d.ts +10 -10
  87. package/dist/seed/cart-seed-data.js +39 -2
  88. package/dist/seed/grouped-listings-seed-data.js +47 -0
  89. package/dist/seed/orders-seed-data.js +150 -0
  90. package/dist/seed/products-standard-seed-data.js +42 -0
  91. package/dist/seed/site-settings-seed-data.js +11 -4
  92. package/dist/seed/users-seed-data.js +1 -1
  93. package/dist/seed/wishlists-seed-data.js +7 -0
  94. package/dist/server-entry.d.ts +2 -1
  95. package/dist/server-entry.js +2 -1
  96. package/dist/server.d.ts +2 -1
  97. package/dist/server.js +3 -1
  98. package/dist/tailwind-utilities.css +1 -1
  99. package/dist/ui/components/Breadcrumb.style.css +1 -1
  100. package/dist/ui/components/ListingLayout.style.css +1 -1
  101. package/dist/ui/components/Select.style.css +1 -1
  102. package/dist/ui/components/SideDrawer.style.css +55 -12
  103. package/dist/ui/components/Typography.style.css +2 -2
  104. package/dist/ui/rich-text/RichText.style.css +4 -5
  105. package/package.json +1 -1
@@ -8,7 +8,7 @@ export async function placeBidAction(input) {
8
8
  const user = await requireRoleUser(["buyer", "seller", "admin"]);
9
9
  const parsed = placeBidSchema.safeParse(input);
10
10
  if (!parsed.success)
11
- throw new ValidationError(parsed.error.errors[0]?.message ?? "Invalid bid input");
11
+ throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid bid input");
12
12
  const { auctionId, amount } = parsed.data;
13
13
  const product = await assertAuctionActive(auctionId);
14
14
  assertNotAuctionOwner(product, user.uid);
@@ -8,7 +8,7 @@ export async function createBlogPostAction(input) {
8
8
  const user = await requireRoleUser(["admin", "moderator"]);
9
9
  const parsed = blogPostInputSchema.safeParse(input);
10
10
  if (!parsed.success)
11
- throw new ValidationError(parsed.error.errors[0]?.message ?? "Invalid blog post input");
11
+ throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid blog post input");
12
12
  await assertBlogSlugUnique(parsed.data.slug);
13
13
  const readTime = computeReadTime(parsed.data.content);
14
14
  return blogRepository.createWithId(parsed.data.slug, {
@@ -25,7 +25,7 @@ export async function updateBlogPostAction(postId, input) {
25
25
  const post = await assertBlogPostExists(postId);
26
26
  const parsed = blogPostUpdateSchema.safeParse(input);
27
27
  if (!parsed.success)
28
- throw new ValidationError(parsed.error.errors[0]?.message ?? "Invalid blog post input");
28
+ throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid blog post input");
29
29
  const updates = { ...parsed.data };
30
30
  if (parsed.data.content)
31
31
  updates.readTimeMinutes = computeReadTime(parsed.data.content);
@@ -40,7 +40,7 @@ export async function createBrandAction(input) {
40
40
  await requireRoleUser("admin");
41
41
  const parsed = brandInputSchema.safeParse(input);
42
42
  if (!parsed.success)
43
- throw new ValidationError(parsed.error.errors[0]?.message ?? "Invalid brand input");
43
+ throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid brand input");
44
44
  await assertBrandSlugUnique(parsed.data.slug);
45
45
  const fields = brandInputToCategoryFields(parsed.data);
46
46
  return categoriesRepository.createWithId(parsed.data.slug, {
@@ -84,7 +84,7 @@ export async function updateBrandAction(brandId, input) {
84
84
  await assertBrandExists(brandId);
85
85
  const parsed = brandUpdateSchema.safeParse(input);
86
86
  if (!parsed.success)
87
- throw new ValidationError(parsed.error.errors[0]?.message ?? "Invalid brand input");
87
+ throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid brand input");
88
88
  return categoriesRepository.update(brandId, brandInputToCategoryFields(parsed.data));
89
89
  }
90
90
  export async function deleteBrandAction(brandId) {
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Bundle server actions — appkit internal.
3
+ *
4
+ * `addBundleToCartAction` is the shared business logic called by the
5
+ * consumer's `"use server"` wrapper. The consumer is responsible for:
6
+ * 1. Resolving the authenticated userId (throw AuthError when unauthenticated
7
+ * so BundleBuyNowCta's isAuthError guard shows the login modal).
8
+ * 2. Calling redirect(ROUTES.PUBLIC.CHECKOUT) after this returns.
9
+ *
10
+ * Bundles are categoryType:"bundle" rows — they bypass the regular
11
+ * addItemToCart capability gate and write directly to cartRepository with
12
+ * bundleCategorySlug + bundleProductIds so checkout's bundle-expansion
13
+ * helpers can fan out stock decrements to member products.
14
+ */
15
+ export declare function addBundleToCartAction(userId: string, bundleSlug: string): Promise<void>;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Bundle server actions — appkit internal.
3
+ *
4
+ * `addBundleToCartAction` is the shared business logic called by the
5
+ * consumer's `"use server"` wrapper. The consumer is responsible for:
6
+ * 1. Resolving the authenticated userId (throw AuthError when unauthenticated
7
+ * so BundleBuyNowCta's isAuthError guard shows the login modal).
8
+ * 2. Calling redirect(ROUTES.PUBLIC.CHECKOUT) after this returns.
9
+ *
10
+ * Bundles are categoryType:"bundle" rows — they bypass the regular
11
+ * addItemToCart capability gate and write directly to cartRepository with
12
+ * bundleCategorySlug + bundleProductIds so checkout's bundle-expansion
13
+ * helpers can fan out stock decrements to member products.
14
+ */
15
+ import { NotFoundError, ValidationError } from "../../../../errors";
16
+ import { categoriesRepository } from "../../../../repositories";
17
+ import { cartRepository } from "../../../../features/cart/repository/cart.repository";
18
+ import { getDefaultCurrency } from "../../../../core/baseline-resolver";
19
+ export async function addBundleToCartAction(userId, bundleSlug) {
20
+ if (!userId)
21
+ throw new ValidationError("User id is required");
22
+ if (!bundleSlug)
23
+ throw new ValidationError("Bundle slug is required");
24
+ const bundle = await categoriesRepository.findBySlugAndType(bundleSlug, "bundle");
25
+ if (!bundle)
26
+ throw new NotFoundError("Bundle not found");
27
+ if (!bundle.isActive)
28
+ throw new ValidationError("Bundle is not available");
29
+ if (bundle.bundleStockStatus === "out_of_stock") {
30
+ throw new ValidationError("Bundle is currently out of stock");
31
+ }
32
+ if (!bundle.bundlePriceInPaise || bundle.bundlePriceInPaise < 1) {
33
+ throw new ValidationError("Bundle price is not configured");
34
+ }
35
+ await cartRepository.addItem(userId, {
36
+ productId: bundle.id,
37
+ productTitle: bundle.name,
38
+ productImage: bundle.display?.coverImage ?? "",
39
+ price: bundle.bundlePriceInPaise,
40
+ currency: getDefaultCurrency(),
41
+ quantity: 1,
42
+ storeId: bundle.createdByStoreId ?? "",
43
+ storeName: bundle.createdByStoreName ?? "",
44
+ listingType: "standard",
45
+ bundleCategorySlug: bundle.slug,
46
+ bundleProductIds: bundle.bundleProductIds ?? [],
47
+ });
48
+ }
@@ -1,3 +1,4 @@
1
1
  export { getBundleForDetail, listBundleMembers, listFeaturedBundles, type BundleDataOptions, } from "./data";
2
2
  export { buildBundleMetadata, type BundleMetadataOptions } from "./metadata";
3
3
  export { renderBundleOg, renderBundleOgImage, type BundleOgData, } from "./og";
4
+ export { addBundleToCartAction } from "./actions";
@@ -2,3 +2,4 @@ export { getBundleForDetail, listBundleMembers, listFeaturedBundles, } from "./d
2
2
  export { buildBundleMetadata } from "./metadata";
3
3
  // S-SBUNI-4 2026-05-13 — bundle OG renderer.
4
4
  export { renderBundleOg, renderBundleOgImage, } from "./og";
5
+ export { addBundleToCartAction } from "./actions";
@@ -8,7 +8,7 @@ export async function addToCartAction(input) {
8
8
  const user = await requireRoleUser(["buyer", "seller", "admin"]);
9
9
  const parsed = addToCartSchema.safeParse(input);
10
10
  if (!parsed.success)
11
- throw new ValidationError(parsed.error.errors[0]?.message ?? "Invalid cart input");
11
+ throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid cart input");
12
12
  await upsertCartItem(user.uid, parsed.data);
13
13
  return { success: true };
14
14
  }
@@ -16,7 +16,7 @@ export async function removeFromCartAction(input) {
16
16
  const user = await requireRoleUser(["buyer", "seller", "admin"]);
17
17
  const parsed = removeFromCartSchema.safeParse(input);
18
18
  if (!parsed.success)
19
- throw new ValidationError(parsed.error.errors[0]?.message ?? "Invalid input");
19
+ throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid input");
20
20
  await cartRepository.removeItem(user.uid, parsed.data.productId);
21
21
  return { success: true };
22
22
  }
@@ -29,7 +29,7 @@ export async function mergeGuestCartAction(input) {
29
29
  const user = await requireRoleUser(["buyer", "seller", "admin"]);
30
30
  const parsed = mergeGuestCartSchema.safeParse(input);
31
31
  if (!parsed.success)
32
- throw new ValidationError(parsed.error.errors[0]?.message ?? "Invalid cart data");
32
+ throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid cart data");
33
33
  await mergeGuestItems(user.uid, parsed.data.guestItems);
34
34
  return { success: true };
35
35
  }
@@ -16,6 +16,10 @@ export interface CreateCheckoutOrderInput {
16
16
  paymentMethod: CheckoutPaymentMethod;
17
17
  notes?: string;
18
18
  excludedProductIds?: string[];
19
+ /** When true this is an admin test order — OTP consent is pre-granted and the order is created paid/processing. */
20
+ adminBypass?: boolean;
21
+ /** UID of the admin who triggered the bypass. Set when adminBypass is true. */
22
+ adminBypassBy?: string;
19
23
  }
20
24
  /**
21
25
  * Place order(s) from the user's cart in a single Firestore transaction.
@@ -76,7 +76,7 @@ function emitOrderPlacedNotifications(args) {
76
76
  * auth + rate-limiting before calling this and supply the resolved user fields.
77
77
  */
78
78
  export async function createCheckoutOrderAction(input) {
79
- const { userId: uid, userName, userEmail, addressId, paymentMethod, notes, excludedProductIds = [], } = input;
79
+ const { userId: uid, userName, userEmail, addressId, paymentMethod, notes, excludedProductIds = [], adminBypass = false, adminBypassBy, } = input;
80
80
  const siteSettings = await siteSettingsRepository.getSingleton();
81
81
  const commissions = siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS;
82
82
  const cart = await unitOfWork.carts.getOrCreate(uid);
@@ -147,7 +147,7 @@ export async function createCheckoutOrderAction(input) {
147
147
  const reason = !otpData ? "otp_not_verified" : "consent_expired";
148
148
  throw Object.assign(new ApiError(403, "Order verification required. Please complete OTP verification before placing this order."), { _failReason: reason });
149
149
  }
150
- const emailOtpUsed = otpData.verifiedVia !== "sms";
150
+ const emailOtpUsed = otpData.verifiedVia !== "sms" && otpData.verifiedVia !== "admin_bypass";
151
151
  // SB-UNI-5 2026-05-13 — bundle-aware stock fan-out. Build the unique
152
152
  // product-id set across the whole cart (regular items + each bundle's
153
153
  // members) and fetch each one ONCE. Validation walks per cart-line and
@@ -249,6 +249,11 @@ export async function createCheckoutOrderAction(input) {
249
249
  }
250
250
  const appliedCoupons = cart.appliedCoupons ?? [];
251
251
  const orderGroups = splitCartIntoOrderGroups(available);
252
+ // Admin-bypass orders are immediately paid and processing; generate a shared
253
+ // transaction reference for all orders in this checkout batch.
254
+ const adminBatchId = adminBypass
255
+ ? `ADMIN-${Date.now().toString(36).toUpperCase()}-${Math.random().toString(36).slice(2, 6).toUpperCase()}`
256
+ : undefined;
252
257
  const orderIds = [];
253
258
  let total = 0;
254
259
  const emailsToSend = [];
@@ -389,14 +394,16 @@ export async function createCheckoutOrderAction(input) {
389
394
  items: orderItems,
390
395
  orderType,
391
396
  offerId: firstItem.offerId ?? undefined,
392
- status: OrderStatusValues.PENDING,
393
- paymentStatus: PaymentStatusValues.PENDING,
394
- paymentMethod,
397
+ status: adminBypass ? OrderStatusValues.PROCESSING : OrderStatusValues.PENDING,
398
+ paymentStatus: adminBypass ? PaymentStatusValues.PAID : PaymentStatusValues.PENDING,
399
+ paymentMethod: adminBypass ? PaymentMethodValues.ADMIN_BYPASS : paymentMethod,
400
+ paymentId: adminBatchId,
401
+ adminBypassBy: adminBypassBy,
395
402
  shippingAddress,
396
403
  notes,
397
404
  shippingFee: shippingFee > 0 ? shippingFee : undefined,
398
- depositAmount,
399
- codRemainingAmount,
405
+ depositAmount: adminBypass ? undefined : depositAmount,
406
+ codRemainingAmount: adminBypass ? undefined : codRemainingAmount,
400
407
  couponCode: appliedDiscounts[0]?.code,
401
408
  couponDiscount: couponDiscount > 0 ? couponDiscount : undefined,
402
409
  appliedDiscounts: appliedDiscounts.length > 0 ? appliedDiscounts : undefined,
@@ -429,7 +436,7 @@ export async function createCheckoutOrderAction(input) {
429
436
  buyerName: userName,
430
437
  storeOwnerId,
431
438
  productLabel: orderItems.length > 1 ? `${orderItems.length} items` : firstItem.productTitle,
432
- paid: false,
439
+ paid: adminBypass,
433
440
  });
434
441
  }
435
442
  if (couponUsageAccumulator.size > 0) {
@@ -487,7 +494,8 @@ export async function attachPaymentAction(input) {
487
494
  export async function verifyAndPlaceRazorpayOrderAction(input) {
488
495
  const { userId: uid, userName, userEmail, razorpay_order_id, razorpay_payment_id, razorpay_signature, addressId, notes, } = input;
489
496
  const siteSettings = await siteSettingsRepository.getSingleton();
490
- const razorpayFeePercent = siteSettings?.commissions?.razorpayFeePercent ?? 5;
497
+ const platformFeePercent = siteSettings?.commissions?.platformFeePercent ?? 5;
498
+ const gstPercent = siteSettings?.commissions?.gstPercent ?? 18;
491
499
  const commissions = siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS;
492
500
  const isValid = await verifyPaymentSignatureWithKeys({
493
501
  razorpay_order_id,
@@ -596,8 +604,9 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
596
604
  const unit = isBundle ? item.price : product.price;
597
605
  return sum + unit * item.quantity;
598
606
  }, 0);
599
- const expectedPlatformFee = Math.round(cartSubtotalRs * (razorpayFeePercent / 100) * 100) / 100;
600
- const expectedPaymentAmountRs = cartSubtotalRs + expectedPlatformFee;
607
+ const expectedPlatformFee = Math.round(cartSubtotalRs * (platformFeePercent / 100) * 100) / 100;
608
+ const expectedGstOnFee = Math.round(expectedPlatformFee * (gstPercent / 100) * 100) / 100;
609
+ const expectedPaymentAmountRs = cartSubtotalRs + expectedPlatformFee + expectedGstOnFee;
601
610
  const rzpOrderRecord = await fetchRazorpayOrder(razorpay_order_id);
602
611
  const paidAmountRs = paiseToRupees(rzpOrderRecord.amount);
603
612
  if (paidAmountRs < expectedPaymentAmountRs - 1) {
@@ -684,7 +693,9 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
684
693
  }
685
694
  }
686
695
  couponDiscount = Math.min(couponDiscount, groupTotal);
687
- const platformFee = Math.round(groupTotal * (razorpayFeePercent / 100) * 100) / 100;
696
+ const rawPlatformFee = Math.round(groupTotal * (platformFeePercent / 100) * 100) / 100;
697
+ const gstOnFee = Math.round(rawPlatformFee * (gstPercent / 100) * 100) / 100;
698
+ const platformFee = rawPlatformFee + gstOnFee;
688
699
  const orderTotal = Math.max(0, groupTotal - couponDiscount) + shippingFee;
689
700
  total += orderTotal;
690
701
  // S-SBUNI-RULES 2026-05-13 — order-item decoration via rule registry.
@@ -9,7 +9,7 @@ export async function createEventAction(input) {
9
9
  const user = await requireRoleUser(["admin", "moderator"]);
10
10
  const parsed = eventInputSchema.safeParse(input);
11
11
  if (!parsed.success)
12
- throw new ValidationError(parsed.error.errors[0]?.message ?? "Invalid event input");
12
+ throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid event input");
13
13
  return eventRepository.createEvent({
14
14
  ...parsed.data,
15
15
  createdBy: user.uid,
@@ -24,14 +24,14 @@ export async function updateEventAction(eventId, input) {
24
24
  throw new EventNotFoundError(eventId);
25
25
  const parsed = eventUpdateSchema.safeParse(input);
26
26
  if (!parsed.success)
27
- throw new ValidationError(parsed.error.errors[0]?.message ?? "Invalid event input");
27
+ throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid event input");
28
28
  return eventRepository.updateEvent(eventId, parsed.data);
29
29
  }
30
30
  export async function registerForEventAction(input) {
31
31
  const user = await requireRoleUser(["buyer", "seller", "admin"]);
32
32
  const parsed = registerForEventSchema.safeParse(input);
33
33
  if (!parsed.success)
34
- throw new ValidationError(parsed.error.errors[0]?.message ?? "Invalid registration input");
34
+ throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid registration input");
35
35
  const { eventId } = parsed.data;
36
36
  await assertEventActive(eventId);
37
37
  // Check if already registered
@@ -9,7 +9,7 @@ export async function createOrderAction(input) {
9
9
  const user = await requireRoleUser(["buyer", "seller", "admin"]);
10
10
  const parsed = createOrderSchema.safeParse(input);
11
11
  if (!parsed.success)
12
- throw new ValidationError(parsed.error.errors[0]?.message ?? "Invalid order input");
12
+ throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid order input");
13
13
  return orderRepository.create({
14
14
  ...parsed.data,
15
15
  userId: user.uid,
@@ -20,7 +20,7 @@ export async function cancelOrderAction(input) {
20
20
  const user = await requireRoleUser(["buyer", "seller", "admin"]);
21
21
  const parsed = cancelOrderSchema.safeParse(input);
22
22
  if (!parsed.success)
23
- throw new ValidationError(parsed.error.errors[0]?.message ?? "Invalid input");
23
+ throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid input");
24
24
  await assertOrderCancellable(parsed.data.orderId, user.uid);
25
25
  return orderRepository.cancelOrder(parsed.data.orderId, parsed.data.reason ?? "Cancelled by user");
26
26
  }
@@ -28,7 +28,7 @@ export async function requestReturnAction(input) {
28
28
  const user = await requireRoleUser(["buyer", "seller", "admin"]);
29
29
  const parsed = cancelOrderSchema.safeParse(input);
30
30
  if (!parsed.success)
31
- throw new ValidationError(parsed.error.errors[0]?.message ?? "Invalid input");
31
+ throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid input");
32
32
  await assertReturnWindowOpen(parsed.data.orderId, user.uid);
33
33
  return orderRepository.updateStatus(parsed.data.orderId, "return_requested");
34
34
  }
@@ -36,7 +36,7 @@ export async function updateOrderStatusAction(input) {
36
36
  const user = await requireRoleUser(["seller", "admin"]);
37
37
  const parsed = updateOrderStatusSchema.safeParse(input);
38
38
  if (!parsed.success)
39
- throw new ValidationError(parsed.error.errors[0]?.message ?? "Invalid input");
39
+ throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid input");
40
40
  const order = await orderRepository.findById(parsed.data.orderId).catch(() => null);
41
41
  if (!order)
42
42
  throw new OrderNotFoundError(parsed.data.orderId);
@@ -1,11 +1,13 @@
1
1
  export interface ResolvedPaymentFee {
2
2
  baseAmount: number;
3
3
  platformFee: number;
4
+ gstOnFee: number;
4
5
  totalAmount: number;
5
6
  }
6
7
  /**
7
- * Returns the cart total inflated by the configured Razorpay processing fee.
8
- * The percent comes from siteSettings.commissions.razorpayFeePercent and
9
- * falls back to the default if unset.
8
+ * Inflates the cart total by our platform fee + GST on that fee.
9
+ * platformFeePercent = siteSettings.commissions.platformFeePercent (our cut %).
10
+ * gstPercent = siteSettings.commissions.gstPercent (18% GST on our fee, not on full order).
11
+ * minimumTransactionFee = per-gateway-transaction floor in rupees.
10
12
  */
11
13
  export declare function resolvePaymentFee(baseAmount: number): Promise<ResolvedPaymentFee>;
@@ -1,14 +1,19 @@
1
1
  import { siteSettingsRepository } from "../../../../repositories";
2
- import { PAYMENTS_DEFAULT_RAZORPAY_FEE_PERCENT } from "../../../shared/features/payments/config";
2
+ import { PAYMENTS_DEFAULT_PLATFORM_FEE_PERCENT, PAYMENTS_DEFAULT_GST_PERCENT, } from "../../../shared/features/payments/config";
3
3
  /**
4
- * Returns the cart total inflated by the configured Razorpay processing fee.
5
- * The percent comes from siteSettings.commissions.razorpayFeePercent and
6
- * falls back to the default if unset.
4
+ * Inflates the cart total by our platform fee + GST on that fee.
5
+ * platformFeePercent = siteSettings.commissions.platformFeePercent (our cut %).
6
+ * gstPercent = siteSettings.commissions.gstPercent (18% GST on our fee, not on full order).
7
+ * minimumTransactionFee = per-gateway-transaction floor in rupees.
7
8
  */
8
9
  export async function resolvePaymentFee(baseAmount) {
9
10
  const siteSettings = await siteSettingsRepository.getSingleton();
10
- const razorpayFeePercent = siteSettings?.commissions?.razorpayFeePercent ?? PAYMENTS_DEFAULT_RAZORPAY_FEE_PERCENT;
11
- const platformFee = Math.round(baseAmount * (razorpayFeePercent / 100) * 100) / 100;
12
- const totalAmount = baseAmount + platformFee;
13
- return { baseAmount, platformFee, totalAmount };
11
+ const platformFeePercent = siteSettings?.commissions?.platformFeePercent ?? PAYMENTS_DEFAULT_PLATFORM_FEE_PERCENT;
12
+ const gstPercent = siteSettings?.commissions?.gstPercent ?? PAYMENTS_DEFAULT_GST_PERCENT;
13
+ const minimumTransactionFee = Math.max(0, siteSettings?.commissions?.minimumTransactionFee ?? 0);
14
+ const platformFee = Math.round(baseAmount * (platformFeePercent / 100) * 100) / 100;
15
+ const gstOnFee = Math.round(platformFee * (gstPercent / 100) * 100) / 100;
16
+ const rawTotal = baseAmount + platformFee + gstOnFee;
17
+ const totalAmount = Math.max(rawTotal, baseAmount + minimumTransactionFee);
18
+ return { baseAmount, platformFee, gstOnFee, totalAmount };
14
19
  }
@@ -1,3 +1,3 @@
1
1
  export { createPaymentIntentAction, verifyPaymentSignatureAction, type CreatePaymentIntentInput, type CreatePaymentIntentResult, type VerifyPaymentSignatureInput, } from "./actions";
2
2
  export { resolvePaymentFee, type ResolvedPaymentFee } from "./data";
3
- export { PAYMENTS_DEFAULT_RAZORPAY_FEE_PERCENT, PAYMENTS_RECEIPT_PREFIX, } from "../../../shared/features/payments/config";
3
+ export { PAYMENTS_DEFAULT_PLATFORM_FEE_PERCENT, PAYMENTS_DEFAULT_GST_PERCENT, PAYMENTS_RECEIPT_PREFIX, } from "../../../shared/features/payments/config";
@@ -1,3 +1,3 @@
1
1
  export { createPaymentIntentAction, verifyPaymentSignatureAction, } from "./actions";
2
2
  export { resolvePaymentFee } from "./data";
3
- export { PAYMENTS_DEFAULT_RAZORPAY_FEE_PERCENT, PAYMENTS_RECEIPT_PREFIX, } from "../../../shared/features/payments/config";
3
+ export { PAYMENTS_DEFAULT_PLATFORM_FEE_PERCENT, PAYMENTS_DEFAULT_GST_PERCENT, PAYMENTS_RECEIPT_PREFIX, } from "../../../shared/features/payments/config";
@@ -8,7 +8,7 @@ export async function reservePreOrderAction(input) {
8
8
  const user = await requireRoleUser(["buyer", "seller", "admin"]);
9
9
  const parsed = reservePreOrderSchema.safeParse(input);
10
10
  if (!parsed.success)
11
- throw new ValidationError(parsed.error.errors[0]?.message ?? "Invalid reservation input");
11
+ throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid reservation input");
12
12
  const { preOrderId, quantity } = parsed.data;
13
13
  const product = await assertPreOrderAvailable(preOrderId, quantity);
14
14
  const deposit = computeDeposit(product);
@@ -8,7 +8,7 @@ export async function createProductAction(input) {
8
8
  const user = await requireRoleUser(["seller", "admin"]);
9
9
  const parsed = productInputSchema.safeParse(input);
10
10
  if (!parsed.success)
11
- throw new ValidationError(parsed.error.errors[0]?.message ?? "Invalid input");
11
+ throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid input");
12
12
  return productRepository.create({
13
13
  ...parsed.data,
14
14
  storeId: user.uid,
@@ -21,7 +21,7 @@ export async function createAuctionAction(input) {
21
21
  const user = await requireRoleUser(["seller", "admin"]);
22
22
  const parsed = auctionInputSchema.safeParse(input);
23
23
  if (!parsed.success)
24
- throw new ValidationError(parsed.error.errors[0]?.message ?? "Invalid input");
24
+ throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid input");
25
25
  return productRepository.create({
26
26
  ...parsed.data,
27
27
  storeId: user.uid,
@@ -34,7 +34,7 @@ export async function createPreOrderAction(input) {
34
34
  const user = await requireRoleUser(["seller", "admin"]);
35
35
  const parsed = preOrderInputSchema.safeParse(input);
36
36
  if (!parsed.success)
37
- throw new ValidationError(parsed.error.errors[0]?.message ?? "Invalid input");
37
+ throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid input");
38
38
  return productRepository.create({
39
39
  ...parsed.data,
40
40
  storeId: user.uid,
@@ -48,7 +48,7 @@ export async function updateProductAction(productId, input) {
48
48
  await assertProductOwnership(productId, user.uid);
49
49
  const parsed = productUpdateSchema.safeParse(input);
50
50
  if (!parsed.success)
51
- throw new ValidationError(parsed.error.errors[0]?.message ?? "Invalid input");
51
+ throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid input");
52
52
  return productRepository.update(productId, parsed.data);
53
53
  }
54
54
  export async function deleteProductAction(productId) {
@@ -60,7 +60,7 @@ export async function setProductStatusAction(input) {
60
60
  const user = await requireRoleUser(["seller", "admin"]);
61
61
  const parsed = setStatusSchema.safeParse(input);
62
62
  if (!parsed.success)
63
- throw new ValidationError(parsed.error.errors[0]?.message ?? "Invalid input");
63
+ throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid input");
64
64
  const product = await assertProductOwnership(parsed.data.productId, user.uid);
65
65
  assertStatusTransition(product.status, parsed.data.status);
66
66
  return productRepository.update(parsed.data.productId, { status: parsed.data.status });
@@ -69,6 +69,6 @@ export async function setProductFeaturedAction(input) {
69
69
  await requireRoleUser("admin");
70
70
  const parsed = setFeaturedSchema.safeParse(input);
71
71
  if (!parsed.success)
72
- throw new ValidationError(parsed.error.errors[0]?.message ?? "Invalid input");
72
+ throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid input");
73
73
  return productRepository.update(parsed.data.productId, { featured: parsed.data.featured });
74
74
  }
@@ -9,7 +9,7 @@ export async function applyCouponAction(input) {
9
9
  const user = await requireRoleUser(["buyer", "seller", "admin"]);
10
10
  const parsed = applyCouponSchema.safeParse(input);
11
11
  if (!parsed.success)
12
- throw new ValidationError(parsed.error.errors[0]?.message ?? "Invalid coupon input");
12
+ throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid coupon input");
13
13
  const coupon = await validateCoupon(parsed.data, user.uid);
14
14
  const discount = computeDiscount(coupon, parsed.data.cartTotal);
15
15
  await couponsRepository.applyCoupon(coupon.id, coupon.code, user.uid, [], discount);
@@ -19,7 +19,7 @@ export async function createCouponAction(input) {
19
19
  const user = await requireRoleUser(["admin", "seller"]);
20
20
  const parsed = createCouponSchema.safeParse(input);
21
21
  if (!parsed.success)
22
- throw new ValidationError(parsed.error.errors[0]?.message ?? "Invalid coupon input");
22
+ throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid coupon input");
23
23
  return couponsRepository.createWithId(`coupon-${parsed.data.code.toLowerCase()}`, { ...parsed.data, createdBy: user.uid, usage: { ...parsed.data.usage, currentUsage: 0 } });
24
24
  }
25
25
  export async function updateCouponAction(couponId, input) {
@@ -29,7 +29,7 @@ export async function updateCouponAction(couponId, input) {
29
29
  throw new CouponNotFoundError(couponId);
30
30
  const parsed = updateCouponSchema.safeParse(input);
31
31
  if (!parsed.success)
32
- throw new ValidationError(parsed.error.errors[0]?.message ?? "Invalid coupon input");
32
+ throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid coupon input");
33
33
  return couponsRepository.update(couponId, parsed.data);
34
34
  }
35
35
  export async function deactivateCouponAction(couponId) {
@@ -15,3 +15,27 @@ export declare const BUNDLE_MAX_PER_USER_DEFAULT: 3;
15
15
  export declare const BUNDLES_PAGE_SIZE: 24;
16
16
  /** Cap on homepage featured-bundles strip. */
17
17
  export declare const BUNDLES_FEATURED_LIMIT: 12;
18
+ /**
19
+ * Listing types that may be added to a bundle.
20
+ * - standard — regular product
21
+ * - pre-order — pre-order product
22
+ * - prize-draw — added as N draws/entries (see BundleItemDetail.drawCount)
23
+ *
24
+ * Auctions and bundles themselves are never eligible.
25
+ */
26
+ export declare const BUNDLE_ELIGIBLE_LISTING_TYPES: readonly ["standard", "pre-order", "prize-draw"];
27
+ export type BundleEligibleListingType = (typeof BUNDLE_ELIGIBLE_LISTING_TYPES)[number];
28
+ /**
29
+ * "special" — explicitly curated admin bundle. Only special bundles write
30
+ * reverse `partOfBundleIds` pointers on member products and are displayed in
31
+ * the "Bundled" badge on product cards / detail pages.
32
+ *
33
+ * "brand" — auto-generated collection driven by a dynamic brand query rule.
34
+ * Does NOT update `partOfBundleIds` on products; treated as a discovery
35
+ * surface, not a priced product grouping.
36
+ */
37
+ export declare const BUNDLE_KIND_SPECIAL: "special";
38
+ export declare const BUNDLE_KIND_BRAND: "brand";
39
+ export type BundleKind = typeof BUNDLE_KIND_SPECIAL | typeof BUNDLE_KIND_BRAND;
40
+ /** Max draw entries a prize-draw item contributes when added to a bundle. */
41
+ export declare const BUNDLE_DRAW_COUNT_MAX: 500;
@@ -15,3 +15,31 @@ export const BUNDLE_MAX_PER_USER_DEFAULT = 3;
15
15
  export const BUNDLES_PAGE_SIZE = 24;
16
16
  /** Cap on homepage featured-bundles strip. */
17
17
  export const BUNDLES_FEATURED_LIMIT = 12;
18
+ // ── Bundle eligibility ───────────────────────────────────────────────────────
19
+ /**
20
+ * Listing types that may be added to a bundle.
21
+ * - standard — regular product
22
+ * - pre-order — pre-order product
23
+ * - prize-draw — added as N draws/entries (see BundleItemDetail.drawCount)
24
+ *
25
+ * Auctions and bundles themselves are never eligible.
26
+ */
27
+ export const BUNDLE_ELIGIBLE_LISTING_TYPES = [
28
+ "standard",
29
+ "pre-order",
30
+ "prize-draw",
31
+ ];
32
+ // ── Bundle kinds ─────────────────────────────────────────────────────────────
33
+ /**
34
+ * "special" — explicitly curated admin bundle. Only special bundles write
35
+ * reverse `partOfBundleIds` pointers on member products and are displayed in
36
+ * the "Bundled" badge on product cards / detail pages.
37
+ *
38
+ * "brand" — auto-generated collection driven by a dynamic brand query rule.
39
+ * Does NOT update `partOfBundleIds` on products; treated as a discovery
40
+ * surface, not a priced product grouping.
41
+ */
42
+ export const BUNDLE_KIND_SPECIAL = "special";
43
+ export const BUNDLE_KIND_BRAND = "brand";
44
+ /** Max draw entries a prize-draw item contributes when added to a bundle. */
45
+ export const BUNDLE_DRAW_COUNT_MAX = 500;
@@ -86,6 +86,13 @@ export declare const BUNDLE_COPY: {
86
86
  readonly ruleTypeLabel: "Bundle members source";
87
87
  readonly ruleTypeStatic: "Hand-picked products";
88
88
  readonly ruleTypeDynamic: "Dynamic query (auto-resolves)";
89
+ readonly bundleKindLabel: "Bundle kind";
90
+ readonly bundleKindHint: "Special bundles show the 'Bundled' badge on product cards and update member product metadata. Brand bundles are discovery collections only.";
91
+ readonly bundleKindSpecial: "Special bundle";
92
+ readonly bundleKindBrand: "Brand collection";
93
+ readonly drawCountLabel: "Draw entries included";
94
+ readonly drawCountHint: "How many raffle entries the buyer receives for this prize-draw when purchasing the bundle.";
95
+ readonly drawCountPlaceholder: "e.g. 5";
89
96
  readonly dynamic: {
90
97
  readonly title: "Dynamic query rule";
91
98
  readonly hint: "onProductStockChange resolves bundleProductIds; bundleQueryResolvedAt tracks the last refresh.";
@@ -96,6 +96,15 @@ export const BUNDLE_COPY = {
96
96
  ruleTypeLabel: "Bundle members source",
97
97
  ruleTypeStatic: "Hand-picked products",
98
98
  ruleTypeDynamic: "Dynamic query (auto-resolves)",
99
+ // Bundle kind
100
+ bundleKindLabel: "Bundle kind",
101
+ bundleKindHint: "Special bundles show the 'Bundled' badge on product cards and update member product metadata. Brand bundles are discovery collections only.",
102
+ bundleKindSpecial: "Special bundle",
103
+ bundleKindBrand: "Brand collection",
104
+ // Prize-draw draw count
105
+ drawCountLabel: "Draw entries included",
106
+ drawCountHint: "How many raffle entries the buyer receives for this prize-draw when purchasing the bundle.",
107
+ drawCountPlaceholder: "e.g. 5",
99
108
  dynamic: {
100
109
  title: "Dynamic query rule",
101
110
  hint: "onProductStockChange resolves bundleProductIds; bundleQueryResolvedAt tracks the last refresh.",