@mohasinac/appkit 4.62.1 → 4.63.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/.build-stamp.json +3 -3
- package/dist/_internal/client/features/layout/DashboardLayoutClient.js +8 -29
- package/dist/_internal/client/features/layout/filterNavItems.d.ts +28 -0
- package/dist/_internal/client/features/layout/filterNavItems.js +35 -0
- package/dist/_internal/server/features/ads/booking.d.ts +118 -0
- package/dist/_internal/server/features/ads/booking.js +326 -0
- package/dist/_internal/server/features/checkout/actions.d.ts +51 -11
- package/dist/_internal/server/features/checkout/actions.js +335 -80
- package/dist/_internal/server/features/checkout/expand-order-items.d.ts +1 -2
- package/dist/_internal/server/features/checkout/expand-order-items.js +32 -0
- package/dist/_internal/server/features/checkout/freeze-parts.d.ts +50 -0
- package/dist/_internal/server/features/checkout/freeze-parts.js +94 -0
- package/dist/_internal/server/features/checkout/value-otp-gate.d.ts +55 -0
- package/dist/_internal/server/features/checkout/value-otp-gate.js +60 -0
- package/dist/_internal/server/features/dashboard/attention.d.ts +128 -0
- package/dist/_internal/server/features/dashboard/attention.js +183 -0
- package/dist/_internal/server/features/dashboard/data.d.ts +47 -0
- package/dist/_internal/server/features/dashboard/data.js +82 -0
- package/dist/_internal/server/features/inventory/reservations.d.ts +99 -0
- package/dist/_internal/server/features/inventory/reservations.js +273 -0
- package/dist/_internal/server/features/offers/destinations.d.ts +40 -0
- package/dist/_internal/server/features/offers/destinations.js +65 -0
- package/dist/_internal/server/features/orders/adapters.js +21 -0
- package/dist/_internal/server/features/orders/amend-delivery.d.ts +56 -0
- package/dist/_internal/server/features/orders/amend-delivery.js +124 -0
- package/dist/_internal/server/features/orders/balance.d.ts +89 -0
- package/dist/_internal/server/features/orders/balance.js +256 -0
- package/dist/_internal/server/features/pricing/actions.d.ts +58 -0
- package/dist/_internal/server/features/pricing/actions.js +144 -0
- package/dist/_internal/server/features/shipping/book-split.d.ts +86 -0
- package/dist/_internal/server/features/shipping/book-split.js +130 -0
- package/dist/_internal/server/features/shipping/book.d.ts +61 -0
- package/dist/_internal/server/features/shipping/book.js +176 -0
- package/dist/_internal/server/features/shipping/courier-block.js +26 -1
- package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
- package/dist/_internal/server/features/site-settings/adapters.js +14 -0
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +14 -0
- package/dist/_internal/server/functions/scheduled.d.ts +2 -1
- package/dist/_internal/server/functions/scheduled.js +57 -1
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +2 -0
- package/dist/_internal/server/jobs/core/jobRunners.js +45 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +4 -1
- package/dist/_internal/server/jobs/core/reservationExpiry.d.ts +17 -0
- package/dist/_internal/server/jobs/core/reservationExpiry.js +40 -0
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +15 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
- package/dist/_internal/server/jobs/handlers/index.js +1 -0
- package/dist/_internal/server/jobs/handlers/reservationExpiry.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/reservationExpiry.js +2 -0
- package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
- package/dist/_internal/shared/actions/action-registry.js +268 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +31 -2
- package/dist/_internal/shared/checkout/order-math.js +22 -36
- package/dist/_internal/shared/checkout/rules/index.d.ts +1 -1
- package/dist/_internal/shared/checkout/rules/index.js +1 -1
- package/dist/_internal/shared/checkout/rules/payment-gate.d.ts +18 -0
- package/dist/_internal/shared/checkout/rules/payment-gate.js +25 -0
- package/dist/_internal/shared/checkout/split.d.ts +0 -12
- package/dist/_internal/shared/checkout/split.js +22 -2
- package/dist/_internal/shared/content/disclaimers.js +7 -7
- package/dist/_internal/shared/content/published-values.d.ts +6 -1
- package/dist/_internal/shared/content/published-values.js +8 -0
- package/dist/_internal/shared/features/admin/integrations.d.ts +32 -2
- package/dist/_internal/shared/features/admin/integrations.js +19 -9
- package/dist/_internal/shared/features/dashboard/hub-sections.d.ts +72 -0
- package/dist/_internal/shared/features/dashboard/hub-sections.js +75 -0
- package/dist/_internal/shared/features/emi/choice.d.ts +98 -0
- package/dist/_internal/shared/features/emi/choice.js +69 -0
- package/dist/_internal/shared/features/emi/group.d.ts +84 -0
- package/dist/_internal/shared/features/emi/group.js +126 -0
- package/dist/_internal/shared/features/ledger/posting.d.ts +107 -0
- package/dist/_internal/shared/features/ledger/posting.js +165 -0
- package/dist/_internal/shared/features/ledger/reports.d.ts +76 -0
- package/dist/_internal/shared/features/ledger/reports.js +127 -0
- package/dist/_internal/shared/features/ledger/rules.d.ts +170 -0
- package/dist/_internal/shared/features/ledger/rules.js +221 -0
- package/dist/_internal/shared/features/offers/coordination.d.ts +122 -0
- package/dist/_internal/shared/features/offers/coordination.js +80 -0
- package/dist/_internal/shared/features/orders/delivery-snapshot.d.ts +114 -0
- package/dist/_internal/shared/features/orders/delivery-snapshot.js +116 -0
- package/dist/_internal/shared/features/payouts/pay-period.d.ts +91 -0
- package/dist/_internal/shared/features/payouts/pay-period.js +104 -0
- package/dist/_internal/shared/features/points/accepts.d.ts +1 -8
- package/dist/_internal/shared/features/points/accepts.js +12 -6
- package/dist/_internal/shared/features/pricing/rate-registry.d.ts +150 -0
- package/dist/_internal/shared/features/pricing/rate-registry.js +246 -0
- package/dist/_internal/shared/features/shipping/parcel.d.ts +123 -0
- package/dist/_internal/shared/features/shipping/parcel.js +139 -0
- package/dist/_internal/shared/features/site-settings/fees.d.ts +16 -8
- package/dist/_internal/shared/features/site-settings/fees.js +12 -8
- package/dist/_internal/shared/fees/explain.d.ts +81 -0
- package/dist/_internal/shared/fees/explain.js +101 -0
- package/dist/_internal/shared/history/index.d.ts +1 -1
- package/dist/_internal/shared/history/index.js +1 -1
- package/dist/_internal/shared/history/types.d.ts +7 -1
- package/dist/_internal/shared/history/types.js +25 -0
- package/dist/client.d.ts +11 -1
- package/dist/client.js +13 -0
- package/dist/constants/api-endpoints.d.ts +72 -0
- package/dist/constants/api-endpoints.js +41 -0
- package/dist/constants/field-names.d.ts +41 -0
- package/dist/constants/field-names.js +47 -0
- package/dist/features/account/components/UserOrdersView.js +28 -2
- package/dist/features/admin/components/AdminIntegrationsView.d.ts +55 -0
- package/dist/features/admin/components/AdminIntegrationsView.js +74 -0
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +16 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +72 -3
- package/dist/features/admin/components/AdminOrdersView.js +20 -1
- package/dist/features/admin/schemas/firestore.d.ts +32 -0
- package/dist/features/ads/schemas/booking-write.d.ts +157 -0
- package/dist/features/ads/schemas/booking-write.js +88 -0
- package/dist/features/ads/schemas/creative-form.d.ts +18 -0
- package/dist/features/ads/schemas/creative-form.js +51 -0
- package/dist/features/ads/schemas/firestore.d.ts +140 -0
- package/dist/features/ads/schemas/firestore.js +109 -0
- package/dist/features/auctions/repository/bid.repository.js +3 -1
- package/dist/features/audit-log/schemas/firestore.d.ts +110 -0
- package/dist/features/audit-log/schemas/firestore.js +74 -0
- package/dist/features/auth/actions/profile-actions.d.ts +69 -1
- package/dist/features/auth/actions/profile-actions.js +62 -1
- package/dist/features/auth/schemas/firestore.d.ts +16 -2
- package/dist/features/auth/schemas/firestore.js +2 -1
- package/dist/features/blog/actions/blog-actions.d.ts +6 -0
- package/dist/features/blog/actions/blog-actions.js +17 -2
- package/dist/features/blog/api/route.js +4 -1
- package/dist/features/blog/columns/index.js +5 -0
- package/dist/features/blog/components/BlogIndexPageView.js +2 -1
- package/dist/features/blog/repository/blog.repository.d.ts +4 -0
- package/dist/features/blog/repository/blog.repository.js +5 -0
- package/dist/features/blog/schemas/blog-form.d.ts +3 -0
- package/dist/features/blog/schemas/blog-form.js +1 -0
- package/dist/features/blog/schemas/firestore.d.ts +38 -1
- package/dist/features/blog/schemas/firestore.js +23 -0
- package/dist/features/blog/schemas/index.d.ts +6 -0
- package/dist/features/blog/schemas/index.js +2 -0
- package/dist/features/blog/types/index.d.ts +2 -0
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +12 -1
- package/dist/features/cart/components/CartPriceBreakdown.js +24 -5
- package/dist/features/cart/repository/cart.repository.d.ts +14 -0
- package/dist/features/cart/repository/cart.repository.js +20 -0
- package/dist/features/cart/schemas/firestore.d.ts +24 -0
- package/dist/features/dashboard/components/DashboardHubView.d.ts +83 -0
- package/dist/features/dashboard/components/DashboardHubView.js +56 -0
- package/dist/features/dashboard/components/DashboardSectionsClient.d.ts +23 -0
- package/dist/features/dashboard/components/DashboardSectionsClient.js +66 -0
- package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.d.ts +55 -0
- package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.js +175 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +24 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +28 -0
- package/dist/features/dispute-reasons/schemas/firestore.d.ts +3 -5
- package/dist/features/events/components/EventSaleBanner.js +1 -1
- package/dist/features/faq/components/FAQPageContent.js +2 -2
- package/dist/features/gateway-events/schemas/index.d.ts +2 -2
- package/dist/features/homepage/schemas/index.d.ts +8 -8
- package/dist/features/inventory/schemas/stock-reservation.d.ts +114 -0
- package/dist/features/inventory/schemas/stock-reservation.js +103 -0
- package/dist/features/layout/AppLayoutShell.d.ts +9 -1
- package/dist/features/layout/AppLayoutShell.js +12 -4
- package/dist/features/layout/BottomActions.js +2 -1
- package/dist/features/layout/BottomNavbar.d.ts +10 -2
- package/dist/features/layout/SideActionDock.js +52 -2
- package/dist/features/layout/TitleBarLayout.js +13 -4
- package/dist/features/layout/TitleBarLayout.style.css +59 -0
- package/dist/features/layout/useCtaReveal.d.ts +16 -1
- package/dist/features/layout/useCtaReveal.js +17 -26
- package/dist/features/ledger/repository/journal.repository.d.ts +110 -0
- package/dist/features/ledger/repository/journal.repository.js +312 -0
- package/dist/features/ledger/schemas/expenses.d.ts +87 -0
- package/dist/features/ledger/schemas/expenses.js +27 -0
- package/dist/features/ledger/schemas/firestore.d.ts +183 -0
- package/dist/features/ledger/schemas/firestore.js +69 -0
- package/dist/features/orders/components/BalancePaymentPanel.d.ts +12 -0
- package/dist/features/orders/components/BalancePaymentPanel.js +94 -0
- package/dist/features/orders/components/RaiseDisputeForm.js +19 -11
- package/dist/features/orders/repository/orders.repository.d.ts +28 -1
- package/dist/features/orders/repository/orders.repository.js +60 -6
- package/dist/features/orders/schemas/balance.d.ts +40 -0
- package/dist/features/orders/schemas/balance.js +47 -0
- package/dist/features/orders/schemas/firestore.d.ts +202 -0
- package/dist/features/orders/schemas/firestore.js +43 -0
- package/dist/features/orders/schemas/index.d.ts +2 -2
- package/dist/features/orders/types/index.d.ts +21 -0
- package/dist/features/payments/repository/payout.repository.js +3 -2
- package/dist/features/payments/schemas/firestore.d.ts +92 -1
- package/dist/features/payments/schemas/firestore.js +23 -0
- package/dist/features/pricing/components/AdminPricingView.d.ts +13 -0
- package/dist/features/pricing/components/AdminPricingView.js +157 -0
- package/dist/features/products/schemas/firestore.d.ts +26 -1
- package/dist/features/products/schemas/firestore.js +2 -0
- package/dist/features/products/schemas/index.d.ts +2 -2
- package/dist/features/reviews/repository/reviews.repository.d.ts +14 -0
- package/dist/features/reviews/repository/reviews.repository.js +16 -0
- package/dist/features/scams/repository/scammer.repository.js +2 -1
- package/dist/features/scams/schemas/firestore.d.ts +2 -1
- package/dist/features/scams/schemas/firestore.js +2 -1
- package/dist/features/seller/actions/offer-actions.js +56 -0
- package/dist/features/seller/actions/seller-actions.d.ts +1 -1
- package/dist/features/seller/actions/seller-actions.js +4 -0
- package/dist/features/seller/components/DispatchForm.d.ts +11 -0
- package/dist/features/seller/components/DispatchForm.js +190 -0
- package/dist/features/seller/components/SellerOrdersView.js +94 -2
- package/dist/features/seller/components/index.d.ts +1 -0
- package/dist/features/seller/components/index.js +1 -0
- package/dist/features/seller/repository/offer.repository.js +4 -3
- package/dist/features/seller/schemas/firestore.d.ts +23 -2
- package/dist/features/seller/schemas/firestore.js +3 -2
- package/dist/features/seller/schemas/index.d.ts +4 -4
- package/dist/features/seller/schemas/order-forms.d.ts +61 -4
- package/dist/features/seller/schemas/order-forms.js +41 -8
- package/dist/features/shipments/schemas/firestore.d.ts +25 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +39 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +55 -0
- package/dist/features/shipping-boxes/schemas/firestore.d.ts +2 -4
- package/dist/features/stores/repository/store.repository.d.ts +18 -0
- package/dist/features/stores/repository/store.repository.js +27 -1
- package/dist/features/stores/schemas/firestore.d.ts +41 -0
- package/dist/features/stores/schemas/firestore.js +20 -0
- package/dist/features/stores/schemas/index.d.ts +6 -6
- package/dist/features/support/repository/support.repository.js +72 -6
- package/dist/features/support/schemas/firestore.d.ts +17 -0
- package/dist/features/support/schemas/index.d.ts +2 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +19 -0
- package/dist/next/routing/route-map.d.ts +92 -0
- package/dist/next/routing/route-map.js +53 -0
- package/dist/react/contexts/SessionContext.d.ts +17 -1
- package/dist/schemas/registry.d.ts +22 -16
- package/dist/security/pii-schemas.d.ts +1 -1
- package/dist/security/pii-schemas.js +45 -0
- package/dist/server-entry.d.ts +1 -1
- package/dist/server-entry.js +10 -1
- package/dist/server.d.ts +29 -1
- package/dist/server.js +158 -0
- package/dist/styles.css +349 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Button.d.ts +10 -1
- package/dist/ui/components/Button.js +25 -3
- package/dist/ui/components/Chart.d.ts +63 -0
- package/dist/ui/components/Chart.js +169 -0
- package/dist/ui/components/Chart.style.css +214 -0
- package/dist/ui/components/Disclaimer.js +10 -1
- package/dist/ui/components/OverflowRow.js +54 -2
- package/dist/ui/components/OverflowRow.style.css +38 -0
- package/dist/ui/components/RichTextEditor.js +99 -8
- package/dist/ui/components/chart-spec.d.ts +68 -0
- package/dist/ui/components/chart-spec.js +140 -0
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/components/surface-tokens.d.ts +1 -1
- package/dist/ui/components/surface-tokens.js +9 -0
- package/dist/ui/components/toolbar-collapse.d.ts +13 -0
- package/dist/ui/components/toolbar-collapse.js +10 -1
- package/dist/ui/index.d.ts +6 -0
- package/dist/ui/index.js +3 -0
- package/dist/ui/rich-text/RichText.d.ts +10 -1
- package/dist/ui/rich-text/RichText.js +33 -2
- package/dist/ui/rich-text/RichText.style.css +35 -0
- package/dist/ui/rich-text/RichTextRenderer.d.ts +30 -1
- package/dist/ui/rich-text/RichTextRenderer.js +34 -2
- package/dist/ui/rich-text/chart-blocks.d.ts +62 -0
- package/dist/ui/rich-text/chart-blocks.js +87 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +3 -0
|
@@ -46,7 +46,7 @@ const CARRIER_OPTIONS = [
|
|
|
46
46
|
{ label: "Other", value: "Other" },
|
|
47
47
|
];
|
|
48
48
|
// --- Component ---------------------------------------------------------------
|
|
49
|
-
export function AdminOrderEditorView({ open, onClose, orderId, orderLabel, currentStatus, items, sourceContext, currency, paymentProofUrl, paymentTransactionId, paymentMethod, paymentStatus, displayedUpiId, buyerReportedUpiId, paymentUpiMismatch, buyerMarkedPaid, buyerFraudAgreementAccepted, paymentReviewOutcome, addons, }) {
|
|
49
|
+
export function AdminOrderEditorView({ open, onClose, orderId, orderLabel, currentStatus, items, sourceContext, currency, paymentProofUrl, paymentTransactionId, paymentMethod, paymentStatus, displayedUpiId, buyerReportedUpiId, paymentUpiMismatch, buyerMarkedPaid, buyerFraudAgreementAccepted, paymentReviewOutcome, addons, balance, }) {
|
|
50
50
|
const queryClient = useQueryClient();
|
|
51
51
|
const { showToast } = useToast();
|
|
52
52
|
const [draft, setDraft] = React.useState({
|
|
@@ -83,6 +83,19 @@ export function AdminOrderEditorView({ open, onClose, orderId, orderLabel, curre
|
|
|
83
83
|
const [reviewNoteError, setReviewNoteError] = React.useState(null);
|
|
84
84
|
const [isRequestingReupload, setIsRequestingReupload] = useState(false);
|
|
85
85
|
const [isRejectingFraud, setIsRejectingFraud] = useState(false);
|
|
86
|
+
/*
|
|
87
|
+
* D55 — the balance review's OWN note and its own pending flags.
|
|
88
|
+
*
|
|
89
|
+
* 🛑 Not shared with `reviewNote` above, which belongs to the deposit's
|
|
90
|
+
* three actions. One note box feeding two reviews is the bug where an admin
|
|
91
|
+
* types "blurry screenshot" meaning the balance and sends it as a
|
|
92
|
+
* fraud-rejection reason on the deposit — and the deposit's rejection bans
|
|
93
|
+
* the account for 7 days.
|
|
94
|
+
*/
|
|
95
|
+
const [balanceNote, setBalanceNote] = React.useState("");
|
|
96
|
+
const [balanceNoteError, setBalanceNoteError] = React.useState(null);
|
|
97
|
+
const [isAcceptingBalance, setIsAcceptingBalance] = useState(false);
|
|
98
|
+
const [isRejectingBalance, setIsRejectingBalance] = useState(false);
|
|
86
99
|
// This used to inline `paymentMethod === "cash" || === "upi_manual"`, which
|
|
87
100
|
// omitted `emi` — so the whole proof panel never rendered for an EMI order and
|
|
88
101
|
// an admin had no way to verify one, even though the list row said "Awaiting
|
|
@@ -105,6 +118,10 @@ export function AdminOrderEditorView({ open, onClose, orderId, orderLabel, curre
|
|
|
105
118
|
notes: "",
|
|
106
119
|
});
|
|
107
120
|
setReviewNote("");
|
|
121
|
+
// Reset with its sibling — a note typed for one order must not reappear
|
|
122
|
+
// under the next one the drawer opens.
|
|
123
|
+
setBalanceNote("");
|
|
124
|
+
setBalanceNoteError(null);
|
|
108
125
|
}
|
|
109
126
|
}, [open, currentStatus]);
|
|
110
127
|
const saveMutation = useApiMutation({
|
|
@@ -216,7 +233,49 @@ export function AdminOrderEditorView({ open, onClose, orderId, orderLabel, curre
|
|
|
216
233
|
setIsRejectingFraud(false);
|
|
217
234
|
}
|
|
218
235
|
};
|
|
219
|
-
|
|
236
|
+
/**
|
|
237
|
+
* D55 — accept or reject the pre-order BALANCE proof.
|
|
238
|
+
*
|
|
239
|
+
* 🛑 One handler, because the two verdicts differ by a boolean and a note
|
|
240
|
+
* and nothing else. The deposit has three handlers because its three
|
|
241
|
+
* outcomes are genuinely different operations (verify / re-ask / cancel +
|
|
242
|
+
* ban); mirroring that shape here would be three copies of one request.
|
|
243
|
+
*
|
|
244
|
+
* The note requirement is checked in BOTH places on purpose: here so the
|
|
245
|
+
* admin is told before the round trip, and in `reviewBalanceProof` because
|
|
246
|
+
* the route is reachable without this component.
|
|
247
|
+
*/
|
|
248
|
+
const handleBalanceReview = async (approve) => {
|
|
249
|
+
if (!orderId)
|
|
250
|
+
return;
|
|
251
|
+
if (!approve && !balanceNote.trim()) {
|
|
252
|
+
setBalanceNoteError("Say what was wrong — without it the buyer will send the same proof again.");
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
setBalanceNoteError(null);
|
|
256
|
+
const setPending = approve ? setIsAcceptingBalance : setIsRejectingBalance;
|
|
257
|
+
setPending(true);
|
|
258
|
+
try {
|
|
259
|
+
await apiClient.post(ADMIN_ENDPOINTS.ORDER_BALANCE_REVIEW(orderId), {
|
|
260
|
+
approve,
|
|
261
|
+
...(balanceNote.trim() ? { note: balanceNote.trim() } : {}),
|
|
262
|
+
});
|
|
263
|
+
showToast(approve
|
|
264
|
+
? "Balance accepted. The order is clear to ship."
|
|
265
|
+
: "Proof returned to the buyer with your note.", "success");
|
|
266
|
+
queryClient.invalidateQueries({ queryKey: ["admin", "orders"] });
|
|
267
|
+
onClose();
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
void normalizeError(err);
|
|
271
|
+
// Authored copy — `err.message` is server text (Root Cause #86).
|
|
272
|
+
showToast("Could not record that decision.", "error");
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
setPending(false);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
return (_jsx(SideDrawer, { isOpen: open, onClose: onClose, title: orderLabel ? `Order: ${orderLabel}` : "Update Order", children: _jsx(FormShellContext.Provider, { value: form.shellCtx, children: _jsxs(Stack, { gap: "md", padding: "md", children: [_jsx(FormErrorSummary, {}), addons && _jsx(OrderAddonBadges, { order: addons, variant: "detail" }), _jsx(OrderProvenanceCard, { sourceContext: sourceContext, currency: currency }), items && items.length > 0 && (_jsxs(Stack, { gap: "xs", children: [_jsxs(Label, { size: "sm", weight: "medium", color: "primary", children: ["Items (", items.length, ")"] }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)] border border-[var(--appkit-color-border)]", rounded: "lg", children: groupOrderItemsByLine(items).map((group, gi) => group.kind === "single" ? (adminOrderItemRow(group.item, `s-${group.index}`)) : (_jsxs(Div, { children: [_jsxs(Div, { layout: "flex", align: "center", gap: "2", padding: "x-sm", surface: "muted", children: [_jsx(Text, { size: "xs", weight: "semibold", children: group.groupTitle ?? "Grouped purchase" }), _jsx(Text, { size: "xs", color: "muted", children: `${group.memberCount} ${group.memberCount === 1 ? "item" : "items"} · one line` })] }), _jsx(Div, { paddingX: "l-md", children: group.items.map(({ item, index }) => adminOrderItemRow(item, `m-${index}`)) })] }, `g-${gi}`))) })] })), _jsx(SectionForm, { sections: sections, values: draft, onChange: (partial) => setDraft((d) => ({ ...d, ...partial })), onSubmit: () => {
|
|
220
279
|
// SectionForm surfaces errors and then submits regardless, so the
|
|
221
280
|
// guard is here.
|
|
222
281
|
form.clearErrors();
|
|
@@ -226,5 +285,15 @@ export function AdminOrderEditorView({ open, onClose, orderId, orderLabel, curre
|
|
|
226
285
|
return;
|
|
227
286
|
}
|
|
228
287
|
saveMutation.mutate();
|
|
229
|
-
}, onValidationChange: () => form.validate(draft), schema: adminOrderUpdateSchema, openIds: nav.openIds, onOpenChange: nav.setOpenIds, submitLabel: "Save changes", cancelLabel: "Cancel", onCancel: onClose, isLoading: saveMutation.isPending }), isManualPayment && (_jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Payment Proof" }), isRejected && (_jsx(Text, { size: "xs", className: "text-error", weight: "medium", children: "Payment rejected as fraud \u2014 no further action available here." })), isReuploadRequested && !isVerified && (_jsx(Text, { size: "xs", className: "text-warning", weight: "medium", children: "Re-upload requested \u2014 waiting for the buyer to submit a new proof." })), paymentProofUrl ? (_jsxs(Stack, { gap: "xs", children: [_jsx(Div, { border: "default", rounded: "lg", overflow: "hidden", children: _jsx(MediaImage, { src: paymentProofUrl, alt: "Payment screenshot", size: "card" }) }), paymentTransactionId && (_jsxs(Text, { size: "xs", color: "muted", children: ["UTR: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: paymentTransactionId })] })), needsVerification && (_jsxs(Stack, { gap: "xs", children: [(displayedUpiId || buyerReportedUpiId) && (_jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { size: "xs", color: "muted", children: ["Expected UPI: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: displayedUpiId || "—" }), " · ", "Buyer reported: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: buyerReportedUpiId || "—" })] }), paymentUpiMismatch && (_jsx(Div, { rounded: "lg", padding: "inlineSm", className: "border border-error/20", surface: "danger-surface", children: _jsx(Text, { size: "xs", className: "text-error", weight: "semibold", children: "UPI mismatch \u2014 buyer-reported ID doesn't match the one shown for this order." }) }))] })), _jsxs(Text, { size: "xs", color: "muted", children: ["Buyer marked as paid: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: buyerMarkedPaid ? "Yes" : "No" }), " · ", "Fraud agreement accepted: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: buyerFraudAgreementAccepted ? "Yes" : "No" })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Review note" }), _jsxs(Text, { size: "xs", color: "muted", children: ["Sent with ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: "Request re-upload" }), " or", " ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: "Reject as fraud" }), " below. Not saved by \u201CSave changes\u201D."] }), _jsx(Textarea, { value: reviewNote, onChange: (e) => handleReviewNoteChange(e.target.value), rows: 2, "aria-invalid": reviewNoteError ? true : undefined, placeholder: "e.g. Screenshot is blurry \u2014 amount not readable / UPI ID doesn't match order" }), reviewNoteError && (_jsx(Text, { size: "xs", color: "error", role: "alert", children: reviewNoteError }))] }), _jsx(Button, { type: "button", action: ACTIONS.ADMIN["verify-payment"], onClick: handleVerifyPayment, isLoading: isVerifyingPayment, disabled: isVerifyingPayment, variant: "primary", className: "w-full" }), _jsx(Button, { type: "button", action: ACTIONS.ADMIN["request-payment-reupload"], onClick: handleRequestReupload, isLoading: isRequestingReupload, disabled: isRequestingReupload, variant: "secondary", className: "w-full" }), _jsx(Button, { type: "button", action: ACTIONS.ADMIN["reject-payment-fraud"], onClick: handleRejectFraud, isLoading: isRejectingFraud, disabled: isRejectingFraud, variant: "danger", className: "w-full" })] })), isVerified && (_jsx(Div, { rounded: "lg", padding: "inlineSm", className: "border border-success/20", surface: "success-surface", children: _jsx(Text, { size: "xs", className: "text-success", weight: "medium", children: "Payment verified" }) }))] })) : (_jsx(Text, { size: "xs", color: "faint", children: "No proof uploaded yet." }))] }))
|
|
288
|
+
}, onValidationChange: () => form.validate(draft), schema: adminOrderUpdateSchema, openIds: nav.openIds, onOpenChange: nav.setOpenIds, submitLabel: "Save changes", cancelLabel: "Cancel", onCancel: onClose, isLoading: saveMutation.isPending }), isManualPayment && (_jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Payment Proof" }), isRejected && (_jsx(Text, { size: "xs", className: "text-error", weight: "medium", children: "Payment rejected as fraud \u2014 no further action available here." })), isReuploadRequested && !isVerified && (_jsx(Text, { size: "xs", className: "text-warning", weight: "medium", children: "Re-upload requested \u2014 waiting for the buyer to submit a new proof." })), paymentProofUrl ? (_jsxs(Stack, { gap: "xs", children: [_jsx(Div, { border: "default", rounded: "lg", overflow: "hidden", children: _jsx(MediaImage, { src: paymentProofUrl, alt: "Payment screenshot", size: "card" }) }), paymentTransactionId && (_jsxs(Text, { size: "xs", color: "muted", children: ["UTR: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: paymentTransactionId })] })), needsVerification && (_jsxs(Stack, { gap: "xs", children: [(displayedUpiId || buyerReportedUpiId) && (_jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { size: "xs", color: "muted", children: ["Expected UPI: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: displayedUpiId || "—" }), " · ", "Buyer reported: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: buyerReportedUpiId || "—" })] }), paymentUpiMismatch && (_jsx(Div, { rounded: "lg", padding: "inlineSm", className: "border border-error/20", surface: "danger-surface", children: _jsx(Text, { size: "xs", className: "text-error", weight: "semibold", children: "UPI mismatch \u2014 buyer-reported ID doesn't match the one shown for this order." }) }))] })), _jsxs(Text, { size: "xs", color: "muted", children: ["Buyer marked as paid: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: buyerMarkedPaid ? "Yes" : "No" }), " · ", "Fraud agreement accepted: ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: buyerFraudAgreementAccepted ? "Yes" : "No" })] }), _jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Review note" }), _jsxs(Text, { size: "xs", color: "muted", children: ["Sent with ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: "Request re-upload" }), " or", " ", _jsx(Text, { as: "span", size: "xs", weight: "medium", children: "Reject as fraud" }), " below. Not saved by \u201CSave changes\u201D."] }), _jsx(Textarea, { value: reviewNote, onChange: (e) => handleReviewNoteChange(e.target.value), rows: 2, "aria-invalid": reviewNoteError ? true : undefined, placeholder: "e.g. Screenshot is blurry \u2014 amount not readable / UPI ID doesn't match order" }), reviewNoteError && (_jsx(Text, { size: "xs", color: "error", role: "alert", children: reviewNoteError }))] }), _jsx(Button, { type: "button", action: ACTIONS.ADMIN["verify-payment"], onClick: handleVerifyPayment, isLoading: isVerifyingPayment, disabled: isVerifyingPayment, variant: "primary", className: "w-full" }), _jsx(Button, { type: "button", action: ACTIONS.ADMIN["request-payment-reupload"], onClick: handleRequestReupload, isLoading: isRequestingReupload, disabled: isRequestingReupload, variant: "secondary", className: "w-full" }), _jsx(Button, { type: "button", action: ACTIONS.ADMIN["reject-payment-fraud"], onClick: handleRejectFraud, isLoading: isRejectingFraud, disabled: isRejectingFraud, variant: "danger", className: "w-full" })] })), isVerified && (_jsx(Div, { rounded: "lg", padding: "inlineSm", className: "border border-success/20", surface: "success-surface", children: _jsx(Text, { size: "xs", className: "text-success", weight: "medium", children: "Payment verified" }) }))] })) : (_jsx(Text, { size: "xs", color: "faint", children: "No proof uploaded yet." }))] })), balance && (_jsxs(Stack, { gap: "xs", children: [_jsx(Label, { size: "sm", weight: "medium", color: "primary", children: "Pre-order balance" }), _jsxs(Text, { size: "xs", color: "muted", children: [toCurrency(balance.amount), " \u00B7", " ", balance.status === "paid"
|
|
289
|
+
? "accepted"
|
|
290
|
+
: balance.status === "submitted"
|
|
291
|
+
? "proof submitted, awaiting your decision"
|
|
292
|
+
: balance.status === "rejected"
|
|
293
|
+
? "returned to the buyer"
|
|
294
|
+
: "open — the buyer has not paid yet", balance.transactionId ? ` · Ref ${balance.transactionId}` : ""] }), balance.status === "rejected" && balance.reviewNote && (_jsxs(Text, { size: "xs", color: "muted", children: ["Last note: ", balance.reviewNote] })), balance.status === "submitted" && (_jsxs(Stack, { gap: "xs", children: [balance.proofUrl && (_jsx(Div, { border: "default", rounded: "lg", overflow: "hidden", children: _jsx(MediaImage, { src: balance.proofUrl, alt: "Balance payment screenshot", size: "card" }) })), _jsx(Text, { size: "xs", color: "muted", children: "Check the reference against the bank before accepting. Accepting does NOT post a ledger entry \u2014 the sale was recognised in full when the order was created, so a second entry would double it." }), _jsx(Textarea, { value: balanceNote, onChange: (e) => {
|
|
295
|
+
setBalanceNote(e.target.value);
|
|
296
|
+
if (balanceNoteError)
|
|
297
|
+
setBalanceNoteError(null);
|
|
298
|
+
}, rows: 2, "aria-invalid": balanceNoteError ? true : undefined, placeholder: "Required when rejecting \u2014 e.g. reference does not match any credit we received" }), balanceNoteError && (_jsx(Text, { size: "xs", color: "error", role: "alert", children: balanceNoteError })), _jsx(Button, { type: "button", action: ACTIONS.ADMIN["verify-balance"], onClick: () => void handleBalanceReview(true), isLoading: isAcceptingBalance, disabled: isAcceptingBalance || isRejectingBalance, variant: "primary", className: "w-full" }), _jsx(Button, { type: "button", action: ACTIONS.ADMIN["reject-balance"], onClick: () => void handleBalanceReview(false), isLoading: isRejectingBalance, disabled: isAcceptingBalance || isRejectingBalance, variant: "secondary", className: "w-full" })] }))] }))] }) }) }));
|
|
230
299
|
}
|
|
@@ -267,6 +267,25 @@ export function AdminOrdersView({ children, ...props }) {
|
|
|
267
267
|
// which coerces every field by hand, this one needs a tag check
|
|
268
268
|
// before it can be trusted. See `toSourceContext`.
|
|
269
269
|
sourceContext: toSourceContext(item.sourceContext),
|
|
270
|
+
/*
|
|
271
|
+
* D55. Coerced field by field like `addons` above, deliberately
|
|
272
|
+
* NOT `as AdminOrderEditorViewProps["balance"]` — a cast at a
|
|
273
|
+
* raw-document boundary is where Root Cause #98 lives, and main
|
|
274
|
+
* had just removed the same cast from `sourceContext` one line up.
|
|
275
|
+
*
|
|
276
|
+
* `proofUrl` is omitted ON PURPOSE: this is the LIST payload, and
|
|
277
|
+
* the screenshot is admin-detail only. A list row never needs it.
|
|
278
|
+
*/
|
|
279
|
+
balance: item.balance
|
|
280
|
+
? {
|
|
281
|
+
amount: Number(item.balance.amount ?? 0),
|
|
282
|
+
status: toStringValue(item.balance.status, "awaiting"),
|
|
283
|
+
transactionId: toStringValue(item.balance.transactionId) ||
|
|
284
|
+
undefined,
|
|
285
|
+
reviewNote: toStringValue(item.balance.reviewNote) ||
|
|
286
|
+
undefined,
|
|
287
|
+
}
|
|
288
|
+
: undefined,
|
|
270
289
|
currency: toStringValue(item.currency, "") || undefined,
|
|
271
290
|
};
|
|
272
291
|
}),
|
|
@@ -351,5 +370,5 @@ export function AdminOrdersView({ children, ...props }) {
|
|
|
351
370
|
] })),
|
|
352
371
|
renderFilterPanel: ({ pendingFilters, setPendingFilters }) => (_jsxs(_Fragment, { children: [_jsx(FilterChipGroup, { label: "Status", tabs: ADMIN_ORDER_STATUS_TABS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id, paymentReview: "" })) }), _jsx(FilterChipGroup, { label: "Manual payment", tabs: ADMIN_ORDER_PAYMENT_REVIEW_TABS, value: pendingFilters.paymentReview ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, paymentReview: id, status: "" })) })] })),
|
|
353
372
|
};
|
|
354
|
-
return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(AdminOrderEditorView, { open: drawerOpen, onClose: () => setDrawerOpen(false), orderId: selectedRow?.id, orderLabel: selectedRow?.primary, currentStatus: selectedRow?.status, items: selectedRow?.items, paymentProofUrl: selectedRow?.paymentProofUrl, paymentTransactionId: selectedRow?.paymentTransactionId, paymentMethod: selectedRow?.paymentMethod, paymentStatus: selectedRow?.paymentStatus, displayedUpiId: selectedRow?.displayedUpiId, buyerReportedUpiId: selectedRow?.buyerReportedUpiId, paymentUpiMismatch: selectedRow?.paymentUpiMismatch, buyerMarkedPaid: selectedRow?.buyerMarkedPaid, buyerFraudAgreementAccepted: selectedRow?.buyerFraudAgreementAccepted, paymentReviewOutcome: selectedRow?.paymentReviewOutcome, addons: selectedRow?.addons, sourceContext: selectedRow?.sourceContext, currency: selectedRow?.currency })] }));
|
|
373
|
+
return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), _jsx(AdminOrderEditorView, { open: drawerOpen, onClose: () => setDrawerOpen(false), orderId: selectedRow?.id, orderLabel: selectedRow?.primary, currentStatus: selectedRow?.status, items: selectedRow?.items, paymentProofUrl: selectedRow?.paymentProofUrl, paymentTransactionId: selectedRow?.paymentTransactionId, paymentMethod: selectedRow?.paymentMethod, paymentStatus: selectedRow?.paymentStatus, displayedUpiId: selectedRow?.displayedUpiId, buyerReportedUpiId: selectedRow?.buyerReportedUpiId, paymentUpiMismatch: selectedRow?.paymentUpiMismatch, buyerMarkedPaid: selectedRow?.buyerMarkedPaid, buyerFraudAgreementAccepted: selectedRow?.buyerFraudAgreementAccepted, paymentReviewOutcome: selectedRow?.paymentReviewOutcome, addons: selectedRow?.addons, sourceContext: selectedRow?.sourceContext, balance: selectedRow?.balance, currency: selectedRow?.currency })] }));
|
|
355
374
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Covers: notifications, chat rooms, site settings
|
|
4
4
|
*/
|
|
5
5
|
import type { AboutContentDocument } from "../../about/schemas/firestore";
|
|
6
|
+
import type { RateChange } from "../../../_internal/shared/features/pricing/rate-registry";
|
|
6
7
|
export declare const ADMIN_CHECKOUT_BYPASS_FLAG_KEY: "adminCheckoutBypass";
|
|
7
8
|
export declare const NOTIFICATION_TYPE_VALUES: readonly ["order_placed", "order_confirmed", "order_shipped", "order_delivered", "order_cancelled", "bid_placed", "bid_outbid", "bid_won", "bid_lost", "auction_ended", "review_approved", "review_replied", "product_available", "promotion", "system", "welcome", "account_action", "support_ticket_update", "scam_report_update", "offer_received", "offer_responded", "offer_expired", "offer_counter_accepted", "refund_initiated", "prize_won", "prize_reveal_expired", "emi_installment_due_soon", "emi_installment_overdue", "payment_review", "catalogue_images_stale", "message_received"];
|
|
8
9
|
export type NotificationType = (typeof NOTIFICATION_TYPE_VALUES)[number];
|
|
@@ -233,6 +234,18 @@ export type SiteSettingsAdPlacement = {
|
|
|
233
234
|
label: string;
|
|
234
235
|
enabled: boolean;
|
|
235
236
|
reservedHeight: number;
|
|
237
|
+
/**
|
|
238
|
+
* D56 — the flat monthly rate for a self-serve booking of this slot, in
|
|
239
|
+
* rupees. Absent means NOT SOLD: `bookAdSlot` refuses rather than
|
|
240
|
+
* defaulting to zero, because a free booking that looks successful takes
|
|
241
|
+
* the slot, bills nothing, and says nothing about the missing price.
|
|
242
|
+
*
|
|
243
|
+
* 🛑 Editing this does NOT reprice a sold slot. `AdBookingDocument.priceRs`
|
|
244
|
+
* is snapshotted at booking, for the same reason `sourceContext` is
|
|
245
|
+
* written once: the rate is a price somebody agreed to, and re-deriving it
|
|
246
|
+
* later silently rewrites a completed sale.
|
|
247
|
+
*/
|
|
248
|
+
priceRsPerPeriod?: number;
|
|
236
249
|
};
|
|
237
250
|
/** Embedded in siteSettings.adSettings — a type alias, not a collection root. */
|
|
238
251
|
export type SiteSettingsAdInventoryItem = {
|
|
@@ -838,6 +851,25 @@ export interface SiteSettingsDocument extends BaseDocument {
|
|
|
838
851
|
* opt-in and require the corresponding credentials to be set.
|
|
839
852
|
*/
|
|
840
853
|
notificationChannels?: NotificationChannelConfig;
|
|
854
|
+
/**
|
|
855
|
+
* The rate-change log behind `/admin/pricing`. Append-only, newest FIRST,
|
|
856
|
+
* capped at `RATE_CHANGE_LOG_MAX`.
|
|
857
|
+
*
|
|
858
|
+
* 🛑 **An array on the singleton rather than a collection, deliberately.**
|
|
859
|
+
* The page's only query is "the recent history of these 16 rates", which is
|
|
860
|
+
* one document read — and the rates themselves already live here, so a
|
|
861
|
+
* separate collection would mean two reads to render one table and two
|
|
862
|
+
* writes to record one change. It is capped for the same reason
|
|
863
|
+
* `statusHistory` is: an uncapped array on a hot singleton eventually makes
|
|
864
|
+
* every settings read expensive.
|
|
865
|
+
*
|
|
866
|
+
* 🛑 **Not an audit trail, and not a substitute for one.** A `pricing_update`
|
|
867
|
+
* row also goes to `adminAuditLog`, which is queryable and answers "what
|
|
868
|
+
* have admins done lately". This answers the narrower question the pricing
|
|
869
|
+
* page asks — "when did THIS rate last move, and to what" — and it is the
|
|
870
|
+
* one a truncation can safely lose.
|
|
871
|
+
*/
|
|
872
|
+
rateChanges?: RateChange[];
|
|
841
873
|
}
|
|
842
874
|
export declare const SITE_SETTINGS_COLLECTION: "siteSettings";
|
|
843
875
|
export declare const SITE_SETTINGS_INDEXED_FIELDS: readonly [];
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The write contract for a self-serve ad booking.
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 The shape cannot express moderation, payment or price
|
|
5
|
+
*
|
|
6
|
+
* This is Root Cause #76's rule applied to a surface where the submitter is
|
|
7
|
+
* a member of the public rather than an admin, which makes it sharper: an
|
|
8
|
+
* advertiser who can send `moderation: { state: "approved" }` has not
|
|
9
|
+
* bypassed a check, they have *become* the check. A schema that merely
|
|
10
|
+
* VALIDATED the field would still let `"approved"` through — the field has
|
|
11
|
+
* to be unrepresentable, so `.strict()` turns it into a 400 instead of
|
|
12
|
+
* something the server has to remember to overwrite.
|
|
13
|
+
*
|
|
14
|
+
* Same for `priceRs` (the server reads the rate card; a client-supplied
|
|
15
|
+
* price is a ₹1 banner) and `paymentState` (a booking that declares itself
|
|
16
|
+
* paid is a free slot).
|
|
17
|
+
*
|
|
18
|
+
* `placementId` and `period` ARE accepted: they are what is being bought,
|
|
19
|
+
* and they are also the booking's identity, so the conflict they can cause
|
|
20
|
+
* is a document collision rather than a validation question.
|
|
21
|
+
*/
|
|
22
|
+
import { z } from "zod";
|
|
23
|
+
/**
|
|
24
|
+
* 🛑 Creative fields are bounded, and `ctaHref` is http(s)-only.
|
|
25
|
+
*
|
|
26
|
+
* An unbounded `body` is a storage cost per booking and a layout break on
|
|
27
|
+
* every page carrying the slot. A `javascript:` or `data:` CTA is script
|
|
28
|
+
* execution on every page the creative renders on, submitted by a member of
|
|
29
|
+
* the public — this is the one field on this schema where a miss is a
|
|
30
|
+
* security defect rather than a tidiness one.
|
|
31
|
+
*/
|
|
32
|
+
export declare const httpUrl: z.ZodEffects<z.ZodString, string, string>;
|
|
33
|
+
export declare const adBookingCreativeSchema: z.ZodObject<{
|
|
34
|
+
title: z.ZodString;
|
|
35
|
+
body: z.ZodOptional<z.ZodString>;
|
|
36
|
+
imageUrl: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
37
|
+
ctaLabel: z.ZodOptional<z.ZodString>;
|
|
38
|
+
ctaHref: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
39
|
+
}, "strict", z.ZodTypeAny, {
|
|
40
|
+
title: string;
|
|
41
|
+
body?: string | undefined;
|
|
42
|
+
imageUrl?: string | undefined;
|
|
43
|
+
ctaLabel?: string | undefined;
|
|
44
|
+
ctaHref?: string | undefined;
|
|
45
|
+
}, {
|
|
46
|
+
title: string;
|
|
47
|
+
body?: string | undefined;
|
|
48
|
+
imageUrl?: string | undefined;
|
|
49
|
+
ctaLabel?: string | undefined;
|
|
50
|
+
ctaHref?: string | undefined;
|
|
51
|
+
}>;
|
|
52
|
+
export declare const adBookingWriteSchema: z.ZodObject<{
|
|
53
|
+
placementId: z.ZodString;
|
|
54
|
+
period: z.ZodEffects<z.ZodString, string, string>;
|
|
55
|
+
creative: z.ZodObject<{
|
|
56
|
+
title: z.ZodString;
|
|
57
|
+
body: z.ZodOptional<z.ZodString>;
|
|
58
|
+
imageUrl: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
59
|
+
ctaLabel: z.ZodOptional<z.ZodString>;
|
|
60
|
+
ctaHref: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
61
|
+
}, "strict", z.ZodTypeAny, {
|
|
62
|
+
title: string;
|
|
63
|
+
body?: string | undefined;
|
|
64
|
+
imageUrl?: string | undefined;
|
|
65
|
+
ctaLabel?: string | undefined;
|
|
66
|
+
ctaHref?: string | undefined;
|
|
67
|
+
}, {
|
|
68
|
+
title: string;
|
|
69
|
+
body?: string | undefined;
|
|
70
|
+
imageUrl?: string | undefined;
|
|
71
|
+
ctaLabel?: string | undefined;
|
|
72
|
+
ctaHref?: string | undefined;
|
|
73
|
+
}>;
|
|
74
|
+
}, "strict", z.ZodTypeAny, {
|
|
75
|
+
placementId: string;
|
|
76
|
+
period: string;
|
|
77
|
+
creative: {
|
|
78
|
+
title: string;
|
|
79
|
+
body?: string | undefined;
|
|
80
|
+
imageUrl?: string | undefined;
|
|
81
|
+
ctaLabel?: string | undefined;
|
|
82
|
+
ctaHref?: string | undefined;
|
|
83
|
+
};
|
|
84
|
+
}, {
|
|
85
|
+
placementId: string;
|
|
86
|
+
period: string;
|
|
87
|
+
creative: {
|
|
88
|
+
title: string;
|
|
89
|
+
body?: string | undefined;
|
|
90
|
+
imageUrl?: string | undefined;
|
|
91
|
+
ctaLabel?: string | undefined;
|
|
92
|
+
ctaHref?: string | undefined;
|
|
93
|
+
};
|
|
94
|
+
}>;
|
|
95
|
+
export type AdBookingWriteInput = z.infer<typeof adBookingWriteSchema>;
|
|
96
|
+
/**
|
|
97
|
+
* A moderator's verdict.
|
|
98
|
+
*
|
|
99
|
+
* 🛑 A rejection REQUIRES a note, for the same reason an admin offer-cancel
|
|
100
|
+
* does: without one the advertiser's next submission is the same creative
|
|
101
|
+
* again, and the moderator reviews it twice. An approval takes no note
|
|
102
|
+
* because there is nothing to say.
|
|
103
|
+
*/
|
|
104
|
+
export declare const adModerationSchema: z.ZodEffects<z.ZodObject<{
|
|
105
|
+
approve: z.ZodBoolean;
|
|
106
|
+
note: z.ZodOptional<z.ZodString>;
|
|
107
|
+
}, "strict", z.ZodTypeAny, {
|
|
108
|
+
approve: boolean;
|
|
109
|
+
note?: string | undefined;
|
|
110
|
+
}, {
|
|
111
|
+
approve: boolean;
|
|
112
|
+
note?: string | undefined;
|
|
113
|
+
}>, {
|
|
114
|
+
approve: boolean;
|
|
115
|
+
note?: string | undefined;
|
|
116
|
+
}, {
|
|
117
|
+
approve: boolean;
|
|
118
|
+
note?: string | undefined;
|
|
119
|
+
}>;
|
|
120
|
+
export type AdModerationInput = z.infer<typeof adModerationSchema>;
|
|
121
|
+
export declare const adCreativeResubmitSchema: z.ZodObject<{
|
|
122
|
+
creative: z.ZodObject<{
|
|
123
|
+
title: z.ZodString;
|
|
124
|
+
body: z.ZodOptional<z.ZodString>;
|
|
125
|
+
imageUrl: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
126
|
+
ctaLabel: z.ZodOptional<z.ZodString>;
|
|
127
|
+
ctaHref: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
128
|
+
}, "strict", z.ZodTypeAny, {
|
|
129
|
+
title: string;
|
|
130
|
+
body?: string | undefined;
|
|
131
|
+
imageUrl?: string | undefined;
|
|
132
|
+
ctaLabel?: string | undefined;
|
|
133
|
+
ctaHref?: string | undefined;
|
|
134
|
+
}, {
|
|
135
|
+
title: string;
|
|
136
|
+
body?: string | undefined;
|
|
137
|
+
imageUrl?: string | undefined;
|
|
138
|
+
ctaLabel?: string | undefined;
|
|
139
|
+
ctaHref?: string | undefined;
|
|
140
|
+
}>;
|
|
141
|
+
}, "strict", z.ZodTypeAny, {
|
|
142
|
+
creative: {
|
|
143
|
+
title: string;
|
|
144
|
+
body?: string | undefined;
|
|
145
|
+
imageUrl?: string | undefined;
|
|
146
|
+
ctaLabel?: string | undefined;
|
|
147
|
+
ctaHref?: string | undefined;
|
|
148
|
+
};
|
|
149
|
+
}, {
|
|
150
|
+
creative: {
|
|
151
|
+
title: string;
|
|
152
|
+
body?: string | undefined;
|
|
153
|
+
imageUrl?: string | undefined;
|
|
154
|
+
ctaLabel?: string | undefined;
|
|
155
|
+
ctaHref?: string | undefined;
|
|
156
|
+
};
|
|
157
|
+
}>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The write contract for a self-serve ad booking.
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 The shape cannot express moderation, payment or price
|
|
5
|
+
*
|
|
6
|
+
* This is Root Cause #76's rule applied to a surface where the submitter is
|
|
7
|
+
* a member of the public rather than an admin, which makes it sharper: an
|
|
8
|
+
* advertiser who can send `moderation: { state: "approved" }` has not
|
|
9
|
+
* bypassed a check, they have *become* the check. A schema that merely
|
|
10
|
+
* VALIDATED the field would still let `"approved"` through — the field has
|
|
11
|
+
* to be unrepresentable, so `.strict()` turns it into a 400 instead of
|
|
12
|
+
* something the server has to remember to overwrite.
|
|
13
|
+
*
|
|
14
|
+
* Same for `priceRs` (the server reads the rate card; a client-supplied
|
|
15
|
+
* price is a ₹1 banner) and `paymentState` (a booking that declares itself
|
|
16
|
+
* paid is a free slot).
|
|
17
|
+
*
|
|
18
|
+
* `placementId` and `period` ARE accepted: they are what is being bought,
|
|
19
|
+
* and they are also the booking's identity, so the conflict they can cause
|
|
20
|
+
* is a document collision rather than a validation question.
|
|
21
|
+
*/
|
|
22
|
+
import { z } from "zod";
|
|
23
|
+
import { isAdPeriod } from "./firestore.js";
|
|
24
|
+
/**
|
|
25
|
+
* 🛑 Creative fields are bounded, and `ctaHref` is http(s)-only.
|
|
26
|
+
*
|
|
27
|
+
* An unbounded `body` is a storage cost per booking and a layout break on
|
|
28
|
+
* every page carrying the slot. A `javascript:` or `data:` CTA is script
|
|
29
|
+
* execution on every page the creative renders on, submitted by a member of
|
|
30
|
+
* the public — this is the one field on this schema where a miss is a
|
|
31
|
+
* security defect rather than a tidiness one.
|
|
32
|
+
*/
|
|
33
|
+
/*
|
|
34
|
+
* Exported so `creative-form.ts` reuses the REFINEMENT rather than restating
|
|
35
|
+
* it — the form and the wire must agree on what a link is, and a second copy
|
|
36
|
+
* of a security check is a second thing to forget.
|
|
37
|
+
*/
|
|
38
|
+
export const httpUrl = z
|
|
39
|
+
.string()
|
|
40
|
+
.url()
|
|
41
|
+
.refine((u) => /^https?:\/\//i.test(u), "Links must start with http:// or https://");
|
|
42
|
+
export const adBookingCreativeSchema = z
|
|
43
|
+
.object({
|
|
44
|
+
title: z.string().min(2).max(80),
|
|
45
|
+
body: z.string().max(200).optional(),
|
|
46
|
+
imageUrl: httpUrl.optional(),
|
|
47
|
+
ctaLabel: z.string().max(40).optional(),
|
|
48
|
+
ctaHref: httpUrl.optional(),
|
|
49
|
+
})
|
|
50
|
+
.strict();
|
|
51
|
+
export const adBookingWriteSchema = z
|
|
52
|
+
.object({
|
|
53
|
+
placementId: z.string().min(1).max(80),
|
|
54
|
+
period: z.string().refine(isAdPeriod, "Pick a month, as YYYY-MM."),
|
|
55
|
+
creative: adBookingCreativeSchema,
|
|
56
|
+
})
|
|
57
|
+
.strict();
|
|
58
|
+
/**
|
|
59
|
+
* A moderator's verdict.
|
|
60
|
+
*
|
|
61
|
+
* 🛑 A rejection REQUIRES a note, for the same reason an admin offer-cancel
|
|
62
|
+
* does: without one the advertiser's next submission is the same creative
|
|
63
|
+
* again, and the moderator reviews it twice. An approval takes no note
|
|
64
|
+
* because there is nothing to say.
|
|
65
|
+
*/
|
|
66
|
+
export const adModerationSchema = z
|
|
67
|
+
.object({
|
|
68
|
+
approve: z.boolean(),
|
|
69
|
+
note: z.string().max(300).optional(),
|
|
70
|
+
})
|
|
71
|
+
.strict()
|
|
72
|
+
.refine((v) => v.approve || (v.note?.trim().length ?? 0) >= 10, {
|
|
73
|
+
message: "Say why it was rejected — the advertiser sees this and will resubmit without it.",
|
|
74
|
+
path: ["note"],
|
|
75
|
+
});
|
|
76
|
+
/*
|
|
77
|
+
* The resubmit body — the creative alone, under the key the route reads.
|
|
78
|
+
*
|
|
79
|
+
* 🛑 Declared HERE rather than rebuilt at the route, and the reason is a type
|
|
80
|
+
* rather than tidiness: a locally-written `z.object({ creative:
|
|
81
|
+
* adBookingCreativeSchema }).strict()` loses the inner type under this repo's
|
|
82
|
+
* zod, so `body!.creative` arrives as `unknown` and the handler cannot pass it
|
|
83
|
+
* to `resubmitAdCreative`. The sibling POST route works precisely because it
|
|
84
|
+
* uses a published schema's own `_output`; this gives PATCH the same.
|
|
85
|
+
*/
|
|
86
|
+
export const adCreativeResubmitSchema = z
|
|
87
|
+
.object({ creative: adBookingCreativeSchema })
|
|
88
|
+
.strict();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const adCreativeFormSchema: z.ZodObject<{
|
|
3
|
+
title: z.ZodString;
|
|
4
|
+
body: z.ZodOptional<z.ZodString>;
|
|
5
|
+
ctaLabel: z.ZodOptional<z.ZodString>;
|
|
6
|
+
ctaHref: z.ZodOptional<z.ZodUnion<[z.ZodLiteral<"">, z.ZodEffects<z.ZodString, string, string>]>>;
|
|
7
|
+
}, "strip", z.ZodTypeAny, {
|
|
8
|
+
title: string;
|
|
9
|
+
body?: string | undefined;
|
|
10
|
+
ctaLabel?: string | undefined;
|
|
11
|
+
ctaHref?: string | undefined;
|
|
12
|
+
}, {
|
|
13
|
+
title: string;
|
|
14
|
+
body?: string | undefined;
|
|
15
|
+
ctaLabel?: string | undefined;
|
|
16
|
+
ctaHref?: string | undefined;
|
|
17
|
+
}>;
|
|
18
|
+
export type AdCreativeFormValues = z.infer<typeof adCreativeFormSchema>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: `/advertise` rendered four hand-wired `<FieldInput>`s inside a bare
|
|
3
|
+
* `<Form>`, so the advertiser's creative had no client-side bounds at
|
|
4
|
+
* all — the only limits were the ones `adBookingCreateSchema` enforces
|
|
5
|
+
* server-side, which the visitor meets as a 400 after submitting. A
|
|
6
|
+
* headline is the single most visible string this platform renders to
|
|
7
|
+
* strangers; it should not be the one field with no feedback until POST.
|
|
8
|
+
*
|
|
9
|
+
* WHAT: The FLAT form shape for the booking creative, annotated so
|
|
10
|
+
* `buildSectionsFromSchema` produces the fields. The wire shape nests
|
|
11
|
+
* these under `creative` (see `booking-write.ts`); the page lifts them
|
|
12
|
+
* at submit.
|
|
13
|
+
*
|
|
14
|
+
* 🛑 It deliberately does NOT carry `placementId` or `period`. Those are
|
|
15
|
+
* chosen by clicking a month in the rate card, not typed — and more to the
|
|
16
|
+
* point, a slot whose availability the server has already decided must not
|
|
17
|
+
* become a text input the advertiser can edit. The booking id IS the lock
|
|
18
|
+
* (`adBookingId`), so a typed period is a way to aim at somebody else's slot.
|
|
19
|
+
*
|
|
20
|
+
* 🛑 Bounds mirror `adCreativeSchema` exactly. Two numbers that merely happen
|
|
21
|
+
* to agree today would drift into a form that accepts what the route refuses,
|
|
22
|
+
* which is the worse of the two failures: it looks like a server fault.
|
|
23
|
+
*
|
|
24
|
+
* EXPORTS: adCreativeFormSchema, type AdCreativeFormValues
|
|
25
|
+
*
|
|
26
|
+
* @tag domain:ads
|
|
27
|
+
* @tag layer:schema
|
|
28
|
+
* @tag pattern:none
|
|
29
|
+
* @tag access:isomorphic
|
|
30
|
+
* @tag consumers:/advertise
|
|
31
|
+
* @tag sideEffects:none
|
|
32
|
+
*/
|
|
33
|
+
import { z } from "zod";
|
|
34
|
+
import { annotate } from "../../shell/field-ui-meta.js";
|
|
35
|
+
import { httpUrl } from "./booking-write.js";
|
|
36
|
+
export const adCreativeFormSchema = z.object({
|
|
37
|
+
title: annotate(z.string().trim().min(2, "A headline is required.").max(80, "Keep the headline under 80 characters."), { section: "creative", sectionLabel: "Your creative", sectionRequired: true, quick: true, order: 1, row: "full" }),
|
|
38
|
+
body: annotate(z.string().trim().max(200, "Keep the body under 200 characters.").optional(), { section: "creative", order: 2, row: "full", kind: "textarea" }),
|
|
39
|
+
ctaLabel: annotate(z.string().trim().max(40, "Keep the button text under 40 characters.").optional(), { section: "creative", order: 3, row: "pair" }),
|
|
40
|
+
/*
|
|
41
|
+
* 🛑 `httpUrl`, the same refinement the wire schema uses — a `javascript:`
|
|
42
|
+
* or `data:` CTA on a page we render to strangers is stored XSS with our
|
|
43
|
+
* own domain's credibility behind it. An empty string is allowed through so
|
|
44
|
+
* clearing the field is not an error; the page drops it before POST.
|
|
45
|
+
*/
|
|
46
|
+
ctaHref: annotate(z.union([z.literal(""), httpUrl]).optional(), {
|
|
47
|
+
section: "creative",
|
|
48
|
+
order: 4,
|
|
49
|
+
row: "pair",
|
|
50
|
+
}),
|
|
51
|
+
});
|