@mohasinac/appkit 4.1.3 → 4.2.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 +15 -0
- package/dist/_internal/server/features/checkout/actions.js +245 -182
- 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/jobRunners.js +10 -0
- package/dist/_internal/server/jobs/core/newsletterExport.d.ts +15 -0
- package/dist/_internal/server/jobs/core/newsletterExport.js +43 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +2 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +1 -0
- package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +1 -0
- package/dist/_internal/server/jobs/core/pendingOrderTimeout.js +1 -0
- package/dist/_internal/server/jobs/core/revenueRollup.d.ts +13 -0
- package/dist/_internal/server/jobs/core/revenueRollup.js +22 -0
- package/dist/_internal/server/jobs/core/whatsappNotify.d.ts +35 -0
- package/dist/_internal/server/jobs/core/whatsappNotify.js +82 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
- package/dist/_internal/server/jobs/handlers/index.js +1 -0
- package/dist/_internal/server/jobs/handlers/revenueRollup.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/revenueRollup.js +2 -0
- package/dist/_internal/shared/features/checkout/config.d.ts +7 -0
- package/dist/_internal/shared/features/checkout/config.js +7 -0
- package/dist/_internal/shared/fees/calculator.d.ts +26 -0
- package/dist/_internal/shared/fees/calculator.js +25 -0
- package/dist/features/about/components/FeesView.js +18 -0
- package/dist/features/admin/actions/notification-actions.d.ts +4 -1
- package/dist/features/admin/actions/notification-actions.js +56 -10
- package/dist/features/admin/components/AdminAdsView.js +1 -1
- package/dist/features/admin/components/AdminNewsletterView.js +42 -11
- package/dist/features/admin/components/AdminSiteSettingsView.js +83 -6
- package/dist/features/admin/repository/analytics-rollup.repository.d.ts +25 -0
- package/dist/features/admin/repository/analytics-rollup.repository.js +31 -0
- package/dist/features/admin/repository/site-settings.repository.js +8 -1
- package/dist/features/admin/schemas/firestore.d.ts +41 -0
- package/dist/features/admin/schemas/firestore.js +62 -24
- package/dist/features/layout/BackToTop.d.ts +9 -5
- package/dist/features/layout/BackToTop.js +17 -21
- package/dist/features/orders/actions/refund-actions.js +1 -0
- package/dist/features/orders/schemas/firestore.d.ts +14 -0
- package/dist/features/products/constants/action-defs.d.ts +28 -3
- package/dist/features/products/constants/action-defs.js +76 -1
- package/dist/features/seller/components/SellerOrdersView.js +1 -1
- package/dist/features/whatsapp-bot/helpers/whatsapp.d.ts +10 -1
- package/dist/features/whatsapp-bot/helpers/whatsapp.js +47 -0
- package/dist/features/whatsapp-bot/types/index.d.ts +19 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/next/routing/route-map.d.ts +2 -0
- package/dist/next/routing/route-map.js +1 -0
- package/dist/repositories/index.d.ts +2 -0
- package/dist/repositories/index.js +1 -0
- package/dist/seed/site-settings-seed-data.js +7 -0
- package/dist/server.d.ts +2 -2
- package/dist/server.js +2 -2
- package/package.json +1 -1
|
@@ -23,6 +23,14 @@ export interface CreateCheckoutOrderInput {
|
|
|
23
23
|
* omits the field.
|
|
24
24
|
*/
|
|
25
25
|
outOfStockPolicy?: OutOfStockPolicy;
|
|
26
|
+
/** Buyer opted into the ₹10 WhatsApp order-updates addon at checkout. Defaults false (unchecked). Only actually charged/honored when siteSettings.commissions.whatsappNotifyFeeEnabled is also true. */
|
|
27
|
+
whatsappNotifyAddon?: boolean;
|
|
28
|
+
/** Buyer opted into gift wrap at checkout. Defaults false (unchecked). Only actually charged/honored when siteSettings.commissions.giftWrapFeeEnabled is also true. */
|
|
29
|
+
giftWrapAddon?: boolean;
|
|
30
|
+
/** Optional gift message, only meaningful when giftWrapAddon is true. */
|
|
31
|
+
giftWrapMessage?: string;
|
|
32
|
+
/** Buyer opted into shipment protection at checkout. Defaults false (unchecked). Only actually charged/honored when siteSettings.commissions.shipmentProtectionFeeEnabled is also true. */
|
|
33
|
+
shipmentProtectionAddon?: boolean;
|
|
26
34
|
}
|
|
27
35
|
/**
|
|
28
36
|
* Place order(s) from the user's cart in a single Firestore transaction.
|
|
@@ -58,5 +66,12 @@ export interface VerifyAndPlaceRazorpayOrderInput {
|
|
|
58
66
|
* on the Razorpay path.
|
|
59
67
|
*/
|
|
60
68
|
outOfStockPolicy?: OutOfStockPolicy;
|
|
69
|
+
/** Buyer opted into the ₹10 WhatsApp order-updates addon at checkout — must match the value sent to /api/payment/create-order so the pre-charged amount and the placed order agree. */
|
|
70
|
+
whatsappNotifyAddon?: boolean;
|
|
71
|
+
/** Buyer opted into gift wrap at checkout — must match the value sent to /api/payment/create-order. */
|
|
72
|
+
giftWrapAddon?: boolean;
|
|
73
|
+
giftWrapMessage?: string;
|
|
74
|
+
/** Buyer opted into shipment protection at checkout — must match the value sent to /api/payment/create-order. */
|
|
75
|
+
shipmentProtectionAddon?: boolean;
|
|
61
76
|
}
|
|
62
77
|
export declare function verifyAndPlaceRazorpayOrderAction(input: VerifyAndPlaceRazorpayOrderInput): Promise<CheckoutOrderResult>;
|
|
@@ -12,14 +12,14 @@ import { roundRupees } from "../../../../utils/number.formatter";
|
|
|
12
12
|
import { ApiError, ValidationError, NotFoundError, ERROR_MESSAGES } from "../../../../errors";
|
|
13
13
|
import { ORDER_FIELDS } from "../../../../constants/field-names";
|
|
14
14
|
import { serverLogger } from "../../../../monitoring";
|
|
15
|
-
import { unitOfWork, siteSettingsRepository, userRepository, storeRepository, couponsRepository,
|
|
15
|
+
import { unitOfWork, siteSettingsRepository, userRepository, storeRepository, couponsRepository, claimedCouponsRepository, addressesRepository, } from "../../../../repositories";
|
|
16
16
|
import { calculateGst } from "../../../shared/fees/calculator";
|
|
17
17
|
import { computePreOrderDepositAmount } from "../../../shared/checkout/order-math";
|
|
18
18
|
import { failedCheckoutRepository } from "../../../../features/checkout/repository/failed-checkout.repository";
|
|
19
19
|
import { sendOrderConfirmationEmail } from "../../../../features/contact/server";
|
|
20
20
|
import { splitCartIntoOrderGroups } from "../../../../features/orders/index";
|
|
21
21
|
import { resolveDate } from "../../../../utils";
|
|
22
|
-
import { computeCodHandlingFee } from "../../../shared/fees/calculator";
|
|
22
|
+
import { computeCodHandlingFee, computeWhatsAppNotifyFee, computeGiftWrapFee, computeShipmentProtectionFee, } from "../../../shared/fees/calculator";
|
|
23
23
|
import { getAdminDb, getAdminRealtimeDb, RTDB_PATHS, } from "../../../../providers/db-firebase";
|
|
24
24
|
import { PRODUCT_COLLECTION, PRODUCT_CODES_SUBCOLLECTION } from "../../../../features/products/schemas/firestore";
|
|
25
25
|
import { CART_COLLECTION } from "../../../../features/cart/schemas/index";
|
|
@@ -112,9 +112,8 @@ function assertLiveJurisdiction(cartItemsArg, productByIdArg, buyerState) {
|
|
|
112
112
|
* either party. We emit the buyer + seller rows here at the create boundary.
|
|
113
113
|
*/
|
|
114
114
|
function emitOrderPlacedNotifications(args) {
|
|
115
|
-
const { orderId, buyerUid, buyerName, storeOwnerId, productLabel, paid } = args;
|
|
116
|
-
const buyerNotif =
|
|
117
|
-
.create({
|
|
115
|
+
const { orderId, buyerUid, buyerName, storeOwnerId, productLabel, paid, whatsappNotifyAddon } = args;
|
|
116
|
+
const buyerNotif = sendNotification({
|
|
118
117
|
userId: buyerUid,
|
|
119
118
|
type: "order_placed",
|
|
120
119
|
priority: "normal",
|
|
@@ -123,14 +122,13 @@ function emitOrderPlacedNotifications(args) {
|
|
|
123
122
|
relatedId: orderId,
|
|
124
123
|
relatedType: "order",
|
|
125
124
|
actionUrl: `/user/orders/view/${orderId}`,
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
orderWhatsappAddonPaid: whatsappNotifyAddon,
|
|
126
|
+
}).catch((err) => serverLogger.warn("Failed to send buyer order_placed notification", {
|
|
128
127
|
err: err instanceof Error ? err.message : String(err),
|
|
129
128
|
orderId,
|
|
130
129
|
}));
|
|
131
130
|
const sellerNotif = storeOwnerId
|
|
132
|
-
?
|
|
133
|
-
.create({
|
|
131
|
+
? sendNotification({
|
|
134
132
|
userId: storeOwnerId,
|
|
135
133
|
type: "order_placed",
|
|
136
134
|
priority: "high",
|
|
@@ -139,8 +137,7 @@ function emitOrderPlacedNotifications(args) {
|
|
|
139
137
|
relatedId: orderId,
|
|
140
138
|
relatedType: "order",
|
|
141
139
|
actionUrl: `/store/orders/${orderId}/view`,
|
|
142
|
-
})
|
|
143
|
-
.catch((err) => serverLogger.warn("Failed to create seller order_placed notification", {
|
|
140
|
+
}).catch((err) => serverLogger.warn("Failed to send seller order_placed notification", {
|
|
144
141
|
err: err instanceof Error ? err.message : String(err),
|
|
145
142
|
orderId,
|
|
146
143
|
}))
|
|
@@ -251,7 +248,7 @@ function unitPriceFor(item, product) {
|
|
|
251
248
|
* so the caller can accumulate the checkout-wide `total`.
|
|
252
249
|
*/
|
|
253
250
|
async function createOrderForGroup(group, orderType, ctx) {
|
|
254
|
-
const { paymentMethod, emiTenureMonths, emiSettings, commissions, appliedCoupons, cartSubtotal, couponUsageAccumulator, uid, userName, userEmail, shippingAddress, notes, adminBypass, adminBypassBy, adminBatchId, orderIds, emailsToSend, outOfStockPolicy, droppedItems, buyerState, gstSettings, siteContactUpiVpa, } = ctx;
|
|
251
|
+
const { paymentMethod, emiTenureMonths, emiSettings, commissions, appliedCoupons, cartSubtotal, couponUsageAccumulator, uid, userName, userEmail, shippingAddress, notes, adminBypass, adminBypassBy, adminBatchId, orderIds, emailsToSend, outOfStockPolicy, droppedItems, buyerState, gstSettings, siteContactUpiVpa, whatsappNotifyAddon, giftWrapAddon, giftWrapMessage, shipmentProtectionAddon, } = ctx;
|
|
255
252
|
const firstItem = group[0].item;
|
|
256
253
|
const firstProduct = group[0].product;
|
|
257
254
|
const groupTotal = group.reduce((sum, { item, product }) => sum + unitPriceFor(item, product) * item.quantity, 0);
|
|
@@ -327,6 +324,9 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
327
324
|
// Handling fee charged to the buyer for choosing COD, on top of the order
|
|
328
325
|
// total — not deducted from the deposit/remaining split above.
|
|
329
326
|
const codHandlingFee = paymentMethod === "cod" ? computeCodHandlingFee(groupTotal, commissions) : 0;
|
|
327
|
+
const whatsappNotifyFee = computeWhatsAppNotifyFee(whatsappNotifyAddon, commissions);
|
|
328
|
+
const giftWrapFee = computeGiftWrapFee(giftWrapAddon, commissions);
|
|
329
|
+
const shipmentProtectionFee = computeShipmentProtectionFee(groupTotal, shipmentProtectionAddon, commissions);
|
|
330
330
|
let couponDiscount = 0;
|
|
331
331
|
const appliedDiscounts = [];
|
|
332
332
|
const groupCouponCodes = new Set();
|
|
@@ -371,7 +371,7 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
371
371
|
}
|
|
372
372
|
}
|
|
373
373
|
couponDiscount = Math.min(couponDiscount, groupTotal);
|
|
374
|
-
const orderTotal = Math.max(0, groupTotal - couponDiscount) + shippingFee + codHandlingFee + (emiSchedule?.surchargeAmount ?? 0) + (gstBreakdown?.gstAmount ?? 0);
|
|
374
|
+
const orderTotal = Math.max(0, groupTotal - couponDiscount) + shippingFee + codHandlingFee + whatsappNotifyFee + giftWrapFee + shipmentProtectionFee + (emiSchedule?.surchargeAmount ?? 0) + (gstBreakdown?.gstAmount ?? 0);
|
|
375
375
|
const imageUrls = [
|
|
376
376
|
...new Set(group
|
|
377
377
|
.map(({ product }) => product.mainImage)
|
|
@@ -428,6 +428,13 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
428
428
|
depositAmount: adminBypass ? undefined : depositAmount,
|
|
429
429
|
codRemainingAmount: adminBypass ? undefined : codRemainingAmount,
|
|
430
430
|
codHandlingFee: !adminBypass && codHandlingFee > 0 ? codHandlingFee : undefined,
|
|
431
|
+
whatsappNotifyAddon: !adminBypass && whatsappNotifyFee > 0 ? true : undefined,
|
|
432
|
+
whatsappNotifyFee: !adminBypass && whatsappNotifyFee > 0 ? whatsappNotifyFee : undefined,
|
|
433
|
+
giftWrapAddon: !adminBypass && giftWrapFee > 0 ? true : undefined,
|
|
434
|
+
giftWrapFee: !adminBypass && giftWrapFee > 0 ? giftWrapFee : undefined,
|
|
435
|
+
giftWrapMessage: !adminBypass && giftWrapFee > 0 ? giftWrapMessage?.slice(0, 500) : undefined,
|
|
436
|
+
shipmentProtectionAddon: !adminBypass && shipmentProtectionFee > 0 ? true : undefined,
|
|
437
|
+
shipmentProtectionFee: !adminBypass && shipmentProtectionFee > 0 ? shipmentProtectionFee : undefined,
|
|
431
438
|
taxableAmount: gstBreakdown?.taxableAmount,
|
|
432
439
|
gstAmount: gstBreakdown?.gstAmount,
|
|
433
440
|
cgst: gstBreakdown?.cgst || undefined,
|
|
@@ -486,6 +493,7 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
486
493
|
storeOwnerId,
|
|
487
494
|
productLabel: orderItems.length > 1 ? `${orderItems.length} items` : firstItem.productTitle,
|
|
488
495
|
paid: adminBypass,
|
|
496
|
+
whatsappNotifyAddon: !adminBypass && whatsappNotifyFee > 0,
|
|
489
497
|
});
|
|
490
498
|
return groupTotal;
|
|
491
499
|
}
|
|
@@ -496,7 +504,7 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
496
504
|
* auth + rate-limiting before calling this and supply the resolved user fields.
|
|
497
505
|
*/
|
|
498
506
|
export async function createCheckoutOrderAction(input) {
|
|
499
|
-
const { userId: uid, userName, userEmail, addressId, paymentMethod, emiTenureMonths, notes, excludedProductIds = [], adminBypass = false, adminBypassBy, outOfStockPolicy = OutOfStockPolicyValues.SKIP_ITEMS, } = input;
|
|
507
|
+
const { userId: uid, userName, userEmail, addressId, paymentMethod, emiTenureMonths, notes, excludedProductIds = [], adminBypass = false, adminBypassBy, outOfStockPolicy = OutOfStockPolicyValues.SKIP_ITEMS, whatsappNotifyAddon = false, giftWrapAddon = false, giftWrapMessage, shipmentProtectionAddon = false, } = input;
|
|
500
508
|
const siteSettings = await siteSettingsRepository.getSingleton();
|
|
501
509
|
const commissions = siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS;
|
|
502
510
|
const emiSettings = siteSettings?.emi ?? CHECKOUT_DEFAULT_EMI_SETTINGS;
|
|
@@ -747,6 +755,10 @@ export async function createCheckoutOrderAction(input) {
|
|
|
747
755
|
buyerState: resolvedAddress?.state,
|
|
748
756
|
gstSettings: siteSettings?.gst,
|
|
749
757
|
siteContactUpiVpa: siteSettings?.contact?.upiVpa,
|
|
758
|
+
whatsappNotifyAddon,
|
|
759
|
+
giftWrapAddon,
|
|
760
|
+
giftWrapMessage,
|
|
761
|
+
shipmentProtectionAddon,
|
|
750
762
|
};
|
|
751
763
|
for (const { items: group, orderType } of orderGroups) {
|
|
752
764
|
total += await createOrderForGroup(group, orderType, groupCtx);
|
|
@@ -872,11 +884,212 @@ async function refundDroppedItemsForRazorpayCheckout(input) {
|
|
|
872
884
|
}
|
|
873
885
|
}
|
|
874
886
|
}
|
|
887
|
+
/**
|
|
888
|
+
* Places one order for one seller-group within `verifyAndPlaceRazorpayOrderAction`
|
|
889
|
+
* — mirrors `createOrderForGroup`'s role on the COD/cash/EMI path (same
|
|
890
|
+
* per-group fee/coupon/order-doc/notification shape), extracted into its own
|
|
891
|
+
* function purely to keep the parent function under the LARGE_COMPONENT
|
|
892
|
+
* audit threshold (`scripts/audit-code-quality.mjs`). No behavior change
|
|
893
|
+
* from the inline version this replaced.
|
|
894
|
+
*/
|
|
895
|
+
async function createRazorpayGroupOrder(group, orderType, ctx) {
|
|
896
|
+
const { appliedCoupons, cartSubtotal, platformFeePercent, gstPercent, whatsappNotifyFee, giftWrapFee, giftWrapMessage, shipmentProtectionAddon, siteSettings, uid, userName, userEmail, razorpay_order_id, razorpay_payment_id, razorpay_signature, shippingAddress, notes, outOfStockPolicy, unavailablePaid, orderIds, emailsToSend, } = ctx;
|
|
897
|
+
// SB-UNI-5 2026-05-13 — bundle cart-lines use item.price (locked bundle
|
|
898
|
+
// price); regular lines use product.price. The helper keeps the math
|
|
899
|
+
// local to this block.
|
|
900
|
+
const unitPriceFor = (item, product) => item.bundleCategorySlug && item.bundleProductIds?.length
|
|
901
|
+
? item.price
|
|
902
|
+
: product.price;
|
|
903
|
+
const firstItem = group[0].item;
|
|
904
|
+
const groupTotal = group.reduce((sum, { item, product }) => sum + unitPriceFor(item, product) * item.quantity, 0);
|
|
905
|
+
// Reuses the same store/seller lookup as the COD/UPI path above instead
|
|
906
|
+
// of re-implementing it inline — was two sequential findById calls per
|
|
907
|
+
// seller group here, duplicated from resolveShippingCost.
|
|
908
|
+
const { shippingFee, storeOwnerId } = await resolveShippingCost(firstItem.storeId);
|
|
909
|
+
let couponDiscount = 0;
|
|
910
|
+
const appliedDiscounts = [];
|
|
911
|
+
for (const coupon of appliedCoupons) {
|
|
912
|
+
let couponGroupDiscount = 0;
|
|
913
|
+
const isSellerScoped = coupon.scope === "seller" && coupon.storeId;
|
|
914
|
+
if (isSellerScoped) {
|
|
915
|
+
if (coupon.storeId !== firstItem.storeId)
|
|
916
|
+
continue;
|
|
917
|
+
if (coupon.applicableItemIds?.length) {
|
|
918
|
+
const eligibleTotal = group
|
|
919
|
+
.filter(({ item }) => coupon.applicableItemIds.includes(item.itemId))
|
|
920
|
+
.reduce((s, { item, product }) => s + unitPriceFor(item, product) * item.quantity, 0);
|
|
921
|
+
couponGroupDiscount =
|
|
922
|
+
eligibleTotal > 0
|
|
923
|
+
? Math.min(roundRupees((eligibleTotal / groupTotal) * coupon.discountAmount), eligibleTotal)
|
|
924
|
+
: 0;
|
|
925
|
+
}
|
|
926
|
+
else {
|
|
927
|
+
couponGroupDiscount = Math.min(coupon.discountAmount, groupTotal);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
else if (cartSubtotal > 0) {
|
|
931
|
+
couponGroupDiscount = Math.min(roundRupees((groupTotal / cartSubtotal) * coupon.discountAmount), groupTotal);
|
|
932
|
+
}
|
|
933
|
+
if (couponGroupDiscount > 0) {
|
|
934
|
+
couponDiscount += couponGroupDiscount;
|
|
935
|
+
appliedDiscounts.push({
|
|
936
|
+
code: coupon.code,
|
|
937
|
+
couponId: coupon.couponId,
|
|
938
|
+
type: "coupon",
|
|
939
|
+
discountAmount: couponGroupDiscount,
|
|
940
|
+
scope: coupon.scope,
|
|
941
|
+
storeId: coupon.storeId,
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
couponDiscount = Math.min(couponDiscount, groupTotal);
|
|
946
|
+
const rawPlatformFee = roundRupees(groupTotal * (platformFeePercent / 100));
|
|
947
|
+
const gstOnFee = roundRupees(rawPlatformFee * (gstPercent / 100));
|
|
948
|
+
const platformFee = rawPlatformFee + gstOnFee;
|
|
949
|
+
const shipmentProtectionFee = computeShipmentProtectionFee(groupTotal, shipmentProtectionAddon, siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS);
|
|
950
|
+
const orderTotal = Math.max(0, groupTotal - couponDiscount) + shippingFee + whatsappNotifyFee + giftWrapFee + shipmentProtectionFee;
|
|
951
|
+
// P-8 GST — deliberately NOT wired into this Razorpay-verify path. The
|
|
952
|
+
// amount-mismatch check above (expectedPaymentAmountRs) compares against
|
|
953
|
+
// what the buyer already paid via the Razorpay order created earlier in
|
|
954
|
+
// the flow; adding product GST here without also adding it to that
|
|
955
|
+
// upstream pre-payment amount calculation would either fail the mismatch
|
|
956
|
+
// check or silently under/over-charge. Wiring GST through the full
|
|
957
|
+
// Razorpay create→verify round-trip is separate follow-up work, tracked
|
|
958
|
+
// alongside P-13 (Razorpay is disabled by default today, so this order
|
|
959
|
+
// type doesn't currently carry a GST breakdown).
|
|
960
|
+
// S-SBUNI-RULES 2026-05-13 — order-item decoration via rule registry.
|
|
961
|
+
const orderItems = group.map(({ item, product }) => {
|
|
962
|
+
const lt = (product?.listingType ?? "standard");
|
|
963
|
+
const itemRule = getListingRule(lt);
|
|
964
|
+
// SB-UNI-5 2026-05-13 — bundle cart-lines surface the locked
|
|
965
|
+
// bundlePrice (item.price), not the representative member's
|
|
966
|
+
// product.price. Stock decrement still runs per member elsewhere.
|
|
967
|
+
const isBundle = Boolean(item.bundleCategorySlug && item.bundleProductIds?.length);
|
|
968
|
+
const unitPrice = isBundle ? item.price : product.price;
|
|
969
|
+
const bundleFields = isBundle
|
|
970
|
+
? {
|
|
971
|
+
bundleCategorySlug: item.bundleCategorySlug,
|
|
972
|
+
bundleProductIds: item.bundleProductIds,
|
|
973
|
+
}
|
|
974
|
+
: {};
|
|
975
|
+
const baseLine = {
|
|
976
|
+
productId: item.productId,
|
|
977
|
+
productTitle: item.productTitle,
|
|
978
|
+
quantity: item.quantity,
|
|
979
|
+
unitPrice,
|
|
980
|
+
totalPrice: unitPrice * item.quantity,
|
|
981
|
+
...bundleFields,
|
|
982
|
+
};
|
|
983
|
+
return itemRule.decorateOrderItem(baseLine, product);
|
|
984
|
+
});
|
|
985
|
+
const totalQuantity = group.reduce((sum, { item }) => sum + item.quantity, 0);
|
|
986
|
+
const imageUrls = [
|
|
987
|
+
...new Set(group
|
|
988
|
+
.map(({ product }) => product?.mainImage)
|
|
989
|
+
.filter((url) => typeof url === "string" && url.length > 0)),
|
|
990
|
+
];
|
|
991
|
+
// S-SBUNI-RULES 2026-05-13 — order-doc decoration via rule registry.
|
|
992
|
+
const lt0Rzp = (group[0].product?.listingType ?? "standard");
|
|
993
|
+
const groupRuleRzp = getListingRule(lt0Rzp);
|
|
994
|
+
const extraOrderFields = {
|
|
995
|
+
...groupRuleRzp.decorateOrderDoc(group[0].item, group[0].product),
|
|
996
|
+
...(orderType === "prize-draw" && group[0].product
|
|
997
|
+
? { prizeRevealDeadline: computePrizeRevealDeadline(group[0].product) }
|
|
998
|
+
: {}),
|
|
999
|
+
};
|
|
1000
|
+
const order = await unitOfWork.orders.create({
|
|
1001
|
+
productId: firstItem.productId,
|
|
1002
|
+
productTitle: firstItem.productTitle,
|
|
1003
|
+
userId: uid,
|
|
1004
|
+
userName,
|
|
1005
|
+
userEmail,
|
|
1006
|
+
quantity: totalQuantity,
|
|
1007
|
+
unitPrice: group[0].product.price,
|
|
1008
|
+
totalPrice: orderTotal,
|
|
1009
|
+
currency: firstItem.currency ?? getDefaultCurrency(),
|
|
1010
|
+
storeId: firstItem.storeId || undefined,
|
|
1011
|
+
storeName: firstItem.storeName || undefined,
|
|
1012
|
+
items: orderItems,
|
|
1013
|
+
orderType,
|
|
1014
|
+
offerId: firstItem.offerId ?? undefined,
|
|
1015
|
+
status: OrderStatusValues.CONFIRMED,
|
|
1016
|
+
paymentStatus: PaymentStatusValues.PAID,
|
|
1017
|
+
paymentMethod: PaymentMethodValues.ONLINE,
|
|
1018
|
+
paymentId: razorpay_payment_id,
|
|
1019
|
+
paymentRecord: {
|
|
1020
|
+
method: "razorpay",
|
|
1021
|
+
transactionId: razorpay_payment_id,
|
|
1022
|
+
amount: orderTotal,
|
|
1023
|
+
paidAt: new Date(),
|
|
1024
|
+
verifiedBy: "razorpay-webhook",
|
|
1025
|
+
verificationMethod: "webhook",
|
|
1026
|
+
gatewayRef: {
|
|
1027
|
+
orderId: razorpay_order_id,
|
|
1028
|
+
paymentId: razorpay_payment_id,
|
|
1029
|
+
signature: razorpay_signature,
|
|
1030
|
+
},
|
|
1031
|
+
},
|
|
1032
|
+
shippingAddress,
|
|
1033
|
+
notes,
|
|
1034
|
+
platformFee,
|
|
1035
|
+
shippingFee: shippingFee > 0 ? shippingFee : undefined,
|
|
1036
|
+
whatsappNotifyAddon: whatsappNotifyFee > 0 ? true : undefined,
|
|
1037
|
+
whatsappNotifyFee: whatsappNotifyFee > 0 ? whatsappNotifyFee : undefined,
|
|
1038
|
+
giftWrapAddon: giftWrapFee > 0 ? true : undefined,
|
|
1039
|
+
giftWrapFee: giftWrapFee > 0 ? giftWrapFee : undefined,
|
|
1040
|
+
giftWrapMessage: giftWrapFee > 0 ? giftWrapMessage?.slice(0, 500) : undefined,
|
|
1041
|
+
shipmentProtectionAddon: shipmentProtectionFee > 0 ? true : undefined,
|
|
1042
|
+
shipmentProtectionFee: shipmentProtectionFee > 0 ? shipmentProtectionFee : undefined,
|
|
1043
|
+
couponCode: appliedDiscounts[0]?.code,
|
|
1044
|
+
couponDiscount: couponDiscount > 0 ? couponDiscount : undefined,
|
|
1045
|
+
appliedDiscounts: appliedDiscounts.length > 0 ? appliedDiscounts : undefined,
|
|
1046
|
+
imageUrls: imageUrls.length > 0 ? imageUrls : undefined,
|
|
1047
|
+
outOfStockPolicy,
|
|
1048
|
+
droppedItems: unavailablePaid.length > 0 ? unavailablePaid : undefined,
|
|
1049
|
+
...extraOrderFields,
|
|
1050
|
+
});
|
|
1051
|
+
orderIds.push(order.id);
|
|
1052
|
+
// SB-UNI-N — claim a digital code for digital-code orders (fire-and-forget,
|
|
1053
|
+
// order is already persisted so a claim failure is logged, not thrown).
|
|
1054
|
+
if ((group[0]?.product?.listingType ?? "standard") === "digital-code") {
|
|
1055
|
+
claimDigitalCodeForOrder(getAdminDb(), firstItem.productId, order.id, uid, {
|
|
1056
|
+
userEmail: userEmail || undefined,
|
|
1057
|
+
userName,
|
|
1058
|
+
productTitle: firstItem.productTitle,
|
|
1059
|
+
}).catch((e) => serverLogger.error("claimDigitalCode", e));
|
|
1060
|
+
}
|
|
1061
|
+
if (userEmail) {
|
|
1062
|
+
emailsToSend.push({
|
|
1063
|
+
to: userEmail,
|
|
1064
|
+
userName,
|
|
1065
|
+
orderId: order.id,
|
|
1066
|
+
productTitle: orderItems.length > 1 ? `${orderItems.length} items` : firstItem.productTitle,
|
|
1067
|
+
quantity: totalQuantity,
|
|
1068
|
+
totalPrice: orderTotal,
|
|
1069
|
+
currency: firstItem.currency ?? getDefaultCurrency(),
|
|
1070
|
+
shippingAddress,
|
|
1071
|
+
paymentMethod: PaymentMethodValues.ONLINE,
|
|
1072
|
+
items: orderItems,
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
emitOrderPlacedNotifications({
|
|
1076
|
+
orderId: order.id,
|
|
1077
|
+
buyerUid: uid,
|
|
1078
|
+
buyerName: userName,
|
|
1079
|
+
storeOwnerId,
|
|
1080
|
+
productLabel: orderItems.length > 1 ? `${orderItems.length} items` : firstItem.productTitle,
|
|
1081
|
+
paid: true,
|
|
1082
|
+
whatsappNotifyAddon: whatsappNotifyFee > 0,
|
|
1083
|
+
});
|
|
1084
|
+
return orderTotal;
|
|
1085
|
+
}
|
|
875
1086
|
export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
876
|
-
const { userId: uid, userName, userEmail, razorpay_order_id, razorpay_payment_id, razorpay_signature, addressId, notes, outOfStockPolicy = OutOfStockPolicyValues.CANCEL_ORDER, } = input;
|
|
1087
|
+
const { userId: uid, userName, userEmail, razorpay_order_id, razorpay_payment_id, razorpay_signature, addressId, notes, outOfStockPolicy = OutOfStockPolicyValues.CANCEL_ORDER, whatsappNotifyAddon = false, giftWrapAddon = false, giftWrapMessage, shipmentProtectionAddon = false, } = input;
|
|
877
1088
|
const siteSettings = await siteSettingsRepository.getSingleton();
|
|
878
1089
|
const platformFeePercent = siteSettings?.commissions?.platformFeePercent ?? 5;
|
|
879
1090
|
const gstPercent = siteSettings?.commissions?.gstPercent ?? 18;
|
|
1091
|
+
const whatsappNotifyFee = computeWhatsAppNotifyFee(whatsappNotifyAddon, siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS);
|
|
1092
|
+
const giftWrapFee = computeGiftWrapFee(giftWrapAddon, siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS);
|
|
880
1093
|
const isValid = await verifyPaymentSignatureWithKeys({
|
|
881
1094
|
razorpay_order_id,
|
|
882
1095
|
razorpay_payment_id,
|
|
@@ -1051,178 +1264,28 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
1051
1264
|
const cartSubtotal = orderGroups.reduce((s, { items: g }) => s +
|
|
1052
1265
|
g.reduce((gs, { item, product }) => gs + unitPriceFor(item, product) * item.quantity, 0), 0);
|
|
1053
1266
|
for (const { items: group, orderType } of orderGroups) {
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
if (isSellerScoped) {
|
|
1066
|
-
if (coupon.storeId !== firstItem.storeId)
|
|
1067
|
-
continue;
|
|
1068
|
-
if (coupon.applicableItemIds?.length) {
|
|
1069
|
-
const eligibleTotal = group
|
|
1070
|
-
.filter(({ item }) => coupon.applicableItemIds.includes(item.itemId))
|
|
1071
|
-
.reduce((s, { item, product }) => s + unitPriceFor(item, product) * item.quantity, 0);
|
|
1072
|
-
couponGroupDiscount =
|
|
1073
|
-
eligibleTotal > 0
|
|
1074
|
-
? Math.min(roundRupees((eligibleTotal / groupTotal) * coupon.discountAmount), eligibleTotal)
|
|
1075
|
-
: 0;
|
|
1076
|
-
}
|
|
1077
|
-
else {
|
|
1078
|
-
couponGroupDiscount = Math.min(coupon.discountAmount, groupTotal);
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
else if (cartSubtotal > 0) {
|
|
1082
|
-
couponGroupDiscount = Math.min(roundRupees((groupTotal / cartSubtotal) * coupon.discountAmount), groupTotal);
|
|
1083
|
-
}
|
|
1084
|
-
if (couponGroupDiscount > 0) {
|
|
1085
|
-
couponDiscount += couponGroupDiscount;
|
|
1086
|
-
appliedDiscounts.push({
|
|
1087
|
-
code: coupon.code,
|
|
1088
|
-
couponId: coupon.couponId,
|
|
1089
|
-
type: "coupon",
|
|
1090
|
-
discountAmount: couponGroupDiscount,
|
|
1091
|
-
scope: coupon.scope,
|
|
1092
|
-
storeId: coupon.storeId,
|
|
1093
|
-
});
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
couponDiscount = Math.min(couponDiscount, groupTotal);
|
|
1097
|
-
const rawPlatformFee = roundRupees(groupTotal * (platformFeePercent / 100));
|
|
1098
|
-
const gstOnFee = roundRupees(rawPlatformFee * (gstPercent / 100));
|
|
1099
|
-
const platformFee = rawPlatformFee + gstOnFee;
|
|
1100
|
-
const orderTotal = Math.max(0, groupTotal - couponDiscount) + shippingFee;
|
|
1101
|
-
total += orderTotal;
|
|
1102
|
-
// P-8 GST — deliberately NOT wired into this Razorpay-verify path. The
|
|
1103
|
-
// amount-mismatch check above (expectedPaymentAmountRs) compares against
|
|
1104
|
-
// what the buyer already paid via the Razorpay order created earlier in
|
|
1105
|
-
// the flow; adding product GST here without also adding it to that
|
|
1106
|
-
// upstream pre-payment amount calculation would either fail the mismatch
|
|
1107
|
-
// check or silently under/over-charge. Wiring GST through the full
|
|
1108
|
-
// Razorpay create→verify round-trip is separate follow-up work, tracked
|
|
1109
|
-
// alongside P-13 (Razorpay is disabled by default today, so this order
|
|
1110
|
-
// type doesn't currently carry a GST breakdown).
|
|
1111
|
-
// S-SBUNI-RULES 2026-05-13 — order-item decoration via rule registry.
|
|
1112
|
-
const orderItems = group.map(({ item, product }) => {
|
|
1113
|
-
const lt = (product?.listingType ?? "standard");
|
|
1114
|
-
const itemRule = getListingRule(lt);
|
|
1115
|
-
// SB-UNI-5 2026-05-13 — bundle cart-lines surface the locked
|
|
1116
|
-
// bundlePrice (item.price), not the representative member's
|
|
1117
|
-
// product.price. Stock decrement still runs per member elsewhere.
|
|
1118
|
-
const isBundle = Boolean(item.bundleCategorySlug && item.bundleProductIds?.length);
|
|
1119
|
-
const unitPrice = isBundle ? item.price : product.price;
|
|
1120
|
-
const bundleFields = isBundle
|
|
1121
|
-
? {
|
|
1122
|
-
bundleCategorySlug: item.bundleCategorySlug,
|
|
1123
|
-
bundleProductIds: item.bundleProductIds,
|
|
1124
|
-
}
|
|
1125
|
-
: {};
|
|
1126
|
-
const baseLine = {
|
|
1127
|
-
productId: item.productId,
|
|
1128
|
-
productTitle: item.productTitle,
|
|
1129
|
-
quantity: item.quantity,
|
|
1130
|
-
unitPrice,
|
|
1131
|
-
totalPrice: unitPrice * item.quantity,
|
|
1132
|
-
...bundleFields,
|
|
1133
|
-
};
|
|
1134
|
-
return itemRule.decorateOrderItem(baseLine, product);
|
|
1135
|
-
});
|
|
1136
|
-
const totalQuantity = group.reduce((sum, { item }) => sum + item.quantity, 0);
|
|
1137
|
-
const imageUrls = [
|
|
1138
|
-
...new Set(group
|
|
1139
|
-
.map(({ product }) => product?.mainImage)
|
|
1140
|
-
.filter((url) => typeof url === "string" && url.length > 0)),
|
|
1141
|
-
];
|
|
1142
|
-
// S-SBUNI-RULES 2026-05-13 — order-doc decoration via rule registry.
|
|
1143
|
-
const lt0Rzp = (group[0].product?.listingType ?? "standard");
|
|
1144
|
-
const groupRuleRzp = getListingRule(lt0Rzp);
|
|
1145
|
-
const extraOrderFields = {
|
|
1146
|
-
...groupRuleRzp.decorateOrderDoc(group[0].item, group[0].product),
|
|
1147
|
-
...(orderType === "prize-draw" && group[0].product
|
|
1148
|
-
? { prizeRevealDeadline: computePrizeRevealDeadline(group[0].product) }
|
|
1149
|
-
: {}),
|
|
1150
|
-
};
|
|
1151
|
-
const order = await unitOfWork.orders.create({
|
|
1152
|
-
productId: firstItem.productId,
|
|
1153
|
-
productTitle: firstItem.productTitle,
|
|
1154
|
-
userId: uid,
|
|
1267
|
+
total += await createRazorpayGroupOrder(group, orderType, {
|
|
1268
|
+
appliedCoupons,
|
|
1269
|
+
cartSubtotal,
|
|
1270
|
+
platformFeePercent,
|
|
1271
|
+
gstPercent,
|
|
1272
|
+
whatsappNotifyFee,
|
|
1273
|
+
giftWrapFee,
|
|
1274
|
+
giftWrapMessage,
|
|
1275
|
+
shipmentProtectionAddon,
|
|
1276
|
+
siteSettings,
|
|
1277
|
+
uid,
|
|
1155
1278
|
userName,
|
|
1156
1279
|
userEmail,
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
currency: firstItem.currency ?? getDefaultCurrency(),
|
|
1161
|
-
storeId: firstItem.storeId || undefined,
|
|
1162
|
-
storeName: firstItem.storeName || undefined,
|
|
1163
|
-
items: orderItems,
|
|
1164
|
-
orderType,
|
|
1165
|
-
offerId: firstItem.offerId ?? undefined,
|
|
1166
|
-
status: OrderStatusValues.CONFIRMED,
|
|
1167
|
-
paymentStatus: PaymentStatusValues.PAID,
|
|
1168
|
-
paymentMethod: PaymentMethodValues.ONLINE,
|
|
1169
|
-
paymentId: razorpay_payment_id,
|
|
1170
|
-
paymentRecord: {
|
|
1171
|
-
method: "razorpay",
|
|
1172
|
-
transactionId: razorpay_payment_id,
|
|
1173
|
-
amount: orderTotal,
|
|
1174
|
-
paidAt: new Date(),
|
|
1175
|
-
verifiedBy: "razorpay-webhook",
|
|
1176
|
-
verificationMethod: "webhook",
|
|
1177
|
-
gatewayRef: {
|
|
1178
|
-
orderId: razorpay_order_id,
|
|
1179
|
-
paymentId: razorpay_payment_id,
|
|
1180
|
-
signature: razorpay_signature,
|
|
1181
|
-
},
|
|
1182
|
-
},
|
|
1280
|
+
razorpay_order_id,
|
|
1281
|
+
razorpay_payment_id,
|
|
1282
|
+
razorpay_signature,
|
|
1183
1283
|
shippingAddress,
|
|
1184
1284
|
notes,
|
|
1185
|
-
platformFee,
|
|
1186
|
-
shippingFee: shippingFee > 0 ? shippingFee : undefined,
|
|
1187
|
-
couponCode: appliedDiscounts[0]?.code,
|
|
1188
|
-
couponDiscount: couponDiscount > 0 ? couponDiscount : undefined,
|
|
1189
|
-
appliedDiscounts: appliedDiscounts.length > 0 ? appliedDiscounts : undefined,
|
|
1190
|
-
imageUrls: imageUrls.length > 0 ? imageUrls : undefined,
|
|
1191
1285
|
outOfStockPolicy,
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
orderIds.push(order.id);
|
|
1196
|
-
// SB-UNI-N — claim a digital code for digital-code orders (fire-and-forget,
|
|
1197
|
-
// order is already persisted so a claim failure is logged, not thrown).
|
|
1198
|
-
if ((group[0]?.product?.listingType ?? "standard") === "digital-code") {
|
|
1199
|
-
claimDigitalCodeForOrder(getAdminDb(), firstItem.productId, order.id, uid, {
|
|
1200
|
-
userEmail: userEmail || undefined,
|
|
1201
|
-
userName,
|
|
1202
|
-
productTitle: firstItem.productTitle,
|
|
1203
|
-
}).catch((e) => serverLogger.error("claimDigitalCode", e));
|
|
1204
|
-
}
|
|
1205
|
-
if (userEmail) {
|
|
1206
|
-
emailsToSend.push({
|
|
1207
|
-
to: userEmail,
|
|
1208
|
-
userName,
|
|
1209
|
-
orderId: order.id,
|
|
1210
|
-
productTitle: orderItems.length > 1 ? `${orderItems.length} items` : firstItem.productTitle,
|
|
1211
|
-
quantity: totalQuantity,
|
|
1212
|
-
totalPrice: orderTotal,
|
|
1213
|
-
currency: firstItem.currency ?? getDefaultCurrency(),
|
|
1214
|
-
shippingAddress,
|
|
1215
|
-
paymentMethod: PaymentMethodValues.ONLINE,
|
|
1216
|
-
items: orderItems,
|
|
1217
|
-
});
|
|
1218
|
-
}
|
|
1219
|
-
emitOrderPlacedNotifications({
|
|
1220
|
-
orderId: order.id,
|
|
1221
|
-
buyerUid: uid,
|
|
1222
|
-
buyerName: userName,
|
|
1223
|
-
storeOwnerId,
|
|
1224
|
-
productLabel: orderItems.length > 1 ? `${orderItems.length} items` : firstItem.productTitle,
|
|
1225
|
-
paid: true,
|
|
1286
|
+
unavailablePaid,
|
|
1287
|
+
orderIds,
|
|
1288
|
+
emailsToSend,
|
|
1226
1289
|
});
|
|
1227
1290
|
}
|
|
1228
1291
|
// SB-UNI-5 2026-05-13 — bundle-aware batch decrement. We iterate the
|
|
@@ -6,6 +6,7 @@ export declare const paymentReviewAutoApprove: import("./types").ScheduledFuncti
|
|
|
6
6
|
export declare const couponExpiry: import("./types").ScheduledFunctionDefinition;
|
|
7
7
|
export declare const offerExpiry: import("./types").ScheduledFunctionDefinition;
|
|
8
8
|
export declare const productStatsSync: import("./types").ScheduledFunctionDefinition;
|
|
9
|
+
export declare const revenueRollup: import("./types").ScheduledFunctionDefinition;
|
|
9
10
|
export declare const dailyDataCleanup: import("./types").ScheduledFunctionDefinition;
|
|
10
11
|
export declare const countersReconcile: import("./types").ScheduledFunctionDefinition;
|
|
11
12
|
export declare const positionsReconcile: import("./types").ScheduledFunctionDefinition;
|
|
@@ -25,4 +26,4 @@ export declare const prizeRevealReminder: import("./types").ScheduledFunctionDef
|
|
|
25
26
|
export declare const bundleStockSync: import("./types").ScheduledFunctionDefinition;
|
|
26
27
|
export declare const emiInstallmentReminder: import("./types").ScheduledFunctionDefinition;
|
|
27
28
|
export declare const catalogueImageStalenessReminder: import("./types").ScheduledFunctionDefinition;
|
|
28
|
-
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, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition];
|
|
29
|
+
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, 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, emiInstallmentReminderHandler, catalogueImageStalenessReminderHandler, mediaTmpCleanupHandler, notificationPruneHandler, offerExpiryHandler, payoutBatchHandler, paymentWindowTimeoutHandler, hardBanReinstatementHandler, paymentReviewAutoApproveHandler, pendingOrderTimeoutHandler, positionsReconcileHandler, prizeRevealCloseHandler, prizeRevealExpiryHandler, prizeRevealOpenHandler, prizeRevealReminderHandler, productStatsSyncHandler, weeklyPayoutEligibilityHandler, testerSandboxCleanupHandler, } from "../jobs/handlers";
|
|
3
|
+
import { auctionSettlementHandler, autoPayoutEligibilityHandler, bundleStockSyncHandler, cartPruneHandler, cleanupRtdbEventsHandler, countersReconcileHandler, couponExpiryHandler, dailyDataCleanupHandler, draftPruneHandler, emiInstallmentReminderHandler, catalogueImageStalenessReminderHandler, mediaTmpCleanupHandler, notificationPruneHandler, offerExpiryHandler, payoutBatchHandler, paymentWindowTimeoutHandler, hardBanReinstatementHandler, paymentReviewAutoApproveHandler, pendingOrderTimeoutHandler, positionsReconcileHandler, prizeRevealCloseHandler, prizeRevealExpiryHandler, prizeRevealOpenHandler, prizeRevealReminderHandler, productStatsSyncHandler, revenueRollupHandler, weeklyPayoutEligibilityHandler, testerSandboxCleanupHandler, } from "../jobs/handlers";
|
|
4
4
|
import { defineFunction } from "./define";
|
|
5
5
|
const REGION = "asia-south1";
|
|
6
6
|
const EVERY_5_MIN = "every 5 minutes";
|
|
@@ -60,6 +60,13 @@ export const productStatsSync = defineFunction({
|
|
|
60
60
|
handler: productStatsSyncHandler,
|
|
61
61
|
options: { region: REGION, timeoutSeconds: 540, memory: "256MiB", maxInstances: 1 },
|
|
62
62
|
});
|
|
63
|
+
export const revenueRollup = defineFunction({
|
|
64
|
+
name: "revenueRollup",
|
|
65
|
+
description: "Pre-compute total delivered-order revenue into analytics/dashboardRollup, replacing an unbounded per-request scan (daily 01:30 UTC).",
|
|
66
|
+
trigger: { kind: "schedule", cron: "30 1 * * *", timeZone: "UTC" },
|
|
67
|
+
handler: revenueRollupHandler,
|
|
68
|
+
options: { region: REGION, timeoutSeconds: 300, memory: "256MiB", maxInstances: 1 },
|
|
69
|
+
});
|
|
63
70
|
export const dailyDataCleanup = defineFunction({
|
|
64
71
|
name: "dailyDataCleanup",
|
|
65
72
|
description: "Daily data cleanup (drafts, transient records) at 02:00 UTC.",
|
|
@@ -202,6 +209,7 @@ export const SCHEDULED_FUNCTIONS = [
|
|
|
202
209
|
couponExpiry,
|
|
203
210
|
offerExpiry,
|
|
204
211
|
productStatsSync,
|
|
212
|
+
revenueRollup,
|
|
205
213
|
dailyDataCleanup,
|
|
206
214
|
countersReconcile,
|
|
207
215
|
positionsReconcile,
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
import { runWeeklyPayoutEligibility } from "./weeklyPayoutEligibility";
|
|
12
12
|
import { runHardBanCascade } from "./hardBanCascade";
|
|
13
13
|
import { runResetOtpVerification } from "./resetOtpVerification";
|
|
14
|
+
import { runWhatsAppNotify } from "./whatsappNotify";
|
|
15
|
+
import { runNewsletterExport } from "./newsletterExport";
|
|
14
16
|
async function runPayoutsWeeklyJob(_payload, ctx) {
|
|
15
17
|
const { payoutsCreated, ordersProcessed, payoutIds } = await runWeeklyPayoutEligibility(ctx);
|
|
16
18
|
return {
|
|
@@ -37,8 +39,16 @@ async function runHardBanCascadeJob(payload, ctx) {
|
|
|
37
39
|
async function runResetOtpVerificationJob(_payload, ctx) {
|
|
38
40
|
return runResetOtpVerification(ctx);
|
|
39
41
|
}
|
|
42
|
+
async function runWhatsAppNotifyJob(payload, ctx) {
|
|
43
|
+
return runWhatsAppNotify(payload, ctx);
|
|
44
|
+
}
|
|
45
|
+
async function runNewsletterExportJob(_payload, ctx) {
|
|
46
|
+
return runNewsletterExport(ctx);
|
|
47
|
+
}
|
|
40
48
|
export const JOB_RUNNERS = {
|
|
41
49
|
payoutsWeekly: runPayoutsWeeklyJob,
|
|
42
50
|
hardBanCascade: runHardBanCascadeJob,
|
|
43
51
|
resetOtpVerification: runResetOtpVerificationJob,
|
|
52
|
+
whatsappNotify: runWhatsAppNotifyJob,
|
|
53
|
+
newsletterExport: runNewsletterExportJob,
|
|
44
54
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core: async newsletter-subscriber CSV export (`newsletterExport` job).
|
|
3
|
+
*
|
|
4
|
+
* `GET /api/admin/newsletter/export` used to build the full CSV in-process,
|
|
5
|
+
* synchronously, for up to 10,000 subscribers — a real 10s-timeout risk on
|
|
6
|
+
* Vercel Hobby as the list grows (CLAUDE.md Rule #6). This runner does the
|
|
7
|
+
* same work inside a Firebase Function instead and writes the CSV text onto
|
|
8
|
+
* the job's `result.data.csv` field — small enough for a Firestore doc at
|
|
9
|
+
* realistic subscriber counts, and avoids routing through Storage (which
|
|
10
|
+
* would need a `/media/...` proxy URL per the Media Architecture rules
|
|
11
|
+
* rather than a raw signed URL).
|
|
12
|
+
*/
|
|
13
|
+
import type { JobContext } from "../runtime/types";
|
|
14
|
+
import type { JobRunResult } from "./jobRunners";
|
|
15
|
+
export declare function runNewsletterExport(ctx: JobContext): Promise<JobRunResult>;
|