@mohasinac/appkit 3.2.6 → 3.2.8

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 (53) hide show
  1. package/dist/_internal/shared/actions/action-registry.js +62 -0
  2. package/dist/constants/api-endpoints.d.ts +12 -0
  3. package/dist/constants/api-endpoints.js +4 -0
  4. package/dist/features/about/components/PublicProfileView.js +8 -5
  5. package/dist/features/addresses/repository/addresses.repository.d.ts +14 -1
  6. package/dist/features/addresses/repository/addresses.repository.js +122 -1
  7. package/dist/features/addresses/schemas/firestore.d.ts +22 -3
  8. package/dist/features/addresses/schemas/firestore.js +25 -0
  9. package/dist/features/addresses/schemas/index.d.ts +2 -2
  10. package/dist/features/admin/components/AdminAddressClustersView.d.ts +5 -0
  11. package/dist/features/admin/components/AdminAddressClustersView.js +38 -0
  12. package/dist/features/admin/components/AdminAddressesView.d.ts +5 -0
  13. package/dist/features/admin/components/AdminAddressesView.js +137 -0
  14. package/dist/features/admin/components/AdminPaymentClustersView.d.ts +5 -0
  15. package/dist/features/admin/components/AdminPaymentClustersView.js +37 -0
  16. package/dist/features/admin/components/AdminPaymentMethodsView.d.ts +5 -0
  17. package/dist/features/admin/components/AdminPaymentMethodsView.js +130 -0
  18. package/dist/features/admin/components/index.d.ts +8 -0
  19. package/dist/features/admin/components/index.js +4 -0
  20. package/dist/features/auth/actions/profile-actions.d.ts +3 -1
  21. package/dist/features/auth/actions/profile-actions.js +29 -2
  22. package/dist/features/cart/schemas/index.d.ts +6 -6
  23. package/dist/features/orders/schemas/index.d.ts +4 -4
  24. package/dist/features/payments/repository/saved-payment-methods.repository.d.ts +48 -0
  25. package/dist/features/payments/repository/saved-payment-methods.repository.js +236 -0
  26. package/dist/features/payments/schemas/index.d.ts +1 -0
  27. package/dist/features/payments/schemas/index.js +1 -0
  28. package/dist/features/payments/schemas/saved-methods-firestore.d.ts +63 -0
  29. package/dist/features/payments/schemas/saved-methods-firestore.js +60 -0
  30. package/dist/features/payments/server.d.ts +2 -0
  31. package/dist/features/payments/server.js +2 -0
  32. package/dist/features/promotions/schemas/index.d.ts +6 -6
  33. package/dist/features/reviews/repository/reviews.repository.d.ts +12 -0
  34. package/dist/features/reviews/repository/reviews.repository.js +24 -0
  35. package/dist/features/reviews/schemas/firestore.d.ts +5 -1
  36. package/dist/features/reviews/schemas/firestore.js +2 -0
  37. package/dist/features/reviews/schemas/index.d.ts +6 -6
  38. package/dist/features/seller/components/SellerAddressesView.js +7 -3
  39. package/dist/features/support/schemas/index.d.ts +8 -8
  40. package/dist/features/wishlist/schemas/index.d.ts +2 -2
  41. package/dist/index.d.ts +9 -0
  42. package/dist/index.js +10 -0
  43. package/dist/next/routing/route-map.d.ts +8 -0
  44. package/dist/next/routing/route-map.js +4 -0
  45. package/dist/repositories/index.d.ts +1 -0
  46. package/dist/repositories/index.js +1 -0
  47. package/dist/schemas/registry.d.ts +66 -66
  48. package/dist/schemas/webhooks/razorpay.d.ts +50 -50
  49. package/dist/security/index.d.ts +1 -1
  50. package/dist/security/index.js +1 -1
  51. package/dist/security/pii-schemas.d.ts +2 -0
  52. package/dist/security/pii-schemas.js +2 -0
  53. package/package.json +1 -2
@@ -1559,6 +1559,68 @@ export const ACTIONS = {
1559
1559
  confirmKind: "danger",
1560
1560
  },
1561
1561
  },
