@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.
- package/dist/_internal/server/features/auctions/actions.js +1 -1
- package/dist/_internal/server/features/blog/actions.js +2 -2
- package/dist/_internal/server/features/brands/actions.js +2 -2
- package/dist/_internal/server/features/bundles/actions.d.ts +15 -0
- package/dist/_internal/server/features/bundles/actions.js +48 -0
- package/dist/_internal/server/features/bundles/index.d.ts +1 -0
- package/dist/_internal/server/features/bundles/index.js +1 -0
- package/dist/_internal/server/features/cart/actions.js +3 -3
- package/dist/_internal/server/features/checkout/actions.d.ts +4 -0
- package/dist/_internal/server/features/checkout/actions.js +23 -12
- package/dist/_internal/server/features/events/actions.js +3 -3
- package/dist/_internal/server/features/orders/actions.js +4 -4
- package/dist/_internal/server/features/payments/data.d.ts +5 -3
- package/dist/_internal/server/features/payments/data.js +13 -8
- package/dist/_internal/server/features/payments/index.d.ts +1 -1
- package/dist/_internal/server/features/payments/index.js +1 -1
- package/dist/_internal/server/features/pre-orders/actions.js +1 -1
- package/dist/_internal/server/features/products/actions.js +6 -6
- package/dist/_internal/server/features/promotions/actions.js +3 -3
- package/dist/_internal/shared/features/categories/bundle-config.d.ts +24 -0
- package/dist/_internal/shared/features/categories/bundle-config.js +28 -0
- package/dist/_internal/shared/features/categories/bundle-copy.d.ts +7 -0
- package/dist/_internal/shared/features/categories/bundle-copy.js +9 -0
- package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +69 -19
- package/dist/_internal/shared/features/categories/bundle-schemas.js +20 -2
- package/dist/_internal/shared/features/checkout/config.d.ts +1 -1
- package/dist/_internal/shared/features/checkout/config.js +1 -1
- package/dist/_internal/shared/features/payments/config.d.ts +2 -1
- package/dist/_internal/shared/features/payments/config.js +2 -1
- package/dist/_internal/shared/features/products/schema.d.ts +8 -8
- package/dist/constants/field-names.d.ts +8 -1
- package/dist/constants/field-names.js +9 -2
- package/dist/constants/table-keys.d.ts +3 -0
- package/dist/constants/table-keys.js +3 -0
- package/dist/features/admin/components/AdminSiteSettingsView.js +17 -15
- package/dist/features/admin/components/DashboardStats.js +20 -12
- package/dist/features/admin/components/analytics/AdminAnalyticsCharts.js +18 -2
- package/dist/features/admin/components/analytics/AdminStatCard.d.ts +7 -1
- package/dist/features/admin/components/analytics/AdminStatCard.js +13 -2
- package/dist/features/admin/schemas/firestore.d.ts +18 -4
- package/dist/features/admin/schemas/firestore.js +11 -1
- package/dist/features/categories/components/BundleBuyNowCta.d.ts +3 -1
- package/dist/features/categories/components/BundleBuyNowCta.js +3 -3
- package/dist/features/categories/components/BundleDynamicRuleEditor.d.ts +1 -1
- package/dist/features/categories/components/BundleDynamicRuleEditor.js +2 -1
- package/dist/features/categories/components/BundlesListView.d.ts +4 -1
- package/dist/features/categories/components/BundlesListView.js +2 -2
- package/dist/features/categories/components/CategoryBundlesListing.d.ts +4 -1
- package/dist/features/categories/components/CategoryBundlesListing.js +9 -8
- package/dist/features/categories/schemas/firestore.d.ts +29 -1
- package/dist/features/checkout/actions/checkout-actions.d.ts +11 -0
- package/dist/features/checkout/actions/checkout-actions.js +26 -0
- package/dist/features/filters/FilterFacetSection.d.ts +6 -1
- package/dist/features/filters/FilterFacetSection.js +25 -2
- package/dist/features/homepage/components/FeaturedBundlesSection.d.ts +4 -1
- package/dist/features/homepage/components/FeaturedBundlesSection.js +9 -8
- package/dist/features/orders/actions/refund-actions.js +10 -7
- package/dist/features/orders/schemas/firestore.d.ts +16 -13
- package/dist/features/orders/schemas/firestore.js +6 -5
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +1 -1
- package/dist/features/pre-orders/components/PreOrdersIndexListing.js +2 -1
- package/dist/features/products/components/AuctionsIndexListing.js +2 -1
- package/dist/features/products/components/ProductDetailPageView.js +3 -1
- package/dist/features/products/components/ProductDetailView.d.ts +3 -1
- package/dist/features/products/components/ProductDetailView.js +2 -2
- package/dist/features/products/components/ProductFilters.d.ts +10 -4
- package/dist/features/products/components/ProductFilters.js +20 -16
- package/dist/features/products/components/ProductGrid.js +3 -3
- package/dist/features/products/components/ProductsIndexListing.js +2 -1
- package/dist/features/products/constants/product-features.constants.js +5 -1
- package/dist/features/products/schemas/index.d.ts +2 -2
- package/dist/features/products/schemas/product-features.d.ts +1 -1
- package/dist/features/products/schemas/product-features.validators.d.ts +10 -10
- package/dist/features/products/schemas/product-features.validators.js +4 -0
- package/dist/features/seller/components/SellerCouponEditorView.js +2 -2
- package/dist/features/seller/components/analytics/SellerAnalyticsStats.js +10 -4
- package/dist/features/shell/FormShell.js +1 -1
- package/dist/features/stores/components/StoreBundlesPageView.d.ts +4 -1
- package/dist/features/stores/components/StoreBundlesPageView.js +2 -2
- package/dist/features/stores/components/StoresIndexListing.js +2 -1
- package/dist/features/wishlist/hooks/useWishlistCount.d.ts +2 -2
- package/dist/features/wishlist/hooks/useWishlistCount.js +5 -8
- package/dist/providers/db-firebase/admin-app-lite.js +8 -2
- package/dist/providers/db-firebase/admin.js +8 -2
- package/dist/seed/addresses-seed-data.js +24 -7
- package/dist/seed/cart-seed-data.d.ts +10 -10
- package/dist/seed/cart-seed-data.js +39 -2
- package/dist/seed/grouped-listings-seed-data.js +47 -0
- package/dist/seed/orders-seed-data.js +150 -0
- package/dist/seed/products-standard-seed-data.js +42 -0
- package/dist/seed/site-settings-seed-data.js +11 -4
- package/dist/seed/users-seed-data.js +1 -1
- package/dist/seed/wishlists-seed-data.js +7 -0
- package/dist/server-entry.d.ts +2 -1
- package/dist/server-entry.js +2 -1
- package/dist/server.d.ts +2 -1
- package/dist/server.js +3 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Breadcrumb.style.css +1 -1
- package/dist/ui/components/ListingLayout.style.css +1 -1
- package/dist/ui/components/Select.style.css +1 -1
- package/dist/ui/components/SideDrawer.style.css +55 -12
- package/dist/ui/components/Typography.style.css +2 -2
- package/dist/ui/rich-text/RichText.style.css +4 -5
- 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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:
|
|
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
|
|
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 * (
|
|
600
|
-
const
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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 {
|
|
2
|
+
import { PAYMENTS_DEFAULT_PLATFORM_FEE_PERCENT, PAYMENTS_DEFAULT_GST_PERCENT, } from "../../../shared/features/payments/config";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
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 {
|
|
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 {
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.",
|