@mohasinac/appkit 3.2.19 → 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.
Files changed (202) hide show
  1. package/dist/_internal/server/features/checkout/actions.d.ts +2 -0
  2. package/dist/_internal/server/features/checkout/actions.js +239 -190
  3. package/dist/_internal/server/functions/scheduled.d.ts +2 -1
  4. package/dist/_internal/server/functions/scheduled.js +9 -1
  5. package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +8 -11
  6. package/dist/_internal/server/jobs/core/emiInstallmentReminder.d.ts +8 -0
  7. package/dist/_internal/server/jobs/core/emiInstallmentReminder.js +78 -0
  8. package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.d.ts +2 -2
  9. package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +15 -10
  10. package/dist/_internal/server/jobs/handlers/emiInstallmentReminder.d.ts +2 -0
  11. package/dist/_internal/server/jobs/handlers/emiInstallmentReminder.js +2 -0
  12. package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
  13. package/dist/_internal/server/jobs/handlers/index.js +2 -0
  14. package/dist/_internal/server/providers/index.d.ts +4 -4
  15. package/dist/_internal/server/providers/index.js +4 -4
  16. package/dist/_internal/server/providers/payment/razorpay-mock.d.ts +3 -2
  17. package/dist/_internal/server/providers/payment/razorpay-mock.js +3 -1
  18. package/dist/_internal/server/providers/resolve.d.ts +10 -13
  19. package/dist/_internal/server/providers/resolve.js +9 -15
  20. package/dist/_internal/shared/actions/action-registry.js +28 -0
  21. package/dist/_internal/shared/checkout/rules/_defaults.js +1 -0
  22. package/dist/_internal/shared/checkout/rules/_registry.js +4 -0
  23. package/dist/_internal/shared/checkout/rules/art.rule.d.ts +9 -0
  24. package/dist/_internal/shared/checkout/rules/art.rule.js +5 -0
  25. package/dist/_internal/shared/checkout/rules/auction.rule.js +1 -0
  26. package/dist/_internal/shared/checkout/rules/classified.rule.js +1 -0
  27. package/dist/_internal/shared/checkout/rules/index.d.ts +2 -0
  28. package/dist/_internal/shared/checkout/rules/index.js +2 -0
  29. package/dist/_internal/shared/checkout/rules/stickers.rule.d.ts +9 -0
  30. package/dist/_internal/shared/checkout/rules/stickers.rule.js +5 -0
  31. package/dist/_internal/shared/checkout/rules/types.d.ts +6 -0
  32. package/dist/_internal/shared/features/checkout/config.d.ts +16 -1
  33. package/dist/_internal/shared/features/checkout/config.js +16 -1
  34. package/dist/_internal/shared/features/emi/schedule.d.ts +63 -0
  35. package/dist/_internal/shared/features/emi/schedule.js +62 -0
  36. package/dist/_internal/shared/fees/calculator.d.ts +53 -0
  37. package/dist/_internal/shared/fees/calculator.js +40 -0
  38. package/dist/_internal/shared/listing-types/_registry.d.ts +22 -66
  39. package/dist/_internal/shared/listing-types/_registry.js +19 -2
  40. package/dist/_internal/shared/listing-types/art/config.d.ts +15 -0
  41. package/dist/_internal/shared/listing-types/art/config.js +14 -0
  42. package/dist/_internal/shared/listing-types/art/ctas.d.ts +1 -0
  43. package/dist/_internal/shared/listing-types/art/ctas.js +3 -0
  44. package/dist/_internal/shared/listing-types/art/og.d.ts +1 -0
  45. package/dist/_internal/shared/listing-types/art/og.js +1 -0
  46. package/dist/_internal/shared/listing-types/art/schema.d.ts +2 -0
  47. package/dist/_internal/shared/listing-types/art/schema.js +3 -0
  48. package/dist/_internal/shared/listing-types/art/seed-factory.d.ts +1 -0
  49. package/dist/_internal/shared/listing-types/art/seed-factory.js +1 -0
  50. package/dist/_internal/shared/listing-types/auction/config.d.ts +8 -0
  51. package/dist/_internal/shared/listing-types/auction/config.js +6 -0
  52. package/dist/_internal/shared/listing-types/capabilities.js +17 -0
  53. package/dist/_internal/shared/listing-types/classified/config.d.ts +8 -0
  54. package/dist/_internal/shared/listing-types/classified/config.js +6 -0
  55. package/dist/_internal/shared/listing-types/digital-code/config.d.ts +8 -0
  56. package/dist/_internal/shared/listing-types/digital-code/config.js +6 -0
  57. package/dist/_internal/shared/listing-types/live/config.d.ts +8 -0
  58. package/dist/_internal/shared/listing-types/live/config.js +6 -0
  59. package/dist/_internal/shared/listing-types/pre-order/config.d.ts +8 -0
  60. package/dist/_internal/shared/listing-types/pre-order/config.js +6 -0
  61. package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +8 -0
  62. package/dist/_internal/shared/listing-types/prize-draw/config.js +6 -0
  63. package/dist/_internal/shared/listing-types/standard/config.d.ts +8 -0
  64. package/dist/_internal/shared/listing-types/standard/config.js +6 -0
  65. package/dist/_internal/shared/listing-types/stickers/config.d.ts +15 -0
  66. package/dist/_internal/shared/listing-types/stickers/config.js +14 -0
  67. package/dist/_internal/shared/listing-types/stickers/ctas.d.ts +1 -0
  68. package/dist/_internal/shared/listing-types/stickers/ctas.js +3 -0
  69. package/dist/_internal/shared/listing-types/stickers/og.d.ts +1 -0
  70. package/dist/_internal/shared/listing-types/stickers/og.js +1 -0
  71. package/dist/_internal/shared/listing-types/stickers/schema.d.ts +2 -0
  72. package/dist/_internal/shared/listing-types/stickers/schema.js +3 -0
  73. package/dist/_internal/shared/listing-types/stickers/seed-factory.d.ts +1 -0
  74. package/dist/_internal/shared/listing-types/stickers/seed-factory.js +1 -0
  75. package/dist/client.d.ts +7 -2
  76. package/dist/client.js +4 -2
  77. package/dist/constants/field-names.d.ts +2 -6
  78. package/dist/constants/field-names.js +2 -6
  79. package/dist/contracts/index.d.ts +4 -2
  80. package/dist/contracts/index.js +6 -0
  81. package/dist/contracts/payment.d.ts +14 -9
  82. package/dist/contracts/payment.js +11 -1
  83. package/dist/contracts/shipping.d.ts +14 -9
  84. package/dist/contracts/shipping.js +11 -1
  85. package/dist/features/about/components/PublicProfileView.js +3 -6
  86. package/dist/features/account/components/UserOffersPanel.js +0 -1
  87. package/dist/features/admin/components/AdminAddressBookView.d.ts +11 -0
  88. package/dist/features/admin/components/AdminAddressBookView.js +30 -0
  89. package/dist/features/admin/components/AdminArtView.d.ts +3 -0
  90. package/dist/features/admin/components/AdminArtView.js +45 -0
  91. package/dist/features/admin/components/AdminCarouselEditorView.js +2 -3
  92. package/dist/features/admin/components/AdminFulfillmentView.js +0 -1
  93. package/dist/features/admin/components/AdminSiteConfigGuideView.js +1 -1
  94. package/dist/features/admin/components/AdminSiteSettingsView.js +14 -9
  95. package/dist/features/admin/components/AdminStickersView.d.ts +3 -0
  96. package/dist/features/admin/components/AdminStickersView.js +45 -0
  97. package/dist/features/admin/components/index.d.ts +6 -0
  98. package/dist/features/admin/components/index.js +4 -0
  99. package/dist/features/admin/hooks/useChat.js +0 -1
  100. package/dist/features/admin/schemas/firestore.d.ts +19 -11
  101. package/dist/features/admin/schemas/firestore.js +12 -2
  102. package/dist/features/auctions/components/MarketplaceAuctionCard.d.ts +3 -2
  103. package/dist/features/auth/hooks/useAuth.js +35 -10
  104. package/dist/features/auth/schemas/firestore.d.ts +2 -6
  105. package/dist/features/cart/schemas/firestore.d.ts +3 -2
  106. package/dist/features/consultation/schemas/index.d.ts +2 -2
  107. package/dist/features/history/hooks/useHistory.js +0 -1
  108. package/dist/features/loyalty/schemas/index.d.ts +6 -6
  109. package/dist/features/messages/hooks/useConversation.js +0 -1
  110. package/dist/features/messages/hooks/useConversations.js +0 -1
  111. package/dist/features/orders/repository/orders.repository.d.ts +20 -2
  112. package/dist/features/orders/repository/orders.repository.js +34 -5
  113. package/dist/features/orders/schemas/firestore.d.ts +33 -7
  114. package/dist/features/orders/schemas/firestore.js +1 -1
  115. package/dist/features/orders/schemas/index.d.ts +176 -31
  116. package/dist/features/orders/schemas/index.js +20 -6
  117. package/dist/features/orders/types/index.d.ts +3 -3
  118. package/dist/features/payments/repository/payout.repository.js +2 -2
  119. package/dist/features/payments/schemas/firestore.d.ts +3 -3
  120. package/dist/features/payments/schemas/firestore.js +2 -2
  121. package/dist/features/payments/schemas/index.d.ts +5 -5
  122. package/dist/features/payments/schemas/index.js +2 -2
  123. package/dist/features/products/api/[id]/route.js +4 -1
  124. package/dist/features/products/api/route.js +4 -1
  125. package/dist/features/products/components/CompareOverlay.js +7 -9
  126. package/dist/features/products/components/ProductDetailPageView.d.ts +3 -1
  127. package/dist/features/products/components/ProductDetailPageView.js +2 -2
  128. package/dist/features/products/components/ProductForm.js +18 -7
  129. package/dist/features/products/components/ProductGrid.js +7 -12
  130. package/dist/features/products/components/ShowGroupSection.js +3 -5
  131. package/dist/features/products/components/SublistingCarouselSection.js +3 -6
  132. package/dist/features/products/constants/listing-tabs.d.ts +32 -0
  133. package/dist/features/products/constants/listing-tabs.js +8 -0
  134. package/dist/features/products/index.d.ts +1 -1
  135. package/dist/features/products/index.js +3 -1
  136. package/dist/features/products/schemas/firestore.d.ts +20 -1
  137. package/dist/features/products/schemas/firestore.js +2 -0
  138. package/dist/features/products/schemas/index.d.ts +48 -0
  139. package/dist/features/products/schemas/index.js +9 -0
  140. package/dist/features/products/schemas/product-features.d.ts +1 -1
  141. package/dist/features/products/types/index.d.ts +5 -3
  142. package/dist/features/products/utils/listing-type.d.ts +6 -0
  143. package/dist/features/products/utils/listing-type.js +3 -0
  144. package/dist/features/reviews/api/[id]/route.js +5 -4
  145. package/dist/features/seller/actions/seller-actions.d.ts +29 -14
  146. package/dist/features/seller/actions/seller-actions.js +101 -27
  147. package/dist/features/seller/components/FulfillmentView.js +0 -1
  148. package/dist/features/seller/components/SellerAddressesView.js +0 -1
  149. package/dist/features/seller/components/SellerArtView.d.ts +7 -0
  150. package/dist/features/seller/components/SellerArtView.js +114 -0
  151. package/dist/features/seller/components/SellerOffersPanel.js +0 -1
  152. package/dist/features/seller/components/SellerOrdersView.js +38 -1
  153. package/dist/features/seller/components/SellerProductShell.d.ts +5 -1
  154. package/dist/features/seller/components/SellerProductShell.js +69 -59
  155. package/dist/features/seller/components/SellerReviewsView.js +0 -1
  156. package/dist/features/seller/components/SellerShippingView.js +6 -25
  157. package/dist/features/seller/components/SellerStickersView.d.ts +7 -0
  158. package/dist/features/seller/components/SellerStickersView.js +114 -0
  159. package/dist/features/seller/components/index.d.ts +4 -0
  160. package/dist/features/seller/components/index.js +2 -0
  161. package/dist/features/seller/messages/en.json +1 -1
  162. package/dist/features/seller/schemas/index.d.ts +7 -7
  163. package/dist/features/seller/schemas/index.js +1 -1
  164. package/dist/features/seller/types/index.d.ts +1 -1
  165. package/dist/features/stores/components/StoreDetailLayoutView.js +18 -2
  166. package/dist/features/stores/schemas/firestore.d.ts +7 -1
  167. package/dist/features/stores/schemas/index.d.ts +19 -16
  168. package/dist/features/stores/schemas/index.js +1 -1
  169. package/dist/index.d.ts +21 -36
  170. package/dist/index.js +27 -39
  171. package/dist/next/routing/route-map.d.ts +22 -0
  172. package/dist/next/routing/route-map.js +10 -0
  173. package/dist/providers/payment-manual/index.d.ts +40 -0
  174. package/dist/providers/payment-manual/index.js +91 -0
  175. package/dist/providers/payment-razorpay/index.d.ts +4 -2
  176. package/dist/providers/payment-razorpay/index.js +4 -1
  177. package/dist/providers/shipping-manual/index.d.ts +22 -0
  178. package/dist/providers/shipping-manual/index.js +47 -0
  179. package/dist/providers.d.ts +2 -2
  180. package/dist/providers.js +5 -3
  181. package/dist/schemas/registry.d.ts +129 -70
  182. package/dist/schemas/registry.js +0 -4
  183. package/dist/security/pii-encrypt.js +0 -7
  184. package/dist/security/pii-redact.js +0 -1
  185. package/dist/seed/conversations-seed-data.js +2 -2
  186. package/dist/seed/faq-seed-data.js +7 -7
  187. package/dist/seed/grouped-listings-seed-data.d.ts +2 -0
  188. package/dist/seed/index.d.ts +2 -0
  189. package/dist/seed/index.js +2 -0
  190. package/dist/seed/manifest.js +20 -16
  191. package/dist/seed/payouts-seed-data.js +5 -5
  192. package/dist/seed/products-art-seed-data.d.ts +11 -0
  193. package/dist/seed/products-art-seed-data.js +125 -0
  194. package/dist/seed/products-preorders-seed-data.d.ts +2 -0
  195. package/dist/seed/products-stickers-seed-data.d.ts +11 -0
  196. package/dist/seed/products-stickers-seed-data.js +125 -0
  197. package/dist/seed/site-settings-seed-data.js +13 -4
  198. package/dist/seed/stores-seed-data.js +11 -11
  199. package/dist/server.d.ts +7 -15
  200. package/dist/server.js +17 -42
  201. package/dist/ui/rich-text/RichText.js +1 -1
  202. 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, groupTotal, commissions) {
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
- if (shippingConfig.method === "custom") {
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
- const firstItem = group[0].item;
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
- if (shippingConfig.method === "custom") {
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 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];
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 = Math.round(grossAmount * PLATFORM_COMMISSION_RATE * 100) / 100;
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: PLATFORM_COMMISSION_RATE,
61
+ platformFeeRate: commissions.platformFeePercent / 100,
65
62
  gatewayFee,
66
- gatewayFeeRate: GATEWAY_FEE_RATE,
63
+ gatewayFeeRate: (commissions.gatewayFeePercent ?? 0) / 100,
67
64
  gstAmount,
68
- gstRate: GST_RATE,
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>;