1562
+ // ── Address ban management ─────────────────────────────────────────────
1563
+ "ban-address": {
1564
+ id: "admin.ban-address",
1565
+ label: "Ban Address",
1566
+ ariaLabel: "Ban this address",
1567
+ description: "Ban an address from being used on the platform.",
1568
+ kind: "danger",
1569
+ permissions: ["admin", "moderator"],
1570
+ confirmation: {
1571
+ title: "Ban this address?",
1572
+ body: "The address will be blocked from use at checkout across all accounts.",
1573
+ confirmLabel: "Ban address",
1574
+ confirmKind: "danger",
1575
+ },
1576
+ },
1577
+ "approve-unban": {
1578
+ id: "admin.approve-unban",
1579
+ label: "Approve Unban",
1580
+ ariaLabel: "Approve unban request",
1581
+ description: "Approve an address unban request and restore it to active.",
1582
+ kind: "primary",
1583
+ permissions: ["admin"],
1584
+ },
1585
+ "reject-unban": {
1586
+ id: "admin.reject-unban",
1587
+ label: "Reject Unban",
1588
+ ariaLabel: "Reject unban request",
1589
+ description: "Reject an address unban request — address stays banned.",
1590
+ kind: "secondary",
1591
+ permissions: ["admin"],
1592
+ },
1593
+ // ── Payment method ban management ──────────────────────────────────────
1594
+ "ban-payment-method": {
1595
+ id: "admin.ban-payment-method",
1596
+ label: "Ban Payment Method",
1597
+ ariaLabel: "Ban this payment method",
1598
+ description: "Ban a payment identifier from being used on the platform.",
1599
+ kind: "danger",
1600
+ permissions: ["admin", "moderator"],
1601
+ confirmation: {
1602
+ title: "Ban this payment method?",
1603
+ body: "The payment identifier will be blocked from checkout across all accounts.",
1604
+ confirmLabel: "Ban payment method",
1605
+ confirmKind: "danger",
1606
+ },
1607
+ },
1608
+ "approve-payment-unban": {
1609
+ id: "admin.approve-payment-unban",
1610
+ label: "Approve Unban",
1611
+ ariaLabel: "Approve payment method unban",
1612
+ description: "Approve a payment method unban request.",
1613
+ kind: "primary",
1614
+ permissions: ["admin"],
1615
+ },
1616
+ "reject-payment-unban": {
1617
+ id: "admin.reject-payment-unban",
1618
+ label: "Reject Unban",
1619
+ ariaLabel: "Reject payment method unban",
1620
+ description: "Reject a payment method unban request — stays banned.",
1621
+ kind: "secondary",
1622
+ permissions: ["admin"],
1623
+ },
1562
1624
  },
