@mohasinac/appkit 3.2.5 → 3.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/dist/_internal/client/features/layout/SidebarCollapseToggle.js +1 -1
  2. package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +3 -3
  3. package/dist/_internal/server/features/products/data.d.ts +2 -1
  4. package/dist/_internal/server/jobs/core/countersReconcile.js +2 -1
  5. package/dist/_internal/shared/actions/action-registry.js +62 -0
  6. package/dist/_internal/shared/features/products/types.d.ts +3 -2
  7. package/dist/client.d.ts +2 -0
  8. package/dist/client.js +1 -0
  9. package/dist/constants/api-endpoints.d.ts +12 -0
  10. package/dist/constants/api-endpoints.js +4 -0
  11. package/dist/constants/field-names.d.ts +2 -0
  12. package/dist/constants/field-names.js +2 -0
  13. package/dist/features/about/components/PublicProfileView.js +8 -5
  14. package/dist/features/account/components/UserOffersPanel.js +0 -1
  15. package/dist/features/addresses/repository/addresses.repository.d.ts +14 -1
  16. package/dist/features/addresses/repository/addresses.repository.js +122 -1
  17. package/dist/features/addresses/schemas/firestore.d.ts +22 -3
  18. package/dist/features/addresses/schemas/firestore.js +25 -0
  19. package/dist/features/addresses/schemas/index.d.ts +2 -2
  20. package/dist/features/admin/components/AdminAddressClustersView.d.ts +5 -0
  21. package/dist/features/admin/components/AdminAddressClustersView.js +38 -0
  22. package/dist/features/admin/components/AdminAddressesView.d.ts +5 -0
  23. package/dist/features/admin/components/AdminAddressesView.js +137 -0
  24. package/dist/features/admin/components/AdminCarouselEditorView.js +1 -4
  25. package/dist/features/admin/components/AdminFulfillmentView.js +2 -3
  26. package/dist/features/admin/components/AdminOrderEditorView.js +1 -3
  27. package/dist/features/admin/components/AdminPaymentClustersView.d.ts +5 -0
  28. package/dist/features/admin/components/AdminPaymentClustersView.js +37 -0
  29. package/dist/features/admin/components/AdminPaymentMethodsView.d.ts +5 -0
  30. package/dist/features/admin/components/AdminPaymentMethodsView.js +130 -0
  31. package/dist/features/admin/components/AdminSiteSettingsView.js +2 -2
  32. package/dist/features/admin/components/index.d.ts +8 -0
  33. package/dist/features/admin/components/index.js +4 -0
  34. package/dist/features/admin/schemas/firestore.d.ts +3 -5
  35. package/dist/features/admin/types/product.types.d.ts +2 -1
  36. package/dist/features/auth/actions/profile-actions.d.ts +3 -1
  37. package/dist/features/auth/actions/profile-actions.js +29 -2
  38. package/dist/features/auth/schemas/firestore.d.ts +8 -10
  39. package/dist/features/cart/schemas/index.d.ts +6 -6
  40. package/dist/features/categories/components/CategoryBundlesListing.js +1 -3
  41. package/dist/features/checkout/schemas/firestore.d.ts +4 -4
  42. package/dist/features/copilot/components/AdminCopilotView.js +4 -4
  43. package/dist/features/events/schemas/firestore.d.ts +2 -2
  44. package/dist/features/grouped/schemas/firestore.d.ts +2 -4
  45. package/dist/features/history/hooks/useHistory.js +3 -2
  46. package/dist/features/homepage/actions/homepage-section-actions.d.ts +6 -6
  47. package/dist/features/homepage/actions/homepage-section-actions.js +2 -2
  48. package/dist/features/homepage/components/PromoGrid.js +1 -1
  49. package/dist/features/homepage/schemas/firestore.d.ts +15 -14
  50. package/dist/features/layout/AppLayoutShell.js +3 -18
  51. package/dist/features/layout/background-seed-defaults.d.ts +19 -0
  52. package/dist/features/layout/background-seed-defaults.js +14 -0
  53. package/dist/features/lottery/schemas/firestore.d.ts +2 -2
  54. package/dist/features/messages/hooks/useConversation.js +5 -13
  55. package/dist/features/messages/hooks/useConversations.js +0 -1
  56. package/dist/features/messages/schemas/firestore.d.ts +2 -2
  57. package/dist/features/orders/components/RefundHistoryTable.js +1 -3
  58. package/dist/features/orders/schemas/firestore.d.ts +2 -1
  59. package/dist/features/orders/schemas/index.d.ts +11 -7
  60. package/dist/features/orders/schemas/index.js +1 -1
  61. package/dist/features/payments/repository/saved-payment-methods.repository.d.ts +48 -0
  62. package/dist/features/payments/repository/saved-payment-methods.repository.js +236 -0
  63. package/dist/features/payments/schemas/index.d.ts +1 -0
  64. package/dist/features/payments/schemas/index.js +1 -0
  65. package/dist/features/payments/schemas/saved-methods-firestore.d.ts +63 -0
  66. package/dist/features/payments/schemas/saved-methods-firestore.js +60 -0
  67. package/dist/features/payments/server.d.ts +2 -0
  68. package/dist/features/payments/server.js +2 -0
  69. package/dist/features/products/actions/product-actions.d.ts +2 -1
  70. package/dist/features/products/api/[id]/route.js +1 -1
  71. package/dist/features/products/api/route.js +1 -1
  72. package/dist/features/products/components/CompareOverlay.d.ts +18 -1
  73. package/dist/features/products/components/CompareOverlay.js +0 -16
  74. package/dist/features/products/components/ProductDetailActions.js +2 -1
  75. package/dist/features/products/components/ShowGroupSection.js +1 -3
  76. package/dist/features/products/repository/product-features.repository.js +1 -1
  77. package/dist/features/products/schemas/firestore.d.ts +3 -6
  78. package/dist/features/promotions/actions/coupon-actions.d.ts +2 -1
  79. package/dist/features/promotions/hooks/useCouponValidate.d.ts +2 -1
  80. package/dist/features/promotions/repository/coupons.repository.d.ts +2 -1
  81. package/dist/features/promotions/schemas/firestore.d.ts +4 -4
  82. package/dist/features/promotions/schemas/index.d.ts +6 -6
  83. package/dist/features/reviews/repository/reviews.repository.d.ts +12 -0
  84. package/dist/features/reviews/repository/reviews.repository.js +24 -0
  85. package/dist/features/reviews/schemas/firestore.d.ts +5 -1
  86. package/dist/features/reviews/schemas/firestore.js +2 -0
  87. package/dist/features/reviews/schemas/index.d.ts +6 -6
  88. package/dist/features/scams/components/ScamRegistryView.js +2 -2
  89. package/dist/features/scams/schemas/firestore.d.ts +6 -6
  90. package/dist/features/search/types/index.d.ts +3 -2
  91. package/dist/features/search/types/index.js +0 -1
  92. package/dist/features/seller/components/FulfillmentView.js +2 -3
  93. package/dist/features/seller/components/PrintCenterView.js +1 -3
  94. package/dist/features/seller/components/SellerAddressesView.js +7 -4
  95. package/dist/features/seller/components/SellerOffersPanel.js +0 -1
  96. package/dist/features/seller/components/SellerReviewsView.js +0 -1
  97. package/dist/features/seller/schemas/firestore.d.ts +2 -4
  98. package/dist/features/server-errors/schemas/firestore.d.ts +2 -2
  99. package/dist/features/store-extensions/schemas/firestore.d.ts +2 -2
  100. package/dist/features/stores/components/StoreScopedSearch.js +2 -4
  101. package/dist/features/stores/repository/store.repository.js +3 -1
  102. package/dist/features/stores/types/index.d.ts +2 -1
  103. package/dist/features/stores/types/index.js +0 -1
  104. package/dist/features/support/schemas/firestore.d.ts +2 -2
  105. package/dist/features/support/schemas/index.d.ts +8 -8
  106. package/dist/features/wishlist/schemas/index.d.ts +2 -2
  107. package/dist/features/wishlist/types/index.d.ts +2 -1
  108. package/dist/index.d.ts +9 -0
  109. package/dist/index.js +10 -0
  110. package/dist/next/routing/route-map.d.ts +8 -0
  111. package/dist/next/routing/route-map.js +4 -0
  112. package/dist/providers/auth-firebase/provider.js +3 -1
  113. package/dist/providers/auth-firebase/session.js +3 -1
  114. package/dist/repositories/index.d.ts +1 -0
  115. package/dist/repositories/index.js +1 -0
  116. package/dist/schemas/registry.d.ts +66 -66
  117. package/dist/schemas/webhooks/razorpay.d.ts +50 -50
  118. package/dist/security/index.d.ts +1 -1
  119. package/dist/security/index.js +1 -1
  120. package/dist/security/pii-schemas.d.ts +2 -0
  121. package/dist/security/pii-schemas.js +2 -0
  122. package/dist/seo/json-ld.d.ts +2 -1
  123. package/dist/styles.css +1 -1
  124. package/dist/tailwind-utilities.css +1 -1
  125. package/dist/ui/components/Form.d.ts +3 -1
  126. package/dist/ui/components/Form.js +2 -1
  127. package/dist/ui/components/Radio.d.ts +7 -0
  128. package/dist/ui/components/Radio.js +7 -0
  129. package/dist/ui/components/surface-tokens.d.ts +4 -0
  130. package/dist/ui/components/surface-tokens.js +24 -0
  131. package/dist/ui/index.d.ts +2 -2
  132. package/dist/ui/index.js +1 -1
  133. package/dist/ui/rich-text/RichText.js +0 -1
  134. package/package.json +5 -2
