@mohasinac/appkit 4.41.17 → 4.42.1
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/client/features/layout/DashboardLayoutClient.js +2 -30
- package/dist/_internal/client/features/maintenance/views/MaintenanceDashboardView.js +1 -1
- package/dist/_internal/server/features/checkout/actions.d.ts +16 -14
- package/dist/_internal/server/features/checkout/actions.js +145 -97
- package/dist/_internal/server/features/checkout/bundle-expansion.d.ts +1 -1
- package/dist/_internal/server/features/checkout/index.d.ts +1 -1
- package/dist/_internal/server/features/checkout/index.js +1 -1
- package/dist/_internal/server/features/checkout/locked-lines.js +1 -1
- package/dist/_internal/server/features/checkout/source-context.js +2 -2
- package/dist/_internal/server/features/maintenance/analyze.js +1 -1
- package/dist/_internal/server/features/refunds/actions.d.ts +4 -4
- package/dist/_internal/server/features/refunds/actions.js +9 -8
- package/dist/_internal/server/functions/scheduled.d.ts +1 -2
- package/dist/_internal/server/functions/scheduled.js +1 -9
- package/dist/_internal/server/jobs/core/index.d.ts +0 -1
- package/dist/_internal/server/jobs/core/index.js +0 -1
- package/dist/_internal/server/jobs/handlers/index.d.ts +0 -1
- package/dist/_internal/server/jobs/handlers/index.js +0 -1
- package/dist/_internal/shared/actions/action-registry.js +2 -2
- package/dist/_internal/shared/checkout/order-math.d.ts +1 -1
- package/dist/_internal/shared/checkout/order-math.js +1 -1
- package/dist/_internal/shared/checkout/rules/prize-draw.rule.js +2 -2
- package/dist/_internal/shared/features/blog/schema.d.ts +4 -4
- package/dist/_internal/shared/features/cart/schema.d.ts +6 -6
- package/dist/_internal/shared/features/orders/refund-copy.d.ts +1 -1
- package/dist/_internal/shared/features/orders/refund-copy.js +1 -1
- package/dist/_internal/shared/features/orders/schema.d.ts +5 -5
- package/dist/_internal/shared/features/orders/schema.js +1 -1
- package/dist/_internal/shared/features/products/schema.d.ts +2 -2
- package/dist/_internal/shared/features/promotions/schema.d.ts +4 -4
- package/dist/_internal/shared/features/reviews/schema.d.ts +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/configs/next.js +2 -2
- package/dist/constants/field-names.d.ts +1 -1
- package/dist/constants/field-names.js +1 -1
- package/dist/contracts/index.d.ts +0 -2
- package/dist/contracts/index.js +0 -1
- package/dist/contracts/payment.d.ts +13 -2
- package/dist/contracts/payment.js +2 -2
- package/dist/core/integration-keys.d.ts +7 -3
- package/dist/core/integration-keys.js +13 -7
- package/dist/core/money.d.ts +11 -0
- package/dist/core/money.js +15 -0
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/errors/messages.d.ts +1 -0
- package/dist/errors/messages.js +1 -0
- package/dist/errors/{razorpay-unreachable.d.ts → payment-provider-unreachable.d.ts} +1 -1
- package/dist/errors/{razorpay-unreachable.js → payment-provider-unreachable.js} +1 -1
- package/dist/features/addresses/schemas/address-form.d.ts +20 -20
- package/dist/features/addresses/schemas/index.d.ts +2 -2
- package/dist/features/admin/components/AdminSiteSettingsView.js +32 -16
- package/dist/features/admin/constants/site-settings-tabs.d.ts +1 -1
- package/dist/features/admin/schemas/admin-ops-forms.d.ts +10 -10
- package/dist/features/admin/schemas/admin-user-form.d.ts +2 -2
- package/dist/features/admin/schemas/firestore.d.ts +17 -8
- package/dist/features/admin/schemas/firestore.js +1 -1
- package/dist/features/admin/schemas/nav-item-form.d.ts +2 -2
- package/dist/features/auctions/schemas/index.d.ts +2 -2
- package/dist/features/auth/schemas/index.d.ts +2 -2
- package/dist/features/before-after/schemas/index.d.ts +2 -2
- package/dist/features/blog/actions/blog-actions.d.ts +4 -4
- package/dist/features/blog/schemas/blog-form.d.ts +2 -2
- package/dist/features/blog/schemas/index.d.ts +4 -4
- package/dist/features/cart/schemas/index.d.ts +3 -3
- package/dist/features/catalogue/schemas/validation.d.ts +4 -4
- package/dist/features/categories/schemas/index.d.ts +2 -2
- package/dist/features/checkout/index.d.ts +0 -4
- package/dist/features/checkout/index.js +0 -3
- package/dist/features/checkout/types/index.d.ts +0 -25
- package/dist/features/checkout/types/index.js +1 -2
- package/dist/features/collections/schemas/index.d.ts +2 -2
- package/dist/features/consultation/schemas/index.d.ts +4 -4
- package/dist/features/corporate/schemas/index.d.ts +2 -2
- package/dist/features/events/schemas/index.d.ts +4 -4
- package/dist/features/grouped/schemas/index.d.ts +2 -2
- package/dist/features/homepage/schemas/index.d.ts +2 -2
- package/dist/features/layout/AppLayoutShell.js +2 -2
- package/dist/features/layout/BottomNavLayout.d.ts +9 -3
- package/dist/features/layout/BottomNavLayout.js +9 -3
- package/dist/features/lottery/schemas/zod.d.ts +4 -4
- package/dist/features/loyalty/schemas/index.d.ts +6 -6
- package/dist/features/orders/components/OrderPaymentSummary.d.ts +1 -1
- package/dist/features/orders/components/OrderPaymentSummary.js +5 -5
- package/dist/features/orders/components/RefundHistoryTable.js +1 -1
- package/dist/features/orders/repository/orders.repository.d.ts +10 -1
- package/dist/features/orders/repository/orders.repository.js +17 -1
- package/dist/features/orders/schemas/firestore.d.ts +11 -12
- package/dist/features/orders/schemas/firestore.js +0 -1
- package/dist/features/orders/schemas/index.d.ts +48 -48
- package/dist/features/orders/schemas/index.js +1 -1
- package/dist/features/orders/types/index.d.ts +1 -3
- package/dist/features/orders/types/index.js +1 -2
- package/dist/features/payments/manifest.js +1 -1
- package/dist/features/payments/repository/payout.repository.d.ts +1 -4
- package/dist/features/payments/repository/payout.repository.js +0 -29
- package/dist/features/payments/schemas/firestore.d.ts +8 -15
- package/dist/features/payments/schemas/firestore.js +7 -5
- package/dist/features/payments/schemas/index.d.ts +52 -125
- package/dist/features/payments/schemas/index.js +0 -58
- package/dist/features/payments/types/index.d.ts +2 -4
- package/dist/features/payments/types/index.js +1 -2
- package/dist/features/products/schemas/index.d.ts +14 -14
- package/dist/features/products/schemas/product-features.validators.d.ts +10 -10
- package/dist/features/promotions/schemas/index.d.ts +20 -20
- package/dist/features/reviews/schemas/index.d.ts +8 -8
- package/dist/features/scams/schemas/index.d.ts +10 -10
- package/dist/features/search/action-index/settings-entries.js +4 -4
- package/dist/features/seller/schemas/index.d.ts +12 -12
- package/dist/features/seller/schemas/order-forms.d.ts +4 -4
- package/dist/features/shipments/schemas/validation.d.ts +28 -28
- package/dist/features/store-extensions/schemas/payout-method-form.d.ts +50 -50
- package/dist/features/store-extensions/schemas/report-create-form.d.ts +2 -2
- package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +10 -10
- package/dist/features/support/schemas/index.d.ts +14 -14
- package/dist/features/tester/actions/checklist-item-actions.d.ts +4 -4
- package/dist/features/tester/schemas/checklist-item-form.d.ts +2 -2
- package/dist/features/tester/seed-data/_money-flows.js +145 -0
- package/dist/features/tester/seed-data/authored/admin__site-system.js +38 -0
- package/dist/features/tester/seed-data/authored/buying__buying-checkout.js +10 -10
- package/dist/features/tester/seed-data/authored/buying__offers.js +11 -10
- package/dist/features/tester/seed-data/authored/buying__user-dashboard-navigation.js +6 -5
- package/dist/features/tester/seed-data/authored/cta-layout__checkout-bottom-bar.js +1 -1
- package/dist/features/tester/seed-data/authored/design-ux__hand-mode-layout.js +3 -3
- package/dist/features/tester/seed-data/authored/selling__store-dashboard-navigation.js +6 -5
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +27 -15
- package/dist/features/wishlist/schemas/index.d.ts +2 -2
- package/dist/index.d.ts +6 -28
- package/dist/index.js +8 -51
- package/dist/jobs.d.ts +1 -1
- package/dist/jobs.js +1 -1
- package/dist/providers/payment-phonepe/index.d.ts +117 -0
- package/dist/providers/payment-phonepe/index.js +221 -0
- package/dist/providers.d.ts +3 -2
- package/dist/providers.js +3 -2
- package/dist/schemas/registry.d.ts +163 -2623
- package/dist/schemas/registry.js +1 -6
- package/dist/seed/faq-seed-data.js +5 -5
- package/dist/seed/payouts-seed-data.js +3 -3
- package/dist/seed/site-settings-seed-data.js +7 -4
- package/dist/server-entry.d.ts +1 -2
- package/dist/server-entry.js +1 -3
- package/dist/server.d.ts +3 -9
- package/dist/server.js +5 -23
- package/dist/validation/schemas.d.ts +2 -2
- package/package.json +5 -4
- package/dist/_internal/server/features/payments/actions.d.ts +0 -30
- package/dist/_internal/server/features/payments/actions.js +0 -45
- package/dist/_internal/server/features/payments/data.d.ts +0 -13
- package/dist/_internal/server/features/payments/data.js +0 -20
- package/dist/_internal/server/features/payments/index.d.ts +0 -3
- package/dist/_internal/server/features/payments/index.js +0 -3
- package/dist/_internal/server/jobs/core/payoutBatch.d.ts +0 -2
- package/dist/_internal/server/jobs/core/payoutBatch.js +0 -120
- package/dist/_internal/server/jobs/handlers/payoutBatch.d.ts +0 -2
- package/dist/_internal/server/jobs/handlers/payoutBatch.js +0 -2
- package/dist/_internal/shared/features/payments/config.d.ts +0 -3
- package/dist/_internal/shared/features/payments/config.js +0 -3
- package/dist/contracts/client-payment-gateway.d.ts +0 -64
- package/dist/contracts/client-payment-gateway.js +0 -25
- package/dist/features/checkout/hooks/useCheckout.d.ts +0 -9
- package/dist/features/checkout/hooks/useCheckout.js +0 -24
- package/dist/features/checkout/hooks/useCheckoutApi.d.ts +0 -72
- package/dist/features/checkout/hooks/useCheckoutApi.js +0 -47
- package/dist/features/checkout/hooks/usePaymentCheckout.d.ts +0 -77
- package/dist/features/checkout/hooks/usePaymentCheckout.js +0 -192
- package/dist/providers/payment-razorpay/index.d.ts +0 -115
- package/dist/providers/payment-razorpay/index.js +0 -208
- package/dist/schemas/webhooks/razorpay.d.ts +0 -2634
- package/dist/schemas/webhooks/razorpay.js +0 -104
|
@@ -34,8 +34,7 @@ import { filterNavItems } from "./filterNavItems";
|
|
|
34
34
|
import { useSiteSettings } from "../../../../core/hooks/useSiteSettings";
|
|
35
35
|
import { useTheme } from "../../theme";
|
|
36
36
|
import { useHandMode } from "../../hand-mode";
|
|
37
|
-
import { BackgroundRenderer, Div,
|
|
38
|
-
import { BottomNavLayout } from "../../../../features/layout/BottomNavLayout";
|
|
37
|
+
import { BackgroundRenderer, Div, Nav } from "../../../../ui";
|
|
39
38
|
import { useToast } from "../../../../ui/components/Toast";
|
|
40
39
|
import { useSession } from "../../../../react/contexts/SessionContext";
|
|
41
40
|
import { ROUTES } from "../../../../next/routing/route-map";
|
|
@@ -147,33 +146,6 @@ function filterGroups(groups, navConfig, permissions) {
|
|
|
147
146
|
}))
|
|
148
147
|
.filter((group) => group.items.length > 0);
|
|
149
148
|
}
|
|
150
|
-
const MAX_BOTTOM_NAV_ITEMS = 5;
|
|
151
|
-
/**
|
|
152
|
-
* Mobile bottom tab bar (<lg) for the dashboard shells. Admin/store/user
|
|
153
|
-
* previously had no bottom-nav — this was the source of "hard to navigate
|
|
154
|
-
* on mobile" (the sidebar's only mobile affordance was an off-canvas drawer
|
|
155
|
-
* behind a hamburger toggle). Pattern lifted from the unused
|
|
156
|
-
* `DashboardScaffold` scaffold, which has since been retired in favour of
|
|
157
|
-
* wiring it directly here where the sidebars already live.
|
|
158
|
-
*
|
|
159
|
-
* Renders through the shared `<BottomNavLayout>` rather than hand-rolling its
|
|
160
|
-
* own `fixed bottom-0` chrome. That is not cosmetic: `BottomNavLayout` is what
|
|
161
|
-
* publishes `--bottom-nav-height`, which every bottom-anchored surface above it
|
|
162
|
-
* (the CTA tier, the pagination bar, BackToTop, the footer's clearance) offsets
|
|
163
|
-
* against. The hand-rolled version published nothing, so on dashboard routes
|
|
164
|
-
* that whole stack was positioned against the PUBLIC nav's height instead —
|
|
165
|
-
* which was itself a bug, since both bars were rendering on the same pixels.
|
|
166
|
-
* `AppLayoutShell` is now told to suppress the public one here via
|
|
167
|
-
* `showBottomNav={!isDashboard}`; exactly one nav must be mounted at a time.
|
|
168
|
-
*/
|
|
169
|
-
function DashboardBottomNav({ items, activeHref }) {
|
|
170
|
-
if (items.length === 0)
|
|
171
|
-
return null;
|
|
172
|
-
return (_jsx(BottomNavLayout, { id: "dashboard-bottom-navbar", ariaLabel: "Dashboard bottom navigation", children: items.slice(0, MAX_BOTTOM_NAV_ITEMS).map((item) => {
|
|
173
|
-
const isActive = activeHref === item.href || activeHref.startsWith(`${item.href}/`);
|
|
174
|
-
return (_jsx(Li, { className: "flex-1", children: _jsxs(Link, { href: item.href, className: `flex h-full flex-col items-center justify-center gap-[var(--appkit-space-0-5)] py-[var(--appkit-space-2)] ${isActive ? "text-[var(--appkit-color-primary)]" : "text-[var(--appkit-color-text-muted)]"}`, "aria-current": isActive ? "page" : undefined, children: [item.icon && _jsx(Span, { size: "base", children: item.icon }), _jsx(Span, { size: "xs", className: "max-w-full truncate", children: item.label })] }) }, item.href));
|
|
175
|
-
}) }));
|
|
176
|
-
}
|
|
177
149
|
// Asymmetric padding clears the persistent sidebar rail — right-heavy by
|
|
178
150
|
// default (rail docks left), left-heavy in left-hand mode (rail docks right).
|
|
179
151
|
const DEFAULT_CONTENT_PADDING_RIGHT = "px-[var(--appkit-space-5)] py-[var(--appkit-space-8)] lg:pl-14 lg:pr-6 xl:pl-16 xl:pr-10";
|
|
@@ -227,5 +199,5 @@ responsive: _responsive, crossNav, className, contentPadding, contentSurface, co
|
|
|
227
199
|
"pb-[var(--appkit-space-16)] lg:pb-[0]", // clears the fixed mobile bottom-nav
|
|
228
200
|
contentPadding ?? (hand === "left" ? DEFAULT_CONTENT_PADDING_LEFT : DEFAULT_CONTENT_PADDING_RIGHT),
|
|
229
201
|
contentSurface,
|
|
230
|
-
].filter(Boolean).join(" "), children: _jsx(Div, { className: ["w-full flex-1 mx-auto", contentMaxWidth ?? DEFAULT_CONTENT_MAX_WIDTH].filter(Boolean).join(" "), children: children }) })
|
|
202
|
+
].filter(Boolean).join(" "), children: _jsx(Div, { className: ["w-full flex-1 mx-auto", contentMaxWidth ?? DEFAULT_CONTENT_MAX_WIDTH].filter(Boolean).join(" "), children: children }) })] }));
|
|
231
203
|
}
|
|
@@ -9,5 +9,5 @@ const card = {
|
|
|
9
9
|
export function MaintenanceDashboardView({ counts, basePath, }) {
|
|
10
10
|
return (_jsxs(Div, { style: { padding: "1.5rem", maxWidth: "1200px", margin: "0 auto" }, children: [_jsx(Heading, { level: 1, style: { fontSize: "1.5rem", fontWeight: 600 }, children: "Maintenance" }), _jsx(Text, { as: "p", style: { color: "var(--appkit-color-text-muted)", marginBottom: "1.25rem" }, children: "Errors and failures across the platform \u2014 server, client, and Cloud Functions." }), _jsxs(Div, { style: { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(170px, 1fr))", gap: "0.75rem", marginBottom: "1.5rem" }, children: [_jsxs(Div, { style: card, children: [_jsx(Div, { style: { fontSize: "0.78rem", color: "var(--appkit-color-text-muted)" }, children: "Last 24 hours" }), _jsx(Div, { style: { fontSize: "1.75rem", fontWeight: 700 }, children: counts.last24h })] }), _jsxs(Div, { style: card, children: [_jsx(Div, { style: { fontSize: "0.78rem", color: "var(--appkit-color-text-muted)" }, children: "Last 7 days" }), _jsx(Div, { style: { fontSize: "1.75rem", fontWeight: 700 }, children: counts.last7d })] }), _jsxs(Div, { style: card, children: [_jsx(Div, { style: { fontSize: "0.78rem", color: "var(--appkit-color-text-muted)" }, children: "Last 30 days" }), _jsx(Div, { style: { fontSize: "1.75rem", fontWeight: 700 }, children: counts.last30d })] }), _jsxs(Div, { style: card, children: [_jsx(Div, { style: { fontSize: "0.78rem", color: "var(--appkit-color-text-muted)" }, children: "Server (Vercel)" }), _jsx(Div, { style: { fontSize: "1.5rem", fontWeight: 700 }, children: counts.bySource.vercel })] }), _jsxs(Div, { style: card, children: [_jsx(Div, { style: { fontSize: "0.78rem", color: "var(--appkit-color-text-muted)" }, children: "Client" }), _jsx(Div, { style: { fontSize: "1.5rem", fontWeight: 700 }, children: counts.bySource.client })] }), _jsxs(Div, { style: card, children: [_jsx(Div, { style: { fontSize: "0.78rem", color: "var(--appkit-color-text-muted)" }, children: "Cloud Functions" }), _jsx(Div, { style: { fontSize: "1.5rem", fontWeight: 700 }, children: counts.bySource.function })] })] }), _jsxs(Section, { style: { marginBottom: "1.5rem" }, children: [_jsx(Heading, { level: 2, style: { fontSize: "1rem", fontWeight: 600 }, children: "Top codes (30d)" }), _jsx(Ul, { style: { listStyle: "none", padding: 0 }, children: counts.topCodes.length === 0 ? _jsx(Li, { style: { color: "var(--appkit-color-text-muted)" }, children: "No errors recorded." }) :
|
|
11
11
|
counts.topCodes.map((c) => (_jsxs(Li, { style: { display: "flex", justifyContent: "space-between", padding: "0.25rem 0", borderBottom: "1px solid var(--appkit-color-border-subtle)" }, children: [_jsx(Span, { style: { fontFamily: "monospace" }, children: c.code }), _jsx(Span, { children: c.count })] }, c.code))) })] }), _jsxs(Section, { style: { marginBottom: "1.5rem" }, children: [_jsx(Heading, { level: 2, style: { fontSize: "1rem", fontWeight: 600 }, children: "Top routes (30d)" }), _jsx(Ul, { style: { listStyle: "none", padding: 0 }, children: counts.topRoutes.length === 0 ? _jsx(Li, { style: { color: "var(--appkit-color-text-muted)" }, children: "No errors recorded." }) :
|
|
12
|
-
counts.topRoutes.map((r) => (_jsxs(Li, { style: { display: "flex", justifyContent: "space-between", padding: "0.25rem 0", borderBottom: "1px solid var(--appkit-color-border-subtle)" }, children: [_jsxs(Span, { style: { fontFamily: "monospace" }, children: ["[", r.source, "] ", r.route] }), _jsx(Span, { children: r.count })] }, `${r.source}:${r.route}`))) })] }), _jsxs(Nav, { "aria-label": "Maintenance sections", style: { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))", gap: "0.5rem" }, children: [_jsxs(Anchor, { href: `${basePath}/server-errors`, tone: "default", underline: "none", style: { ...card }, children: [_jsx(Span, { weight: "bold", children: "Server errors" }), _jsx(Div, { style: { color: "var(--appkit-color-text-muted)", fontSize: "0.85rem" }, children: "Vercel function failures" })] }), _jsxs(Anchor, { href: `${basePath}/client-errors`, tone: "default", underline: "none", style: { ...card }, children: [_jsx(Span, { weight: "bold", children: "Client errors" }), _jsx(Div, { style: { color: "var(--appkit-color-text-muted)", fontSize: "0.85rem" }, children: "window.onerror + unhandledrejection + boundaries" })] }), _jsxs(Anchor, { href: `${basePath}/function-errors`, tone: "default", underline: "none", style: { ...card }, children: [_jsx(Span, { weight: "bold", children: "Function errors" }), _jsx(Div, { style: { color: "var(--appkit-color-text-muted)", fontSize: "0.85rem" }, children: "Cloud Functions handler exceptions" })] }), _jsxs(Anchor, { href: `${basePath}/cloud-logs`, tone: "default", underline: "none", style: { ...card }, children: [_jsx(Span, { weight: "bold", children: "Cloud Logging" }), _jsx(Div, { style: { color: "var(--appkit-color-text-muted)", fontSize: "0.85rem" }, children: "Live stream from Cloud Logging API" })] }), _jsxs(Anchor, { href: `${basePath}/payment-rollbacks`, tone: "default", underline: "none", style: { ...card }, children: [_jsx(Span, { weight: "bold", children: "Payment rollbacks" }), _jsx(Div, { style: { color: "var(--appkit-color-text-muted)", fontSize: "0.85rem" }, children: "
|
|
12
|
+
counts.topRoutes.map((r) => (_jsxs(Li, { style: { display: "flex", justifyContent: "space-between", padding: "0.25rem 0", borderBottom: "1px solid var(--appkit-color-border-subtle)" }, children: [_jsxs(Span, { style: { fontFamily: "monospace" }, children: ["[", r.source, "] ", r.route] }), _jsx(Span, { children: r.count })] }, `${r.source}:${r.route}`))) })] }), _jsxs(Nav, { "aria-label": "Maintenance sections", style: { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))", gap: "0.5rem" }, children: [_jsxs(Anchor, { href: `${basePath}/server-errors`, tone: "default", underline: "none", style: { ...card }, children: [_jsx(Span, { weight: "bold", children: "Server errors" }), _jsx(Div, { style: { color: "var(--appkit-color-text-muted)", fontSize: "0.85rem" }, children: "Vercel function failures" })] }), _jsxs(Anchor, { href: `${basePath}/client-errors`, tone: "default", underline: "none", style: { ...card }, children: [_jsx(Span, { weight: "bold", children: "Client errors" }), _jsx(Div, { style: { color: "var(--appkit-color-text-muted)", fontSize: "0.85rem" }, children: "window.onerror + unhandledrejection + boundaries" })] }), _jsxs(Anchor, { href: `${basePath}/function-errors`, tone: "default", underline: "none", style: { ...card }, children: [_jsx(Span, { weight: "bold", children: "Function errors" }), _jsx(Div, { style: { color: "var(--appkit-color-text-muted)", fontSize: "0.85rem" }, children: "Cloud Functions handler exceptions" })] }), _jsxs(Anchor, { href: `${basePath}/cloud-logs`, tone: "default", underline: "none", style: { ...card }, children: [_jsx(Span, { weight: "bold", children: "Cloud Logging" }), _jsx(Div, { style: { color: "var(--appkit-color-text-muted)", fontSize: "0.85rem" }, children: "Live stream from Cloud Logging API" })] }), _jsxs(Anchor, { href: `${basePath}/payment-rollbacks`, tone: "default", underline: "none", style: { ...card }, children: [_jsx(Span, { weight: "bold", children: "Payment rollbacks" }), _jsx(Div, { style: { color: "var(--appkit-color-text-muted)", fontSize: "0.85rem" }, children: "PhonePe refund + upstream-unavailable rows" })] }), _jsxs(Anchor, { href: `${basePath}/analysis`, tone: "default", underline: "none", style: { ...card }, children: [_jsx(Span, { weight: "bold", children: "Analysis" }), _jsx(Div, { style: { color: "var(--appkit-color-text-muted)", fontSize: "0.85rem" }, children: "Run the maintenance analyzer + recommendations" })] })] })] }));
|
|
13
13
|
}
|
|
@@ -104,7 +104,7 @@ export interface CheckoutPricingPreview {
|
|
|
104
104
|
/**
|
|
105
105
|
* Read-only "what will I actually be charged" preview for the checkout
|
|
106
106
|
* Order Summary. Reuses the exact same building blocks
|
|
107
|
-
* createCheckoutOrderAction /
|
|
107
|
+
* createCheckoutOrderAction / verifyAndPlacePhonePeOrderAction use when an
|
|
108
108
|
* order is actually placed — resolveShippingCost, the compute*Fee helpers,
|
|
109
109
|
* computeGroupCouponDiscount, and the GST calc — so the number shown to the
|
|
110
110
|
* buyer can never diverge from what gets charged/recorded. Never decrements
|
|
@@ -120,23 +120,25 @@ export declare function attachPaymentAction(input: {
|
|
|
120
120
|
paymentId: string;
|
|
121
121
|
paidAmount: number;
|
|
122
122
|
}): Promise<void>;
|
|
123
|
-
export interface
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
123
|
+
export interface VerifyAndPlacePhonePeOrderInput {
|
|
124
|
+
/** PhonePe's merchantOrderId — our own id, generated in PhonePeProvider.createOrder(). */
|
|
125
|
+
merchantOrderId: string;
|
|
126
|
+
/**
|
|
127
|
+
* Buyer identity. Omit on the webhook path — no session exists there, so
|
|
128
|
+
* it's resolved from `uid` packed into PhonePe's `metaInfo` at
|
|
129
|
+
* create-order time and re-fetched from `userRepository`.
|
|
130
|
+
*/
|
|
131
|
+
userId?: string;
|
|
132
|
+
userName?: string;
|
|
133
|
+
userEmail?: string;
|
|
134
|
+
/** Present on the buyer-present path; falls back to metaInfo (webhook path) when omitted. Required for physical carts. */
|
|
131
135
|
addressId?: string;
|
|
132
136
|
notes?: string;
|
|
133
137
|
/**
|
|
134
138
|
* Buyer's choice for what to do when a cart item is unavailable at
|
|
135
|
-
* checkout time. Defaults to "cancel_order"
|
|
136
|
-
*
|
|
137
|
-
* field, since "skip_items" partial fulfillment has no prior precedent
|
|
138
|
-
* on the Razorpay path.
|
|
139
|
+
* checkout time. Defaults to "cancel_order" when neither the caller nor
|
|
140
|
+
* metaInfo supplies one — matches this path's historical (only) behavior.
|
|
139
141
|
*/
|
|
140
142
|
outOfStockPolicy?: OutOfStockPolicy;
|
|
141
143
|
}
|
|
142
|
-
export declare function
|
|
144
|
+
export declare function verifyAndPlacePhonePeOrderAction(input: VerifyAndPlacePhonePeOrderInput): Promise<CheckoutOrderResult>;
|
|
@@ -29,7 +29,8 @@ import { getCartItemMemberIds, getExpandedDecrements, bucketCartItemsByStock, is
|
|
|
29
29
|
import { expandCartLineToOrderRows } from "./expand-order-items";
|
|
30
30
|
import { OrderStatusValues, PaymentStatusValues, PaymentMethodValues, OutOfStockPolicyValues, } from "../../../../features/orders/schemas/index";
|
|
31
31
|
import { getDefaultCurrency } from "../../../../core/index";
|
|
32
|
-
import {
|
|
32
|
+
import { getProviders } from "../../../../contracts";
|
|
33
|
+
import { paiseToRupees } from "../../../../core/money";
|
|
33
34
|
import { CHECKOUT_DEFAULT_COMMISSIONS, CHECKOUT_DEFAULT_EMI_SETTINGS } from "../../../shared/features/checkout/config";
|
|
34
35
|
import { checkEmiEligibility, computeEmiSchedule } from "../../../shared/features/emi/schedule";
|
|
35
36
|
import { formatShippingAddress } from "./data";
|
|
@@ -204,7 +205,7 @@ async function resolveStoreState(storeId) {
|
|
|
204
205
|
* Prorates every buyer-applied coupon against one seller-group's total. Pure
|
|
205
206
|
* — no side effects (usage-accumulator bookkeeping is layered on by callers
|
|
206
207
|
* that actually place an order). Shared by createOrderForGroup,
|
|
207
|
-
*
|
|
208
|
+
* createPhonePeGroupOrder, and the read-only previewCheckoutPricing so the
|
|
208
209
|
* three can never drift on how a coupon splits across sellers (was three
|
|
209
210
|
* near-identical copies before this extraction — Duplication Decision
|
|
210
211
|
* Framework's Rule of Three).
|
|
@@ -455,8 +456,8 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
455
456
|
}
|
|
456
457
|
}
|
|
457
458
|
// The platform commission is now charged on EVERY payment method. It used to
|
|
458
|
-
// be added only on the
|
|
459
|
-
// silently never paid it.
|
|
459
|
+
// be added only on the online-payment path, so COD / UPI-manual / cash / EMI
|
|
460
|
+
// buyers silently never paid it.
|
|
460
461
|
const orderTotal = Math.max(0, groupTotal - couponDiscount) + shippingFee + codHandlingFee + whatsappNotifyFee + giftWrapFee + shipmentProtectionFee + platformFee + platformFeeGst + (emiSchedule?.surchargeAmount ?? 0) + (gstBreakdown?.gstAmount ?? 0);
|
|
461
462
|
const imageUrls = [
|
|
462
463
|
...new Set(group
|
|
@@ -471,7 +472,7 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
471
472
|
};
|
|
472
473
|
// ── 15-minute payment window + seller UPI resolution (Tier PP) ──────────
|
|
473
474
|
// Deadline + displayed UPI apply only to methods with a buyer-uploaded
|
|
474
|
-
// proof step — never cod (paid on delivery),
|
|
475
|
+
// proof step — never cod (paid on delivery), online (paid before order
|
|
475
476
|
// creation), or admin_bypass (already marked paid above).
|
|
476
477
|
const hasPaymentWindow = !adminBypass &&
|
|
477
478
|
(paymentMethod === PaymentMethodValues.UPI_MANUAL ||
|
|
@@ -888,7 +889,7 @@ const EMPTY_PRICING_PREVIEW = {
|
|
|
888
889
|
/**
|
|
889
890
|
* Read-only "what will I actually be charged" preview for the checkout
|
|
890
891
|
* Order Summary. Reuses the exact same building blocks
|
|
891
|
-
* createCheckoutOrderAction /
|
|
892
|
+
* createCheckoutOrderAction / verifyAndPlacePhonePeOrderAction use when an
|
|
892
893
|
* order is actually placed — resolveShippingCost, the compute*Fee helpers,
|
|
893
894
|
* computeGroupCouponDiscount, and the GST calc — so the number shown to the
|
|
894
895
|
* buyer can never diverge from what gets charged/recorded. Never decrements
|
|
@@ -1039,23 +1040,27 @@ export async function attachPaymentAction(input) {
|
|
|
1039
1040
|
});
|
|
1040
1041
|
}
|
|
1041
1042
|
/**
|
|
1042
|
-
* Place order(s) from the user's cart after a
|
|
1043
|
-
*
|
|
1043
|
+
* Place order(s) from the user's cart after a PhonePe payment is confirmed.
|
|
1044
|
+
* Called from both /api/payment/verify (buyer present) and the webhook
|
|
1045
|
+
* (buyer may be gone) — see the module-level comment above.
|
|
1044
1046
|
*
|
|
1045
|
-
*
|
|
1046
|
-
* 1.
|
|
1047
|
-
*
|
|
1048
|
-
*
|
|
1049
|
-
*
|
|
1050
|
-
*
|
|
1051
|
-
*
|
|
1052
|
-
*
|
|
1047
|
+
* Performs:
|
|
1048
|
+
* 1. Order Status confirmation via `getProviders().payment.getOrder()` —
|
|
1049
|
+
* the sole source of truth; PhonePe never hands the browser a
|
|
1050
|
+
* verifiable proof of payment.
|
|
1051
|
+
* 2. An idempotency claim so only one of the two callers proceeds.
|
|
1052
|
+
* 3. Cart re-validation against current product prices/stock.
|
|
1053
|
+
* 4. Amount cross-check against the confirmed PhonePe order.
|
|
1054
|
+
* 5. Atomic stock decrement + cart clear via unitOfWork batch.
|
|
1055
|
+
* 6. Multi-coupon pro-rating per order group.
|
|
1056
|
+
* 7. order_placed notifications (buyer + seller).
|
|
1057
|
+
* 8. Confirmation email + RTDB success signal (both fire-and-forget).
|
|
1053
1058
|
*/
|
|
1054
1059
|
/**
|
|
1055
|
-
* Auto-refund the value of items dropped from a
|
|
1056
|
-
* "skip_items" out-of-stock policy.
|
|
1060
|
+
* Auto-refund the value of items dropped from a PhonePe checkout under the
|
|
1061
|
+
* "skip_items" out-of-stock policy. PhonePe captures payment for the FULL
|
|
1057
1062
|
* cart before the stock check runs, so anything not placed as an order must
|
|
1058
|
-
* be given back. Extracted from `
|
|
1063
|
+
* be given back. Extracted from `verifyAndPlacePhonePeOrderAction` — same
|
|
1059
1064
|
* behavior, kept as a named step so the parent function stays readable.
|
|
1060
1065
|
*
|
|
1061
1066
|
* Awaited by the caller (this moves money, never fire-and-forget). On
|
|
@@ -1063,8 +1068,8 @@ export async function attachPaymentAction(input) {
|
|
|
1063
1068
|
* created and paid); the failure is surfaced via `order.refundPending` + an
|
|
1064
1069
|
* admin notification fan-out, never silently dropped (Rule #8).
|
|
1065
1070
|
*/
|
|
1066
|
-
async function
|
|
1067
|
-
const { unavailablePaid, orderIds, productByIdPaid,
|
|
1071
|
+
async function refundDroppedItemsForPhonePeCheckout(input) {
|
|
1072
|
+
const { unavailablePaid, orderIds, productByIdPaid, phonepeOrderId } = input;
|
|
1068
1073
|
if (unavailablePaid.length === 0 || orderIds.length === 0)
|
|
1069
1074
|
return;
|
|
1070
1075
|
const droppedValue = unavailablePaid.reduce((sum, u) => sum + (productByIdPaid.get(u.productId)?.price ?? 0) * u.requestedQty, 0);
|
|
@@ -1076,9 +1081,9 @@ async function refundDroppedItemsForRazorpayCheckout(input) {
|
|
|
1076
1081
|
// Cap defensively at the primary order's total — processRefundAction
|
|
1077
1082
|
// rejects any amount exceeding order.totalPrice, and a multi-order
|
|
1078
1083
|
// batch's dropped-items value isn't cleanly attributable to a single
|
|
1079
|
-
// order (
|
|
1080
|
-
// FIRST order created in this batch is used as the
|
|
1081
|
-
// keeping anchor).
|
|
1084
|
+
// order (PhonePe refunds are keyed by the original order id, not a
|
|
1085
|
+
// payment id; the FIRST order created in this batch is used as the
|
|
1086
|
+
// refund's book-keeping anchor).
|
|
1082
1087
|
const refundAmount = primaryOrder
|
|
1083
1088
|
? Math.min(droppedValue, primaryOrder.totalPrice)
|
|
1084
1089
|
: droppedValue;
|
|
@@ -1095,8 +1100,8 @@ async function refundDroppedItemsForRazorpayCheckout(input) {
|
|
|
1095
1100
|
*/
|
|
1096
1101
|
reasonCode: "not_received",
|
|
1097
1102
|
reasonNote: `Automatic refund — ${unavailablePaid.length} item(s) unavailable at checkout: ${unavailablePaid.map((u) => u.productTitle).join(", ")}`,
|
|
1098
|
-
method: "
|
|
1099
|
-
|
|
1103
|
+
method: "phonepe",
|
|
1104
|
+
phonepeOrderId,
|
|
1100
1105
|
confirmIrrevocable: true,
|
|
1101
1106
|
refundedBy: "system:checkout-auto-refund",
|
|
1102
1107
|
});
|
|
@@ -1106,7 +1111,7 @@ async function refundDroppedItemsForRazorpayCheckout(input) {
|
|
|
1106
1111
|
}
|
|
1107
1112
|
catch (refundErr) {
|
|
1108
1113
|
void normalizeError(refundErr);
|
|
1109
|
-
serverLogger.warn("
|
|
1114
|
+
serverLogger.warn("verifyAndPlacePhonePeOrderAction: automatic partial refund for dropped items failed — flagging for manual follow-up", {
|
|
1110
1115
|
orderId: primaryOrderId,
|
|
1111
1116
|
droppedValue,
|
|
1112
1117
|
err: refundErr instanceof Error ? refundErr.message : String(refundErr),
|
|
@@ -1115,7 +1120,7 @@ async function refundDroppedItemsForRazorpayCheckout(input) {
|
|
|
1115
1120
|
.update(primaryOrderId, { refundPending: true })
|
|
1116
1121
|
.catch((updErr) => {
|
|
1117
1122
|
void normalizeError(updErr);
|
|
1118
|
-
serverLogger.error("
|
|
1123
|
+
serverLogger.error("verifyAndPlacePhonePeOrderAction: failed to flag refundPending after auto-refund failure", { orderId: primaryOrderId, err: updErr instanceof Error ? updErr.message : String(updErr) });
|
|
1119
1124
|
});
|
|
1120
1125
|
/*
|
|
1121
1126
|
* One admin-inbox row, not one notification per admin.
|
|
@@ -1143,20 +1148,20 @@ async function refundDroppedItemsForRazorpayCheckout(input) {
|
|
|
1143
1148
|
})
|
|
1144
1149
|
.catch((notifyErr) => {
|
|
1145
1150
|
void normalizeError(notifyErr);
|
|
1146
|
-
serverLogger.error("
|
|
1151
|
+
serverLogger.error("verifyAndPlacePhonePeOrderAction: failed to write admin notification for failed auto-refund", { orderId: primaryOrderId });
|
|
1147
1152
|
});
|
|
1148
1153
|
}
|
|
1149
1154
|
}
|
|
1150
1155
|
/**
|
|
1151
|
-
* Places one order for one seller-group within `
|
|
1156
|
+
* Places one order for one seller-group within `verifyAndPlacePhonePeOrderAction`
|
|
1152
1157
|
* — mirrors `createOrderForGroup`'s role on the COD/cash/EMI path (same
|
|
1153
1158
|
* per-group fee/coupon/order-doc/notification shape), extracted into its own
|
|
1154
1159
|
* function purely to keep the parent function under the LARGE_COMPONENT
|
|
1155
1160
|
* audit threshold (`scripts/audit-code-quality.mjs`). No behavior change
|
|
1156
1161
|
* from the inline version this replaced.
|
|
1157
1162
|
*/
|
|
1158
|
-
async function
|
|
1159
|
-
const { appliedCoupons, cartSubtotal, storeAddons, platformFeeByStore, siteSettings, uid, userName, userEmail,
|
|
1163
|
+
async function createPhonePeGroupOrder(group, orderType, ctx) {
|
|
1164
|
+
const { appliedCoupons, cartSubtotal, storeAddons, platformFeeByStore, siteSettings, uid, userName, userEmail, merchantOrderId, phonepeOrderId, transactionId, shippingAddress, notes, outOfStockPolicy, unavailablePaid, orderIds, emailsToSend, couponUsageAccumulator, productById: phonepeProductById, } = ctx;
|
|
1160
1165
|
const firstItem = group[0].item;
|
|
1161
1166
|
const groupTotal = group.reduce((sum, { item, product }) => sum + lineTotalFor(item, product), 0);
|
|
1162
1167
|
// Reuses the same store/seller lookup as the COD/UPI path above instead
|
|
@@ -1167,7 +1172,7 @@ async function createRazorpayGroupOrder(group, orderType, ctx) {
|
|
|
1167
1172
|
const { couponDiscount, appliedDiscounts } = computeGroupCouponDiscount(appliedCoupons, groupLines, groupTotal, cartSubtotal, firstItem.storeId);
|
|
1168
1173
|
// Mirrors createOrderForGroup — without this the online-payment path wrote
|
|
1169
1174
|
// the discount onto the order but never incremented usage.currentUsage or
|
|
1170
|
-
// the per-user counter, so limits were unenforceable for
|
|
1175
|
+
// the per-user counter, so limits were unenforceable for PhonePe orders.
|
|
1171
1176
|
const groupCouponCodes = new Set();
|
|
1172
1177
|
for (const discount of appliedDiscounts) {
|
|
1173
1178
|
if (discount.couponId) {
|
|
@@ -1194,25 +1199,25 @@ async function createRazorpayGroupOrder(group, orderType, ctx) {
|
|
|
1194
1199
|
const giftWrapMessage = addons.giftWrapMessage;
|
|
1195
1200
|
const shipmentProtectionFee = computeShipmentProtectionFee(groupTotal, addons.shipmentProtectionAddon ?? false, commissionRates);
|
|
1196
1201
|
const orderTotal = Math.max(0, groupTotal - couponDiscount) + shippingFee + whatsappNotifyFee + giftWrapFee + shipmentProtectionFee + platformFee + platformFeeGst;
|
|
1197
|
-
// P-8 GST — deliberately NOT wired into this
|
|
1202
|
+
// P-8 GST — deliberately NOT wired into this PhonePe-verify path. The
|
|
1198
1203
|
// amount-mismatch check above (expectedPaymentAmountRs) compares against
|
|
1199
|
-
// what the buyer already paid via the
|
|
1204
|
+
// what the buyer already paid via the PhonePe order created earlier in
|
|
1200
1205
|
// the flow; adding product GST here without also adding it to that
|
|
1201
1206
|
// upstream pre-payment amount calculation would either fail the mismatch
|
|
1202
1207
|
// check or silently under/over-charge. Wiring GST through the full
|
|
1203
|
-
//
|
|
1204
|
-
// alongside P-13 (
|
|
1208
|
+
// PhonePe create→confirm round-trip is separate follow-up work, tracked
|
|
1209
|
+
// alongside P-13 (PhonePe is disabled by default today, so this order
|
|
1205
1210
|
// type doesn't currently carry a GST breakdown).
|
|
1206
1211
|
// S-SBUNI-RULES 2026-05-13 — order-item decoration via rule registry.
|
|
1207
1212
|
// Same expansion as the COD/UPI path. This was a THIRD hand-rolled copy of
|
|
1208
1213
|
// the price rule (`isBundle ? item.price : product.price`) that — like the
|
|
1209
|
-
// two in the consumer's
|
|
1210
|
-
// an accepted offer paid through
|
|
1214
|
+
// two in the consumer's checkout route — silently omitted `lockedPrice`, so
|
|
1215
|
+
// an accepted offer paid through PhonePe was RECORDED at the seller's list
|
|
1211
1216
|
// price. `lineTotalFor` inside the expansion is the single definition.
|
|
1212
1217
|
const orderItems = group.flatMap(({ item, product }) => {
|
|
1213
1218
|
const lt = (product?.listingType ?? "standard");
|
|
1214
1219
|
const itemRule = getListingRule(lt);
|
|
1215
|
-
return expandCartLineToOrderRows(item, product,
|
|
1220
|
+
return expandCartLineToOrderRows(item, product, phonepeProductById).map((baseLine) => itemRule.decorateOrderItem(baseLine, product));
|
|
1216
1221
|
});
|
|
1217
1222
|
const totalQuantity = group.reduce((sum, { item }) => sum + item.quantity, 0);
|
|
1218
1223
|
const imageUrls = [
|
|
@@ -1221,10 +1226,10 @@ async function createRazorpayGroupOrder(group, orderType, ctx) {
|
|
|
1221
1226
|
.filter((url) => typeof url === "string" && url.length > 0)),
|
|
1222
1227
|
];
|
|
1223
1228
|
// S-SBUNI-RULES 2026-05-13 — order-doc decoration via rule registry.
|
|
1224
|
-
const
|
|
1225
|
-
const
|
|
1229
|
+
const lt0Pp = (group[0].product?.listingType ?? "standard");
|
|
1230
|
+
const groupRulePp = getListingRule(lt0Pp);
|
|
1226
1231
|
const extraOrderFields = {
|
|
1227
|
-
...
|
|
1232
|
+
...groupRulePp.decorateOrderDoc(group[0].item, group[0].product),
|
|
1228
1233
|
};
|
|
1229
1234
|
// Both creation paths call the SAME builder — two hand-written copies is the
|
|
1230
1235
|
// create-vs-create axis of Root Cause #39, and these two functions have
|
|
@@ -1256,18 +1261,20 @@ async function createRazorpayGroupOrder(group, orderType, ctx) {
|
|
|
1256
1261
|
status: OrderStatusValues.CONFIRMED,
|
|
1257
1262
|
paymentStatus: PaymentStatusValues.PAID,
|
|
1258
1263
|
paymentMethod: PaymentMethodValues.ONLINE,
|
|
1259
|
-
|
|
1264
|
+
// Our own generated id, not PhonePe's — this is what refunds/lookups key
|
|
1265
|
+
// on (PhonePe's refund API is `originalMerchantOrderId`-keyed anyway).
|
|
1266
|
+
paymentId: merchantOrderId,
|
|
1260
1267
|
paymentRecord: {
|
|
1261
|
-
method: "
|
|
1262
|
-
transactionId:
|
|
1268
|
+
method: "phonepe",
|
|
1269
|
+
transactionId: transactionId ?? merchantOrderId,
|
|
1263
1270
|
amount: orderTotal,
|
|
1264
1271
|
paidAt: new Date(),
|
|
1265
|
-
verifiedBy: "
|
|
1266
|
-
verificationMethod: "
|
|
1272
|
+
verifiedBy: "phonepe-order-status",
|
|
1273
|
+
verificationMethod: "order-status",
|
|
1267
1274
|
gatewayRef: {
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1275
|
+
merchantOrderId,
|
|
1276
|
+
phonepeOrderId,
|
|
1277
|
+
transactionId,
|
|
1271
1278
|
},
|
|
1272
1279
|
},
|
|
1273
1280
|
shippingAddress,
|
|
@@ -1334,53 +1341,93 @@ async function createRazorpayGroupOrder(group, orderType, ctx) {
|
|
|
1334
1341
|
});
|
|
1335
1342
|
return orderTotal;
|
|
1336
1343
|
}
|
|
1337
|
-
export async function
|
|
1338
|
-
const {
|
|
1339
|
-
//
|
|
1340
|
-
//
|
|
1341
|
-
//
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1344
|
+
export async function verifyAndPlacePhonePeOrderAction(input) {
|
|
1345
|
+
const { merchantOrderId } = input;
|
|
1346
|
+
// Idempotency: the buyer's /verify call and the async webhook can both
|
|
1347
|
+
// race to confirm+place for the same PhonePe payment (see the module
|
|
1348
|
+
// comment above). `.create()` throws on conflict, so whichever caller
|
|
1349
|
+
// creates this claim doc first is the sole placer; the loser returns
|
|
1350
|
+
// whatever the winner already placed, or a retryable "in progress" error
|
|
1351
|
+
// if the winner genuinely hasn't finished yet.
|
|
1352
|
+
const claimRef = getAdminDb().collection("phonepeOrderClaims").doc(merchantOrderId);
|
|
1353
|
+
try {
|
|
1354
|
+
await claimRef.create({ claimedAt: new Date() });
|
|
1355
|
+
}
|
|
1356
|
+
catch (claimErr) {
|
|
1357
|
+
void normalizeError(claimErr);
|
|
1358
|
+
const existingOrders = await unitOfWork.orders.findByPaymentId(merchantOrderId);
|
|
1359
|
+
if (existingOrders.length > 0) {
|
|
1360
|
+
return {
|
|
1361
|
+
orderIds: existingOrders.map((o) => o.id),
|
|
1362
|
+
total: existingOrders.reduce((sum, o) => sum + o.totalPrice, 0),
|
|
1363
|
+
itemCount: existingOrders.length,
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
throw new ValidationError(ERROR_MESSAGES.CHECKOUT.ORDER_PLACEMENT_IN_PROGRESS);
|
|
1367
|
+
}
|
|
1368
|
+
// Order Status confirmation — PhonePe never hands the browser a
|
|
1369
|
+
// verifiable proof of payment, so this call IS the "signature check".
|
|
1370
|
+
const phonepeOrder = await getProviders().payment.getOrder(merchantOrderId);
|
|
1371
|
+
if (phonepeOrder.status !== "paid") {
|
|
1372
|
+
serverLogger.warn(`PhonePe order not completed: merchantOrderId=${merchantOrderId} state=${phonepeOrder.status}`);
|
|
1350
1373
|
failedCheckoutRepository
|
|
1351
|
-
.logPayment(
|
|
1352
|
-
gatewayOrderId:
|
|
1353
|
-
gatewayPaymentId:
|
|
1354
|
-
addressId,
|
|
1374
|
+
.logPayment(input.userId ?? "unknown", "signature_mismatch", "PhonePe order not COMPLETED", {
|
|
1375
|
+
gatewayOrderId: phonepeOrder.gatewayOrderId,
|
|
1376
|
+
gatewayPaymentId: merchantOrderId,
|
|
1377
|
+
addressId: input.addressId,
|
|
1355
1378
|
})
|
|
1356
1379
|
.catch((logErr) => { void normalizeError(logErr); serverLogger.warn(AUDIT_LOG_FAIL_MSG, { error: logErr instanceof Error ? logErr.message : String(logErr) }); });
|
|
1357
1380
|
throw new ValidationError(ERROR_MESSAGES.CHECKOUT.PAYMENT_FAILED);
|
|
1358
1381
|
}
|
|
1382
|
+
// The buyer-present /verify call always supplies these; the webhook path
|
|
1383
|
+
// (no session) reconstructs them from PhonePe's metaInfo, packed at
|
|
1384
|
+
// create-order time — see `PhonePeCheckoutIntent`.
|
|
1385
|
+
const intent = phonepeOrder.metadata;
|
|
1386
|
+
const uid = input.userId ?? intent?.uid;
|
|
1387
|
+
if (!uid) {
|
|
1388
|
+
throw new ValidationError(ERROR_MESSAGES.CHECKOUT.PAYMENT_FAILED);
|
|
1389
|
+
}
|
|
1390
|
+
let userName = input.userName;
|
|
1391
|
+
let userEmail = input.userEmail;
|
|
1392
|
+
if (userName === undefined || userEmail === undefined) {
|
|
1393
|
+
const userDoc = await userRepository.findById(uid);
|
|
1394
|
+
userName = userName ?? userDoc?.displayName ?? "Customer";
|
|
1395
|
+
userEmail = userEmail ?? userDoc?.email ?? "";
|
|
1396
|
+
}
|
|
1397
|
+
const addressId = input.addressId ?? intent?.addressId;
|
|
1398
|
+
const notes = input.notes ?? intent?.notes;
|
|
1399
|
+
const outOfStockPolicy = input.outOfStockPolicy ??
|
|
1400
|
+
intent?.outOfStockPolicy ??
|
|
1401
|
+
OutOfStockPolicyValues.CANCEL_ORDER;
|
|
1402
|
+
// Add-on fees are no longer resolved here. They are per store, and this
|
|
1403
|
+
// function has no single store — each group reads its own selection off
|
|
1404
|
+
// `cart.storeAddons` inside createPhonePeGroupOrder.
|
|
1405
|
+
const siteSettings = await siteSettingsRepository.getSingleton();
|
|
1359
1406
|
const cart = await unitOfWork.carts.getOrCreate(uid);
|
|
1360
1407
|
if (!cart.items || cart.items.length === 0) {
|
|
1361
1408
|
throw new ValidationError(ERROR_MESSAGES.CHECKOUT.CART_EMPTY);
|
|
1362
1409
|
}
|
|
1363
1410
|
// Same lane priority + locked-line revalidation the manual/COD path runs.
|
|
1364
|
-
// The
|
|
1411
|
+
// The PhonePe path settles the whole cart (no selectedItemIds), so the
|
|
1365
1412
|
// "selected" set is the cart itself.
|
|
1366
1413
|
assertCheckoutLane(cart.items, cart.items);
|
|
1367
1414
|
await assertLockedLinesStillValid(cart.items, uid);
|
|
1368
|
-
const
|
|
1415
|
+
const isDigitalCartPp = cartIsDigitalOnly(cart.items);
|
|
1369
1416
|
let shippingAddress;
|
|
1370
|
-
let
|
|
1371
|
-
if (!
|
|
1417
|
+
let resolvedAddressPp = null;
|
|
1418
|
+
if (!isDigitalCartPp) {
|
|
1372
1419
|
if (!addressId) {
|
|
1373
1420
|
throw new NotFoundError(ERROR_MESSAGES.CHECKOUT.ADDRESS_REQUIRED);
|
|
1374
1421
|
}
|
|
1375
1422
|
const addressDoc = await unitOfWork.addresses.findById(addressId);
|
|
1376
|
-
|
|
1423
|
+
resolvedAddressPp =
|
|
1377
1424
|
addressDoc && addressDoc.ownerType === "user" && addressDoc.ownerId === uid
|
|
1378
1425
|
? addressDoc
|
|
1379
1426
|
: null;
|
|
1380
|
-
if (!
|
|
1427
|
+
if (!resolvedAddressPp) {
|
|
1381
1428
|
throw new NotFoundError(ERROR_MESSAGES.CHECKOUT.ADDRESS_REQUIRED);
|
|
1382
1429
|
}
|
|
1383
|
-
shippingAddress = formatShippingAddress(
|
|
1430
|
+
shippingAddress = formatShippingAddress(resolvedAddressPp);
|
|
1384
1431
|
}
|
|
1385
1432
|
// SB-UNI-5 2026-05-13 — bundle-aware product fetch + validation. Each
|
|
1386
1433
|
// cart line gets paired with a "representative" product (first member id
|
|
@@ -1412,17 +1459,17 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
1412
1459
|
// S-SBUNI-RULES 2026-05-13 — sync preflight via rule registry.
|
|
1413
1460
|
// Multi-member lines skip the prize-pool cap, same reasoning as the COD path.
|
|
1414
1461
|
const preflightPairs = productChecks.filter((p) => p.product !== null && p.product !== undefined && !isMultiMemberLine(p.item));
|
|
1415
|
-
// This whole action is the
|
|
1462
|
+
// This whole action is the online-payment path — paymentMethod is always "online".
|
|
1416
1463
|
runSyncPreflight(preflightPairs, "online");
|
|
1417
1464
|
// SB-UNI-O 2026-05-15 — Live-item jurisdiction guard.
|
|
1418
|
-
if (!
|
|
1419
|
-
assertLiveJurisdiction(cart.items, productByIdPaid,
|
|
1465
|
+
if (!isDigitalCartPp && resolvedAddressPp) {
|
|
1466
|
+
assertLiveJurisdiction(cart.items, productByIdPaid, resolvedAddressPp.state);
|
|
1420
1467
|
}
|
|
1421
1468
|
// SB-UNI-5 — validate every required member product across the cart with
|
|
1422
1469
|
// cumulative decrement awareness (two bundles sharing a member must NOT
|
|
1423
1470
|
// both succeed unless the product has enough stock for the sum).
|
|
1424
1471
|
//
|
|
1425
|
-
// Out-of-stock policy: unlike the COD/UPI path,
|
|
1472
|
+
// Out-of-stock policy: unlike the COD/UPI path, PhonePe payment has
|
|
1426
1473
|
// ALREADY been captured for the full cart by the time this runs — so
|
|
1427
1474
|
// "skip_items" here means placing orders for the available items only and
|
|
1428
1475
|
// auto-refunding the dropped items' value (below), not skipping payment.
|
|
@@ -1436,8 +1483,8 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
1436
1483
|
.logPayment(uid, reason, exists
|
|
1437
1484
|
? `Product ${u.productId} has ${u.availableQty} left, requested ${u.requestedQty}`
|
|
1438
1485
|
: `Product ${u.productId} not published`, {
|
|
1439
|
-
gatewayOrderId:
|
|
1440
|
-
gatewayPaymentId:
|
|
1486
|
+
gatewayOrderId: phonepeOrder.gatewayOrderId,
|
|
1487
|
+
gatewayPaymentId: merchantOrderId,
|
|
1441
1488
|
addressId,
|
|
1442
1489
|
})
|
|
1443
1490
|
.catch((err) => { void normalizeError(err); serverLogger.warn(AUDIT_LOG_FAIL_MSG, { error: err instanceof Error ? err.message : String(err) }); });
|
|
@@ -1445,7 +1492,7 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
1445
1492
|
}
|
|
1446
1493
|
// outOfStockPolicy: "cancel_order" (also the default for old clients that
|
|
1447
1494
|
// omit the field) — reject the whole checkout batch, exactly like every
|
|
1448
|
-
//
|
|
1495
|
+
// online-payment checkout behaved before this policy existed.
|
|
1449
1496
|
if (outOfStockPolicy === OutOfStockPolicyValues.CANCEL_ORDER && unavailablePaid.length > 0) {
|
|
1450
1497
|
throw new ValidationError(ERROR_MESSAGES.CHECKOUT.INSUFFICIENT_STOCK);
|
|
1451
1498
|
}
|
|
@@ -1469,7 +1516,7 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
1469
1516
|
const commissionRates = siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS;
|
|
1470
1517
|
const { platformFee: expectedPlatformFee, gstOnFee: expectedGstOnFee } = computeCheckoutFees(cartSubtotalRs, commissionRates);
|
|
1471
1518
|
// Same per-seller-group shipping sum /api/payment/create-order charged
|
|
1472
|
-
// upfront (and the same resolveShippingCost
|
|
1519
|
+
// upfront (and the same resolveShippingCost createPhonePeGroupOrder
|
|
1473
1520
|
// below will use to build each order's recorded totalPrice) — keeps this
|
|
1474
1521
|
// floor check from passing an amount that's short by the shipping fee.
|
|
1475
1522
|
const expectedShippingGroups = splitCartIntoOrderGroups(bucketedPaid.available);
|
|
@@ -1488,14 +1535,15 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
1488
1535
|
computeShipmentProtectionFee(groupSubtotal, addons.shipmentProtectionAddon ?? false, commissionRates));
|
|
1489
1536
|
}, 0);
|
|
1490
1537
|
const expectedPaymentAmountRs = cartSubtotalRs + expectedPlatformFee + expectedGstOnFee + expectedAddonFees + expectedShippingFee;
|
|
1491
|
-
|
|
1492
|
-
|
|
1538
|
+
// Reuses the order already fetched above to confirm COMPLETED — PhonePe's
|
|
1539
|
+
// own amount is in its wire-format smallest unit. // audit-money-units-ok: describes the PhonePe boundary conversion this line performs, not our storage convention
|
|
1540
|
+
const paidAmountRs = paiseToRupees(phonepeOrder.amount);
|
|
1493
1541
|
if (paidAmountRs < expectedPaymentAmountRs - 1) {
|
|
1494
1542
|
serverLogger.warn(`Payment amount mismatch for user ${uid}: paid ₹${paidAmountRs}, expected ≥ ₹${expectedPaymentAmountRs}`);
|
|
1495
1543
|
failedCheckoutRepository
|
|
1496
1544
|
.logPayment(uid, "amount_mismatch", `Paid ₹${paidAmountRs}, expected ≥ ₹${expectedPaymentAmountRs}`, {
|
|
1497
|
-
gatewayOrderId:
|
|
1498
|
-
gatewayPaymentId:
|
|
1545
|
+
gatewayOrderId: phonepeOrder.gatewayOrderId,
|
|
1546
|
+
gatewayPaymentId: merchantOrderId,
|
|
1499
1547
|
amountRs: paidAmountRs,
|
|
1500
1548
|
addressId,
|
|
1501
1549
|
})
|
|
@@ -1506,7 +1554,7 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
1506
1554
|
const { coupons: appliedCoupons, dropped: droppedCoupons } = await revalidateAppliedCoupons(cart.appliedCoupons ?? [], bucketedPaid.available, uid);
|
|
1507
1555
|
// "skip_items" (or an all-available cart under any policy) — build order
|
|
1508
1556
|
// groups from the available bucket only. Dropped items are never ordered;
|
|
1509
|
-
// their value is refunded below since
|
|
1557
|
+
// their value is refunded below since PhonePe already captured payment
|
|
1510
1558
|
// for the full cart before this stock check ran.
|
|
1511
1559
|
const orderGroups = splitCartIntoOrderGroups(bucketedPaid.available);
|
|
1512
1560
|
const orderIds = [];
|
|
@@ -1521,7 +1569,7 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
1521
1569
|
g.reduce((sum, { item, product }) => sum + lineTotalFor(item, product), 0),
|
|
1522
1570
|
]), computeCheckoutFees(cartSubtotal, siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS));
|
|
1523
1571
|
for (const { items: group, orderType } of orderGroups) {
|
|
1524
|
-
total += await
|
|
1572
|
+
total += await createPhonePeGroupOrder(group, orderType, {
|
|
1525
1573
|
appliedCoupons,
|
|
1526
1574
|
cartSubtotal,
|
|
1527
1575
|
storeAddons: cart.storeAddons,
|
|
@@ -1530,9 +1578,9 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
1530
1578
|
uid,
|
|
1531
1579
|
userName,
|
|
1532
1580
|
userEmail,
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1581
|
+
merchantOrderId,
|
|
1582
|
+
phonepeOrderId: phonepeOrder.gatewayOrderId,
|
|
1583
|
+
transactionId: phonepeOrder.transactionId,
|
|
1536
1584
|
shippingAddress,
|
|
1537
1585
|
notes,
|
|
1538
1586
|
outOfStockPolicy,
|
|
@@ -1567,24 +1615,24 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
|
|
|
1567
1615
|
});
|
|
1568
1616
|
});
|
|
1569
1617
|
// outOfStockPolicy: "skip_items" — auto-refund the dropped items' value.
|
|
1570
|
-
//
|
|
1618
|
+
// PhonePe already captured payment for the FULL cart before this stock
|
|
1571
1619
|
// check ran, so anything not placed as an order must be given back.
|
|
1572
1620
|
// AWAITED (not fire-and-forget) — this moves money. On failure the orders
|
|
1573
1621
|
// themselves are NOT rolled back (they're already validly created and
|
|
1574
1622
|
// paid); instead the failure is surfaced via order.refundPending + an
|
|
1575
1623
|
// admin notification, never silently dropped (Rule #8).
|
|
1576
|
-
await
|
|
1624
|
+
await refundDroppedItemsForPhonePeCheckout({
|
|
1577
1625
|
unavailablePaid,
|
|
1578
1626
|
orderIds,
|
|
1579
1627
|
productByIdPaid,
|
|
1580
|
-
|
|
1628
|
+
phonepeOrderId: merchantOrderId,
|
|
1581
1629
|
});
|
|
1582
1630
|
if (emailsToSend.length > 0) {
|
|
1583
1631
|
Promise.all(emailsToSend.map((e) => sendOrderConfirmationEmail(e))).catch((err) => serverLogger.error("Order confirmation email error:", err));
|
|
1584
1632
|
}
|
|
1585
|
-
serverLogger.info(`
|
|
1633
|
+
serverLogger.info(`verifyAndPlacePhonePeOrderAction: ${orderIds.length} order(s) placed for uid=${uid} — payment ${merchantOrderId}`);
|
|
1586
1634
|
getAdminRealtimeDb()
|
|
1587
|
-
.ref(`${RTDB_PATHS.PAYMENT_EVENTS}/${
|
|
1635
|
+
.ref(`${RTDB_PATHS.PAYMENT_EVENTS}/${merchantOrderId}`)
|
|
1588
1636
|
.update({ status: "success", orderIds, updatedAt: Date.now() })
|
|
1589
1637
|
.catch((err) => serverLogger.warn("Payment event RTDB signal failed (non-critical)", { err }));
|
|
1590
1638
|
return {
|