1563
1625
  CART: {
1564
1626
  "clear-cart": {
@@ -137,6 +137,10 @@ export declare const ADMIN_ENDPOINTS: {
137
137
  readonly SCAMMER_BY_ID: (id: string) => string;
138
138
  readonly ADDRESSES: "/api/admin/addresses";
139
139
  readonly ADDRESS_BY_ID: (id: string) => string;
140
+ readonly ADDRESS_CLUSTERS: "/api/admin/addresses/clusters";
141
+ readonly PAYMENT_METHODS: "/api/admin/payment-methods";
142
+ readonly PAYMENT_METHOD_BY_ID: (id: string) => string;
143
+ readonly PAYMENT_METHOD_CLUSTERS: "/api/admin/payment-methods/clusters";
140
144
  readonly GROUPED_LISTINGS: "/api/admin/grouped-listings";
141
145
  readonly GROUPED_LISTING_BY_ID: (id: string) => string;
142
146
  readonly ADMIN_FULFILLMENT: (storeId: string) => string;
@@ -472,6 +476,10 @@ export declare const API_ENDPOINTS: {
472
476
  readonly SCAMMER_BY_ID: (id: string) => string;
473
477
  readonly ADDRESSES: "/api/admin/addresses";
474
478
  readonly ADDRESS_BY_ID: (id: string) => string;
479
+ readonly ADDRESS_CLUSTERS: "/api/admin/addresses/clusters";
480
+ readonly PAYMENT_METHODS: "/api/admin/payment-methods";
481
+ readonly PAYMENT_METHOD_BY_ID: (id: string) => string;
482
+ readonly PAYMENT_METHOD_CLUSTERS: "/api/admin/payment-methods/clusters";
475
483
  readonly GROUPED_LISTINGS: "/api/admin/grouped-listings";
476
484
  readonly GROUPED_LISTING_BY_ID: (id: string) => string;
477
485
  readonly ADMIN_FULFILLMENT: (storeId: string) => string;
@@ -809,6 +817,10 @@ export declare const API_ROUTES: {
809
817
  readonly SCAMMER_BY_ID: (id: string) => string;
810
818
  readonly ADDRESSES: "/api/admin/addresses";
811
819
  readonly ADDRESS_BY_ID: (id: string) => string;
820
+ readonly ADDRESS_CLUSTERS: "/api/admin/addresses/clusters";
821
+ readonly PAYMENT_METHODS: "/api/admin/payment-methods";
822
+ readonly PAYMENT_METHOD_BY_ID: (id: string) => string;
823
+ readonly PAYMENT_METHOD_CLUSTERS: "/api/admin/payment-methods/clusters";
812
824
  readonly GROUPED_LISTINGS: "/api/admin/grouped-listings";
813
825
  readonly GROUPED_LISTING_BY_ID: (id: string) => string;
814
826
  readonly ADMIN_FULFILLMENT: (storeId: string) => string;
@@ -156,6 +156,10 @@ export const ADMIN_ENDPOINTS = {
156
156
  SCAMMER_BY_ID: (id) => `/api/admin/scammers/${id}`,
157
157
  ADDRESSES: "/api/admin/addresses",
158
158
  ADDRESS_BY_ID: (id) => `/api/admin/addresses/${id}`,
159
+ ADDRESS_CLUSTERS: "/api/admin/addresses/clusters",
160
+ PAYMENT_METHODS: "/api/admin/payment-methods",
161
+ PAYMENT_METHOD_BY_ID: (id) => `/api/admin/payment-methods/${id}`,
162
+ PAYMENT_METHOD_CLUSTERS: "/api/admin/payment-methods/clusters",
159
163
  GROUPED_LISTINGS: "/api/admin/grouped-listings",
160
164
  GROUPED_LISTING_BY_ID: (id) => `/api/admin/grouped-listings/${id}`,
161
165
  ADMIN_FULFILLMENT: (storeId) => `/api/store/fulfillment?storeId=${encodeURIComponent(storeId)}`,
@@ -1,6 +1,6 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import Link from "next/link";
3
- import { getPublicUserProfile, getProfileStoreProducts, getSellerReviews, getReviewsAuthoredBy } from "../../auth/actions/profile-actions";
3
+ import { getPublicUserProfile, getProfileStoreProducts, getReviewsAuthoredBy, getReviewsReceivedBy } from "../../auth/actions/profile-actions";
4
4
  import { storeRepository } from "../../stores/repository/store.repository";
5
5
  import { ROUTES } from "../../../constants";
6
6
  import { PAGE_CONTAINER } from "../../../_internal/shared/styles/page";
@@ -63,11 +63,11 @@ export async function PublicProfileView({ userId, }) {
63
63
  const t = await getTranslations("publicProfile");
64
64
  const profile = await getPublicUserProfile(userId).catch(() => null);
65
65
  const storeId = profile?.storeSlug ?? null;
66
- const [products, reviewsReceived, store, reviewsAuthored] = await Promise.all([
66
+ const [products, store, reviewsAuthored, reviewsReceivedAsBuyer] = await Promise.all([
67
67
  storeId ? getProfileStoreProducts(storeId).catch(() => []) : Promise.resolve([]),
68
- storeId ? getSellerReviews(storeId).catch(() => []) : Promise.resolve([]),
69
68
  storeId ? storeRepository.findById(storeId).catch(() => null) : Promise.resolve(null),
70
69
  getReviewsAuthoredBy(userId).catch(() => []),
70
+ getReviewsReceivedBy(userId).catch(() => []),
71
71
  ]);
72
72
  const isSeller = isSellerUser(profile) || isAdminUser(profile);
73
73
  // For sellers: lead with store identity (name + logo). For buyers: user identity.
@@ -83,7 +83,7 @@ export async function PublicProfileView({ userId, }) {
83
83
  const pub = profile?.publicProfile;
84
84
  const stats = profile?.stats;
85
85
  const listingCount = products.length;
86
- const reviewCount = reviewsAuthored.length + reviewsReceived.length;
86
+ const reviewCount = reviewsAuthored.length + reviewsReceivedAsBuyer.length;
87
87
  const itemsSold = stats?.itemsSold ?? 0;
88
88
  const auctionsWon = stats?.auctionsWon ?? 0;
89
89
  const totalOrders = stats?.totalOrders ?? 0;
@@ -92,7 +92,7 @@ export async function PublicProfileView({ userId, }) {
92
92
  const storeDescription = store?.storeDescription ?? pub?.storeDescription;
93
93
  const profileHeroCtx = { displayName, photoURL, memberSince, isSeller, storeSlug, flex, page };
94
94
  const statItems = buildProfileStatItems(t, { listingCount, reviewCount, itemsSold, auctionsWon, totalOrders, isSeller });
95
- return (_jsxs(Div, { className: "-mx-4 md:-mx-6 lg:-mx-8 -mt-6 sm:-mt-8 lg:-mt-10", children: [renderProfileHero(t, profileHeroCtx), _jsxs(Stack, { gap: "2xl", className: `${page.container.md}`, paddingY: "y-3xl", children: [renderProfileStatsRow(flex, statItems), renderProfileBioSection(pub), renderStoreDescriptionSection(isSeller, storeSlug ?? null, storeDescription ?? null, storeName, t), renderProfileListingsSection(t, products, storeSlug ?? null), renderAuthoredReviewsSection(t, reviewsAuthored, displayName), isSeller && storeSlug && renderProfileReviewsSection(t, reviewsReceived, storeSlug), _jsx(Row, { justify: "center", padding: "t-xs", children: _jsxs(Link, { href: String(ROUTES.HOME), className: "text-sm text-zinc-400 dark:text-zinc-400 hover:text-neutral-600 dark:hover:text-zinc-300", children: ["\u2190 ", t("backHome")] }) })] })] }));
95
+ return (_jsxs(Div, { className: "-mx-4 md:-mx-6 lg:-mx-8 -mt-6 sm:-mt-8 lg:-mt-10", children: [renderProfileHero(t, profileHeroCtx), _jsxs(Stack, { gap: "2xl", className: `${page.container.md}`, paddingY: "y-3xl", children: [renderProfileStatsRow(flex, statItems), renderProfileBioSection(pub), renderStoreDescriptionSection(isSeller, storeSlug ?? null, storeDescription ?? null, storeName, t), renderProfileListingsSection(t, products, storeSlug ?? null), renderAuthoredReviewsSection(t, reviewsAuthored, displayName), reviewsReceivedAsBuyer.length > 0 && renderBuyerReceivedReviewsSection(t, reviewsReceivedAsBuyer), _jsx(Row, { justify: "center", padding: "t-xs", children: _jsxs(Link, { href: String(ROUTES.HOME), className: "text-sm text-zinc-400 dark:text-zinc-400 hover:text-neutral-600 dark:hover:text-zinc-300", children: ["\u2190 ", t("backHome")] }) })] })] }));
96
96
  }
97
97
  function buildProfileStatItems(t, ctx) {
98
98
  const { listingCount, reviewCount, itemsSold, auctionsWon, totalOrders, isSeller } = ctx;
@@ -136,3 +136,6 @@ function renderProfileReviewsSection(t, reviews, storeSlug) {
136
136
  function renderAuthoredReviewsSection(t, reviews, displayName) {
137
137
  return (_jsxs(Section, { children: [_jsx(Heading, { level: 2, className: "mb-4", children: t("reviewsAuthoredTitle", { name: displayName }) }), reviews.length === 0 ? (_jsxs(Div, { rounded: "2xl", padding: "3xl", className: "text-center", border: "default", surface: "subtle", children: [_jsx(Star, { className: CLS_EMPTY_ICON }), _jsx(Text, { variant: "secondary", size: "sm", children: t("noReviewsAuthored", { name: displayName }) })] })) : (_jsx(Grid, { gap: "md", className: "grid-cols-1 sm:grid-cols-2", children: reviews.slice(0, 6).map((review) => _jsx(ReviewCard, { review: review }, review.id)) }))] }));
138
138
  }
139
+ function renderBuyerReceivedReviewsSection(t, reviews) {
140
+ return (_jsxs(Section, { children: [_jsx(Heading, { level: 2, className: "mb-4", children: t("reviewsReceivedAsBuyerTitle") || "Reviews Received" }), _jsx(Grid, { gap: "md", className: "grid-cols-1 sm:grid-cols-2", children: reviews.slice(0, 6).map((review) => _jsx(ReviewCard, { review: review }, review.id)) })] }));
141
+ }
@@ -1,5 +1,5 @@
1
1
  import { BaseRepository, type DocumentSnapshot } from "../../../providers/db-firebase";
2
- import { type AddressCreateInput, type AddressDocument, type AddressOwnerType, type AddressUpdateInput } from "../schemas";
2
+ import { type AddressBanStatus, type AddressCreateInput, type AddressDocument, type AddressOwnerType, type AddressUpdateInput } from "../schemas";
3
3
  export declare class AddressesRepository extends BaseRepository<AddressDocument> {
4
4
  constructor();
5
5
  private decryptAddress;
@@ -15,6 +15,19 @@ export declare class AddressesRepository extends BaseRepository<AddressDocument>
15
15
  deleteForOwner(ownerType: AddressOwnerType, ownerId: string, addressId: string): Promise<void>;
16
16
  setDefault(ownerType: AddressOwnerType, ownerId: string, addressId: string): Promise<AddressDocument>;
17
17
  deleteAllForOwner(ownerType: AddressOwnerType, ownerId: string): Promise<number>;
18
+ private computeAddressHash;
19
+ banAllForOwner(ownerType: AddressOwnerType, ownerId: string, banData: {
20
+ banReason: string;
21
+ bannedBy: string;
22
+ }): Promise<number>;
23
+ unbanAutoForOwner(ownerType: AddressOwnerType, ownerId: string): Promise<number>;
24
+ listByAddressHash(addressHash: string): Promise<AddressDocument[]>;
25
+ listByBanStatus(banStatus: AddressBanStatus, limit?: number, offset?: number): Promise<AddressDocument[]>;
26
+ banById(id: string, banData: {
27
+ banReason: string;
28
+ bannedBy: string;
29
+ }): Promise<AddressDocument>;
30
+ clearBanById(id: string): Promise<void>;
18
31
  private clearDefaultFlag;
19
32
  }
20
33
  export declare const addressesRepository: AddressesRepository;
@@ -1,3 +1,4 @@
1
+ import { createHash } from "crypto";
1
2
  import { normalizeError } from "../../../errors/normalize";
2
3
  /**
3
4
  * AddressesRepository — SB-UNI-A 2026-05-13
@@ -86,6 +87,7 @@ export class AddressesRepository extends BaseRepository {
86
87
  ...input,
87
88
  ownerType,
88
89
  ownerId,
90
+ addressHash: this.computeAddressHash(input.postalCode, input.addressLine1, input.addressLine2),
89
91
  createdAt: now,
90
92
  updatedAt: now,
91
93
  };
@@ -121,7 +123,14 @@ export class AddressesRepository extends BaseRepository {
121
123
  if (input.isDefault) {
122
124
  await this.clearDefaultFlag(ownerType, ownerId);
123
125
  }
124
- return this.update(addressId, input);
126
+ const updateData = { ...input };
127
+ if (input.postalCode || input.addressLine1 || input.addressLine2 !== undefined) {
128
+ const postalCode = input.postalCode ?? existing.postalCode;
129
+ const line1 = input.addressLine1 ?? existing.addressLine1;
130
+ const line2 = input.addressLine2 ?? existing.addressLine2;
131
+ updateData.addressHash = this.computeAddressHash(postalCode, line1, line2);
132
+ }
133
+ return this.update(addressId, updateData);
125
134
  }
126
135
  async deleteForOwner(ownerType, ownerId, addressId) {
127
136
  const existing = await this.findById(addressId);
@@ -157,6 +166,118 @@ export class AddressesRepository extends BaseRepository {
157
166
  throw new DatabaseError(`Failed to delete all addresses for ${ownerType}:${ownerId}`, error);
158
167
  }
159
168
  }
169
+ computeAddressHash(postalCode, line1, line2) {
170
+ const norm = (s) => s.toLowerCase().replace(/\s+/g, " ").trim();
171
+ return createHash("sha256")
172
+ .update([postalCode, line1, line2 ?? ""].map(norm).join("|"))
173
+ .digest("hex");
174
+ }
175
+ async banAllForOwner(ownerType, ownerId, banData) {
176
+ try {
177
+ const snap = await this.getCollection()
178
+ .where(ADDRESS_FIELDS.OWNER_TYPE, "==", ownerType)
179
+ .where(ADDRESS_FIELDS.OWNER_ID, "==", ownerId)
180
+ .get();
181
+ if (snap.empty)
182
+ return 0;
183
+ const now = new Date();
184
+ const batch = this.db.batch();
185
+ snap.docs.forEach((doc) => {
186
+ batch.update(doc.ref, {
187
+ [ADDRESS_FIELDS.BAN_STATUS]: "banned",
188
+ [ADDRESS_FIELDS.BAN_REASON]: banData.banReason,
189
+ [ADDRESS_FIELDS.BANNED_BY]: banData.bannedBy,
190
+ [ADDRESS_FIELDS.BANNED_AT]: now,
191
+ [ADDRESS_FIELDS.AUTO_BANNED]: true,
192
+ [ADDRESS_FIELDS.UPDATED_AT]: now,
193
+ });
194
+ });
195
+ await batch.commit();
196
+ return snap.size;
197
+ }
198
+ catch (error) {
199
+ void normalizeError(error);
200
+ throw new DatabaseError(`Failed to ban addresses for ${ownerType}:${ownerId}`, error);
201
+ }
202
+ }
203
+ async unbanAutoForOwner(ownerType, ownerId) {
204
+ try {
205
+ const snap = await this.getCollection()
206
+ .where(ADDRESS_FIELDS.OWNER_TYPE, "==", ownerType)
207
+ .where(ADDRESS_FIELDS.OWNER_ID, "==", ownerId)
208
+ .where(ADDRESS_FIELDS.AUTO_BANNED, "==", true)
209
+ .get();
210
+ if (snap.empty)
211
+ return 0;
212
+ const now = new Date();
213
+ const batch = this.db.batch();
214
+ snap.docs.forEach((doc) => {
215
+ batch.update(doc.ref, {
216
+ [ADDRESS_FIELDS.BAN_STATUS]: null,
217
+ [ADDRESS_FIELDS.BAN_REASON]: null,
218
+ [ADDRESS_FIELDS.BANNED_BY]: null,
219
+ [ADDRESS_FIELDS.BANNED_AT]: null,
220
+ [ADDRESS_FIELDS.AUTO_BANNED]: null,
221
+ [ADDRESS_FIELDS.UPDATED_AT]: now,
222
+ });
223
+ });
224
+ await batch.commit();
225
+ return snap.size;
226
+ }
227
+ catch (error) {
228
+ void normalizeError(error);
229
+ throw new DatabaseError(`Failed to unban addresses for ${ownerType}:${ownerId}`, error);
230
+ }
231
+ }
232
+ async listByAddressHash(addressHash) {
233
+ try {
234
+ const snap = await this.getCollection()
235
+ .where(ADDRESS_FIELDS.ADDRESS_HASH, "==", addressHash)
236
+ .get();
237
+ return snap.docs.map((d) => this.mapDoc(d));
238
+ }
239
+ catch (error) {
240
+ void normalizeError(error);
241
+ throw new DatabaseError(`Failed to lookup addresses by hash`, error);
242
+ }
243
+ }
244
+ async listByBanStatus(banStatus, limit = 50, offset = 0) {
245
+ try {
246
+ const snap = await this.getCollection()
247
+ .where(ADDRESS_FIELDS.BAN_STATUS, "==", banStatus)
248
+ .orderBy(ADDRESS_FIELDS.BANNED_AT, "desc")
249
+ .limit(limit)
250
+ .offset(offset)
251
+ .get();
252
+ return snap.docs.map((d) => this.mapDoc(d));
253
+ }
254
+ catch (error) {
255
+ void normalizeError(error);
256
+ throw new DatabaseError(`Failed to list addresses by banStatus:${banStatus}`, error);
257
+ }
258
+ }
259
+ async banById(id, banData) {
260
+ return this.update(id, {
261
+ banStatus: "banned",
262
+ banReason: banData.banReason,
263
+ bannedBy: banData.bannedBy,
264
+ bannedAt: new Date(),
265
+ autoBanned: false,
266
+ });
267
+ }
268
+ async clearBanById(id) {
269
+ const now = new Date();
270
+ await this.db.collection(ADDRESSES_COLLECTION).doc(id).update({
271
+ [ADDRESS_FIELDS.BAN_STATUS]: null,
272
+ [ADDRESS_FIELDS.BAN_REASON]: null,
273
+ [ADDRESS_FIELDS.BANNED_BY]: null,
274
+ [ADDRESS_FIELDS.BANNED_AT]: null,
275
+ [ADDRESS_FIELDS.AUTO_BANNED]: null,
276
+ [ADDRESS_FIELDS.UNBAN_REQUEST_NOTE]: null,
277
+ [ADDRESS_FIELDS.UNBAN_REQUESTED_AT]: null,
278
+ [ADDRESS_FIELDS.UPDATED_AT]: now,
279
+ });
280
+ }
160
281
  async clearDefaultFlag(ownerType, ownerId) {
161
282
  try {
162
283
  const snapshot = await this.getCollection()
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import type { BaseDocument } from "../../../_internal/shared/types/base-document";
11
11
  export type AddressOwnerType = "user" | "store";
12
+ export type AddressBanStatus = "banned" | "unban_requested" | "suspicious";
12
13
  export interface AddressDocument extends BaseDocument {
13
14
  ownerType: AddressOwnerType;
14
15
  ownerId: string;
@@ -23,12 +24,22 @@ export interface AddressDocument extends BaseDocument {
23
24
  postalCode: string;
24
25
  country: string;
25
26
  isDefault: boolean;
27
+ /** SHA-256(normalised postalCode|line1|line2) — stored unencrypted for cross-account dedup. */
28
+ addressHash?: string;
29
+ banStatus?: AddressBanStatus;
30
+ banReason?: string;
31
+ bannedBy?: string;
32
+ bannedAt?: Date;
33
+ /** true when cascaded from a user hard-ban (vs manually banned by admin). */
34
+ autoBanned?: boolean;
35
+ unbanRequestNote?: string;
36
+ unbanRequestedAt?: Date;
26
37
  }
27
38
  export declare const ADDRESSES_COLLECTION: "addresses";
28
- export declare const ADDRESS_INDEXED_FIELDS: readonly ["ownerType", "ownerId", "isDefault", "createdAt"];
39
+ export declare const ADDRESS_INDEXED_FIELDS: readonly ["ownerType", "ownerId", "isDefault", "addressHash", "banStatus", "autoBanned", "createdAt"];
29
40
  export declare const DEFAULT_ADDRESS_DATA: Partial<AddressDocument>;
30
- export declare const ADDRESS_PUBLIC_FIELDS: readonly ["id", "ownerType", "ownerId", "label", "fullName", "phone", "addressLine1", "addressLine2", "landmark", "city", "state", "postalCode", "country", "isDefault", "createdAt", "updatedAt"];
31
- export declare const ADDRESS_UPDATABLE_FIELDS: readonly ["label", "fullName", "phone", "addressLine1", "addressLine2", "landmark", "city", "state", "postalCode", "country", "isDefault"];
41
+ export declare const ADDRESS_PUBLIC_FIELDS: readonly ["id", "ownerType", "ownerId", "label", "fullName", "phone", "addressLine1", "addressLine2", "landmark", "city", "state", "postalCode", "country", "isDefault", "addressHash", "banStatus", "banReason", "bannedAt", "autoBanned", "unbanRequestNote", "unbanRequestedAt", "createdAt", "updatedAt"];
42
+ export declare const ADDRESS_UPDATABLE_FIELDS: readonly ["label", "fullName", "phone", "addressLine1", "addressLine2", "landmark", "city", "state", "postalCode", "country", "isDefault", "banStatus", "banReason", "bannedBy", "bannedAt", "autoBanned", "unbanRequestNote", "unbanRequestedAt"];
32
43
  export type AddressCreateInput = Omit<AddressDocument, "id" | "ownerType" | "ownerId" | "createdAt" | "updatedAt">;
33
44
  export type AddressUpdateInput = Partial<Pick<AddressDocument, (typeof ADDRESS_UPDATABLE_FIELDS)[number]>>;
34
45
  export declare const ADDRESS_FIELDS: {
@@ -46,6 +57,14 @@ export declare const ADDRESS_FIELDS: {
46
57
  readonly POSTAL_CODE: "postalCode";
47
58
  readonly COUNTRY: "country";
48
59
  readonly IS_DEFAULT: "isDefault";
60
+ readonly ADDRESS_HASH: "addressHash";
61
+ readonly BAN_STATUS: "banStatus";
62
+ readonly BAN_REASON: "banReason";
63
+ readonly BANNED_BY: "bannedBy";
64
+ readonly BANNED_AT: "bannedAt";
65
+ readonly AUTO_BANNED: "autoBanned";
66
+ readonly UNBAN_REQUEST_NOTE: "unbanRequestNote";
67
+ readonly UNBAN_REQUESTED_AT: "unbanRequestedAt";
49
68
  readonly CREATED_AT: "createdAt";
50
69
  readonly UPDATED_AT: "updatedAt";
51
70
  };
@@ -12,6 +12,9 @@ export const ADDRESS_INDEXED_FIELDS = [
12
12
  "ownerType",
13
13
  "ownerId",
14
14
  "isDefault",
15
+ "addressHash",
16
+ "banStatus",
17
+ "autoBanned",
15
18
  "createdAt",
16
19
  ];
17
20
  export const DEFAULT_ADDRESS_DATA = {
@@ -32,6 +35,13 @@ export const ADDRESS_PUBLIC_FIELDS = [
32
35
  "postalCode",
33
36
  "country",
34
37
  "isDefault",
38
+ "addressHash",
39
+ "banStatus",
40
+ "banReason",
41
+ "bannedAt",
42
+ "autoBanned",
43
+ "unbanRequestNote",
44
+ "unbanRequestedAt",
35
45
  "createdAt",
36
46
  "updatedAt",
37
47
  ];
@@ -47,6 +57,13 @@ export const ADDRESS_UPDATABLE_FIELDS = [
47
57
  "postalCode",
48
58
  "country",
49
59
  "isDefault",
60
+ "banStatus",
61
+ "banReason",
62
+ "bannedBy",
63
+ "bannedAt",
64
+ "autoBanned",
65
+ "unbanRequestNote",
66
+ "unbanRequestedAt",
50
67
  ];
51
68
  export const ADDRESS_FIELDS = {
52
69
  ID: "id",
@@ -63,6 +80,14 @@ export const ADDRESS_FIELDS = {
63
80
  POSTAL_CODE: "postalCode",
64
81
  COUNTRY: "country",
65
82
  IS_DEFAULT: "isDefault",
83
+ ADDRESS_HASH: "addressHash",
84
+ BAN_STATUS: "banStatus",
85
+ BAN_REASON: "banReason",
86
+ BANNED_BY: "bannedBy",
87
+ BANNED_AT: "bannedAt",
88
+ AUTO_BANNED: "autoBanned",
89
+ UNBAN_REQUEST_NOTE: "unbanRequestNote",
90
+ UNBAN_REQUESTED_AT: "unbanRequestedAt",
66
91
  CREATED_AT: "createdAt",
67
92
  UPDATED_AT: "updatedAt",
68
93
  };
@@ -41,13 +41,13 @@ export declare const addressFirestoreSchema: z.ZodObject<{
41
41
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
42
42
  }, z.ZodTypeAny, "passthrough">;
43
43
  label: string;
44
+ isDefault: boolean;
44
45
  state: string;
45
46
  city: string;
46
47
  addressLine1: string;
47
48
  fullName: string;
48
49
  ownerType: "user" | "store";
49
50
  ownerId: string;
50
- isDefault: boolean;
51
51
  postalCode: string;
52
52
  addressLine2?: string | undefined;
53
53
  landmark?: string | undefined;
@@ -62,13 +62,13 @@ export declare const addressFirestoreSchema: z.ZodObject<{
62
62
  toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
63
63
  }, z.ZodTypeAny, "passthrough">;
64
64
  label: string;
65
+ isDefault: boolean;
65
66
  state: string;
66
67
  city: string;
67
68
  addressLine1: string;
68
69
  fullName: string;
69
70
  ownerType: "user" | "store";
70
71
  ownerId: string;
71
- isDefault: boolean;
72
72
  postalCode: string;
73
73
  addressLine2?: string | undefined;
74
74
  landmark?: string | undefined;
@@ -0,0 +1,5 @@
1
+ import React from "react";
2
+ export interface AdminAddressClustersViewProps {
3
+ children?: React.ReactNode;
4
+ }
5
+ export declare function AdminAddressClustersView(_props: AdminAddressClustersViewProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,38 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useQuery } from "@tanstack/react-query";
4
+ import { useQueryClient } from "@tanstack/react-query";
5
+ import { Badge, Button, Div, Heading, Row, Span, Stack, Text, useToast } from "../../../ui";
6
+ import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
7
+ import { apiClient } from "../../../http";
8
+ import { useApiMutation } from "@mohasinac/appkit/client";
9
+ const BAN_BADGE = {
10
+ banned: "danger",
11
+ unban_requested: "warning",
12
+ suspicious: "secondary",
13
+ };
14
+ export function AdminAddressClustersView(_props) {
15
+ const queryClient = useQueryClient();
16
+ const { showToast } = useToast();
17
+ const { data, isLoading, error } = useQuery({
18
+ queryKey: ["admin", "address-clusters"],
19
+ queryFn: async () => {
20
+ const res = await apiClient.get(ADMIN_ENDPOINTS.ADDRESS_CLUSTERS);
21
+ return res;
22
+ },
23
+ });
24
+ const flagMutation = useApiMutation({
25
+ mutationFn: async (id) => {
26
+ await apiClient.patch(ADMIN_ENDPOINTS.ADDRESS_BY_ID(id), { action: "flag_suspicious" });
27
+ },
28
+ onSuccess: () => {
29
+ showToast("Address flagged as suspicious.", "success");
30
+ void queryClient.invalidateQueries({ queryKey: ["admin", "address-clusters"] });
31
+ },
32
+ onError: (err) => {
33
+ showToast(err.message ?? "Failed to flag address.", "error");
34
+ },
35
+ });
36
+ const clusters = data?.clusters ?? [];
37
+ return (_jsxs(Stack, { gap: "lg", padding: "page", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Heading, { level: 2, size: "lg", weight: "semibold", color: "primary", children: "Address Clusters" }), _jsx(Text, { size: "sm", color: "muted", className: "mt-1", children: "Users sharing the same physical address. Flagging is informational only \u2014 users are not blocked." })] }), isLoading && (_jsx(Row, { justify: "center", padding: "y-4xl", children: _jsx(Div, { className: "h-6 w-6 animate-spin border-2 border-[var(--appkit-color-primary)] border-t-transparent", rounded: "full" }) })), error && (_jsx(Div, { surface: "danger-surface", color: "error", padding: "inline", rounded: "xl", textSize: "sm", children: "Failed to load clusters." })), !isLoading && clusters.length === 0 && (_jsx(Div, { surface: "muted", padding: "y-4xl", rounded: "xl", className: "text-center", children: _jsx(Text, { color: "muted", size: "sm", children: "No shared addresses found." }) })), clusters.map((cluster) => (_jsxs(Stack, { surface: "card", padding: "md", gap: "md", rounded: "xl", border: "default", children: [_jsxs(Row, { justify: "between", align: "start", gap: "sm", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Text, { size: "xs", color: "muted", weight: "medium", children: "Hash" }), _jsx(Span, { size: "xs", color: "muted", className: "font-mono truncate max-w-xs", children: cluster.addressHash })] }), _jsxs(Badge, { variant: "secondary", size: "sm", children: [cluster.addresses.length, " accounts"] })] }), _jsx(Stack, { gap: "xs", children: cluster.addresses.map((addr) => (_jsxs(Row, { justify: "between", align: "center", gap: "sm", surface: "muted", padding: "inlineSm", rounded: "lg", children: [_jsxs(Stack, { gap: "none", className: "min-w-0", children: [_jsxs(Text, { size: "sm", weight: "medium", color: "primary", className: "truncate", children: [addr.ownerType === "user" ? "User" : "Store", ": ", addr.ownerId] }), addr.city && (_jsx(Text, { size: "xs", color: "muted", children: [addr.city, addr.state, addr.postalCode].filter(Boolean).join(", ") }))] }), _jsxs(Row, { gap: "xs", align: "center", children: [addr.banStatus && (_jsx(Badge, { variant: BAN_BADGE[addr.banStatus] ?? "secondary", size: "sm", children: addr.banStatus.replace(/_/g, " ") })), addr.banStatus !== "suspicious" && (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => flagMutation.mutate(addr.id), disabled: flagMutation.isPending, children: "Flag Suspicious" }))] })] }, addr.id))) })] }, cluster.addressHash)))] }));
38
+ }
@@ -0,0 +1,5 @@
1
+ import React from "react";
2
+ export interface AdminAddressesViewProps {
3
+ children?: React.ReactNode;
4
+ }
5
+ export declare function AdminAddressesView(_props: AdminAddressesViewProps): import("react/jsx-runtime").JSX.Element;