@@ -0,0 +1,14 @@
1
+ // Seed defaults for siteSettings.theme.background — Firestore data values, not CSS styling.
2
+ // These are the first-boot theme configuration and are exempt from the hex-tokens audit.
3
+ // Override via the admin theme editor (Site Settings → Themes).
4
+ export const SEED_LIGHT_BG = {
5
+ type: "color",
6
+ value: "#f9fafb",
7
+ overlay: { enabled: false, color: "#000000", opacity: 0 },
8
+ };
9
+ export const SEED_DARK_BG = {
10
+ type: "color",
11
+ value: "#030712",
12
+ overlay: { enabled: false, color: "#000000", opacity: 0 },
13
+ };
14
+ export const OVERLAY_FALLBACK_COLOR = "#000000";
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import type { LotteryEntryStatus } from "../types";
5
5
  export declare const LOTTERY_ENTRIES_COLLECTION: "lotteryEntries";
6
- export interface LotteryEntryDocument {
6
+ export type LotteryEntryDocument = {
7
7
  id: string;
8
8
  sourceType: "event" | "product";
9
9
  /** Set when sourceType === "event" */
@@ -33,7 +33,7 @@ export interface LotteryEntryDocument {
33
33
  flaggedAt?: Date;
34
34
  submittedAt: Date;
35
35
  drawnAt?: Date;
36
- }
36
+ };
37
37
  export declare const LOTTERY_ENTRY_INDEXED_FIELDS: readonly ["sourceType", "eventId", "productId", "userId", "status", "submittedAt", "userLotteryNumber"];
38
38
  /** Canonical field name constants for lotteryEntries collection — use instead of hardcoded strings. */
39
39
  export declare const LOTTERY_ENTRY_FIELDS: {
@@ -51,7 +51,6 @@ export function useConversation(conversationId) {
51
51
  setConversation(await fetchDetail(conversationId));
52
52
  }
53
53
  catch (e) {
54
- // toast-intentionally-silent: background refetch, error surfaced via error state
55
54
  void normalizeError(e);
56
55
  setError(e instanceof Error ? e : new Error(String(e)));
57
56
  }
@@ -102,19 +101,12 @@ export function useConversation(conversationId) {
102
101
  // the ping path isn't subscribed (e.g. provider not registered).
103
102
  await refetch();
104
103
  }, [conversationId, refetch]);
105
- const markRead = useCallback(async () => {
104
+ const markRead = useCallback(() => {
106
105
  if (!conversationId)
107
- return;
108
- try {
109
- await fetch(READ_ENDPOINT(conversationId), {
110
- method: "POST",
111
- credentials: "include",
112
- });
113
- await refetch();
114
- }
115
- catch {
116
- // toast-intentionally-silent: read-receipt is best-effort, non-critical
117
- }
106
+ return Promise.resolve();
107
+ return fetch(READ_ENDPOINT(conversationId), { method: "POST", credentials: "include" })
108
+ .then(() => void refetch())
109
+ .catch((err) => void normalizeError(err));
118
110
  }, [conversationId, refetch]);
119
111
  return {
120
112
  conversation,
@@ -31,7 +31,6 @@ export function useConversations(userId) {
31
31
  setConversations(await fetchList());
32
32
  }
33
33
  catch (e) {
34
- // toast-intentionally-silent: background refetch, error surfaced via error state
35
34
  void normalizeError(e);
36
35
  setError(e instanceof Error ? e : new Error(String(e)));
37
36
  }
@@ -6,7 +6,7 @@
6
6
  * a subcollection when the thread exceeds ~200 messages.
7
7
  */
8
8
  import type { BaseDocument } from "../../../_internal/shared/types/base-document";
9
- export interface ConversationMessage {
9
+ export type ConversationMessage = {
10
10
  id: string;
11
11
  senderId: string;
12
12
  senderRole: "buyer" | "seller";
@@ -14,7 +14,7 @@ export interface ConversationMessage {
14
14
  isRead: boolean;
15
15
  sentAt: Date;
16
16
  attachments?: string[];
17
- }
17
+ };
18
18
  export interface ConversationDocument extends BaseDocument {
19
19
  buyerId: string;
20
20
  buyerDisplayName: string;
@@ -21,7 +21,5 @@ export function RefundHistoryTable({ order, className = "" }) {
21
21
  const currency = order.currency ?? "INR";
22
22
  if (events.length === 0 && order.contestable !== false)
23
23
  return null;
24
- return (_jsxs(Div, { className: `${className}`, rounded: "xl", surface: "default", border: "default", children: [order.contestable === false && (
25
- // audit-inline-style-ok: asymmetric border-radius (top corners only); no appkit rounded= variant covers partial corners
26
- _jsx(Div, { surface: "warning-surface", padding: "inline", style: { borderTopLeftRadius: "0.75rem", borderTopRightRadius: "0.75rem" }, children: _jsxs(Row, { gap: "sm", align: "center", children: [_jsx(WarningIcon, {}), _jsx(Text, { size: "sm", weight: "semibold", className: "text-warning", children: REFUND_COPY.history.nonContestableBanner })] }) })), events.length > 0 && (_jsxs(Div, { className: `${__P.p4}`, children: [_jsx(Heading, { level: 4, className: "mb-3", color: "muted", size: "sm", weight: "semibold", children: REFUND_COPY.history.heading }), _jsx(Stack, { gap: "none", children: events.map((e) => (_jsx(RefundRow, { event: e, currency: currency }, e.refundId))) })] }))] }));
24
+ return (_jsxs(Div, { className: `${className}`, rounded: "xl", surface: "default", border: "default", children: [order.contestable === false && (_jsx(Div, { surface: "warning-surface", padding: "inline", roundedTop: "lg", children: _jsxs(Row, { gap: "sm", align: "center", children: [_jsx(WarningIcon, {}), _jsx(Text, { size: "sm", weight: "semibold", className: "text-warning", children: REFUND_COPY.history.nonContestableBanner })] }) })), events.length > 0 && (_jsxs(Div, { className: `${__P.p4}`, children: [_jsx(Heading, { level: 4, className: "mb-3", color: "muted", size: "sm", weight: "semibold", children: REFUND_COPY.history.heading }), _jsx(Stack, { gap: "none", children: events.map((e) => (_jsx(RefundRow, { event: e, currency: currency }, e.refundId))) })] }))] }));
27
25
  }
@@ -4,6 +4,7 @@
4
4
  import type { OrderStatus, PaymentStatus } from "../types";
5
5
  import type { OrderType } from "../utils/order-splitter";
6
6
  import type { BaseDocument } from "../../../_internal/shared/types/base-document";
7
+ import type { ListingType } from "../../products/types";
7
8
  export type ShippingMethod = "custom" | "shiprocket";
8
9
  export type RefundType = "full" | "partial";
9
10
  export type OrderPayoutStatus = "eligible" | "requested" | "paid";
@@ -58,7 +59,7 @@ export interface OrderDocumentItem {
58
59
  unitPrice: number;
59
60
  totalPrice: number;
60
61
  /** SB8-F â€" set when the item is a prize-draw entry; drives the reveals badge. */
61
- listingType?: "standard" | "auction" | "pre-order" | "prize-draw" | "classified" | "digital-code" | "live";
62
+ listingType?: ListingType;
62
63
  /** SB8-F â€" per-item reveal status; flips through pending â†' open â†' revealed/closed. */
63
64
  prizeRevealStatus?: "pending" | "open" | "closed" | "revealed";
64
65
  /** SB8-F â€" ISO timestamp; deadline by which the buyer must claim the prize. */
@@ -417,10 +417,10 @@ export declare const orderFirestoreSchema: z.ZodObject<{
417
417
  updatedAt: string | Date | z.objectOutputType<{
418
418
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
419
419
  }, z.ZodTypeAny, "passthrough">;
420
+ userId: string;
420
421
  productId: string;
421
422
  quantity: number;
422
423
  productTitle: string;
423
- userId: string;
424
424
  userEmail: string;
425
425
  paymentStatus: "pending" | "paid" | "failed" | "processing" | "refunded" | "partial_refund";
426
426
  orderDate: string | Date | z.objectOutputType<{
@@ -546,10 +546,10 @@ export declare const orderFirestoreSchema: z.ZodObject<{
546
546
  updatedAt: string | Date | z.objectInputType<{
547
547
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
548
548
  }, z.ZodTypeAny, "passthrough">;
549
+ userId: string;
549
550
  productId: string;
550
551
  quantity: number;
551
552
  productTitle: string;
552
- userId: string;
553
553
  userEmail: string;
554
554
  paymentStatus: "pending" | "paid" | "failed" | "processing" | "refunded" | "partial_refund";
555
555
  orderDate: string | Date | z.objectInputType<{
@@ -756,7 +756,7 @@ export declare const orderSchema: z.ZodObject<{
756
756
  storeId?: string | undefined;
757
757
  attributes?: Record<string, string> | undefined;
758
758
  }>, "many">;
759
- address: z.ZodRecord<z.ZodString, z.ZodUnknown>;
759
+ address: z.ZodObject<{}, "passthrough", z.ZodTypeAny, z.objectOutputType<{}, z.ZodTypeAny, "passthrough">, z.objectInputType<{}, z.ZodTypeAny, "passthrough">>;
760
760
  orderStatus: z.ZodEnum<["pending", "confirmed", "processing", "shipped", "delivered", "cancelled", "refunded", "return_requested", "returned"]>;
761
761
  paymentStatus: z.ZodString;
762
762
  paymentGateway: z.ZodOptional<z.ZodString>;
@@ -791,7 +791,10 @@ export declare const orderSchema: z.ZodObject<{
791
791
  }, "strip", z.ZodTypeAny, {
792
792
  currency: string;
793
793
  id: string;
794
- address: Record<string, unknown>;
794
+ address: {} & {
795
+ [k: string]: unknown;
796
+ };
797
+ userId: string;
795
798
  items: {
796
799
  title: string;
797
800
  price: number;
@@ -803,7 +806,6 @@ export declare const orderSchema: z.ZodObject<{
803
806
  attributes?: Record<string, string> | undefined;
804
807
  }[];
805
808
  total: number;
806
- userId: string;
807
809
  paymentStatus: string;
808
810
  subtotal: number;
809
811
  orderStatus: "cancelled" | "pending" | "processing" | "refunded" | "confirmed" | "shipped" | "delivered" | "return_requested" | "returned";
@@ -826,7 +828,10 @@ export declare const orderSchema: z.ZodObject<{
826
828
  }, {
827
829
  currency: string;
828
830
  id: string;
829
- address: Record<string, unknown>;
831
+ address: {} & {
832
+ [k: string]: unknown;
833
+ };
834
+ userId: string;
830
835
  items: {
831
836
  title: string;
832
837
  price: number;
@@ -838,7 +843,6 @@ export declare const orderSchema: z.ZodObject<{
838
843
  attributes?: Record<string, string> | undefined;
839
844
  }[];
840
845
  total: number;
841
- userId: string;
842
846
  paymentStatus: string;
843
847
  subtotal: number;
844
848
  orderStatus: "cancelled" | "pending" | "processing" | "refunded" | "confirmed" | "shipped" | "delivered" | "return_requested" | "returned";
@@ -194,7 +194,7 @@ export const orderSchema = z.object({
194
194
  userId: z.string(),
195
195
  items: z.array(orderItemSchema),
196
196
  // address shape is intentionally open to allow extending apps
197
- address: z.record(z.string(), z.unknown()), // audit-z-any-ok: extending-apps extend this with their own address schema
197
+ address: z.object({}).passthrough(),
198
198
  orderStatus: orderStatusSchema,
199
199
  paymentStatus: z.string(),
200
200
  paymentGateway: z.string().optional(),
@@ -0,0 +1,48 @@
1
+ /**
2
+ * SavedPaymentMethodsRepository
3
+ *
4
+ * Persists user payment identifiers (UPI VPAs, card tokens, etc.) for
5
+ * checkout pre-fill and cross-account fraud detection.
6
+ *
7
+ * PII: `identifier` is encrypted at rest (AES-256-GCM) — never returned
8
+ * to clients. `identifierHash` is unencrypted SHA-256 for cross-account
9
+ * dedup queries. `displayLabel` is pre-masked and safe to display.
10
+ */
11
+ import { BaseRepository, type DocumentSnapshot } from "../../../providers/db-firebase";
12
+ import { type SavedPaymentMethodBanStatus, type SavedPaymentMethodCreateInput, type SavedPaymentMethodDocument } from "../schemas/saved-methods-firestore";
13
+ export declare class SavedPaymentMethodsRepository extends BaseRepository<SavedPaymentMethodDocument> {
14
+ constructor();
15
+ private decrypt;
16
+ private encrypt;
17
+ protected mapDoc<D = SavedPaymentMethodDocument>(snap: DocumentSnapshot): D;
18
+ createWithId(id: string, data: Partial<SavedPaymentMethodDocument>): Promise<SavedPaymentMethodDocument>;
19
+ update(id: string, data: Partial<SavedPaymentMethodDocument>): Promise<SavedPaymentMethodDocument>;
20
+ /** List all saved methods for a user. Never returns raw `identifier` — only `displayLabel`. */
21
+ listByUser(userId: string): Promise<SavedPaymentMethodDocument[]>;
22
+ /**
23
+ * Idempotent upsert by (userId + identifierHash).
24
+ * Computes hash + encrypts identifier before write.
25
+ * Updates lastUsedAt on re-add.
26
+ */
27
+ upsertForUser(userId: string, input: SavedPaymentMethodCreateInput): Promise<SavedPaymentMethodDocument>;
28
+ /** Cross-account lookup by identifierHash. Returns display-safe docs (identifier stripped). */
29
+ listByIdentifierHash(hash: string): Promise<SavedPaymentMethodDocument[]>;
30
+ /** Batch-ban all methods for a user. Used by hard-ban cascade. */
31
+ banAllForUser(userId: string, banData: {
32
+ banReason: string;
33
+ bannedBy: string;
34
+ }): Promise<number>;
35
+ /** Reverse auto-ban cascade on user unban. Leaves manually-banned methods untouched. */
36
+ unbanAutoForUser(userId: string): Promise<number>;
37
+ /** List by banStatus for admin view. Returns display-safe docs. */
38
+ listByBanStatus(banStatus: SavedPaymentMethodBanStatus, limit?: number, offset?: number): Promise<SavedPaymentMethodDocument[]>;
39
+ banById(id: string, banData: {
40
+ banReason: string;
41
+ bannedBy: string;
42
+ }): Promise<void>;
43
+ clearBanById(id: string): Promise<void>;
44
+ deleteForUser(userId: string, id: string): Promise<void>;
45
+ /** Public wrapper — compute identifier hash from outside the class (e.g. in API routes). */
46
+ computeIdentifierHash(type: string, identifier: string): string;
47
+ }
48
+ export declare const savedPaymentMethodsRepository: SavedPaymentMethodsRepository;
@@ -0,0 +1,236 @@
1
+ /**
2
+ * SavedPaymentMethodsRepository
3
+ *
4
+ * Persists user payment identifiers (UPI VPAs, card tokens, etc.) for
5
+ * checkout pre-fill and cross-account fraud detection.
6
+ *
7
+ * PII: `identifier` is encrypted at rest (AES-256-GCM) — never returned
8
+ * to clients. `identifierHash` is unencrypted SHA-256 for cross-account
9
+ * dedup queries. `displayLabel` is pre-masked and safe to display.
10
+ */
11
+ import { createHash } from "crypto";
12
+ import { normalizeError } from "../../../errors/normalize";
13
+ import { DatabaseError } from "../../../errors";
14
+ import { serverLogger } from "../../../monitoring";
15
+ import { BaseRepository, prepareForFirestore, } from "../../../providers/db-firebase";
16
+ import { PAYMENT_METHOD_PII_FIELDS, decryptPiiFields, encryptPiiFields, } from "../../../security";
17
+ import { SAVED_PAYMENT_METHOD_FIELDS, SAVED_PAYMENT_METHODS_COLLECTION, } from "../schemas/saved-methods-firestore";
18
+ function normaliseIdentifier(s) {
19
+ return s.toLowerCase().replace(/\s+/g, "").trim();
20
+ }
21
+ function computeIdentifierHash(type, identifier) {
22
+ return createHash("sha256")
23
+ .update(`${type}|${normaliseIdentifier(identifier)}`)
24
+ .digest("hex");
25
+ }
26
+ export class SavedPaymentMethodsRepository extends BaseRepository {
27
+ constructor() {
28
+ super(SAVED_PAYMENT_METHODS_COLLECTION);
29
+ }
30
+ decrypt(doc) {
31
+ return decryptPiiFields(doc, [...PAYMENT_METHOD_PII_FIELDS]);
32
+ }
33
+ encrypt(data) {
34
+ return encryptPiiFields(data, [...PAYMENT_METHOD_PII_FIELDS]);
35
+ }
36
+ mapDoc(snap) {
37
+ const raw = super.mapDoc(snap);
38
+ return this.decrypt(raw);
39
+ }
40
+ async createWithId(id, data) {
41
+ return super.createWithId(id, this.encrypt(data));
42
+ }
43
+ async update(id, data) {
44
+ return super.update(id, this.encrypt(data));
45
+ }
46
+ /** List all saved methods for a user. Never returns raw `identifier` — only `displayLabel`. */
47
+ async listByUser(userId) {
48
+ try {
49
+ const snap = await this.getCollection()
50
+ .where(SAVED_PAYMENT_METHOD_FIELDS.USER_ID, "==", userId)
51
+ .orderBy(SAVED_PAYMENT_METHOD_FIELDS.LAST_USED_AT, "desc")
52
+ .get();
53
+ return snap.docs.map((d) => {
54
+ const doc = this.mapDoc(d);
55
+ doc.identifier = ""; // never expose decrypted PII to caller
56
+ return doc;
57
+ });
58
+ }
59
+ catch (error) {
60
+ void normalizeError(error);
61
+ throw new DatabaseError(`Failed to list payment methods for user:${userId}`, error);
62
+ }
63
+ }
64
+ /**
65
+ * Idempotent upsert by (userId + identifierHash).
66
+ * Computes hash + encrypts identifier before write.
67
+ * Updates lastUsedAt on re-add.
68
+ */
69
+ async upsertForUser(userId, input) {
70
+ try {
71
+ const hash = computeIdentifierHash(input.type, input.identifier);
72
+ // Check for existing
73
+ const existing = await this.getCollection()
74
+ .where(SAVED_PAYMENT_METHOD_FIELDS.USER_ID, "==", userId)
75
+ .where(SAVED_PAYMENT_METHOD_FIELDS.IDENTIFIER_HASH, "==", hash)
76
+ .limit(1)
77
+ .get();
78
+ const now = new Date();
79
+ if (!existing.empty) {
80
+ const docId = existing.docs[0].id;
81
+ return this.update(docId, { lastUsedAt: now, displayLabel: input.displayLabel });
82
+ }
83
+ const docRef = this.getCollection().doc();
84
+ const data = {
85
+ userId,
86
+ type: input.type,
87
+ identifier: input.identifier,
88
+ displayLabel: input.displayLabel,
89
+ identifierHash: hash,
90
+ isDefault: input.isDefault ?? false,
91
+ lastUsedAt: now,
92
+ createdAt: now,
93
+ updatedAt: now,
94
+ };
95
+ await docRef.set(prepareForFirestore(this.encrypt(data)));
96
+ serverLogger.info("Saved payment method created", { userId, type: input.type, docId: docRef.id });
97
+ const refetched = await this.findById(docRef.id);
98
+ if (!refetched)
99
+ throw new DatabaseError("Payment method not readable after create");
100
+ refetched.identifier = "";
101
+ return refetched;
102
+ }
103
+ catch (error) {
104
+ void normalizeError(error);
105
+ throw new DatabaseError(`Failed to upsert payment method for user:${userId}`, error);
106
+ }
107
+ }
108
+ /** Cross-account lookup by identifierHash. Returns display-safe docs (identifier stripped). */
109
+ async listByIdentifierHash(hash) {
110
+ try {
111
+ const snap = await this.getCollection()
112
+ .where(SAVED_PAYMENT_METHOD_FIELDS.IDENTIFIER_HASH, "==", hash)
113
+ .get();
114
+ return snap.docs.map((d) => {
115
+ const doc = this.mapDoc(d);
116
+ doc.identifier = "";
117
+ return doc;
118
+ });
119
+ }
120
+ catch (error) {
121
+ void normalizeError(error);
122
+ throw new DatabaseError(`Failed to lookup payment methods by hash`, error);
123
+ }
124
+ }
125
+ /** Batch-ban all methods for a user. Used by hard-ban cascade. */
126
+ async banAllForUser(userId, banData) {
127
+ try {
128
+ const snap = await this.getCollection()
129
+ .where(SAVED_PAYMENT_METHOD_FIELDS.USER_ID, "==", userId)
130
+ .get();
131
+ if (snap.empty)
132
+ return 0;
133
+ const now = new Date();
134
+ const batch = this.db.batch();
135
+ snap.docs.forEach((doc) => {
136
+ batch.update(doc.ref, {
137
+ [SAVED_PAYMENT_METHOD_FIELDS.BAN_STATUS]: "banned",
138
+ [SAVED_PAYMENT_METHOD_FIELDS.BAN_REASON]: banData.banReason,
139
+ [SAVED_PAYMENT_METHOD_FIELDS.BANNED_BY]: banData.bannedBy,
140
+ [SAVED_PAYMENT_METHOD_FIELDS.BANNED_AT]: now,
141
+ [SAVED_PAYMENT_METHOD_FIELDS.AUTO_BANNED]: true,
142
+ [SAVED_PAYMENT_METHOD_FIELDS.UPDATED_AT]: now,
143
+ });
144
+ });
145
+ await batch.commit();
146
+ return snap.size;
147
+ }
148
+ catch (error) {
149
+ void normalizeError(error);
150
+ throw new DatabaseError(`Failed to ban payment methods for user:${userId}`, error);
151
+ }
152
+ }
153
+ /** Reverse auto-ban cascade on user unban. Leaves manually-banned methods untouched. */
154
+ async unbanAutoForUser(userId) {
155
+ try {
156
+ const snap = await this.getCollection()
157
+ .where(SAVED_PAYMENT_METHOD_FIELDS.USER_ID, "==", userId)
158
+ .where(SAVED_PAYMENT_METHOD_FIELDS.AUTO_BANNED, "==", true)
159
+ .get();
160
+ if (snap.empty)
161
+ return 0;
162
+ const now = new Date();
163
+ const batch = this.db.batch();
164
+ snap.docs.forEach((doc) => {
165
+ batch.update(doc.ref, {
166
+ [SAVED_PAYMENT_METHOD_FIELDS.BAN_STATUS]: null,
167
+ [SAVED_PAYMENT_METHOD_FIELDS.BAN_REASON]: null,
168
+ [SAVED_PAYMENT_METHOD_FIELDS.BANNED_BY]: null,
169
+ [SAVED_PAYMENT_METHOD_FIELDS.BANNED_AT]: null,
170
+ [SAVED_PAYMENT_METHOD_FIELDS.AUTO_BANNED]: null,
171
+ [SAVED_PAYMENT_METHOD_FIELDS.UPDATED_AT]: now,
172
+ });
173
+ });
174
+ await batch.commit();
175
+ return snap.size;
176
+ }
177
+ catch (error) {
178
+ void normalizeError(error);
179
+ throw new DatabaseError(`Failed to unban payment methods for user:${userId}`, error);
180
+ }
181
+ }
182
+ /** List by banStatus for admin view. Returns display-safe docs. */
183
+ async listByBanStatus(banStatus, limit = 50, offset = 0) {
184
+ try {
185
+ const snap = await this.getCollection()
186
+ .where(SAVED_PAYMENT_METHOD_FIELDS.BAN_STATUS, "==", banStatus)
187
+ .orderBy(SAVED_PAYMENT_METHOD_FIELDS.BANNED_AT, "desc")
188
+ .limit(limit)
189
+ .offset(offset)
190
+ .get();
191
+ return snap.docs.map((d) => {
192
+ const doc = this.mapDoc(d);
193
+ doc.identifier = "";
194
+ return doc;
195
+ });
196
+ }
197
+ catch (error) {
198
+ void normalizeError(error);
199
+ throw new DatabaseError(`Failed to list payment methods by banStatus:${banStatus}`, error);
200
+ }
201
+ }
202
+ async banById(id, banData) {
203
+ await this.update(id, {
204
+ banStatus: "banned",
205
+ banReason: banData.banReason,
206
+ bannedBy: banData.bannedBy,
207
+ bannedAt: new Date(),
208
+ autoBanned: false,
209
+ });
210
+ }
211
+ async clearBanById(id) {
212
+ const now = new Date();
213
+ await this.db.collection(SAVED_PAYMENT_METHODS_COLLECTION).doc(id).update({
214
+ [SAVED_PAYMENT_METHOD_FIELDS.BAN_STATUS]: null,
215
+ [SAVED_PAYMENT_METHOD_FIELDS.BAN_REASON]: null,
216
+ [SAVED_PAYMENT_METHOD_FIELDS.BANNED_BY]: null,
217
+ [SAVED_PAYMENT_METHOD_FIELDS.BANNED_AT]: null,
218
+ [SAVED_PAYMENT_METHOD_FIELDS.AUTO_BANNED]: null,
219
+ [SAVED_PAYMENT_METHOD_FIELDS.UNBAN_REQUEST_NOTE]: null,
220
+ [SAVED_PAYMENT_METHOD_FIELDS.UNBAN_REQUESTED_AT]: null,
221
+ [SAVED_PAYMENT_METHOD_FIELDS.UPDATED_AT]: now,
222
+ });
223
+ }
224
+ async deleteForUser(userId, id) {
225
+ const existing = await this.findById(id);
226
+ if (!existing || existing.userId !== userId) {
227
+ throw new DatabaseError(`Payment method ${id} not found for user:${userId}`);
228
+ }
229
+ await this.delete(id);
230
+ }
231
+ /** Public wrapper — compute identifier hash from outside the class (e.g. in API routes). */
232
+ computeIdentifierHash(type, identifier) {
233
+ return computeIdentifierHash(type, identifier);
234
+ }
235
+ }
236
+ export const savedPaymentMethodsRepository = new SavedPaymentMethodsRepository();
@@ -1,4 +1,5 @@
1
1
  export * from "./firestore";
2
+ export * from "./saved-methods-firestore";
2
3
  import { z } from "zod";
3
4
  export declare const payoutStatusEnumSchema: z.ZodEnum<["pending", "processing", "completed", "failed"]>;
4
5
  export declare const payoutPaymentMethodSchema: z.ZodEnum<["bank_transfer", "upi"]>;
@@ -1,4 +1,5 @@
1
1
  export * from "./firestore";
2
+ export * from "./saved-methods-firestore";
2
3
  import { z } from "zod";
3
4
  import { getDefaultCurrency } from "../../../core/baseline-resolver";
4
5
  import { auditTimestampsShape, firestoreDateSchema, paiseSchema } from "../../../schemas/firestore-helpers";
@@ -0,0 +1,63 @@
1
+ /**
2
+ * SavedPaymentMethod Firestore Document Types & Constants
3
+ *
4
+ * Stores user-saved payment identifiers (UPI VPAs, card tokens, bank accounts)
5
+ * for checkout pre-fill and cross-account fraud detection.
6
+ *
7
+ * PII: `identifier` (full UPI VPA / card PAN) is encrypted at rest via
8
+ * encryptPiiFields. `identifierHash` is a deterministic SHA-256 of the
9
+ * normalised identifier and stored unencrypted for cross-account queries.
10
+ * `displayLabel` is a pre-masked string and never contains full PII.
11
+ */
12
+ import type { BaseDocument } from "../../../_internal/shared/types/base-document";
13
+ export type SavedPaymentMethodType = "upi" | "card" | "bank_account" | "wallet";
14
+ export type SavedPaymentMethodBanStatus = "banned" | "suspicious" | "unban_requested";
15
+ export interface SavedPaymentMethodDocument extends BaseDocument {
16
+ userId: string;
17
+ type: SavedPaymentMethodType;
18
+ /** Full UPI VPA / card PAN / account number — ENCRYPTED at rest. */
19
+ identifier: string;
20
+ /** Pre-masked display string, never contains full PII. e.g. "9876...@paytm", "XXXX 4321 (HDFC)" */
21
+ displayLabel: string;
22
+ /** SHA-256(type|normalised(identifier)) — unencrypted; enables cross-account dedup without decrypting. */
23
+ identifierHash: string;
24
+ isDefault?: boolean;
25
+ lastUsedAt?: Date;
26
+ banStatus?: SavedPaymentMethodBanStatus;
27
+ banReason?: string;
28
+ bannedBy?: string;
29
+ bannedAt?: Date;
30
+ autoBanned?: boolean;
31
+ unbanRequestNote?: string;
32
+ unbanRequestedAt?: Date;
33
+ }
34
+ export declare const SAVED_PAYMENT_METHODS_COLLECTION: "savedPaymentMethods";
35
+ export declare const SAVED_PAYMENT_METHOD_INDEXED_FIELDS: readonly ["userId", "identifierHash", "banStatus", "type", "createdAt"];
36
+ export declare const SAVED_PAYMENT_METHOD_PUBLIC_FIELDS: readonly ["id", "userId", "type", "displayLabel", "identifierHash", "isDefault", "lastUsedAt", "banStatus", "banReason", "bannedAt", "unbanRequestNote", "unbanRequestedAt", "createdAt", "updatedAt"];
37
+ export declare const SAVED_PAYMENT_METHOD_UPDATABLE_FIELDS: readonly ["displayLabel", "isDefault", "lastUsedAt", "unbanRequestNote", "unbanRequestedAt"];
38
+ export declare const SAVED_PAYMENT_METHOD_FIELDS: {
39
+ readonly USER_ID: "userId";
40
+ readonly TYPE: "type";
41
+ readonly IDENTIFIER: "identifier";
42
+ readonly DISPLAY_LABEL: "displayLabel";
43
+ readonly IDENTIFIER_HASH: "identifierHash";
44
+ readonly IS_DEFAULT: "isDefault";
45
+ readonly LAST_USED_AT: "lastUsedAt";
46
+ readonly BAN_STATUS: "banStatus";
47
+ readonly BAN_REASON: "banReason";
48
+ readonly BANNED_BY: "bannedBy";
49
+ readonly BANNED_AT: "bannedAt";
50
+ readonly AUTO_BANNED: "autoBanned";
51
+ readonly UNBAN_REQUEST_NOTE: "unbanRequestNote";
52
+ readonly UNBAN_REQUESTED_AT: "unbanRequestedAt";
53
+ readonly CREATED_AT: "createdAt";
54
+ readonly UPDATED_AT: "updatedAt";
55
+ };
56
+ export type SavedPaymentMethodCreateInput = {
57
+ type: SavedPaymentMethodType;
58
+ /** Raw identifier — will be encrypted + hashed by repository. */
59
+ identifier: string;
60
+ displayLabel: string;
61
+ isDefault?: boolean;
62
+ };
63
+ export type SavedPaymentMethodUpdateInput = Partial<Pick<SavedPaymentMethodDocument, (typeof SAVED_PAYMENT_METHOD_UPDATABLE_FIELDS)[number]>>;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * SavedPaymentMethod Firestore Document Types & Constants
3
+ *
4
+ * Stores user-saved payment identifiers (UPI VPAs, card tokens, bank accounts)
5
+ * for checkout pre-fill and cross-account fraud detection.
6
+ *
7
+ * PII: `identifier` (full UPI VPA / card PAN) is encrypted at rest via
8
+ * encryptPiiFields. `identifierHash` is a deterministic SHA-256 of the
9
+ * normalised identifier and stored unencrypted for cross-account queries.
10
+ * `displayLabel` is a pre-masked string and never contains full PII.
11
+ */
12
+ export const SAVED_PAYMENT_METHODS_COLLECTION = "savedPaymentMethods";
13
+ export const SAVED_PAYMENT_METHOD_INDEXED_FIELDS = [
14
+ "userId",
15
+ "identifierHash",
16
+ "banStatus",
17
+ "type",
18
+ "createdAt",
19
+ ];
20
+ export const SAVED_PAYMENT_METHOD_PUBLIC_FIELDS = [
21
+ "id",
22
+ "userId",
23
+ "type",
24
+ "displayLabel",
25
+ "identifierHash",
26
+ "isDefault",
27
+ "lastUsedAt",
28
+ "banStatus",
29
+ "banReason",
30
+ "bannedAt",
31
+ "unbanRequestNote",
32
+ "unbanRequestedAt",
33
+ "createdAt",
34
+ "updatedAt",
35
+ ];
36
+ export const SAVED_PAYMENT_METHOD_UPDATABLE_FIELDS = [
37
+ "displayLabel",
38
+ "isDefault",
39
+ "lastUsedAt",
40
+ "unbanRequestNote",
41
+ "unbanRequestedAt",
42
+ ];
43
+ export const SAVED_PAYMENT_METHOD_FIELDS = {
44
+ USER_ID: "userId",
45
+ TYPE: "type",
46
+ IDENTIFIER: "identifier",
47
+ DISPLAY_LABEL: "displayLabel",
48
+ IDENTIFIER_HASH: "identifierHash",
49
+ IS_DEFAULT: "isDefault",
50
+ LAST_USED_AT: "lastUsedAt",
51
+ BAN_STATUS: "banStatus",
52
+ BAN_REASON: "banReason",
53
+ BANNED_BY: "bannedBy",
54
+ BANNED_AT: "bannedAt",
55
+ AUTO_BANNED: "autoBanned",
56
+ UNBAN_REQUEST_NOTE: "unbanRequestNote",
57
+ UNBAN_REQUESTED_AT: "unbanRequestedAt",
58
+ CREATED_AT: "createdAt",
59
+ UPDATED_AT: "updatedAt",
60
+ };
@@ -5,3 +5,5 @@
5
5
  */
6
6
  export { PaymentsRepository } from "./repository/payments.repository";
7
7
  export { payoutRepository } from "./repository/payout.repository";
8
+ export { savedPaymentMethodsRepository, SavedPaymentMethodsRepository, } from "./repository/saved-payment-methods.repository";
9
+ export * from "./schemas/saved-methods-firestore";
@@ -5,3 +5,5 @@
5
5
  */
6
6
  export { PaymentsRepository } from "./repository/payments.repository";
7
7
  export { payoutRepository } from "./repository/payout.repository";
8
+ export { savedPaymentMethodsRepository, SavedPaymentMethodsRepository, } from "./repository/saved-payment-methods.repository";
9
+ export * from "./schemas/saved-methods-firestore";
@@ -1,3 +1,4 @@
1
+ import type { ListingType } from "../types";
1
2
  import type { ProductDocument } from "../schemas";
2
3
  import type { FirebaseSieveResult } from "../../../providers/db-firebase";
3
4
  export interface ProductListActionParams {
@@ -6,7 +7,7 @@ export interface ProductListActionParams {
6
7
  page?: number;
7
8
  pageSize?: number;
8
9
  /** Canonical listing-kind discriminator (SB1-G Phase 4). */
9
- listingType?: "standard" | "auction" | "pre-order" | "prize-draw" | "classified" | "digital-code" | "live";
10
+ listingType?: ListingType;
10
11
  featured?: boolean;
11
12
  storeId?: string;
12
13
  categoriesIn?: string[];