@mohasinac/appkit 3.2.20 → 3.3.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.
- package/dist/_internal/server/features/checkout/actions.d.ts +2 -0
- package/dist/_internal/server/features/checkout/actions.js +239 -190
- package/dist/_internal/server/functions/scheduled.d.ts +2 -1
- package/dist/_internal/server/functions/scheduled.js +9 -1
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +8 -11
- package/dist/_internal/server/jobs/core/emiInstallmentReminder.d.ts +8 -0
- package/dist/_internal/server/jobs/core/emiInstallmentReminder.js +78 -0
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.d.ts +2 -2
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +15 -10
- package/dist/_internal/server/jobs/handlers/emiInstallmentReminder.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/emiInstallmentReminder.js +2 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
- package/dist/_internal/server/jobs/handlers/index.js +2 -0
- package/dist/_internal/server/providers/index.d.ts +4 -4
- package/dist/_internal/server/providers/index.js +4 -4
- package/dist/_internal/server/providers/payment/razorpay-mock.d.ts +3 -2
- package/dist/_internal/server/providers/payment/razorpay-mock.js +3 -1
- package/dist/_internal/server/providers/resolve.d.ts +10 -13
- package/dist/_internal/server/providers/resolve.js +9 -15
- package/dist/_internal/shared/actions/action-registry.js +28 -0
- package/dist/_internal/shared/checkout/rules/_defaults.js +1 -0
- package/dist/_internal/shared/checkout/rules/_registry.js +4 -0
- package/dist/_internal/shared/checkout/rules/art.rule.d.ts +9 -0
- package/dist/_internal/shared/checkout/rules/art.rule.js +5 -0
- package/dist/_internal/shared/checkout/rules/auction.rule.js +1 -0
- package/dist/_internal/shared/checkout/rules/classified.rule.js +1 -0
- package/dist/_internal/shared/checkout/rules/index.d.ts +2 -0
- package/dist/_internal/shared/checkout/rules/index.js +2 -0
- package/dist/_internal/shared/checkout/rules/stickers.rule.d.ts +9 -0
- package/dist/_internal/shared/checkout/rules/stickers.rule.js +5 -0
- package/dist/_internal/shared/checkout/rules/types.d.ts +6 -0
- package/dist/_internal/shared/features/checkout/config.d.ts +16 -1
- package/dist/_internal/shared/features/checkout/config.js +16 -1
- package/dist/_internal/shared/features/emi/schedule.d.ts +63 -0
- package/dist/_internal/shared/features/emi/schedule.js +62 -0
- package/dist/_internal/shared/fees/calculator.d.ts +53 -0
- package/dist/_internal/shared/fees/calculator.js +40 -0
- package/dist/_internal/shared/listing-types/_registry.d.ts +22 -66
- package/dist/_internal/shared/listing-types/_registry.js +19 -2
- package/dist/_internal/shared/listing-types/art/config.d.ts +15 -0
- package/dist/_internal/shared/listing-types/art/config.js +14 -0
- package/dist/_internal/shared/listing-types/art/ctas.d.ts +1 -0
- package/dist/_internal/shared/listing-types/art/ctas.js +3 -0
- package/dist/_internal/shared/listing-types/art/og.d.ts +1 -0
- package/dist/_internal/shared/listing-types/art/og.js +1 -0
- package/dist/_internal/shared/listing-types/art/schema.d.ts +2 -0
- package/dist/_internal/shared/listing-types/art/schema.js +3 -0
- package/dist/_internal/shared/listing-types/art/seed-factory.d.ts +1 -0
- package/dist/_internal/shared/listing-types/art/seed-factory.js +1 -0
- package/dist/_internal/shared/listing-types/auction/config.d.ts +8 -0
- package/dist/_internal/shared/listing-types/auction/config.js +6 -0
- package/dist/_internal/shared/listing-types/capabilities.js +17 -0
- package/dist/_internal/shared/listing-types/classified/config.d.ts +8 -0
- package/dist/_internal/shared/listing-types/classified/config.js +6 -0
- package/dist/_internal/shared/listing-types/digital-code/config.d.ts +8 -0
- package/dist/_internal/shared/listing-types/digital-code/config.js +6 -0
- package/dist/_internal/shared/listing-types/live/config.d.ts +8 -0
- package/dist/_internal/shared/listing-types/live/config.js +6 -0
- package/dist/_internal/shared/listing-types/pre-order/config.d.ts +8 -0
- package/dist/_internal/shared/listing-types/pre-order/config.js +6 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +8 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.js +6 -0
- package/dist/_internal/shared/listing-types/standard/config.d.ts +8 -0
- package/dist/_internal/shared/listing-types/standard/config.js +6 -0
- package/dist/_internal/shared/listing-types/stickers/config.d.ts +15 -0
- package/dist/_internal/shared/listing-types/stickers/config.js +14 -0
- package/dist/_internal/shared/listing-types/stickers/ctas.d.ts +1 -0
- package/dist/_internal/shared/listing-types/stickers/ctas.js +3 -0
- package/dist/_internal/shared/listing-types/stickers/og.d.ts +1 -0
- package/dist/_internal/shared/listing-types/stickers/og.js +1 -0
- package/dist/_internal/shared/listing-types/stickers/schema.d.ts +2 -0
- package/dist/_internal/shared/listing-types/stickers/schema.js +3 -0
- package/dist/_internal/shared/listing-types/stickers/seed-factory.d.ts +1 -0
- package/dist/_internal/shared/listing-types/stickers/seed-factory.js +1 -0
- package/dist/client.d.ts +7 -2
- package/dist/client.js +4 -2
- package/dist/constants/field-names.d.ts +2 -6
- package/dist/constants/field-names.js +2 -6
- package/dist/contracts/index.d.ts +4 -2
- package/dist/contracts/index.js +6 -0
- package/dist/contracts/payment.d.ts +14 -9
- package/dist/contracts/payment.js +11 -1
- package/dist/contracts/shipping.d.ts +14 -9
- package/dist/contracts/shipping.js +11 -1
- package/dist/features/about/components/PublicProfileView.js +3 -6
- package/dist/features/admin/components/AdminAddressBookView.d.ts +11 -0
- package/dist/features/admin/components/AdminAddressBookView.js +30 -0
- package/dist/features/admin/components/AdminArtView.d.ts +3 -0
- package/dist/features/admin/components/AdminArtView.js +45 -0
- package/dist/features/admin/components/AdminSiteConfigGuideView.js +1 -1
- package/dist/features/admin/components/AdminSiteSettingsView.js +14 -9
- package/dist/features/admin/components/AdminStickersView.d.ts +3 -0
- package/dist/features/admin/components/AdminStickersView.js +45 -0
- package/dist/features/admin/components/index.d.ts +6 -0
- package/dist/features/admin/components/index.js +4 -0
- package/dist/features/admin/schemas/firestore.d.ts +19 -11
- package/dist/features/admin/schemas/firestore.js +12 -2
- package/dist/features/auctions/components/MarketplaceAuctionCard.d.ts +3 -2
- package/dist/features/auth/hooks/useAuth.js +35 -9
- package/dist/features/auth/schemas/firestore.d.ts +2 -6
- package/dist/features/cart/schemas/firestore.d.ts +3 -2
- package/dist/features/consultation/schemas/index.d.ts +2 -2
- package/dist/features/loyalty/schemas/index.d.ts +6 -6
- package/dist/features/orders/repository/orders.repository.d.ts +20 -2
- package/dist/features/orders/repository/orders.repository.js +34 -5
- package/dist/features/orders/schemas/firestore.d.ts +33 -7
- package/dist/features/orders/schemas/firestore.js +1 -1
- package/dist/features/orders/schemas/index.d.ts +176 -31
- package/dist/features/orders/schemas/index.js +20 -6
- package/dist/features/orders/types/index.d.ts +3 -3
- package/dist/features/payments/repository/payout.repository.js +2 -2
- package/dist/features/payments/schemas/firestore.d.ts +3 -3
- package/dist/features/payments/schemas/firestore.js +2 -2
- package/dist/features/payments/schemas/index.d.ts +5 -5
- package/dist/features/payments/schemas/index.js +2 -2
- package/dist/features/products/api/[id]/route.js +4 -1
- package/dist/features/products/api/route.js +4 -1
- package/dist/features/products/components/CompareOverlay.js +7 -9
- package/dist/features/products/components/ProductDetailPageView.d.ts +3 -1
- package/dist/features/products/components/ProductDetailPageView.js +2 -2
- package/dist/features/products/components/ProductForm.js +18 -7
- package/dist/features/products/components/ProductGrid.js +7 -12
- package/dist/features/products/components/ShowGroupSection.js +3 -5
- package/dist/features/products/components/SublistingCarouselSection.js +3 -6
- package/dist/features/products/constants/listing-tabs.d.ts +32 -0
- package/dist/features/products/constants/listing-tabs.js +8 -0
- package/dist/features/products/index.d.ts +1 -1
- package/dist/features/products/index.js +3 -1
- package/dist/features/products/schemas/firestore.d.ts +20 -1
- package/dist/features/products/schemas/firestore.js +2 -0
- package/dist/features/products/schemas/index.d.ts +48 -0
- package/dist/features/products/schemas/index.js +9 -0
- package/dist/features/products/schemas/product-features.d.ts +1 -1
- package/dist/features/products/types/index.d.ts +5 -3
- package/dist/features/products/utils/listing-type.d.ts +6 -0
- package/dist/features/products/utils/listing-type.js +3 -0
- package/dist/features/reviews/api/[id]/route.js +5 -4
- package/dist/features/seller/actions/seller-actions.d.ts +29 -14
- package/dist/features/seller/actions/seller-actions.js +101 -27
- package/dist/features/seller/components/SellerArtView.d.ts +7 -0
- package/dist/features/seller/components/SellerArtView.js +114 -0
- package/dist/features/seller/components/SellerOrdersView.js +38 -1
- package/dist/features/seller/components/SellerProductShell.d.ts +5 -1
- package/dist/features/seller/components/SellerProductShell.js +69 -59
- package/dist/features/seller/components/SellerShippingView.js +6 -25
- package/dist/features/seller/components/SellerStickersView.d.ts +7 -0
- package/dist/features/seller/components/SellerStickersView.js +114 -0
- package/dist/features/seller/components/index.d.ts +4 -0
- package/dist/features/seller/components/index.js +2 -0
- package/dist/features/seller/messages/en.json +1 -1
- package/dist/features/seller/schemas/index.d.ts +7 -7
- package/dist/features/seller/schemas/index.js +1 -1
- package/dist/features/seller/types/index.d.ts +1 -1
- package/dist/features/stores/components/StoreDetailLayoutView.js +18 -2
- package/dist/features/stores/schemas/firestore.d.ts +7 -1
- package/dist/features/stores/schemas/index.d.ts +19 -16
- package/dist/features/stores/schemas/index.js +1 -1
- package/dist/index.d.ts +21 -36
- package/dist/index.js +27 -39
- package/dist/next/routing/route-map.d.ts +22 -0
- package/dist/next/routing/route-map.js +10 -0
- package/dist/providers/payment-manual/index.d.ts +40 -0
- package/dist/providers/payment-manual/index.js +91 -0
- package/dist/providers/payment-razorpay/index.d.ts +4 -2
- package/dist/providers/payment-razorpay/index.js +4 -1
- package/dist/providers/shipping-manual/index.d.ts +22 -0
- package/dist/providers/shipping-manual/index.js +47 -0
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +5 -3
- package/dist/schemas/registry.d.ts +129 -70
- package/dist/schemas/registry.js +0 -4
- package/dist/security/pii-encrypt.js +0 -7
- package/dist/security/pii-redact.js +0 -1
- package/dist/seed/conversations-seed-data.js +2 -2
- package/dist/seed/faq-seed-data.js +7 -7
- package/dist/seed/grouped-listings-seed-data.d.ts +2 -0
- package/dist/seed/index.d.ts +2 -0
- package/dist/seed/index.js +2 -0
- package/dist/seed/manifest.js +20 -16
- package/dist/seed/payouts-seed-data.js +5 -5
- package/dist/seed/products-art-seed-data.d.ts +11 -0
- package/dist/seed/products-art-seed-data.js +125 -0
- package/dist/seed/products-preorders-seed-data.d.ts +2 -0
- package/dist/seed/products-stickers-seed-data.d.ts +11 -0
- package/dist/seed/products-stickers-seed-data.js +125 -0
- package/dist/seed/site-settings-seed-data.js +13 -4
- package/dist/seed/stores-seed-data.js +11 -11
- package/dist/server.d.ts +7 -15
- package/dist/server.js +17 -42
- package/package.json +1 -1
|
@@ -7,6 +7,8 @@ export interface CreateCheckoutOrderInput {
|
|
|
7
7
|
/** Required for physical carts; omitted for digital-code-only carts (no shipping). */
|
|
8
8
|
addressId?: string;
|
|
9
9
|
paymentMethod: CheckoutPaymentMethod;
|
|
10
|
+
/** Required when paymentMethod === "emi" — the buyer's chosen tenure (2–6 months). */
|
|
11
|
+
emiTenureMonths?: number;
|
|
10
12
|
notes?: string;
|
|
11
13
|
excludedProductIds?: string[];
|
|
12
14
|
/** When true this is an admin test order — OTP consent is pre-granted and the order is created paid/processing. */
|
|
@@ -16,6 +16,7 @@ import { failedCheckoutRepository } from "../../../../features/checkout/reposito
|
|
|
16
16
|
import { sendOrderConfirmationEmail } from "../../../../features/contact/server";
|
|
17
17
|
import { splitCartIntoOrderGroups } from "../../../../features/orders/index";
|
|
18
18
|
import { resolveDate } from "../../../../utils";
|
|
19
|
+
import { computeCodHandlingFee } from "../../../shared/fees/calculator";
|
|
19
20
|
import { getAdminDb, getAdminRealtimeDb, RTDB_PATHS, } from "../../../../providers/db-firebase";
|
|
20
21
|
import { PRODUCT_COLLECTION, PRODUCT_CODES_SUBCOLLECTION } from "../../../../features/products/schemas/firestore";
|
|
21
22
|
import { CART_COLLECTION } from "../../../../features/cart/schemas/index";
|
|
@@ -25,7 +26,8 @@ import { consentOtpRef, consentOtpRateLimitRef, CONSENT_OTP_MAX_BYPASS_CREDITS,
|
|
|
25
26
|
import { OrderStatusValues, PaymentStatusValues, PaymentMethodValues, } from "../../../../features/orders/schemas/index";
|
|
26
27
|
import { getDefaultCurrency } from "../../../../core/index";
|
|
27
28
|
import { verifyPaymentSignatureWithKeys, fetchRazorpayOrder, paiseToRupees, } from "../../../../providers/payment-razorpay/index";
|
|
28
|
-
import { CHECKOUT_DEFAULT_COMMISSIONS } from "../../../shared/features/checkout/config";
|
|
29
|
+
import { CHECKOUT_DEFAULT_COMMISSIONS, CHECKOUT_DEFAULT_EMI_SETTINGS } from "../../../shared/features/checkout/config";
|
|
30
|
+
import { checkEmiEligibility, computeEmiSchedule } from "../../../shared/features/emi/schedule";
|
|
29
31
|
import { formatShippingAddress } from "./data";
|
|
30
32
|
import { enforceMaxPerUserForCart, computePrizeRevealDeadline, } from "./prize-bundle-gates";
|
|
31
33
|
import { getListingRule, runSyncPreflight, } from "../../../shared/checkout/rules";
|
|
@@ -147,25 +149,19 @@ function accumulateCouponUsage(accumulator, couponCode, couponId, discountAmount
|
|
|
147
149
|
entry.totalDiscount += discountAmount;
|
|
148
150
|
accumulator.set(couponCode, entry);
|
|
149
151
|
}
|
|
150
|
-
async function resolveShippingCost(storeId
|
|
152
|
+
async function resolveShippingCost(storeId) {
|
|
151
153
|
if (!storeId)
|
|
152
|
-
return { shippingFee: 0, storeOwnerId: undefined };
|
|
154
|
+
return { shippingFee: 0, storeOwnerId: undefined, storeEmiEnabled: false };
|
|
153
155
|
const store = await storeRepository.findById(storeId);
|
|
154
156
|
const storeOwnerId = store?.ownerId;
|
|
157
|
+
const storeEmiEnabled = store?.emiEnabled === true;
|
|
155
158
|
if (!storeOwnerId)
|
|
156
|
-
return { shippingFee: 0, storeOwnerId: undefined };
|
|
159
|
+
return { shippingFee: 0, storeOwnerId: undefined, storeEmiEnabled };
|
|
157
160
|
const sellerUser = await userRepository.findById(storeOwnerId);
|
|
158
161
|
const shippingConfig = sellerUser?.shippingConfig;
|
|
159
162
|
if (!shippingConfig?.isConfigured)
|
|
160
|
-
return { shippingFee: 0, storeOwnerId };
|
|
161
|
-
|
|
162
|
-
return { shippingFee: shippingConfig.customShippingPrice ?? 0, storeOwnerId };
|
|
163
|
-
}
|
|
164
|
-
if (shippingConfig.method === "shiprocket") {
|
|
165
|
-
const percentFee = groupTotal * (commissions.platformShippingPercent / 100);
|
|
166
|
-
return { shippingFee: Math.max(percentFee, commissions.platformShippingFixedMin), storeOwnerId };
|
|
167
|
-
}
|
|
168
|
-
return { shippingFee: 0, storeOwnerId };
|
|
163
|
+
return { shippingFee: 0, storeOwnerId, storeEmiEnabled };
|
|
164
|
+
return { shippingFee: shippingConfig.customShippingPrice ?? 0, storeOwnerId, storeEmiEnabled };
|
|
169
165
|
}
|
|
170
166
|
function buildStockUpdatePayload(product, qtyDelta) {
|
|
171
167
|
const lt = (product.listingType ?? "standard");
|
|
@@ -224,6 +220,208 @@ function dispatchOrderConfirmationEmails(emailsToSend) {
|
|
|
224
220
|
return;
|
|
225
221
|
Promise.all(emailsToSend.map((e) => sendOrderConfirmationEmail(e))).catch((err) => serverLogger.error("Order confirmation email error:", err));
|
|
226
222
|
}
|
|
223
|
+
// SB-UNI-5 2026-05-13 — bundle cart-lines use item.price (locked bundle price
|
|
224
|
+
// at add-time); regular lines use product.price (current Firestore). Prevents
|
|
225
|
+
// stale cart-cached prices from being charged on COD/UPI orders.
|
|
226
|
+
function unitPriceFor(item, product) {
|
|
227
|
+
return item.bundleCategorySlug && item.bundleProductIds?.length
|
|
228
|
+
? item.price
|
|
229
|
+
: product.price;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Places one order for one seller-group of the checkout cart — fee math
|
|
233
|
+
* (COD deposit/handling, EMI schedule), coupon apportionment, order-doc
|
|
234
|
+
* creation, digital-code claim, and notification fan-out. Pushes into
|
|
235
|
+
* `ctx.orderIds` / `ctx.emailsToSend` and returns the group's pre-fee total
|
|
236
|
+
* so the caller can accumulate the checkout-wide `total`.
|
|
237
|
+
*/
|
|
238
|
+
async function createOrderForGroup(group, orderType, ctx) {
|
|
239
|
+
const { paymentMethod, emiTenureMonths, emiSettings, commissions, appliedCoupons, cartSubtotal, couponUsageAccumulator, uid, userName, userEmail, shippingAddress, notes, adminBypass, adminBypassBy, adminBatchId, orderIds, emailsToSend, } = ctx;
|
|
240
|
+
const firstItem = group[0].item;
|
|
241
|
+
const firstProduct = group[0].product;
|
|
242
|
+
const groupTotal = group.reduce((sum, { item, product }) => sum + unitPriceFor(item, product) * item.quantity, 0);
|
|
243
|
+
// S-SBUNI-RULES 2026-05-13 — order-item decoration via rule registry.
|
|
244
|
+
const orderItems = group.map(({ item, product }) => {
|
|
245
|
+
const lt = (product.listingType ?? "standard");
|
|
246
|
+
const itemRule = getListingRule(lt);
|
|
247
|
+
// SB-UNI-5 2026-05-13 — forward bundle identifiers so the order-detail
|
|
248
|
+
// UI can collapse expanded lines back under a "Bundle: <name>" header.
|
|
249
|
+
const bundleFields = item.bundleCategorySlug && item.bundleProductIds
|
|
250
|
+
? {
|
|
251
|
+
bundleCategorySlug: item.bundleCategorySlug,
|
|
252
|
+
bundleProductIds: item.bundleProductIds,
|
|
253
|
+
}
|
|
254
|
+
: {};
|
|
255
|
+
const unitPrice = unitPriceFor(item, product);
|
|
256
|
+
const baseLine = {
|
|
257
|
+
productId: item.productId,
|
|
258
|
+
productTitle: item.productTitle,
|
|
259
|
+
quantity: item.quantity,
|
|
260
|
+
unitPrice,
|
|
261
|
+
totalPrice: unitPrice * item.quantity,
|
|
262
|
+
...bundleFields,
|
|
263
|
+
};
|
|
264
|
+
return itemRule.decorateOrderItem(baseLine, product);
|
|
265
|
+
});
|
|
266
|
+
const totalQuantity = group.reduce((sum, { item }) => sum + item.quantity, 0);
|
|
267
|
+
const { shippingFee, storeOwnerId, storeEmiEnabled } = await resolveShippingCost(firstItem.storeId);
|
|
268
|
+
const isCodLike = paymentMethod === "cod" || paymentMethod === "upi_manual" || paymentMethod === "cash";
|
|
269
|
+
const depositAmount = isCodLike
|
|
270
|
+
? Math.round(groupTotal * (commissions.codDepositPercent / 100) * 100) / 100
|
|
271
|
+
: undefined;
|
|
272
|
+
const codRemainingAmount = isCodLike
|
|
273
|
+
? Math.round((groupTotal - (depositAmount ?? 0)) * 100) / 100
|
|
274
|
+
: undefined;
|
|
275
|
+
let emiSchedule;
|
|
276
|
+
if (paymentMethod === "emi" && emiTenureMonths) {
|
|
277
|
+
const eligibility = checkEmiEligibility(groupTotal, storeEmiEnabled, emiSettings);
|
|
278
|
+
if (!eligibility.eligible) {
|
|
279
|
+
throw new ValidationError(`EMI is not available for this order (${eligibility.reason}). Minimum order value per seller is ₹${Math.round(emiSettings.minOrderValueInPaise / 100)}.`);
|
|
280
|
+
}
|
|
281
|
+
emiSchedule = computeEmiSchedule(groupTotal, emiTenureMonths, emiSettings);
|
|
282
|
+
}
|
|
283
|
+
// Handling fee charged to the buyer for choosing COD, on top of the order
|
|
284
|
+
// total — not deducted from the deposit/remaining split above.
|
|
285
|
+
const codHandlingFee = paymentMethod === "cod" ? computeCodHandlingFee(groupTotal, commissions) : 0;
|
|
286
|
+
let couponDiscount = 0;
|
|
287
|
+
const appliedDiscounts = [];
|
|
288
|
+
const groupCouponCodes = new Set();
|
|
289
|
+
for (const coupon of appliedCoupons) {
|
|
290
|
+
let couponGroupDiscount = 0;
|
|
291
|
+
const isSellerScoped = coupon.scope === "seller" && coupon.storeId;
|
|
292
|
+
if (isSellerScoped) {
|
|
293
|
+
if (coupon.storeId !== firstItem.storeId)
|
|
294
|
+
continue;
|
|
295
|
+
if (coupon.applicableItemIds?.length) {
|
|
296
|
+
const eligibleTotal = group
|
|
297
|
+
.filter(({ item }) => coupon.applicableItemIds.includes(item.itemId))
|
|
298
|
+
.reduce((s, { item }) => s + item.price * item.quantity, 0);
|
|
299
|
+
couponGroupDiscount =
|
|
300
|
+
eligibleTotal > 0
|
|
301
|
+
? Math.min(Math.round((eligibleTotal / groupTotal) * coupon.discountAmount * 100) / 100, eligibleTotal)
|
|
302
|
+
: 0;
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
couponGroupDiscount = Math.min(coupon.discountAmount, groupTotal);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
if (cartSubtotal > 0) {
|
|
310
|
+
couponGroupDiscount = Math.min(Math.round((groupTotal / cartSubtotal) * coupon.discountAmount * 100) / 100, groupTotal);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (couponGroupDiscount > 0) {
|
|
314
|
+
couponDiscount += couponGroupDiscount;
|
|
315
|
+
appliedDiscounts.push({
|
|
316
|
+
code: coupon.code,
|
|
317
|
+
couponId: coupon.couponId,
|
|
318
|
+
type: "coupon",
|
|
319
|
+
discountAmount: couponGroupDiscount,
|
|
320
|
+
scope: coupon.scope,
|
|
321
|
+
storeId: coupon.storeId,
|
|
322
|
+
});
|
|
323
|
+
if (coupon.couponId) {
|
|
324
|
+
accumulateCouponUsage(couponUsageAccumulator, coupon.code, coupon.couponId, couponGroupDiscount);
|
|
325
|
+
groupCouponCodes.add(coupon.code);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
couponDiscount = Math.min(couponDiscount, groupTotal);
|
|
330
|
+
const orderTotal = Math.max(0, groupTotal - couponDiscount) + shippingFee + codHandlingFee + (emiSchedule?.surchargeAmount ?? 0);
|
|
331
|
+
const imageUrls = [
|
|
332
|
+
...new Set(group
|
|
333
|
+
.map(({ product }) => product.mainImage)
|
|
334
|
+
.filter((url) => typeof url === "string" && url.length > 0)),
|
|
335
|
+
];
|
|
336
|
+
// S-SBUNI-RULES 2026-05-13 — order-doc decoration via rule registry.
|
|
337
|
+
const lt0 = (group[0].product.listingType ?? "standard");
|
|
338
|
+
const groupRule = getListingRule(lt0);
|
|
339
|
+
const extraOrderFields = {
|
|
340
|
+
...groupRule.decorateOrderDoc(group[0].item, group[0].product),
|
|
341
|
+
...(orderType === "prize-draw"
|
|
342
|
+
? { prizeRevealDeadline: computePrizeRevealDeadline(group[0].product) }
|
|
343
|
+
: {}),
|
|
344
|
+
};
|
|
345
|
+
const order = await unitOfWork.orders.create({
|
|
346
|
+
productId: firstItem.productId,
|
|
347
|
+
productTitle: firstItem.productTitle,
|
|
348
|
+
userId: uid,
|
|
349
|
+
userName,
|
|
350
|
+
userEmail,
|
|
351
|
+
quantity: totalQuantity,
|
|
352
|
+
unitPrice: unitPriceFor(firstItem, firstProduct),
|
|
353
|
+
totalPrice: orderTotal,
|
|
354
|
+
currency: firstItem.currency ?? getDefaultCurrency(),
|
|
355
|
+
storeId: firstItem.storeId || undefined,
|
|
356
|
+
storeName: firstItem.storeName || undefined,
|
|
357
|
+
items: orderItems,
|
|
358
|
+
orderType,
|
|
359
|
+
offerId: firstItem.offerId ?? undefined,
|
|
360
|
+
status: adminBypass ? OrderStatusValues.PROCESSING : OrderStatusValues.PENDING,
|
|
361
|
+
paymentStatus: adminBypass ? PaymentStatusValues.PAID : PaymentStatusValues.PENDING,
|
|
362
|
+
paymentMethod: adminBypass ? PaymentMethodValues.ADMIN_BYPASS : paymentMethod,
|
|
363
|
+
paymentId: adminBatchId,
|
|
364
|
+
adminBypassBy: adminBypassBy,
|
|
365
|
+
shippingAddress,
|
|
366
|
+
notes,
|
|
367
|
+
shippingFee: shippingFee > 0 ? shippingFee : undefined,
|
|
368
|
+
depositAmount: adminBypass ? undefined : depositAmount,
|
|
369
|
+
codRemainingAmount: adminBypass ? undefined : codRemainingAmount,
|
|
370
|
+
codHandlingFee: !adminBypass && codHandlingFee > 0 ? codHandlingFee : undefined,
|
|
371
|
+
emiEnabled: !adminBypass && !!emiSchedule ? true : undefined,
|
|
372
|
+
emiTenureMonths: !adminBypass && emiSchedule ? emiTenureMonths : undefined,
|
|
373
|
+
emiTokenAmount: !adminBypass ? emiSchedule?.tokenAmount : undefined,
|
|
374
|
+
emiSurchargeAmount: !adminBypass ? emiSchedule?.surchargeAmount : undefined,
|
|
375
|
+
emiInstallments: !adminBypass && emiSchedule
|
|
376
|
+
? emiSchedule.installments.map((i) => ({ index: i.index, dueDate: i.dueDate, amount: i.amount, status: "pending" }))
|
|
377
|
+
: undefined,
|
|
378
|
+
emiRemainingBalance: !adminBypass ? emiSchedule?.remainingBalance : undefined,
|
|
379
|
+
emiComplete: !adminBypass && emiSchedule ? false : undefined,
|
|
380
|
+
couponCode: appliedDiscounts[0]?.code,
|
|
381
|
+
couponDiscount: couponDiscount > 0 ? couponDiscount : undefined,
|
|
382
|
+
appliedDiscounts: appliedDiscounts.length > 0 ? appliedDiscounts : undefined,
|
|
383
|
+
imageUrls: imageUrls.length > 0 ? imageUrls : undefined,
|
|
384
|
+
...extraOrderFields,
|
|
385
|
+
});
|
|
386
|
+
orderIds.push(order.id);
|
|
387
|
+
// SB-UNI-N — claim a digital code for digital-code orders (fire-and-forget,
|
|
388
|
+
// order is already persisted so a claim failure is logged, not thrown).
|
|
389
|
+
if ((group[0]?.product?.listingType ?? "standard") === "digital-code") {
|
|
390
|
+
claimDigitalCodeForOrder(getAdminDb(), firstItem.productId, order.id, uid, {
|
|
391
|
+
userEmail: userEmail || undefined,
|
|
392
|
+
userName,
|
|
393
|
+
productTitle: firstItem.productTitle,
|
|
394
|
+
}).catch((e) => serverLogger.error("claimDigitalCode", e));
|
|
395
|
+
}
|
|
396
|
+
for (const code of groupCouponCodes) {
|
|
397
|
+
const entry = couponUsageAccumulator.get(code);
|
|
398
|
+
if (entry)
|
|
399
|
+
entry.orderIds.push(order.id);
|
|
400
|
+
}
|
|
401
|
+
if (userEmail) {
|
|
402
|
+
emailsToSend.push({
|
|
403
|
+
to: userEmail,
|
|
404
|
+
userName,
|
|
405
|
+
orderId: order.id,
|
|
406
|
+
productTitle: orderItems.length > 1 ? `${orderItems.length} items` : firstItem.productTitle,
|
|
407
|
+
quantity: totalQuantity,
|
|
408
|
+
totalPrice: orderTotal,
|
|
409
|
+
currency: firstItem.currency ?? getDefaultCurrency(),
|
|
410
|
+
shippingAddress,
|
|
411
|
+
paymentMethod,
|
|
412
|
+
items: orderItems,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
emitOrderPlacedNotifications({
|
|
416
|
+
orderId: order.id,
|
|
417
|
+
buyerUid: uid,
|
|
418
|
+
buyerName: userName,
|
|
419
|
+
storeOwnerId,
|
|
420
|
+
productLabel: orderItems.length > 1 ? `${orderItems.length} items` : firstItem.productTitle,
|
|
421
|
+
paid: adminBypass,
|
|
422
|
+
});
|
|
423
|
+
return groupTotal;
|
|
424
|
+
}
|
|
227
425
|
/**
|
|
228
426
|
* Place order(s) from the user's cart in a single Firestore transaction.
|
|
229
427
|
*
|
|
@@ -231,9 +429,15 @@ function dispatchOrderConfirmationEmails(emailsToSend) {
|
|
|
231
429
|
* auth + rate-limiting before calling this and supply the resolved user fields.
|
|
232
430
|
*/
|
|
233
431
|
export async function createCheckoutOrderAction(input) {
|
|
234
|
-
const { userId: uid, userName, userEmail, addressId, paymentMethod, notes, excludedProductIds = [], adminBypass = false, adminBypassBy, } = input;
|
|
432
|
+
const { userId: uid, userName, userEmail, addressId, paymentMethod, emiTenureMonths, notes, excludedProductIds = [], adminBypass = false, adminBypassBy, } = input;
|
|
235
433
|
const siteSettings = await siteSettingsRepository.getSingleton();
|
|
236
434
|
const commissions = siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS;
|
|
435
|
+
const emiSettings = siteSettings?.emi ?? CHECKOUT_DEFAULT_EMI_SETTINGS;
|
|
436
|
+
if (paymentMethod === "emi") {
|
|
437
|
+
if (!emiTenureMonths || !emiSettings.tenureOptions.includes(emiTenureMonths)) {
|
|
438
|
+
throw new ValidationError(`emiTenureMonths must be one of: ${emiSettings.tenureOptions.join(", ")}`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
237
441
|
// W1-43 — enforce COD toggle.
|
|
238
442
|
if (!adminBypass &&
|
|
239
443
|
paymentMethod === "cod" &&
|
|
@@ -440,178 +644,29 @@ export async function createCheckoutOrderAction(input) {
|
|
|
440
644
|
const orderIds = [];
|
|
441
645
|
let total = 0;
|
|
442
646
|
const emailsToSend = [];
|
|
443
|
-
// SB-UNI-5 2026-05-13 — bundle cart-lines use item.price (locked bundle
|
|
444
|
-
// price at add-time); regular lines use product.price (current Firestore).
|
|
445
|
-
// This prevents stale cart-cached prices from being charged on COD/UPI orders.
|
|
446
|
-
const unitPriceFor = (item, product) => item.bundleCategorySlug && item.bundleProductIds?.length
|
|
447
|
-
? item.price
|
|
448
|
-
: product.price;
|
|
449
647
|
const cartSubtotal = orderGroups.reduce((s, { items: g }) => s + g.reduce((gs, { item, product }) => gs + unitPriceFor(item, product) * item.quantity, 0), 0);
|
|
450
648
|
const couponUsageAccumulator = new Map();
|
|
649
|
+
const groupCtx = {
|
|
650
|
+
paymentMethod,
|
|
651
|
+
emiTenureMonths,
|
|
652
|
+
emiSettings,
|
|
653
|
+
commissions,
|
|
654
|
+
appliedCoupons,
|
|
655
|
+
cartSubtotal,
|
|
656
|
+
couponUsageAccumulator,
|
|
657
|
+
uid,
|
|
658
|
+
userName,
|
|
659
|
+
userEmail,
|
|
660
|
+
shippingAddress,
|
|
661
|
+
notes,
|
|
662
|
+
adminBypass,
|
|
663
|
+
adminBypassBy,
|
|
664
|
+
adminBatchId,
|
|
665
|
+
orderIds,
|
|
666
|
+
emailsToSend,
|
|
667
|
+
};
|
|
451
668
|
for (const { items: group, orderType } of orderGroups) {
|
|
452
|
-
|
|
453
|
-
const firstProduct = group[0].product;
|
|
454
|
-
const groupTotal = group.reduce((sum, { item, product }) => sum + unitPriceFor(item, product) * item.quantity, 0);
|
|
455
|
-
total += groupTotal;
|
|
456
|
-
// S-SBUNI-RULES 2026-05-13 — order-item decoration via rule registry.
|
|
457
|
-
const orderItems = group.map(({ item, product }) => {
|
|
458
|
-
const lt = (product.listingType ?? "standard");
|
|
459
|
-
const itemRule = getListingRule(lt);
|
|
460
|
-
// SB-UNI-5 2026-05-13 — forward bundle identifiers so the order-detail
|
|
461
|
-
// UI can collapse expanded lines back under a "Bundle: <name>" header.
|
|
462
|
-
const bundleFields = item.bundleCategorySlug && item.bundleProductIds
|
|
463
|
-
? {
|
|
464
|
-
bundleCategorySlug: item.bundleCategorySlug,
|
|
465
|
-
bundleProductIds: item.bundleProductIds,
|
|
466
|
-
}
|
|
467
|
-
: {};
|
|
468
|
-
const unitPrice = unitPriceFor(item, product);
|
|
469
|
-
const baseLine = {
|
|
470
|
-
productId: item.productId,
|
|
471
|
-
productTitle: item.productTitle,
|
|
472
|
-
quantity: item.quantity,
|
|
473
|
-
unitPrice,
|
|
474
|
-
totalPrice: unitPrice * item.quantity,
|
|
475
|
-
...bundleFields,
|
|
476
|
-
};
|
|
477
|
-
return itemRule.decorateOrderItem(baseLine, product);
|
|
478
|
-
});
|
|
479
|
-
const totalQuantity = group.reduce((sum, { item }) => sum + item.quantity, 0);
|
|
480
|
-
const { shippingFee, storeOwnerId } = await resolveShippingCost(firstItem.storeId, groupTotal, commissions);
|
|
481
|
-
const isCodLike = paymentMethod === "cod" || paymentMethod === "upi_manual" || paymentMethod === "cash";
|
|
482
|
-
const depositAmount = isCodLike
|
|
483
|
-
? Math.round(groupTotal * (commissions.codDepositPercent / 100) * 100) / 100
|
|
484
|
-
: undefined;
|
|
485
|
-
const codRemainingAmount = isCodLike
|
|
486
|
-
? Math.round((groupTotal - (depositAmount ?? 0)) * 100) / 100
|
|
487
|
-
: undefined;
|
|
488
|
-
let couponDiscount = 0;
|
|
489
|
-
const appliedDiscounts = [];
|
|
490
|
-
const groupCouponCodes = new Set();
|
|
491
|
-
for (const coupon of appliedCoupons) {
|
|
492
|
-
let couponGroupDiscount = 0;
|
|
493
|
-
const isSellerScoped = coupon.scope === "seller" && coupon.storeId;
|
|
494
|
-
if (isSellerScoped) {
|
|
495
|
-
if (coupon.storeId !== firstItem.storeId)
|
|
496
|
-
continue;
|
|
497
|
-
if (coupon.applicableItemIds?.length) {
|
|
498
|
-
const eligibleTotal = group
|
|
499
|
-
.filter(({ item }) => coupon.applicableItemIds.includes(item.itemId))
|
|
500
|
-
.reduce((s, { item }) => s + item.price * item.quantity, 0);
|
|
501
|
-
couponGroupDiscount =
|
|
502
|
-
eligibleTotal > 0
|
|
503
|
-
? Math.min(Math.round((eligibleTotal / groupTotal) * coupon.discountAmount * 100) / 100, eligibleTotal)
|
|
504
|
-
: 0;
|
|
505
|
-
}
|
|
506
|
-
else {
|
|
507
|
-
couponGroupDiscount = Math.min(coupon.discountAmount, groupTotal);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
else {
|
|
511
|
-
if (cartSubtotal > 0) {
|
|
512
|
-
couponGroupDiscount = Math.min(Math.round((groupTotal / cartSubtotal) * coupon.discountAmount * 100) / 100, groupTotal);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
if (couponGroupDiscount > 0) {
|
|
516
|
-
couponDiscount += couponGroupDiscount;
|
|
517
|
-
appliedDiscounts.push({
|
|
518
|
-
code: coupon.code,
|
|
519
|
-
couponId: coupon.couponId,
|
|
520
|
-
type: "coupon",
|
|
521
|
-
discountAmount: couponGroupDiscount,
|
|
522
|
-
scope: coupon.scope,
|
|
523
|
-
storeId: coupon.storeId,
|
|
524
|
-
});
|
|
525
|
-
if (coupon.couponId) {
|
|
526
|
-
accumulateCouponUsage(couponUsageAccumulator, coupon.code, coupon.couponId, couponGroupDiscount);
|
|
527
|
-
groupCouponCodes.add(coupon.code);
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
couponDiscount = Math.min(couponDiscount, groupTotal);
|
|
532
|
-
const orderTotal = Math.max(0, groupTotal - couponDiscount) + shippingFee;
|
|
533
|
-
const imageUrls = [
|
|
534
|
-
...new Set(group
|
|
535
|
-
.map(({ product }) => product.mainImage)
|
|
536
|
-
.filter((url) => typeof url === "string" && url.length > 0)),
|
|
537
|
-
];
|
|
538
|
-
// S-SBUNI-RULES 2026-05-13 — order-doc decoration via rule registry.
|
|
539
|
-
const lt0 = (group[0].product.listingType ?? "standard");
|
|
540
|
-
const groupRule = getListingRule(lt0);
|
|
541
|
-
const extraOrderFields = {
|
|
542
|
-
...groupRule.decorateOrderDoc(group[0].item, group[0].product),
|
|
543
|
-
...(orderType === "prize-draw"
|
|
544
|
-
? { prizeRevealDeadline: computePrizeRevealDeadline(group[0].product) }
|
|
545
|
-
: {}),
|
|
546
|
-
};
|
|
547
|
-
const order = await unitOfWork.orders.create({
|
|
548
|
-
productId: firstItem.productId,
|
|
549
|
-
productTitle: firstItem.productTitle,
|
|
550
|
-
userId: uid,
|
|
551
|
-
userName,
|
|
552
|
-
userEmail,
|
|
553
|
-
quantity: totalQuantity,
|
|
554
|
-
unitPrice: unitPriceFor(firstItem, firstProduct),
|
|
555
|
-
totalPrice: orderTotal,
|
|
556
|
-
currency: firstItem.currency ?? getDefaultCurrency(),
|
|
557
|
-
storeId: firstItem.storeId || undefined,
|
|
558
|
-
storeName: firstItem.storeName || undefined,
|
|
559
|
-
items: orderItems,
|
|
560
|
-
orderType,
|
|
561
|
-
offerId: firstItem.offerId ?? undefined,
|
|
562
|
-
status: adminBypass ? OrderStatusValues.PROCESSING : OrderStatusValues.PENDING,
|
|
563
|
-
paymentStatus: adminBypass ? PaymentStatusValues.PAID : PaymentStatusValues.PENDING,
|
|
564
|
-
paymentMethod: adminBypass ? PaymentMethodValues.ADMIN_BYPASS : paymentMethod,
|
|
565
|
-
paymentId: adminBatchId,
|
|
566
|
-
adminBypassBy: adminBypassBy,
|
|
567
|
-
shippingAddress,
|
|
568
|
-
notes,
|
|
569
|
-
shippingFee: shippingFee > 0 ? shippingFee : undefined,
|
|
570
|
-
depositAmount: adminBypass ? undefined : depositAmount,
|
|
571
|
-
codRemainingAmount: adminBypass ? undefined : codRemainingAmount,
|
|
572
|
-
couponCode: appliedDiscounts[0]?.code,
|
|
573
|
-
couponDiscount: couponDiscount > 0 ? couponDiscount : undefined,
|
|
574
|
-
appliedDiscounts: appliedDiscounts.length > 0 ? appliedDiscounts : undefined,
|
|
575
|
-
imageUrls: imageUrls.length > 0 ? imageUrls : undefined,
|
|
576
|
-
...extraOrderFields,
|
|
577
|
-
});
|
|
578
|
-
orderIds.push(order.id);
|
|
579
|
-
// SB-UNI-N — claim a digital code for digital-code orders (fire-and-forget,
|
|
580
|
-
// order is already persisted so a claim failure is logged, not thrown).
|
|
581
|
-
if ((group[0]?.product?.listingType ?? "standard") === "digital-code") {
|
|
582
|
-
claimDigitalCodeForOrder(getAdminDb(), firstItem.productId, order.id, uid, {
|
|
583
|
-
userEmail: userEmail || undefined,
|
|
584
|
-
userName,
|
|
585
|
-
productTitle: firstItem.productTitle,
|
|
586
|
-
}).catch((e) => serverLogger.error("claimDigitalCode", e));
|
|
587
|
-
}
|
|
588
|
-
for (const code of groupCouponCodes) {
|
|
589
|
-
const entry = couponUsageAccumulator.get(code);
|
|
590
|
-
if (entry)
|
|
591
|
-
entry.orderIds.push(order.id);
|
|
592
|
-
}
|
|
593
|
-
if (userEmail) {
|
|
594
|
-
emailsToSend.push({
|
|
595
|
-
to: userEmail,
|
|
596
|
-
userName,
|
|
597
|
-
orderId: order.id,
|
|
598
|
-
productTitle: orderItems.length > 1 ? `${orderItems.length} items` : firstItem.productTitle,
|
|
599
|
-
quantity: totalQuantity,
|
|
600
|
-
totalPrice: orderTotal,
|
|
601
|
-
currency: firstItem.currency ?? getDefaultCurrency(),
|
|
602
|
-
shippingAddress,
|
|
603
|
-
paymentMethod,
|
|
604
|
-
items: orderItems,
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
emitOrderPlacedNotifications({
|
|
608
|
-
orderId: order.id,
|
|
609
|
-
buyerUid: uid,
|
|
610
|
-
buyerName: userName,
|
|
611
|
-
storeOwnerId,
|
|
612
|
-
productLabel: orderItems.length > 1 ? `${orderItems.length} items` : firstItem.productTitle,
|
|
613
|
-
paid: adminBypass,
|
|
614
|
-
});
|
|
669
|
+
total += await createOrderForGroup(group, orderType, groupCtx);
|
|
615
670
|
}
|
|
616
671
|
await flushCouponUsageAccumulator(couponUsageAccumulator, uid);
|
|
617
672
|
grantConsentOtpBypassCredit(db, uid, emailOtpUsed, unavailable.length);
|
|
@@ -821,13 +876,7 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
821
876
|
const sellerUser = storeOwnerId ? await userRepository.findById(storeOwnerId) : null;
|
|
822
877
|
const shippingConfig = sellerUser?.shippingConfig;
|
|
823
878
|
if (shippingConfig?.isConfigured) {
|
|
824
|
-
|
|
825
|
-
shippingFee = shippingConfig.customShippingPrice ?? 0;
|
|
826
|
-
}
|
|
827
|
-
else if (shippingConfig.method === "shiprocket") {
|
|
828
|
-
const percentFee = groupTotal * (commissions.platformShippingPercent / 100);
|
|
829
|
-
shippingFee = Math.max(percentFee, commissions.platformShippingFixedMin);
|
|
830
|
-
}
|
|
879
|
+
shippingFee = shippingConfig.customShippingPrice ?? 0;
|
|
831
880
|
}
|
|
832
881
|
}
|
|
833
882
|
let couponDiscount = 0;
|
|
@@ -19,4 +19,5 @@ export declare const prizeRevealClose: import("./types").ScheduledFunctionDefini
|
|
|
19
19
|
export declare const prizeRevealExpiry: import("./types").ScheduledFunctionDefinition;
|
|
20
20
|
export declare const prizeRevealReminder: import("./types").ScheduledFunctionDefinition;
|
|
21
21
|
export declare const bundleStockSync: import("./types").ScheduledFunctionDefinition;
|
|
22
|
-
export declare const
|
|
22
|
+
export declare const emiInstallmentReminder: import("./types").ScheduledFunctionDefinition;
|
|
23
|
+
export declare const SCHEDULED_FUNCTIONS: readonly [import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Scheduled function definitions (Cloud Scheduler triggers).
|
|
2
2
|
// Cron syntax accepts both Firebase shorthand and standard cron expressions.
|
|
3
|
-
import { auctionSettlementHandler, autoPayoutEligibilityHandler, bundleStockSyncHandler, cartPruneHandler, cleanupRtdbEventsHandler, countersReconcileHandler, couponExpiryHandler, dailyDataCleanupHandler, draftPruneHandler, mediaTmpCleanupHandler, notificationPruneHandler, offerExpiryHandler, payoutBatchHandler, pendingOrderTimeoutHandler, positionsReconcileHandler, prizeRevealCloseHandler, prizeRevealExpiryHandler, prizeRevealOpenHandler, prizeRevealReminderHandler, productStatsSyncHandler, weeklyPayoutEligibilityHandler, } from "../jobs/handlers";
|
|
3
|
+
import { auctionSettlementHandler, autoPayoutEligibilityHandler, bundleStockSyncHandler, cartPruneHandler, cleanupRtdbEventsHandler, countersReconcileHandler, couponExpiryHandler, dailyDataCleanupHandler, draftPruneHandler, emiInstallmentReminderHandler, mediaTmpCleanupHandler, notificationPruneHandler, offerExpiryHandler, payoutBatchHandler, pendingOrderTimeoutHandler, positionsReconcileHandler, prizeRevealCloseHandler, prizeRevealExpiryHandler, prizeRevealOpenHandler, prizeRevealReminderHandler, productStatsSyncHandler, weeklyPayoutEligibilityHandler, } from "../jobs/handlers";
|
|
4
4
|
import { defineFunction } from "./define";
|
|
5
5
|
const REGION = "asia-south1";
|
|
6
6
|
const EVERY_5_MIN = "every 5 minutes";
|
|
@@ -151,6 +151,13 @@ export const bundleStockSync = defineFunction({
|
|
|
151
151
|
handler: bundleStockSyncHandler,
|
|
152
152
|
options: { region: REGION, timeoutSeconds: 540, memory: "256MiB", maxInstances: 1 },
|
|
153
153
|
});
|
|
154
|
+
export const emiInstallmentReminder = defineFunction({
|
|
155
|
+
name: "emiInstallmentReminder",
|
|
156
|
+
description: "Nudge buyers on upcoming EMI installments and flag overdue ones (daily 09:00 IST).",
|
|
157
|
+
trigger: { kind: "schedule", cron: "0 9 * * *", timeZone: "Asia/Kolkata" },
|
|
158
|
+
handler: emiInstallmentReminderHandler,
|
|
159
|
+
options: { region: REGION, timeoutSeconds: 300, memory: "256MiB", maxInstances: 1 },
|
|
160
|
+
});
|
|
154
161
|
export const SCHEDULED_FUNCTIONS = [
|
|
155
162
|
auctionSettlement,
|
|
156
163
|
pendingOrderTimeout,
|
|
@@ -173,4 +180,5 @@ export const SCHEDULED_FUNCTIONS = [
|
|
|
173
180
|
prizeRevealExpiry,
|
|
174
181
|
prizeRevealReminder,
|
|
175
182
|
bundleStockSync,
|
|
183
|
+
emiInstallmentReminder,
|
|
176
184
|
];
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import { orderRepository, payoutRepository, storeRepository, userRepository, } from "../../../../repositories";
|
|
1
|
+
import { orderRepository, payoutRepository, storeRepository, userRepository, siteSettingsRepository, } from "../../../../repositories";
|
|
2
2
|
import { getDefaultCurrency } from "../../../../core/index";
|
|
3
|
+
import { computePayoutDeduction } from "../../../shared/fees/calculator";
|
|
3
4
|
import { BATCH_LIMIT } from "../handlers/messages";
|
|
4
5
|
const AUTO_PAYOUT_WINDOW_DAYS = 7;
|
|
5
|
-
const PLATFORM_COMMISSION_RATE = 0.05;
|
|
6
|
-
const GATEWAY_FEE_RATE = 0.0236;
|
|
7
|
-
const GST_RATE = 0.18;
|
|
8
6
|
function getBusinessDayCutoff(daysAgo) {
|
|
9
7
|
const now = new Date();
|
|
10
8
|
const cutoff = new Date(now);
|
|
@@ -20,6 +18,8 @@ export async function runAutoPayoutEligibility(ctx) {
|
|
|
20
18
|
ctx.logger.info("Starting daily auto-payout eligibility sweep", {
|
|
21
19
|
windowDays: AUTO_PAYOUT_WINDOW_DAYS,
|
|
22
20
|
});
|
|
21
|
+
const siteSettings = await siteSettingsRepository.getSingleton();
|
|
22
|
+
const commissions = siteSettings.commissions;
|
|
23
23
|
const cutoff = getBusinessDayCutoff(AUTO_PAYOUT_WINDOW_DAYS);
|
|
24
24
|
const eligible = await orderRepository.getEligibleAutomatic(cutoff);
|
|
25
25
|
if (eligible.length === 0) {
|
|
@@ -49,10 +49,7 @@ export async function runAutoPayoutEligibility(ctx) {
|
|
|
49
49
|
}
|
|
50
50
|
const sellerId = store.ownerId;
|
|
51
51
|
const grossAmount = orders.reduce((sum, o) => sum + (o.data.totalPrice ?? 0), 0);
|
|
52
|
-
const platformFee
|
|
53
|
-
const gatewayFee = Math.round(grossAmount * GATEWAY_FEE_RATE * 100) / 100;
|
|
54
|
-
const gstAmount = Math.round(platformFee * GST_RATE * 100) / 100;
|
|
55
|
-
const netAmount = Math.round((grossAmount - platformFee - gatewayFee - gstAmount) * 100) / 100;
|
|
52
|
+
const { platformFee, gatewayFee, gstOnFee: gstAmount, netAmount } = computePayoutDeduction(grossAmount, commissions);
|
|
56
53
|
const payoutInput = {
|
|
57
54
|
storeId,
|
|
58
55
|
sellerName: (seller.displayName ?? seller.email ?? sellerId),
|
|
@@ -61,11 +58,11 @@ export async function runAutoPayoutEligibility(ctx) {
|
|
|
61
58
|
amount: netAmount,
|
|
62
59
|
grossAmount,
|
|
63
60
|
platformFee,
|
|
64
|
-
platformFeeRate:
|
|
61
|
+
platformFeeRate: commissions.platformFeePercent / 100,
|
|
65
62
|
gatewayFee,
|
|
66
|
-
gatewayFeeRate:
|
|
63
|
+
gatewayFeeRate: (commissions.gatewayFeePercent ?? 0) / 100,
|
|
67
64
|
gstAmount,
|
|
68
|
-
gstRate:
|
|
65
|
+
gstRate: commissions.gstPercent / 100,
|
|
69
66
|
isAutomatic: true,
|
|
70
67
|
currency: getDefaultCurrency(),
|
|
71
68
|
status: "pending",
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { JobContext } from "../runtime/types";
|
|
2
|
+
/**
|
|
3
|
+
* EMI reminder sweep — nudges buyers ahead of an upcoming installment due
|
|
4
|
+
* date and flips overdue-but-unpaid installments to `status: "overdue"`.
|
|
5
|
+
* `reminderSentAt` gates re-notification so a buyer isn't paged on every
|
|
6
|
+
* sweep run for the same installment.
|
|
7
|
+
*/
|
|
8
|
+
export declare function runEmiInstallmentReminder(ctx: JobContext): Promise<void>;
|