@mohasinac/appkit 4.69.1 → 4.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PROVIDERS.md +190 -0
- package/dist/.build-stamp.json +3 -3
- package/dist/_internal/client/features/lottery/LotteryAdminSlotView.js +2 -1
- package/dist/_internal/server/features/checkout/actions.js +43 -8
- package/dist/_internal/server/features/orders/actions.js +2 -1
- package/dist/_internal/server/features/orders/adapters.js +18 -0
- package/dist/_internal/server/features/orders/balance.js +2 -1
- package/dist/_internal/server/features/orders/cancellation-refund.d.ts +0 -89
- package/dist/_internal/server/features/orders/cancellation-refund.js +3 -2
- package/dist/_internal/server/features/payouts/actions.js +2 -1
- package/dist/_internal/server/features/points/award.js +3 -2
- package/dist/_internal/server/features/protection-claims/actions.js +2 -1
- package/dist/_internal/server/features/refunds/actions.js +3 -2
- package/dist/_internal/server/features/shipping/courier-block.js +5 -2
- package/dist/_internal/server/features/shipping/reconcile.js +3 -2
- package/dist/_internal/server/features/shipping/reimbursement.js +3 -2
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +12 -2
- package/dist/_internal/server/jobs/core/dailyStatusDigest.js +7 -3
- package/dist/_internal/server/jobs/core/emiInstallmentReminder.js +2 -1
- package/dist/_internal/server/jobs/core/obligationSweep.js +5 -4
- package/dist/_internal/server/jobs/core/onScamReportCreate.js +2 -1
- package/dist/_internal/server/jobs/core/paymentReconcile.js +5 -4
- package/dist/_internal/shared/checkout/rules/auction.rule.js +25 -7
- package/dist/_internal/shared/checkout/rules/prize-draw.rule.js +26 -8
- package/dist/_internal/shared/config/schema.d.ts +15 -0
- package/dist/_internal/shared/content/published-values.d.ts +2 -2
- package/dist/_internal/shared/content/published-values.js +31 -12
- package/dist/_internal/shared/features/auctions/errors.js +2 -1
- package/dist/_internal/shared/features/orders/refund-copy.js +3 -2
- package/dist/_internal/shared/features/promotions/errors.js +2 -1
- package/dist/constants/field-names.d.ts +23 -0
- package/dist/constants/field-names.js +23 -0
- package/dist/constants/storage-keys.d.ts +90 -0
- package/dist/constants/storage-keys.js +119 -0
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/payment.d.ts +42 -0
- package/dist/contracts/registry.d.ts +8 -0
- package/dist/contracts/tax.d.ts +118 -0
- package/dist/contracts/tax.js +3 -0
- package/dist/core/brand-resolver.d.ts +92 -0
- package/dist/core/brand-resolver.js +120 -0
- package/dist/core/brand-text.d.ts +15 -0
- package/dist/core/brand-text.js +73 -0
- package/dist/core/hooks/useNewsletterSubscribe.d.ts +30 -0
- package/dist/core/hooks/useNewsletterSubscribe.js +121 -0
- package/dist/features/about/components/DeveloperView.js +2 -1
- package/dist/features/about/index.d.ts +0 -2
- package/dist/features/about/index.js +0 -1
- package/dist/features/account/components/BuyerAccountGuideView.js +2 -1
- package/dist/features/account/components/BuyerAuctionsGuideView.js +4 -3
- package/dist/features/account/components/BuyerOrdersGuideView.js +6 -5
- package/dist/features/account/components/BuyerShoppingGuideView.js +3 -2
- package/dist/features/account/components/UserOffersPanel.js +2 -1
- package/dist/features/account/hooks/useCollapsedSections.js +5 -3
- package/dist/features/account/schemas/index.js +1 -1
- package/dist/features/admin/actions/notification-actions.js +9 -2
- package/dist/features/admin/components/AdminAnalyticsGuideView.js +2 -1
- package/dist/features/admin/components/AdminBundlesView.js +2 -1
- package/dist/features/admin/components/AdminCatalogGuideView.js +2 -1
- package/dist/features/admin/components/AdminContentGuideView.js +2 -1
- package/dist/features/admin/components/AdminFaqEditorView.js +2 -1
- package/dist/features/admin/components/AdminGatewayEventsView.js +2 -1
- package/dist/features/admin/components/AdminGuideHubView.js +2 -1
- package/dist/features/admin/components/AdminMoneyCasesView.js +2 -1
- package/dist/features/admin/components/AdminOrdersGuideView.js +2 -1
- package/dist/features/admin/components/AdminProductsView.js +3 -2
- package/dist/features/admin/components/AdminSectionsView.js +3 -2
- package/dist/features/admin/components/AdminSiteSettingsView.js +8 -7
- package/dist/features/admin/components/AdminStoresGuideView.js +2 -1
- package/dist/features/admin/components/AdminSupportTicketDetailView.js +2 -1
- package/dist/features/admin/components/AdminTeamGuideView.js +2 -1
- package/dist/features/admin/components/AdminTrustGuideView.js +3 -2
- package/dist/features/admin/components/AdminUsersGuideView.js +2 -1
- package/dist/features/admin/components/sections/adminSectionsTypes.js +1 -1
- package/dist/features/admin/schemas/firestore.js +0 -1
- package/dist/features/auth/actions/user-slug.js +21 -2
- package/dist/features/auth/hooks/useAuth.js +11 -4
- package/dist/features/cart/utils/pending-ops.js +16 -3
- package/dist/features/catalogue/utils/product-from-item.d.ts +18 -2
- package/dist/features/catalogue/utils/product-from-item.js +23 -4
- package/dist/features/categories/components/BundleItemsPicker.js +2 -1
- package/dist/features/categories/components/CategoryDetailPageView.js +2 -1
- package/dist/features/categories/components/ContextualTrail.d.ts +9 -2
- package/dist/features/categories/components/ContextualTrail.js +11 -3
- package/dist/features/categories/components/LastCategoryBeacon.js +2 -2
- package/dist/features/checkout/actions/checkout-value-otp-actions.js +9 -1
- package/dist/features/contact/email.js +11 -12
- package/dist/features/email/notification-templates.js +4 -5
- package/dist/features/email/render-static-markup.d.ts +34 -0
- package/dist/features/email/render-static-markup.js +4 -0
- package/dist/features/events/utils/event-charts.js +2 -1
- package/dist/features/homepage/components/AnnouncementBar.js +5 -3
- package/dist/features/homepage/components/FAQSection.js +1 -1
- package/dist/features/homepage/components/MarketplaceHomepageView.js +34 -0
- package/dist/features/homepage/components/WelcomeSection.d.ts +3 -1
- package/dist/features/homepage/components/WelcomeSection.js +2 -2
- package/dist/features/homepage/lib/section-renderer.d.ts +8 -0
- package/dist/features/homepage/lib/section-renderer.js +1 -1
- package/dist/features/layout/AppLayoutShell.d.ts +3 -1
- package/dist/features/layout/AppLayoutShell.js +2 -2
- package/dist/features/layout/TitleBarLayout.d.ts +8 -1
- package/dist/features/layout/TitleBarLayout.js +2 -2
- package/dist/features/media/MediaVideo.js +2 -1
- package/dist/features/orders/actions/refund-actions.js +3 -2
- package/dist/features/orders/components/RefundRequestView.js +15 -2
- package/dist/features/orders/repository/orders.repository.js +2 -1
- package/dist/features/orders/types/index.d.ts +14 -0
- package/dist/features/orders/utils/payment-proof-message.js +5 -2
- package/dist/features/price-index/components/PriceIndexToolView.js +6 -5
- package/dist/features/pricing/components/AdminPricingView.js +2 -1
- package/dist/features/print/components/labels.js +2 -1
- package/dist/features/products/components/CityCollectiblesView.js +2 -1
- package/dist/features/products/components/MakeOfferButton.js +2 -1
- package/dist/features/products/components/PrizeDrawCollage.js +3 -2
- package/dist/features/products/components/PrizeRevealModal.js +2 -1
- package/dist/features/products/repository/products.repository.js +2 -1
- package/dist/features/promotions/components/CouponCard.js +3 -2
- package/dist/features/promotions/components/CouponHelpDetails.js +2 -1
- package/dist/features/promotions/constants/coupon-help.d.ts +1 -1
- package/dist/features/promotions/constants/coupon-help.js +8 -1
- package/dist/features/scams/components/ScamAwarenessModal.js +2 -1
- package/dist/features/scams/components/ScamProfileView.js +5 -3
- package/dist/features/scams/components/ScamRegistryView.js +2 -1
- package/dist/features/scams/constants/scam-types.d.ts +13 -1
- package/dist/features/scams/constants/scam-types.js +67 -23
- package/dist/features/scams/schemas/firestore.js +1 -1
- package/dist/features/search/components/Search.js +5 -1
- package/dist/features/seller/actions/offer-actions.js +47 -9
- package/dist/features/seller/components/DispatchForm.js +3 -2
- package/dist/features/seller/components/PrintCenterView.js +4 -2
- package/dist/features/seller/components/SellerBundlesView.js +2 -1
- package/dist/features/seller/components/SellerPayoutSettingsView.js +3 -2
- package/dist/features/seller/components/SellerProductShell.js +3 -2
- package/dist/features/seller/components/SellerProductsView.js +4 -3
- package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +30 -4
- package/dist/features/store-extensions/repository/store-extensions.repositories.js +46 -7
- package/dist/features/store-extensions/schemas/firestore.d.ts +22 -3
- package/dist/features/store-extensions/schemas/firestore.js +20 -0
- package/dist/features/stores/components/StoreCapabilitiesGuideView.js +7 -6
- package/dist/features/stores/components/StoreFinanceGuideView.js +6 -5
- package/dist/features/stores/components/StoreGuideHubView.js +2 -1
- package/dist/features/stores/components/StoreListingsGuideView.js +2 -1
- package/dist/features/stores/components/StoreOrdersGuideView.js +5 -4
- package/dist/features/stores/components/StoreSettingsGuideView.js +2 -1
- package/dist/features/support/schemas/index.d.ts +2 -2
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.d.ts +1 -1
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +3 -2
- package/dist/features/whatsapp-bot/helpers/whatsapp.js +5 -2
- package/dist/index.d.ts +0 -3
- package/dist/index.js +0 -6
- package/dist/providers/payment-manual/index.d.ts +11 -0
- package/dist/providers/payment-manual/index.js +13 -0
- package/dist/providers/payment-phonepe/index.d.ts +23 -7
- package/dist/providers/payment-phonepe/index.js +15 -0
- package/dist/providers/payment-razorpay/index.d.ts +13 -0
- package/dist/providers/payment-razorpay/index.js +21 -1
- package/dist/providers/tax-india-gst/index.d.ts +45 -0
- package/dist/providers/tax-india-gst/index.js +78 -0
- package/dist/react/hooks/useScrollToTop.d.ts +5 -0
- package/dist/react/hooks/useScrollToTop.js +70 -0
- package/dist/schemas/registry.d.ts +2 -2
- package/dist/seo/json-ld.js +25 -4
- package/dist/styles.css +20 -0
- package/dist/ui/components/SiteLogo.d.ts +20 -2
- package/dist/ui/components/SiteLogo.js +28 -6
- package/dist/ui/components/SiteMark.d.ts +1 -1
- package/dist/ui/components/SiteMark.js +21 -6
- package/dist/ui/components/Tabs.d.ts +9 -1
- package/dist/ui/components/Tabs.js +20 -3
- package/dist/ui/components/Tabs.style.css +20 -0
- package/dist/ui/rich-text/RichText.d.ts +0 -12
- package/dist/ui/rich-text/RichText.js +52 -1
- package/dist/utils/date.formatter.d.ts +20 -0
- package/dist/utils/date.formatter.js +25 -0
- package/dist/utils/number.formatter.d.ts +47 -0
- package/dist/utils/number.formatter.js +79 -1
- package/dist/validation/phone.validator.d.ts +0 -1
- package/dist/validation/phone.validator.js +17 -3
- package/package.json +17 -3
- package/dist/features/about/components/AboutView.d.ts +0 -30
- package/dist/features/about/components/AboutView.js +0 -14
|
@@ -20,6 +20,7 @@ import { isFinalSale } from "../../products/constants/final-sale.js";
|
|
|
20
20
|
import { productImageUrls } from "../../products/schemas/firestore.js";
|
|
21
21
|
import { ProductFeaturesSelector } from "../../products/components/ProductFeaturesSelector.js";
|
|
22
22
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
23
|
+
import { getBrandName } from "../../../core/brand-resolver.js";
|
|
23
24
|
/**
|
|
24
25
|
* "bundle" is a categoryType, not a ListingType, so it can't live in
|
|
25
26
|
* LISTING_TYPE_REGISTRY — this is the single, explicit escape hatch that
|
|
@@ -132,7 +133,7 @@ function StepBasic({ values, onChange, renderCategorySelector, renderBrandSelect
|
|
|
132
133
|
title: values.title ?? "",
|
|
133
134
|
brand: values.brand ?? "",
|
|
134
135
|
condition: CONDITION_OPTIONS.find((o) => o.value === (values.condition ?? "new"))?.label ?? "",
|
|
135
|
-
siteName:
|
|
136
|
+
siteName: `${getBrandName()}`,
|
|
136
137
|
};
|
|
137
138
|
return (_jsxs(Stack, { gap: "md", children: [_jsx(FormField, { name: "title", label: "Title", type: "text", value: values.title ?? "", onChange: (v) => onChange({ title: v }), placeholder: "e.g. Charizard Base Set PSA 9" }), _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { className: "text-[var(--appkit-color-text)]", size: "sm", weight: "medium", children: "What is in this listing?" }), _jsxs(Row, { gap: "sm", align: "center", wrap: true, children: [_jsx(Toggle, { checked: values.isLot === true, onChange: (next) => onChange(next
|
|
138
139
|
? /*
|
|
@@ -347,7 +348,7 @@ const LIVE_TRANSPORT_OPTIONS = [
|
|
|
347
348
|
{ value: "specialist", label: "Third-party specialist transport" },
|
|
348
349
|
];
|
|
349
350
|
function StepLiveItemSettings({ values, onChange, }) {
|
|
350
|
-
return (_jsxs(Stack, { gap: "md", children: [
|
|
351
|
+
return (_jsxs(Stack, { gap: "md", children: [_jsxs(Alert, { variant: "warning", children: ["Live animal / plant listings must comply with all applicable laws, CITES regulations, and local jurisdiction rules. ", getBrandName(), " is not responsible for legal compliance \u2014 this is solely the seller's responsibility."] }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "liveSpecies", label: "Species / Common Name", type: "text", value: values.liveSpecies ?? "", onChange: (v) => onChange({ liveSpecies: v }), placeholder: "e.g. Axolotl (Ambystoma mexicanum)" }), _jsx(FormField, { name: "liveAgeMonths", label: "Age (months, optional)", type: "number", value: String(values.liveAgeMonths ?? ""), onChange: (v) => onChange({ liveAgeMonths: v ? Number(v) : undefined }), placeholder: "e.g. 6" })] }), _jsx(FormField, { name: "liveSex", label: "Sex", type: "select", value: values.liveSex ?? "unknown", onChange: (v) => onChange({ liveSex: v }), options: LIVE_SEX_OPTIONS }), _jsx(FormField, { name: "liveCareInfo", label: "Care Instructions (optional)", type: "textarea", value: values.liveCareInfo ?? "", onChange: (v) => onChange({ liveCareInfo: v }), placeholder: "Diet, temperature, habitat requirements\u2026" }), _jsx(FormField, { name: "liveTransportMethod", label: "Transport Method", type: "select", value: values.liveTransportMethod ?? "in-person", onChange: (v) => onChange({ liveTransportMethod: v }), options: LIVE_TRANSPORT_OPTIONS }), values.liveTransportMethod !== "in-person" && (_jsx(FormField, { name: "liveHandlingFee", label: "Handling / Transport Fee (\u20B9, optional)", type: "number", value: toRupeesString(values.liveHandlingFee), onChange: (v) => onChange({ liveHandlingFee: v ? fromRupeesString(v) : undefined }), placeholder: "0" })), _jsx(TagInput, { label: "Jurisdictions where sale is permitted", value: values.liveJurisdictions ?? [], onChange: (liveJurisdictions) => onChange({ liveJurisdictions }), placeholder: "e.g. Maharashtra, Karnataka", helperText: "List all Indian states / UTs where you can legally sell" }), _jsx(Toggle, { checked: !!values.liveCites, onChange: (checked) => onChange({ liveCites: checked }), label: "This species requires CITES documentation" })] }));
|
|
351
352
|
}
|
|
352
353
|
// ── Step: Print Meta (art / stickers) ───────────────────────────────────────
|
|
353
354
|
// Shared by both "art" and "stickers" — identical field set, only the intro
|
|
@@ -41,6 +41,7 @@ import { AssemblyDrawer, } from "../../parts/components/AssemblyDrawer.js";
|
|
|
41
41
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
|
|
42
42
|
import { buildBulkAction } from "../../../_internal/shared/actions/bulk-helpers.js";
|
|
43
43
|
import { productImageUrls } from "../../products/schemas/firestore.js";
|
|
44
|
+
import { formatMoney } from "../../../utils/number.formatter.js";
|
|
44
45
|
const FILTER_KEYS = [
|
|
45
46
|
"status",
|
|
46
47
|
"category",
|
|
@@ -520,7 +521,7 @@ export function SellerProductsView({ onDeleteProduct, onCreateClick, }) {
|
|
|
520
521
|
const auctionSecondary = kind === "auction"
|
|
521
522
|
? [
|
|
522
523
|
typeof item.reservePrice === "number"
|
|
523
|
-
? `Reserve
|
|
524
|
+
? `Reserve ${formatMoney(item.reservePrice)}`
|
|
524
525
|
: null,
|
|
525
526
|
`${typeof item.bidCount === "number" ? item.bidCount : 0} bids`,
|
|
526
527
|
item.auctionEndDate
|
|
@@ -577,7 +578,7 @@ export function SellerProductsView({ onDeleteProduct, onCreateClick, }) {
|
|
|
577
578
|
const availableRaw = typeof item.availableQuantity === "number" ? item.availableQuantity : null;
|
|
578
579
|
const stockRaw = stockOverride ?? (typeof item.stockQuantity === "number" ? item.stockQuantity : null);
|
|
579
580
|
const stockValue = stockOverride ?? availableRaw ?? stockRaw;
|
|
580
|
-
const priceLabel = kind !== "auction" && priceRaw ?
|
|
581
|
+
const priceLabel = kind !== "auction" && priceRaw ? formatMoney(priceRaw) : "";
|
|
581
582
|
const stockLabel = stockRaw === null ? "" : `${stockRaw} in stock`;
|
|
582
583
|
const secondaryBase = auctionSecondary || toStringValue(item.condition, "");
|
|
583
584
|
return {
|
|
@@ -589,7 +590,7 @@ export function SellerProductsView({ onDeleteProduct, onCreateClick, }) {
|
|
|
589
590
|
imageUrl: toStringValue(item.mainImage, undefined) || productImageUrls(item.images)[0],
|
|
590
591
|
image: toStringValue(item.mainImage, undefined) || productImageUrls(item.images)[0],
|
|
591
592
|
listingKind: kind,
|
|
592
|
-
price: priceRaw ?
|
|
593
|
+
price: priceRaw ? formatMoney(priceRaw) : "—",
|
|
593
594
|
stock: stockLabel || "—",
|
|
594
595
|
stockValue,
|
|
595
596
|
// Carried through ONLY when the document really has it — see the
|
|
@@ -10,11 +10,17 @@
|
|
|
10
10
|
* per-method override is needed (which is the point of the hooks — Pattern #9
|
|
11
11
|
* described overrides that never existed).
|
|
12
12
|
*
|
|
13
|
+
* 🛑 Reads are CIPHERTEXT-BY-DEFAULT since 2026-10-28. Those three fields are
|
|
14
|
+
* `Encrypted<string>` and `mapDoc` does not decrypt; use
|
|
15
|
+
* `maskedPayoutMethod(s)` (the default) or `revealPayoutMethod(s)` (opt-in and
|
|
16
|
+
* named). The interim state — encrypted at rest but decrypted on every read —
|
|
17
|
+
* was invisible to `audit-pii-coverage` purely because this file is named
|
|
18
|
+
* `.repositories.ts` and its filter read `.repository.ts`.
|
|
19
|
+
*
|
|
13
20
|
* The other ten hold no PII and use the BaseRepository public API only.
|
|
14
21
|
*/
|
|
15
22
|
import { BaseRepository } from "../../../providers/db-firebase";
|
|
16
|
-
import type
|
|
17
|
-
import { type AnalyticsAlertDocument, type AnalyticsCardDocument, type ItemRequestDocument, type ItemRequestReply, type ListingTemplateDocument, type ModerationQueueDocument, type PayoutMethodDocument, type ReportDocument, type ShippingConfigDocument, type StoreCategoryDocument, type StoreGoogleConfigDocument, type StoreWhatsAppConfigDocument } from "../schemas/firestore";
|
|
23
|
+
import { type AnalyticsAlertDocument, type AnalyticsCardDocument, type ItemRequestDocument, type ItemRequestReply, type ListingTemplateDocument, type ModerationQueueDocument, type PayoutMethodDocument, type PayoutMethodPlain, type ReportDocument, type ShippingConfigDocument, type StoreCategoryDocument, type StoreGoogleConfigDocument, type StoreWhatsAppConfigDocument } from "../schemas/firestore";
|
|
18
24
|
export declare class PayoutMethodsRepository extends BaseRepository<PayoutMethodDocument> {
|
|
19
25
|
/**
|
|
20
26
|
* Bank/UPI details, encrypted at rest by BaseRepository's write hooks.
|
|
@@ -25,8 +31,28 @@ export declare class PayoutMethodsRepository extends BaseRepository<PayoutMethod
|
|
|
25
31
|
*/
|
|
26
32
|
protected piiFields: readonly ["upiVpa", "accountNumber", "ifscCode"];
|
|
27
33
|
constructor();
|
|
28
|
-
/**
|
|
29
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Plaintext PII. **Opt-in and named** — the four `/api/store/payout-methods`
|
|
36
|
+
* response sites call it explicitly, which is the whole gain here: the
|
|
37
|
+
* decrypt is now four greppable call sites instead of implicit for every
|
|
38
|
+
* reader of this repository.
|
|
39
|
+
*
|
|
40
|
+
* 🛑 There is deliberately NO masked variant yet, and that is not an
|
|
41
|
+
* oversight. Masking payout methods properly means following
|
|
42
|
+
* `SellerPayoutSettingsView`'s convention — a separate `accountNumberMasked`
|
|
43
|
+
* key with the real field ABSENT, the form input starting empty, and "blank
|
|
44
|
+
* keeps existing" enforced server-side — because masking the real key
|
|
45
|
+
* in-place would be double-masked by
|
|
46
|
+
* `SellerPayoutMethodsView.getMaskedIdentifier` and, worse, saved over the
|
|
47
|
+
* real value by `.../[id]/edit`, which PATCHes its whole prefilled form
|
|
48
|
+
* back. When that change lands it is
|
|
49
|
+
* `maskPii(doc, PAYOUT_METHOD_PII_FIELDS)`, one import away; an unused
|
|
50
|
+
* accessor shipped ahead of it is the speculative surface `AK3-yagni-prune`
|
|
51
|
+
* exists to delete.
|
|
52
|
+
*/
|
|
53
|
+
revealPayoutMethod(doc: PayoutMethodDocument): PayoutMethodPlain;
|
|
54
|
+
/** As `revealPayoutMethod`, for a list read. */
|
|
55
|
+
revealPayoutMethods(docs: readonly PayoutMethodDocument[]): PayoutMethodPlain[];
|
|
30
56
|
listByStore(storeId: string): Promise<{
|
|
31
57
|
items: PayoutMethodDocument[];
|
|
32
58
|
}>;
|
|
@@ -10,12 +10,19 @@
|
|
|
10
10
|
* per-method override is needed (which is the point of the hooks — Pattern #9
|
|
11
11
|
* described overrides that never existed).
|
|
12
12
|
*
|
|
13
|
+
* 🛑 Reads are CIPHERTEXT-BY-DEFAULT since 2026-10-28. Those three fields are
|
|
14
|
+
* `Encrypted<string>` and `mapDoc` does not decrypt; use
|
|
15
|
+
* `maskedPayoutMethod(s)` (the default) or `revealPayoutMethod(s)` (opt-in and
|
|
16
|
+
* named). The interim state — encrypted at rest but decrypted on every read —
|
|
17
|
+
* was invisible to `audit-pii-coverage` purely because this file is named
|
|
18
|
+
* `.repositories.ts` and its filter read `.repository.ts`.
|
|
19
|
+
*
|
|
13
20
|
* The other ten hold no PII and use the BaseRepository public API only.
|
|
14
21
|
*/
|
|
15
22
|
import { BaseRepository } from "../../../providers/db-firebase/index.js";
|
|
16
23
|
import { arrayUnion, increment } from "../../../contracts/field-ops.js";
|
|
17
|
-
import {
|
|
18
|
-
import { ANALYTICS_ALERTS_COLLECTION, ANALYTICS_CARDS_COLLECTION, ITEM_REQUESTS_COLLECTION, LISTING_TEMPLATES_COLLECTION, MODERATION_QUEUE_COLLECTION, PAYOUT_METHODS_COLLECTION, REPORTS_COLLECTION, SHIPPING_CONFIGS_COLLECTION, STORE_CATEGORIES_COLLECTION, STORE_GOOGLE_CONFIG_COLLECTION, STORE_WHATSAPP_CONFIG_COLLECTION, } from "../schemas/firestore.js";
|
|
24
|
+
import { revealPii } from "../../../security/pii-accessors.js";
|
|
25
|
+
import { ANALYTICS_ALERTS_COLLECTION, ANALYTICS_CARDS_COLLECTION, ITEM_REQUESTS_COLLECTION, LISTING_TEMPLATES_COLLECTION, MODERATION_QUEUE_COLLECTION, PAYOUT_METHOD_PII_FIELDS, PAYOUT_METHODS_COLLECTION, REPORTS_COLLECTION, SHIPPING_CONFIGS_COLLECTION, STORE_CATEGORIES_COLLECTION, STORE_GOOGLE_CONFIG_COLLECTION, STORE_WHATSAPP_CONFIG_COLLECTION, } from "../schemas/firestore.js";
|
|
19
26
|
export class PayoutMethodsRepository extends BaseRepository {
|
|
20
27
|
constructor() {
|
|
21
28
|
super(PAYOUT_METHODS_COLLECTION);
|
|
@@ -26,12 +33,44 @@ export class PayoutMethodsRepository extends BaseRepository {
|
|
|
26
33
|
* (`listByStore` filters on `storeId`), so no read path depended on their
|
|
27
34
|
* plaintext.
|
|
28
35
|
*/
|
|
29
|
-
this.piiFields =
|
|
36
|
+
this.piiFields = PAYOUT_METHOD_PII_FIELDS;
|
|
37
|
+
}
|
|
38
|
+
/*
|
|
39
|
+
* 🛑 `mapDoc` no longer decrypts — reads return what Firestore holds.
|
|
40
|
+
*
|
|
41
|
+
* It used to override `mapDoc` and call `decryptPiiFields` on every read, so
|
|
42
|
+
* a plain `listByStore()` handed its caller a decrypted bank account number
|
|
43
|
+
* and IFSC code, and `GET /api/store/payout-methods` returned all three in
|
|
44
|
+
* full. That survived because `audit-pii-coverage` filtered
|
|
45
|
+
* `.endsWith(".repository.ts")` and this file is `.repositories.ts`, so the
|
|
46
|
+
* audit whose entire purpose is to find this reported `0 findings` — see the
|
|
47
|
+
* note on `isRepositoryModule` in that script.
|
|
48
|
+
*/
|
|
49
|
+
/**
|
|
50
|
+
* Plaintext PII. **Opt-in and named** — the four `/api/store/payout-methods`
|
|
51
|
+
* response sites call it explicitly, which is the whole gain here: the
|
|
52
|
+
* decrypt is now four greppable call sites instead of implicit for every
|
|
53
|
+
* reader of this repository.
|
|
54
|
+
*
|
|
55
|
+
* 🛑 There is deliberately NO masked variant yet, and that is not an
|
|
56
|
+
* oversight. Masking payout methods properly means following
|
|
57
|
+
* `SellerPayoutSettingsView`'s convention — a separate `accountNumberMasked`
|
|
58
|
+
* key with the real field ABSENT, the form input starting empty, and "blank
|
|
59
|
+
* keeps existing" enforced server-side — because masking the real key
|
|
60
|
+
* in-place would be double-masked by
|
|
61
|
+
* `SellerPayoutMethodsView.getMaskedIdentifier` and, worse, saved over the
|
|
62
|
+
* real value by `.../[id]/edit`, which PATCHes its whole prefilled form
|
|
63
|
+
* back. When that change lands it is
|
|
64
|
+
* `maskPii(doc, PAYOUT_METHOD_PII_FIELDS)`, one import away; an unused
|
|
65
|
+
* accessor shipped ahead of it is the speculative surface `AK3-yagni-prune`
|
|
66
|
+
* exists to delete.
|
|
67
|
+
*/
|
|
68
|
+
revealPayoutMethod(doc) {
|
|
69
|
+
return revealPii(doc, PAYOUT_METHOD_PII_FIELDS);
|
|
30
70
|
}
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return decryptPiiFields(raw, [...this.piiFields]);
|
|
71
|
+
/** As `revealPayoutMethod`, for a list read. */
|
|
72
|
+
revealPayoutMethods(docs) {
|
|
73
|
+
return docs.map((d) => this.revealPayoutMethod(d));
|
|
35
74
|
}
|
|
36
75
|
async listByStore(storeId) {
|
|
37
76
|
const items = await this.findBy("storeId", storeId);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AlertMetric } from "../constants/alert-metrics";
|
|
2
2
|
import type { FirestoreDocument } from "@mohasinac/appkit";
|
|
3
3
|
import type { BaseDocument } from "../../../_internal/shared/types/base-document";
|
|
4
|
+
import type { Encrypted, Plaintext } from "../../../security/encrypted";
|
|
4
5
|
/**
|
|
5
6
|
* S-STORE Extensions — Firestore Document Types
|
|
6
7
|
*
|
|
@@ -29,13 +30,31 @@ export interface PayoutMethodDocument extends BaseDocument {
|
|
|
29
30
|
label: string;
|
|
30
31
|
isDefault: boolean;
|
|
31
32
|
isActive: boolean;
|
|
32
|
-
upiVpa?: string
|
|
33
|
-
accountNumber?: string
|
|
34
|
-
ifscCode?: string
|
|
33
|
+
upiVpa?: Encrypted<string>;
|
|
34
|
+
accountNumber?: Encrypted<string>;
|
|
35
|
+
ifscCode?: Encrypted<string>;
|
|
35
36
|
accountHolderName?: string;
|
|
36
37
|
bankName?: string;
|
|
37
38
|
details?: Record<string, string>;
|
|
38
39
|
}
|
|
40
|
+
/** `Plaintext<T>` is the one definition of this shape; see `security/encrypted.ts`. */
|
|
41
|
+
export type PayoutMethodPlain = Plaintext<PayoutMethodDocument>;
|
|
42
|
+
/**
|
|
43
|
+
* The three fields BaseRepository's write hooks encrypt at rest.
|
|
44
|
+
*
|
|
45
|
+
* 🛑 Exported rather than inlined in the repository, because `revealPii` /
|
|
46
|
+
* `maskPii` need the same list and two copies of a field list is Root Cause
|
|
47
|
+
* #75 — one money predicate hand-written in six places, two of which had
|
|
48
|
+
* silently dropped a field.
|
|
49
|
+
*
|
|
50
|
+
* `accountHolderName` is deliberately ABSENT: it is a person's name on the
|
|
51
|
+
* seller's own payout method, it is not matched by the audit's PII shape
|
|
52
|
+
* regex, and it was never encrypted. Adding it here would brand a field the
|
|
53
|
+
* write hooks have never encrypted, so `decryptValue` would pass the
|
|
54
|
+
* plaintext straight through and the brand would be a lie — exactly the
|
|
55
|
+
* half-change CLAUDE.md warns about.
|
|
56
|
+
*/
|
|
57
|
+
export declare const PAYOUT_METHOD_PII_FIELDS: readonly ["upiVpa", "accountNumber", "ifscCode"];
|
|
39
58
|
export declare const PAYOUT_METHODS_COLLECTION: "payoutMethods";
|
|
40
59
|
export declare const PAYOUT_METHOD_INDEXED_FIELDS: readonly ["sellerId", "storeId", "type", "isDefault", "isActive", "createdAt"];
|
|
41
60
|
export declare const DEFAULT_PAYOUT_METHOD_DATA: Partial<PayoutMethodDocument>;
|
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three fields BaseRepository's write hooks encrypt at rest.
|
|
3
|
+
*
|
|
4
|
+
* 🛑 Exported rather than inlined in the repository, because `revealPii` /
|
|
5
|
+
* `maskPii` need the same list and two copies of a field list is Root Cause
|
|
6
|
+
* #75 — one money predicate hand-written in six places, two of which had
|
|
7
|
+
* silently dropped a field.
|
|
8
|
+
*
|
|
9
|
+
* `accountHolderName` is deliberately ABSENT: it is a person's name on the
|
|
10
|
+
* seller's own payout method, it is not matched by the audit's PII shape
|
|
11
|
+
* regex, and it was never encrypted. Adding it here would brand a field the
|
|
12
|
+
* write hooks have never encrypted, so `decryptValue` would pass the
|
|
13
|
+
* plaintext straight through and the brand would be a lie — exactly the
|
|
14
|
+
* half-change CLAUDE.md warns about.
|
|
15
|
+
*/
|
|
16
|
+
export const PAYOUT_METHOD_PII_FIELDS = [
|
|
17
|
+
"upiVpa",
|
|
18
|
+
"accountNumber",
|
|
19
|
+
"ifscCode",
|
|
20
|
+
];
|
|
1
21
|
export const PAYOUT_METHODS_COLLECTION = "payoutMethods";
|
|
2
22
|
export const PAYOUT_METHOD_INDEXED_FIELDS = [
|
|
3
23
|
"sellerId",
|
|
@@ -8,6 +8,7 @@ import { Row, Stack } from "../../../ui/components/Layout.js";
|
|
|
8
8
|
import { TextLink } from "../../../ui/components/TextLink.js";
|
|
9
9
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
10
10
|
import { SellerStandingPanel } from "./SellerStandingPanel.js";
|
|
11
|
+
import { getBrandName, getBrandDomain, getBrandSupportEmail } from "../../../core/brand-resolver.js";
|
|
11
12
|
const SUBHEADING = "mb-1 font-semibold text-[var(--appkit-color-text)]";
|
|
12
13
|
const LIST_DISC = "list-disc leading-relaxed";
|
|
13
14
|
const SECTION_BODY = "space-y-4 text-[length:var(--appkit-text-sm)] text-[var(--appkit-color-text-muted)]";
|
|
@@ -17,7 +18,7 @@ function GuideSection({ title, children, }) {
|
|
|
17
18
|
}
|
|
18
19
|
// -- Section 1: What are capabilities -----------------------------------------
|
|
19
20
|
function WhatAreCapabilitiesSection() {
|
|
20
|
-
return (_jsxs(Div, { className: SECTION_BODY, children: [
|
|
21
|
+
return (_jsxs(Div, { className: SECTION_BODY, children: [_jsxs(Text, { className: "leading-relaxed", children: ["Capabilities are per-store feature flags granted by the ", getBrandName(), " team. They are not self-serve \u2014 they must be requested and approved. This gives ", getBrandName(), " control over trust, quality, and platform safety as your store grows."] }), _jsxs(Text, { className: "leading-relaxed", children: ["Two capabilities are enabled for every store at creation by default:", _jsx(Code, { className: "mx-1", size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "suggest_brands" }), " and", _jsx(Code, { className: "mx-1", size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "create_coupons" }), ". All others must be requested. Some require contractual agreements before they can be granted."] })] }));
|
|
21
22
|
}
|
|
22
23
|
// -- Section 2: Capability table ----------------------------------------------
|
|
23
24
|
const CAPABILITY_GROUPS = [
|
|
@@ -62,7 +63,7 @@ const CAPABILITY_GROUPS = [
|
|
|
62
63
|
{
|
|
63
64
|
group: "Platform & Technical",
|
|
64
65
|
items: [
|
|
65
|
-
{ cap: "multiple_stores", unlocks:
|
|
66
|
+
{ cap: "multiple_stores", unlocks: `You (same owner) can create more than one store on ${getBrandName()}.`, defaultOn: false },
|
|
66
67
|
{ cap: "custom_store_slug", unlocks: "Change your store URL slug after the 7-day post-creation lock period.", defaultOn: false },
|
|
67
68
|
{ cap: "api_access", unlocks: "Programmatic API credentials for advanced integrations (future).", defaultOn: false },
|
|
68
69
|
{ cap: "lower_commission_rate", unlocks: "Negotiated reduced platform fee. Requires a signed agreement and minimum volume.", defaultOn: false },
|
|
@@ -80,7 +81,7 @@ const LOCKED_FEATURES = [
|
|
|
80
81
|
{
|
|
81
82
|
cap: "host_auctions",
|
|
82
83
|
missing: ["Create auction listings", "Set reserve prices and bid increments", "Auto-notify winner at auction end"],
|
|
83
|
-
howToUnlock:
|
|
84
|
+
howToUnlock: `Contact ${getBrandSupportEmail()} with at least 10 delivered orders and 3 months of store activity. Our team reviews within 5 business days.`,
|
|
84
85
|
contractRequired: false,
|
|
85
86
|
},
|
|
86
87
|
{
|
|
@@ -92,7 +93,7 @@ const LOCKED_FEATURES = [
|
|
|
92
93
|
{
|
|
93
94
|
cap: "lower_commission_rate",
|
|
94
95
|
missing: ["Reduced platform fee (custom rate)", "Higher net margin per order"],
|
|
95
|
-
howToUnlock:
|
|
96
|
+
howToUnlock: `Available to stores with ₹2L+ GMV per month. Contact your account manager or email partner@${getBrandDomain()} with your last 3 months' payout statements.`,
|
|
96
97
|
contractRequired: true,
|
|
97
98
|
},
|
|
98
99
|
{
|
|
@@ -104,7 +105,7 @@ const LOCKED_FEATURES = [
|
|
|
104
105
|
{
|
|
105
106
|
cap: "bulk_listing_import",
|
|
106
107
|
missing: ["CSV batch upload for products", "Template download for bulk edits"],
|
|
107
|
-
howToUnlock:
|
|
108
|
+
howToUnlock: `Contact ${getBrandSupportEmail()} — available to stores with verified status and 50+ published listings.`,
|
|
108
109
|
contractRequired: false,
|
|
109
110
|
},
|
|
110
111
|
];
|
|
@@ -125,7 +126,7 @@ function LockedFeatureGuide() {
|
|
|
125
126
|
* drift from the job that grants it.
|
|
126
127
|
*/
|
|
127
128
|
function VerifiedSellerSection() {
|
|
128
|
-
return (_jsxs(Div, { className: SECTION_BODY, children: [_jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "Two different badges" }), _jsxs(Text, { className: "leading-relaxed", children: [_jsx(Span, { weight: "bold", children: "Verified" }), " means
|
|
129
|
+
return (_jsxs(Div, { className: SECTION_BODY, children: [_jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "Two different badges" }), _jsxs(Text, { className: "leading-relaxed", children: [_jsx(Span, { weight: "bold", children: "Verified" }), " means ", getBrandName(), " has checked who you are. It is granted by an admin after an identity review, it is not earned by volume, and it does not expire on its own.", " ", _jsx(Span, { weight: "bold", children: "Trusted Seller" }), " is earned from your trading record and is recalculated every month \u2014 you can gain it and you can lose it. A store can hold either, both, or neither."] })] }), _jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "Getting Verified" }), _jsx(Text, { className: "leading-relaxed", children: "Open a support ticket from your store dashboard with category \"Account\" and subject \"Verified Seller Application\", and attach a government-issued photo ID. An admin reviews it and sets the badge on your store." })] }), _jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "Earning Trusted Seller" }), _jsx(Text, { className: "leading-relaxed", children: "Every criterion below is measured over the same rolling window, and all of them have to hold at the same time. Meeting them also changes when you are paid: your payout starts when you ship rather than when the parcel is delivered." })] }), _jsx(SellerStandingPanel, { heading: "The criteria" })] }));
|
|
129
130
|
}
|
|
130
131
|
// -- Section 5: Multiple stores -----------------------------------------------
|
|
131
132
|
function MultipleStoresSection() {
|
|
@@ -8,6 +8,7 @@ import { Heading, Span, Text } from "../../../ui/components/Typography.js";
|
|
|
8
8
|
import { Stack } from "../../../ui/components/Layout.js";
|
|
9
9
|
import { TextLink } from "../../../ui/components/TextLink.js";
|
|
10
10
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
11
|
+
import { getBrandName } from "../../../core/brand-resolver.js";
|
|
11
12
|
const SUBHEADING = "mb-1 font-semibold text-[var(--appkit-color-text)]";
|
|
12
13
|
const LIST_DISC = "list-disc leading-relaxed";
|
|
13
14
|
const SECTION_BODY = "space-y-4 text-[length:var(--appkit-text-sm)] text-[var(--appkit-color-text-muted)]";
|
|
@@ -18,7 +19,7 @@ function GuideSection({ title, children, }) {
|
|
|
18
19
|
}
|
|
19
20
|
// -- Section 1: Commission structure ------------------------------------------
|
|
20
21
|
function CommissionSection() {
|
|
21
|
-
return (_jsxs(Div, { className: SECTION_BODY, children: [_jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "How it works" }),
|
|
22
|
+
return (_jsxs(Div, { className: SECTION_BODY, children: [_jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "How it works" }), _jsxs(Text, { className: "leading-relaxed", children: [getBrandName(), " deducts a platform fee from every completed order before paying out to you. The current fee rate is shown in your store Settings \u2192 Finance page and in the platform site settings. Custom commission rates are available to high-volume sellers \u2014 see the Capabilities guide for details."] })] }), _jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "Worked example" }), _jsxs(Stack, { textSize: "xs", className: "border border-[var(--appkit-color-border)] font-mono", gap: "xs", rounded: "lg", padding: "md", children: [_jsx(Text, { className: MONO_MUTED, children: "Order total: \u20B91,000.00" }), _jsx(Text, { className: MONO_MUTED, children: "Platform fee (e.g. 5%): \u2212 \u20B950.00" }), _jsx(Text, { className: MONO_MUTED, children: "Payment processing (Razorpay ~2%): \u2212 \u20B920.00" }), _jsx(Div, { className: "border-t border-[var(--appkit-color-border)]", padding: "t-2xs", children: _jsx(Text, { className: "font-mono text-[var(--appkit-color-text)]", size: "xs", weight: "semibold", children: "Seller receives: \u2248 \u20B9930.00" }) })] }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)] mt-2", size: "xs", children: "Exact amounts depend on the current platform fee configured in Site Settings. Razorpay charges vary by payment method (UPI is lowest)." })] }), _jsx(Alert, { variant: "info", title: "Shipping is separate", children: "Shipping costs you charge buyers are passed through in full \u2014 the platform fee applies only to the product price, not the shipping amount." })] }));
|
|
22
23
|
}
|
|
23
24
|
// -- Section 2: Payout cycle --------------------------------------------------
|
|
24
25
|
const PAYOUT_STATUSES = [
|
|
@@ -62,7 +63,7 @@ const COUPON_FIELDS = [
|
|
|
62
63
|
},
|
|
63
64
|
{
|
|
64
65
|
field: "First-time user only",
|
|
65
|
-
detail:
|
|
66
|
+
detail: `Restricts redemption to buyers with no prior completed orders on ${getBrandName()}.`,
|
|
66
67
|
},
|
|
67
68
|
{
|
|
68
69
|
field: "Combine with seller coupons",
|
|
@@ -70,15 +71,15 @@ const COUPON_FIELDS = [
|
|
|
70
71
|
},
|
|
71
72
|
];
|
|
72
73
|
function CouponsSection() {
|
|
73
|
-
return (_jsxs(Div, { className: SECTION_BODY, children: [_jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "Admin coupons vs store coupons" }), _jsxs(Ul, { spacing: "tight", indent: "lg", children: [_jsxs(Li, { className: LIST_DISC, children: [_jsx(Span, { weight: "bold", children: "Admin coupons" }), " \u2014 created by
|
|
74
|
+
return (_jsxs(Div, { className: SECTION_BODY, children: [_jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "Admin coupons vs store coupons" }), _jsxs(Ul, { spacing: "tight", indent: "lg", children: [_jsxs(Li, { className: LIST_DISC, children: [_jsx(Span, { weight: "bold", children: "Admin coupons" }), " \u2014 created by ", getBrandName(), " and apply platform-wide (all stores). You cannot create these."] }), _jsxs(Li, { className: LIST_DISC, children: [_jsx(Span, { weight: "bold", children: "Store coupons" }), " \u2014 created by you. Apply only to purchases from your store. You control the terms."] }), _jsx(Li, { className: LIST_DISC, children: "Admin coupons and store coupons can stack if the admin coupon has \"Combine with seller coupons\" enabled. If not, only the better coupon is applied automatically." })] })] }), _jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "Coupon field reference" }), _jsx(Stack, { gap: "3", children: COUPON_FIELDS.map(({ field, detail }) => (_jsxs(Row, { gap: "3", children: [_jsx(Text, { className: "shrink-0 w-32 text-[var(--appkit-color-text)]", size: "sm", weight: "semibold", children: field }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)] leading-relaxed", size: "sm", children: detail })] }, field))) })] })] }));
|
|
74
75
|
}
|
|
75
76
|
// -- Section 4: Promoted & featured listings ----------------------------------
|
|
76
77
|
function PromotedSection() {
|
|
77
|
-
return (_jsxs(Div, { className: SECTION_BODY, children: [_jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "isPromoted \u2014 Deals & promoted sections" }),
|
|
78
|
+
return (_jsxs(Div, { className: SECTION_BODY, children: [_jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "isPromoted \u2014 Deals & promoted sections" }), _jsxs(Text, { className: "leading-relaxed", children: ["Promoted listings appear in the Deals carousel on the homepage and in search-result promoted slots. For most stores, this flag is set by a ", getBrandName(), " admin. If you want a product promoted, contact support with the product slug and reason."] })] }), _jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "isFeatured \u2014 Featured badge & priority" }), _jsxs(Text, { className: "leading-relaxed", children: ["Featured listings get a \"Featured\" badge on their listing card and receive priority ordering in search results and category pages. Like isPromoted, this is admin-controlled for most stores. Stores with the ", _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "manage_promoted_listings" }), " capability can self-promote from their dashboard."] })] }), _jsxs(Alert, { variant: "info", title: "Promoted placement vs sponsored listings", children: [getBrandName(), " does not currently offer paid sponsored placements \u2014 promoted and featured flags are granted editorially by the ", getBrandName(), " team based on listing quality and seller performance."] })] }));
|
|
78
79
|
}
|
|
79
80
|
// -- Section 5: Taxes notice --------------------------------------------------
|
|
80
81
|
function TaxesSection() {
|
|
81
|
-
return (_jsxs(Div, { className: SECTION_BODY, children: [
|
|
82
|
+
return (_jsxs(Div, { className: SECTION_BODY, children: [_jsxs(Alert, { variant: "warning", title: "Seller tax responsibility", children: [getBrandName(), " is a marketplace platform. Each seller operates independently and is individually responsible for applicable taxes on their sales, including GST where required by Indian tax law.", getBrandName(), " does not collect or remit GST on your behalf. Consult a qualified tax advisor to understand your obligations based on your turnover and registration status."] }), _jsxs(Text, { className: "leading-relaxed", children: ["TCS (Tax Collected at Source) deducted by ", getBrandName(), " under Section 194-O of the Income Tax Act will be reflected in your annual Form 26AS. Your gross payouts (before TCS) are visible in the Finance \u2192 Payouts dashboard."] })] }));
|
|
82
83
|
}
|
|
83
84
|
export function StoreFinanceGuideView(_props) {
|
|
84
85
|
return (_jsxs(Stack, { gap: "lg", padding: "b-2xl", children: [_jsxs(TextLink, { variant: "bare", href: String(ROUTES.STORE.GUIDE), size: "sm", layout: "inline-flex", align: "center", gap: "sm", className: "text-[var(--appkit-color-text-muted)] hover:text-[var(--appkit-color-text)] transition-colors", children: [_jsx(ArrowLeft, { className: "h-4 w-4" }), "Back to Seller Guide"] }), _jsxs(Div, { children: [_jsx(Heading, { level: 1, className: "text-[var(--appkit-color-text)]", size: "2xl", weight: "bold", children: "Finance Guide" }), _jsx(Text, { className: "mt-1 text-[var(--appkit-color-text-muted)]", size: "sm", children: "Payouts, commissions, coupons, and promoted listings." })] }), _jsx(GuideSection, { title: "1. Commission structure", children: _jsx(CommissionSection, {}) }), _jsx(GuideSection, { title: "2. Payout cycle", children: _jsx(PayoutCycleSection, {}) }), _jsx(GuideSection, { title: "3. Coupons", children: _jsx(CouponsSection, {}) }), _jsx(GuideSection, { title: "4. Promoted & featured listings", children: _jsx(PromotedSection, {}) }), _jsx(GuideSection, { title: "5. Taxes", children: _jsx(TaxesSection, {}) })] }));
|
|
@@ -8,6 +8,7 @@ import { Section } from "../../../ui/components/Semantic.js";
|
|
|
8
8
|
import { TextLink } from "../../../ui/components/TextLink.js";
|
|
9
9
|
import { SellerStandingPanel } from "./SellerStandingPanel.js";
|
|
10
10
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
11
|
+
import { getBrandName } from "../../../core/brand-resolver.js";
|
|
11
12
|
const CLS_CHECK_ICON = "w-5 h-5 flex-shrink-0 text-emerald-500";
|
|
12
13
|
const CLS_SUCCESS_BOX = "rounded-xl border border-emerald-200 bg-emerald-50 dark:bg-emerald-950/30 dark:border-emerald-800 px-[var(--appkit-space-5)] py-[var(--appkit-space-4)]";
|
|
13
14
|
const CLS_SUCCESS_BODY = "text-[length:var(--appkit-text-sm)] font-medium text-emerald-700 dark:text-emerald-400";
|
|
@@ -95,7 +96,7 @@ export function StoreGuideHubView({ store }) {
|
|
|
95
96
|
const storeName = store?.storeName ?? "Your Store";
|
|
96
97
|
const steps = buildChecklist(store);
|
|
97
98
|
const completedCount = steps.filter((s) => s.done).length;
|
|
98
|
-
return (_jsxs(Stack, { gap: "xl", padding: "b-2xl", children: [_jsx(Section, { overflow: "hidden", className: "border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", rounded: "2xl", shadow: "sm", children: _jsxs(Div, { paddingX: "x-md-xl", paddingY: "y-xl", className: "[background:linear-gradient(135deg,var(--appkit-color-primary-700)_0%,var(--appkit-color-cobalt)_60%,var(--appkit-color-secondary-400)_100%)]", children: [_jsxs(Row, { className: "mb-3", align: "center", gap: "3", children: [_jsx(Row, { className: "flex-shrink-0 w-10 h-10", surface: "default", align: "center", justify: "center", rounded: "xl", children: _jsx(BookOpen, { className: "w-5 h-5 text-white" }) }), _jsx(Text, { color: "inverse-muted", className: "tracking-widest", size: "sm", weight: "semibold", transform: "uppercase", children: "Seller Guide" })] }), _jsx(Heading, { color: "inverse", level: 1, className: "mb-2", mdSize: "3xl", size: "2xl", weight: "bold", children: storeName }),
|
|
99
|
+
return (_jsxs(Stack, { gap: "xl", padding: "b-2xl", children: [_jsx(Section, { overflow: "hidden", className: "border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", rounded: "2xl", shadow: "sm", children: _jsxs(Div, { paddingX: "x-md-xl", paddingY: "y-xl", className: "[background:linear-gradient(135deg,var(--appkit-color-primary-700)_0%,var(--appkit-color-cobalt)_60%,var(--appkit-color-secondary-400)_100%)]", children: [_jsxs(Row, { className: "mb-3", align: "center", gap: "3", children: [_jsx(Row, { className: "flex-shrink-0 w-10 h-10", surface: "default", align: "center", justify: "center", rounded: "xl", children: _jsx(BookOpen, { className: "w-5 h-5 text-white" }) }), _jsx(Text, { color: "inverse-muted", className: "tracking-widest", size: "sm", weight: "semibold", transform: "uppercase", children: "Seller Guide" })] }), _jsx(Heading, { color: "inverse", level: 1, className: "mb-2", mdSize: "3xl", size: "2xl", weight: "bold", children: storeName }), _jsxs(Text, { color: "inverse-muted", size: "base", children: ["Everything you need to know about selling on ", getBrandName(), "."] })] }) }), _jsxs(Div, { layout: "grid", gap: "8", className: "grid-cols-1 lg:grid-cols-3", children: [_jsxs(Stack, { className: "lg:col-span-2", gap: "md", children: [_jsx(Heading, { level: 2, className: "text-[var(--appkit-color-text)]", size: "lg", weight: "semibold", children: "Guides" }), _jsx(Grid, { cols: "cardsWide", gap: "md", children: GUIDE_CARDS.map(({ Icon, title, description, href }) => (_jsxs(TextLink, { variant: "bare", href: href, rounded: "xl", layout: "flex-col", gap: "md", paddingX: "lg", paddingY: "lg", shadow: "hover-md", className: "group border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] hover:border-[var(--appkit-color-primary)] transition-all", children: [_jsxs(Row, { align: "center", gap: "3", children: [_jsx(Row, { className: "flex-shrink-0 w-9 h-9 [background:linear-gradient(135deg,var(--appkit-color-primary-700)_0%,var(--appkit-color-cobalt)_100%)]", align: "center", justify: "center", rounded: "lg", children: _jsx(Icon, { className: "w-4 h-4 text-white" }) }), _jsx(Text, { className: "text-[var(--appkit-color-text)] group-hover:text-[var(--appkit-color-primary)] transition-colors", weight: "semibold", children: title })] }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)] leading-relaxed", size: "sm", children: description }), _jsx(Text, { className: "text-[var(--appkit-color-primary)] mt-auto", size: "xs", weight: "medium", children: "Read guide \u2192" })] }, title))) }), _jsx(SellerStandingPanel, { standing: store?.standing, tier: store?.sellerTier, showProgress: true, heading: "Your Trusted Seller standing" })] }), _jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 2, className: "text-[var(--appkit-color-text)]", size: "lg", weight: "semibold", children: "Getting started" }), _jsxs(Div, { overflow: "hidden", className: "border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", rounded: "xl", children: [_jsxs(Div, { className: "px-[1.25rem] pt-[1.25rem] border-b border-[var(--appkit-color-border)]", padding: "b-md", children: [_jsxs(Row, { className: "mb-2", align: "center", justify: "between", children: [_jsxs(Text, { className: "text-[var(--appkit-color-text)]", size: "sm", weight: "medium", children: [completedCount, " of ", steps.length, " complete"] }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: [Math.round((completedCount / steps.length) * 100), "%"] })] }), _jsx(Progress, { value: completedCount, max: steps.length, size: "sm", variant: "gradient" })] }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)]", children: steps.map(({ label, done, href }) => (_jsxs(TextLink, { variant: "bare", href: done ? "#" : href, "aria-disabled": done, layout: "flex", align: "center", gap: "sm", paddingX: "lg", paddingY: "sm-tall", size: "sm", className: `transition-colors ${done
|
|
99
100
|
? "cursor-default"
|
|
100
101
|
: "hover:bg-[var(--appkit-color-border)]/30"}`, children: [done ? (_jsx(CheckCircle2, { className: CLS_CHECK_ICON })) : (_jsx(Circle, { className: "w-5 h-5 flex-shrink-0 text-[var(--appkit-color-text-muted)]" })), _jsx(Text, { className: `leading-snug ${done
|
|
101
102
|
? "line-through text-[var(--appkit-color-text-muted)]"
|
|
@@ -8,6 +8,7 @@ import { Li, Section, Table, Tbody, Td, Th, Thead, Tr, Ul } from "../../../ui/co
|
|
|
8
8
|
import { Stack } from "../../../ui/components/Layout.js";
|
|
9
9
|
import { TextLink } from "../../../ui/components/TextLink.js";
|
|
10
10
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
11
|
+
import { getBrandName } from "../../../core/brand-resolver.js";
|
|
11
12
|
const SUBHEADING = "mb-1 font-semibold text-[var(--appkit-color-text)]";
|
|
12
13
|
const LIST_DISC = "list-disc leading-relaxed";
|
|
13
14
|
// -- Section wrapper -----------------------------------------------------------
|
|
@@ -134,7 +135,7 @@ const MEDIA_TIPS = [
|
|
|
134
135
|
{ label: "Image ratio", detail: "1:1 (square) for card thumbnails; 4:3 works well for product detail gallery." },
|
|
135
136
|
{ label: "Minimum size", detail: "800 × 800 px recommended. Below 400 × 400 px will look blurry on Retina screens." },
|
|
136
137
|
{ label: "Max file size", detail: "50 MB per image. Prefer JPG for photos; PNG for packaging or cards with text." },
|
|
137
|
-
{ label: "Watermark", detail:
|
|
138
|
+
{ label: "Watermark", detail: `${getBrandName()} adds a watermark automatically. Do not add your own — double watermarks look unprofessional.` },
|
|
138
139
|
{ label: "Order", detail: "Drag images to reorder after upload. The first image is the card thumbnail used in search results." },
|
|
139
140
|
{ label: "Video", detail: "Add a YouTube video ID to embed an unboxing or showcase. Not a replacement for photos." },
|
|
140
141
|
];
|
|
@@ -9,6 +9,7 @@ import { Row, Stack } from "../../../ui/components/Layout.js";
|
|
|
9
9
|
import { TextLink } from "../../../ui/components/TextLink.js";
|
|
10
10
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
11
11
|
import { resolvePublishedValues } from "../../../_internal/shared/content/published-values.js";
|
|
12
|
+
import { getBrandName } from "../../../core/brand-resolver.js";
|
|
12
13
|
/* Phase 9 — one definition of the refund timeline. */
|
|
13
14
|
const { refundBusinessDaysMin: REFUND_DAYS_MIN, refundBusinessDaysMax: REFUND_DAYS_MAX } = resolvePublishedValues();
|
|
14
15
|
const SUBHEADING = "mb-1 font-semibold text-[var(--appkit-color-text)]";
|
|
@@ -96,11 +97,11 @@ function CarriersSection() {
|
|
|
96
97
|
}
|
|
97
98
|
// -- Section 4: Cancellations -------------------------------------------------
|
|
98
99
|
function CancellationsSection() {
|
|
99
|
-
return (_jsxs(Div, { className: SECTION_BODY, children: [_jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "Who can cancel" }), _jsxs(Ul, { spacing: "tight", indent: "lg", children: [_jsxs(Li, { className: LIST_DISC, children: [_jsx(Span, { weight: "bold", children: "Buyer" }), " \u2014 can self-cancel any time before the order reaches SHIPPED status."] }), _jsxs(Li, { className: LIST_DISC, children: [_jsx(Span, { weight: "bold", children: "Seller (you)" }), " \u2014 can cancel any time before SHIPPED (e.g. item found damaged during packaging)."] }), _jsxs(Li, { className: LIST_DISC, children: [
|
|
100
|
+
return (_jsxs(Div, { className: SECTION_BODY, children: [_jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "Who can cancel" }), _jsxs(Ul, { spacing: "tight", indent: "lg", children: [_jsxs(Li, { className: LIST_DISC, children: [_jsx(Span, { weight: "bold", children: "Buyer" }), " \u2014 can self-cancel any time before the order reaches SHIPPED status."] }), _jsxs(Li, { className: LIST_DISC, children: [_jsx(Span, { weight: "bold", children: "Seller (you)" }), " \u2014 can cancel any time before SHIPPED (e.g. item found damaged during packaging)."] }), _jsxs(Li, { className: LIST_DISC, children: [_jsxs(Span, { weight: "bold", children: [getBrandName(), " admin"] }), " \u2014 can force-cancel at any status for policy violations or unresolved disputes."] })] })] }), _jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "Refund timeline" }), _jsxs(Text, { className: "leading-relaxed", children: ["Cancellation triggers an automatic Razorpay refund. Funds typically reach the buyer in", _jsx(Span, { weight: "bold", children: " 3\u20137 business days" }), " depending on the payment method (UPI is usually 1\u20132 business days)."] })] }), _jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "Already shipped?" }), _jsx(Text, { className: "leading-relaxed", children: "If you have already dispatched the item but the order is still PROCESSING (tracking not yet entered), enter the tracking number and mark it SHIPPED first \u2014 then contact the buyer via order notes. Do not cancel after shipping; work with the buyer to return the item through the normal return flow." })] }), _jsx(Alert, { variant: "warning", title: "Repeated cancellations affect your rating", children: "Cancelling more than 5% of your orders per month will reduce your store rating and may result in listing restrictions. Only cancel if you genuinely cannot fulfil \u2014 never as a price-change workaround." })] }));
|
|
100
101
|
}
|
|
101
102
|
// -- Section 5: Return requests -----------------------------------------------
|
|
102
103
|
function ReturnsSection() {
|
|
103
|
-
return (_jsxs(Div, { className: SECTION_BODY, children: [_jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "When a buyer can raise a return" }), _jsxs(Text, { className: "leading-relaxed", children: ["Buyers can open a return request within the ", _jsx(Span, { weight: "bold", children: "return window" }), " after the order is marked DELIVERED. The default return window is 7 days. Stores with the ", _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "extended_return_window" }), " capability can offer 30 days."] })] }), _jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "What you see" }), _jsxs(Ul, { spacing: "tight", indent: "lg", children: [_jsx(Li, { className: LIST_DISC, children: "The buyer's reason for return (pre-defined category + optional description)." }), _jsx(Li, { className: LIST_DISC, children: "Photos of the item as received." }),
|
|
104
|
+
return (_jsxs(Div, { className: SECTION_BODY, children: [_jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "When a buyer can raise a return" }), _jsxs(Text, { className: "leading-relaxed", children: ["Buyers can open a return request within the ", _jsx(Span, { weight: "bold", children: "return window" }), " after the order is marked DELIVERED. The default return window is 7 days. Stores with the ", _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "extended_return_window" }), " capability can offer 30 days."] })] }), _jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "What you see" }), _jsxs(Ul, { spacing: "tight", indent: "lg", children: [_jsx(Li, { className: LIST_DISC, children: "The buyer's reason for return (pre-defined category + optional description)." }), _jsx(Li, { className: LIST_DISC, children: "Photos of the item as received." }), _jsxs(Li, { className: LIST_DISC, children: ["A 3-day SLA to respond before ", getBrandName(), " mediates."] })] })] }), _jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "Accepting a return" }), _jsx(Text, { className: "leading-relaxed", children: "Click \"Accept return\" in the order detail. The buyer will receive a return shipping label or instructions. Once the item is received by you and you confirm it, the refund is triggered automatically via Razorpay. The payout for this order is withheld." })] }), _jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "Rejecting a return" }), _jsxs(Text, { className: "leading-relaxed", children: ["You can reject a return request with a reason. If the buyer disagrees, they can escalate to ", getBrandName(), " support.", getBrandName(), " will mediate \u2014 if the evidence supports the buyer, the return will be approved regardless of your rejection. Unjustified rejections count against your seller rating."] })] })] }));
|
|
104
105
|
}
|
|
105
106
|
// -- Section 6: Auction orders ------------------------------------------------
|
|
106
107
|
function AuctionOrdersSection() {
|
|
@@ -110,7 +111,7 @@ function AuctionOrdersSection() {
|
|
|
110
111
|
const FAQS = [
|
|
111
112
|
{
|
|
112
113
|
q: "What if the buyer claims non-delivery but tracking shows delivered?",
|
|
113
|
-
a:
|
|
114
|
+
a: `Do not cancel immediately. Ask the buyer to check with neighbours and at the nearest post office or courier depot. If after 5 days the parcel is still missing, contact the carrier to file a trace. If the trace confirms loss, you may need to refund — document everything and contact ${getBrandName()} support if needed.`,
|
|
114
115
|
},
|
|
115
116
|
{
|
|
116
117
|
q: "Can I cancel an order after I've already shipped it?",
|
|
@@ -122,7 +123,7 @@ const FAQS = [
|
|
|
122
123
|
},
|
|
123
124
|
{
|
|
124
125
|
q: "How do I handle a partial order if I sold multiple items?",
|
|
125
|
-
a:
|
|
126
|
+
a: `Currently each product listing creates a separate order. If you sold a bundle, the bundle order must be fulfilled as a single shipment. If one item in a bundle is unavailable, contact the buyer and ${getBrandName()} support — partial fulfilment is not supported in the standard flow.`,
|
|
126
127
|
},
|
|
127
128
|
];
|
|
128
129
|
function CommonFAQs() {
|
|
@@ -8,6 +8,7 @@ import { Heading, Span, Text } from "../../../ui/components/Typography.js";
|
|
|
8
8
|
import { Stack } from "../../../ui/components/Layout.js";
|
|
9
9
|
import { TextLink } from "../../../ui/components/TextLink.js";
|
|
10
10
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
11
|
+
import { getBrandSupportEmail } from "../../../core/brand-resolver.js";
|
|
11
12
|
const SUBHEADING = "mb-1 font-semibold text-[var(--appkit-color-text)]";
|
|
12
13
|
const LIST_DISC = "list-disc leading-relaxed";
|
|
13
14
|
const SECTION_BODY = "space-y-4 text-[length:var(--appkit-text-sm)] text-[var(--appkit-color-text-muted)]";
|
|
@@ -52,7 +53,7 @@ function VisibilityStatesSection() {
|
|
|
52
53
|
},
|
|
53
54
|
{
|
|
54
55
|
state: "Suspended",
|
|
55
|
-
note:
|
|
56
|
+
note: `Admin action. All listings hidden. Store profile shows 'Unavailable' banner. Contact ${getBrandSupportEmail()} to resolve.`,
|
|
56
57
|
},
|
|
57
58
|
];
|
|
58
59
|
return (_jsx(Stack, { gap: "3", children: STATES.map(({ state, note }) => (_jsxs(Row, { textSize: "sm", gap: "3", children: [_jsx(Text, { className: "shrink-0 w-32 text-[var(--appkit-color-text)]", weight: "semibold", children: state }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)] leading-relaxed", children: note })] }, state))) }));
|
|
@@ -152,7 +152,7 @@ export declare const supportTicketFirestoreSchema: z.ZodObject<{
|
|
|
152
152
|
status: "open" | "closed" | "in_progress" | "waiting_on_user" | "resolved";
|
|
153
153
|
id: string;
|
|
154
154
|
description: string;
|
|
155
|
-
category: "general" | "
|
|
155
|
+
category: "general" | "order_issue" | "billing_payment" | "account" | "listing_dispute" | "scam_report" | "refund_request" | "auction_dispute" | "store_change_request" | "order_modification_request" | "unban_request";
|
|
156
156
|
createdAt: string | Date | z.objectOutputType<{
|
|
157
157
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
158
158
|
}, z.ZodTypeAny, "passthrough">;
|
|
@@ -195,7 +195,7 @@ export declare const supportTicketFirestoreSchema: z.ZodObject<{
|
|
|
195
195
|
status: "open" | "closed" | "in_progress" | "waiting_on_user" | "resolved";
|
|
196
196
|
id: string;
|
|
197
197
|
description: string;
|
|
198
|
-
category: "general" | "
|
|
198
|
+
category: "general" | "order_issue" | "billing_payment" | "account" | "listing_dispute" | "scam_report" | "refund_request" | "auction_dispute" | "store_change_request" | "order_modification_request" | "unban_request";
|
|
199
199
|
createdAt: string | Date | z.objectInputType<{
|
|
200
200
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
201
201
|
}, z.ZodTypeAny, "passthrough">;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* 1. Almost no small seller will create a Meta app and mint a system user
|
|
13
13
|
* token. A setting nobody can complete is a feature nobody has.
|
|
14
|
-
* 2. It made
|
|
14
|
+
* 2. It made {getBrandName()} the custodian of a live Meta credential per seller.
|
|
15
15
|
* `StoreRepository.mapDoc` decrypted that token on EVERY read, so any
|
|
16
16
|
* code path that forgot to project was handling plaintext — a gap
|
|
17
17
|
* CLAUDE.md flags as deserving its own session.
|