@mohasinac/appkit 4.35.0 → 4.35.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -39,7 +39,20 @@ export declare function getUserSessions(userId: string): Promise<import("..").Se
39
39
  * including ones inside sent notification emails.
40
40
  */
41
41
  export declare function resolveProfileUser(identifier: string): Promise<UserDocument | null>;
42
- export declare function getPublicUserProfile(userId: string): Promise<Pick<UserDocument, "id" | "slug" | "displayName" | "photoURL" | "role" | "createdAt" | "storeId" | "storeSlug" | "publicProfile" | "stats"> | null>;
42
+ /**
43
+ * 🛑 React.cache-wrapped because a page and its generateMetadata both read it.
44
+ *
45
+ * `resolveProfileUser` tries up to THREE lookups (slug, id, uid), and
46
+ * `profile/[userId]/page.tsx` called this once in generateMetadata and again in
47
+ * the page body — so an uncached profile view cost up to six Firestore reads to
48
+ * answer one question. `sellers/[id]` had the same pair.
49
+ *
50
+ * This is the rule the SSR guide already states: every data-fetch exposed to a
51
+ * page + generateMetadata pair is memoised per argument list, so the second call
52
+ * is free. Note it caches the NARROW public projection, which is what both
53
+ * callers want; anything needing the whole document still goes to the repository.
54
+ */
55
+ export declare const getPublicUserProfile: (userId: string) => Promise<Pick<UserDocument, "id" | "slug" | "displayName" | "photoURL" | "role" | "createdAt" | "storeId" | "storeSlug" | "publicProfile" | "stats"> | null>;
43
56
  /** Fetch approved reviews for a store. storeId === storeSlug in this project. */
44
57
  export declare function getSellerReviews(storeId: string): Promise<Review[]>;
45
58
  export declare function getProfileStoreProducts(storeId: string): Promise<import("../../products").ProductDocument[]>;
@@ -4,6 +4,7 @@
4
4
  * Pure business functions for profile CRUD, seller stats, and session reads.
5
5
  * Auth, rate-limiting, and Next.js specifics are handled by the consumer.
6
6
  */
7
+ import { cache } from "react";
7
8
  import { maskPublicReview } from "../../../security";
8
9
  import { safeRead } from "../../../errors/safe-read";
9
10
  import { finalizeStagedMediaField } from "../../media/finalize";
@@ -47,7 +48,20 @@ export async function resolveProfileUser(identifier) {
47
48
  return byId;
48
49
  return userRepository.findByUid(identifier);
49
50
  }
50
- export async function getPublicUserProfile(userId) {
51
+ /**
52
+ * 🛑 React.cache-wrapped because a page and its generateMetadata both read it.
53
+ *
54
+ * `resolveProfileUser` tries up to THREE lookups (slug, id, uid), and
55
+ * `profile/[userId]/page.tsx` called this once in generateMetadata and again in
56
+ * the page body — so an uncached profile view cost up to six Firestore reads to
57
+ * answer one question. `sellers/[id]` had the same pair.
58
+ *
59
+ * This is the rule the SSR guide already states: every data-fetch exposed to a
60
+ * page + generateMetadata pair is memoised per argument list, so the second call
61
+ * is free. Note it caches the NARROW public projection, which is what both
62
+ * callers want; anything needing the whole document still goes to the repository.
63
+ */
64
+ export const getPublicUserProfile = cache(async function getPublicUserProfile(userId) {
51
65
  const user = await resolveProfileUser(userId);
52
66
  if (!user)
53
67
  return null;
@@ -63,7 +77,7 @@ export async function getPublicUserProfile(userId) {
63
77
  publicProfile: user.publicProfile,
64
78
  stats: user.stats,
65
79
  };
66
- }
80
+ });
67
81
  /** Fetch approved reviews for a store. storeId === storeSlug in this project. */
68
82
  export async function getSellerReviews(storeId) {
69
83
  const snapshot = await safeRead(() => reviewRepository.findApprovedByStore(storeId), { route: "/profile/[userId]", key: "reviews.findApprovedByStore", fallback: [] });
@@ -0,0 +1,3 @@
1
+ import type { CartDocument } from "../../cart/schemas/firestore";
2
+ /** The cart document id IS the user id — one cart per account. */
3
+ export declare const cartsTesterSeedData: Partial<CartDocument>[];
@@ -0,0 +1,70 @@
1
+ /*
2
+ * WHY: Shared tester sandbox — the LOCKED cart line a settled auction leaves behind.
3
+ * WHAT: One cart for user-tester-qa holding the win from auction-tester-sandbox-won.
4
+ *
5
+ * 🛑 WHY THIS FILE HAD TO EXIST.
6
+ *
7
+ * The sandbox seeded a won auction, its winning bid, and a delivered order made
8
+ * from it — the shape settlement produced BEFORE Root Cause #60, when it wrote an
9
+ * order document directly. That order was in a shape no orders list could render
10
+ * and no checkout could accept, so a winner had no way to pay at all; the fix
11
+ * routed a win through the CART as a locked line instead.
12
+ *
13
+ * The fixtures were never updated to match. So `win-auction` — the case whose
14
+ * label is literally "creates a payable locked cart line, not a stuck order" —
15
+ * could not pass: /user/bids offered "Pay now", it led to /checkout?lane=auction,
16
+ * and the cart was empty. Found by the tester 2026-09-06, which is exactly the
17
+ * kind of thing a case can only prove by being run.
18
+ *
19
+ * The delivered order fixture STAYS. It is the "win already completed" state and
20
+ * other cases read it. This adds the missing "win awaiting payment" state.
21
+ *
22
+ * 🛑 `isAuctionWin` is not decoration. `laneOf()` keys on `isAuctionWin || bidId`
23
+ * and ignores `listingType` and `locked` entirely — a line without it lands in the
24
+ * STANDARD lane, so the Won Auctions tab stays empty while a non-removable line
25
+ * sits in the ordinary cart. Both fields are set here so the lane is unambiguous.
26
+ *
27
+ * @tag domain:tester
28
+ * @tag layer:seed
29
+ * @tag pattern:none
30
+ * @tag access:server-only
31
+ * @tag consumers:seed/index.ts,seed/manifest.ts
32
+ * @tag sideEffects:none
33
+ */
34
+ import { testDataExpiresAt } from "./tester-ttl";
35
+ const NOW = new Date();
36
+ const hoursAgo = (n) => new Date(NOW.getTime() - n * 3600000);
37
+ /** The cart document id IS the user id — one cart per account. */
38
+ export const cartsTesterSeedData = [
39
+ {
40
+ id: "user-tester-qa",
41
+ userId: "user-tester-qa",
42
+ items: [
43
+ {
44
+ itemId: "cartitem-tester-sandbox-auction-won",
45
+ productId: "auction-tester-sandbox-won",
46
+ productTitle: "Test Auction — Already Won",
47
+ price: 15000,
48
+ currency: "INR",
49
+ quantity: 1,
50
+ storeId: "store-tester-qa-seller",
51
+ storeName: "Tester QA Seller",
52
+ listingType: "auction",
53
+ // Both, deliberately — see the lane note in the header.
54
+ isAuctionWin: true,
55
+ bidId: "bid-tester-sandbox-won-tester-qa",
56
+ // A win is non-removable and fixed-quantity: settlement is the only writer
57
+ // of this line, and declining to pay is handled by the forfeit sweep rather
58
+ // than by the buyer deleting it.
59
+ locked: true,
60
+ lockedPrice: 15000,
61
+ addedAt: hoursAgo(2),
62
+ updatedAt: hoursAgo(2),
63
+ },
64
+ ],
65
+ isTestData: true,
66
+ testDataExpiresAt: testDataExpiresAt(),
67
+ createdAt: hoursAgo(2),
68
+ updatedAt: hoursAgo(2),
69
+ },
70
+ ];
@@ -6,5 +6,6 @@ export { blogTesterSeedData } from "./blog-tester-seed-data";
6
6
  export { eventsTesterSeedData } from "./events-tester-seed-data";
7
7
  export { couponsTesterSeedData } from "./coupons-tester-seed-data";
8
8
  export { bidsTesterSeedData } from "./bids-tester-seed-data";
9
+ export { cartsTesterSeedData } from "./carts-tester-seed-data";
9
10
  export { ordersTesterSeedData } from "./orders-tester-seed-data";
10
11
  export { offersTesterSeedData } from "./offers-tester-seed-data";
@@ -6,5 +6,6 @@ export { blogTesterSeedData } from "./blog-tester-seed-data";
6
6
  export { eventsTesterSeedData } from "./events-tester-seed-data";
7
7
  export { couponsTesterSeedData } from "./coupons-tester-seed-data";
8
8
  export { bidsTesterSeedData } from "./bids-tester-seed-data";
9
+ export { cartsTesterSeedData } from "./carts-tester-seed-data";
9
10
  export { ordersTesterSeedData } from "./orders-tester-seed-data";
10
11
  export { offersTesterSeedData } from "./offers-tester-seed-data";
@@ -76,6 +76,17 @@ export const cartsSeedData = [
76
76
  storeId: "store-beyblade-arena",
77
77
  storeName: "Beyblade Arena",
78
78
  listingType: "auction",
79
+ /*
80
+ * 🛑 `isAuctionWin` is what puts this line in the AUCTION lane.
81
+ *
82
+ * `laneOf()` keys on `isAuctionWin || bidId` — `listingType: "auction"`
83
+ * and `locked: true` mean nothing to it. Without this flag the line fell
84
+ * into the STANDARD lane, so the cart's "Won Auctions" tab was empty
85
+ * while a non-removable auction line sat in the ordinary cart inflating
86
+ * its total, and the lane gate never blocked anything. Found by the
87
+ * tester 2026-09-06.
88
+ */
89
+ isAuctionWin: true,
79
90
  locked: true,
80
91
  addedAt: daysAgo(1),
81
92
  updatedAt: daysAgo(1),
@@ -78,7 +78,7 @@ export { historySeedData } from "./history-seed-data";
78
78
  export { groupedListingsSeedData } from "./grouped-listings-seed-data";
79
79
  export { scammersSeedData } from "./scammers-seed-data";
80
80
  export { supportTicketsSeedData } from "./support-tickets-seed-data";
81
- export { testerChecklistSeedData, storesTesterSeedData, categoriesTesterSeedData, productsTesterSeedData, blogTesterSeedData, eventsTesterSeedData, couponsTesterSeedData, bidsTesterSeedData, ordersTesterSeedData, } from "../features/tester/seed-data";
81
+ export { testerChecklistSeedData, storesTesterSeedData, categoriesTesterSeedData, productsTesterSeedData, blogTesterSeedData, eventsTesterSeedData, couponsTesterSeedData, bidsTesterSeedData, cartsTesterSeedData, ordersTesterSeedData, } from "../features/tester/seed-data";
82
82
  export { productFeaturesSeedData } from "./product-features-seed-data";
83
83
  export { offersSeedData } from "./offers-seed-data";
84
84
  export { payoutMethodsSeedData, shippingConfigsSeedData, analyticsCardsSeedData, analyticsAlertsSeedData, storeCategoriesSeedData, listingTemplatesSeedData, moderationQueueSeedData, reportsSeedData, itemRequestsSeedData, storeWhatsAppConfigSeedData, storeGoogleConfigSeedData, } from "./store-extensions-seed-data";
@@ -69,7 +69,7 @@ export { scammersSeedData } from "./scammers-seed-data";
69
69
  export { supportTicketsSeedData } from "./support-tickets-seed-data";
70
70
  // Tester sandbox seed fixtures live in ../features/tester/seed-data (isolated from this
71
71
  // folder on purpose) — re-exported here so every other seed consumer keeps one import surface.
72
- export { testerChecklistSeedData, storesTesterSeedData, categoriesTesterSeedData, productsTesterSeedData, blogTesterSeedData, eventsTesterSeedData, couponsTesterSeedData, bidsTesterSeedData, ordersTesterSeedData, } from "../features/tester/seed-data";
72
+ export { testerChecklistSeedData, storesTesterSeedData, categoriesTesterSeedData, productsTesterSeedData, blogTesterSeedData, eventsTesterSeedData, couponsTesterSeedData, bidsTesterSeedData, cartsTesterSeedData, ordersTesterSeedData, } from "../features/tester/seed-data";
73
73
  export { productFeaturesSeedData } from "./product-features-seed-data";
74
74
  export { offersSeedData } from "./offers-seed-data";
75
75
  // S-STORE Extensions (Tier S-STORE) — 11 new collections
@@ -50,7 +50,7 @@ import { offersSeedData } from "./offers-seed-data";
50
50
  import { couponUsageSeedData } from "./coupon-usage-seed-data";
51
51
  import { claimedCouponsSeedData } from "./claimed-coupons-seed-data";
52
52
  // Tester sandbox seed fixtures live in ../features/tester/seed-data (isolated on purpose).
53
- import { testerChecklistSeedData, storesTesterSeedData, categoriesTesterSeedData, productsTesterSeedData, blogTesterSeedData, eventsTesterSeedData, couponsTesterSeedData, bidsTesterSeedData, ordersTesterSeedData, offersTesterSeedData, } from "../features/tester/seed-data";
53
+ import { testerChecklistSeedData, storesTesterSeedData, categoriesTesterSeedData, productsTesterSeedData, blogTesterSeedData, eventsTesterSeedData, couponsTesterSeedData, bidsTesterSeedData, cartsTesterSeedData, ordersTesterSeedData, offersTesterSeedData, } from "../features/tester/seed-data";
54
54
  function asArr(items) {
55
55
  return items ?? [];
56
56
  }
@@ -166,7 +166,7 @@ export const SEED_MANIFEST = {
166
166
  name: a.label ?? a.fullName ?? a.id,
167
167
  })),
168
168
  ]),
169
- carts: pick(asArr(cartsSeedData).map((c) => ({
169
+ carts: pick([...asArr(cartsSeedData), ...asArr(cartsTesterSeedData)].map((c) => ({
170
170
  ...c,
171
171
  name: c.userId ?? c.id,
172
172
  }))),
@@ -403,7 +403,11 @@ const notificationRecords = [
403
403
  title: "Your auction ended with no winner",
404
404
  message: "The highest bid did not meet your reserve price. The listing has been archived.",
405
405
  isRead: false,
406
- relatedId: "auction-beyblade-lord-spryzen-ended-unsold",
406
+ // 🛑 The real id carries `burst` — see products-auctions-seed-data.ts. Without
407
+ // it this notification's "view details" link resolves to nothing, which is the
408
+ // dangling-FK shape Root Cause #26 describes: the row renders perfectly and its
409
+ // only affordance goes nowhere.
410
+ relatedId: "auction-beyblade-burst-lord-spryzen-ended-unsold",
407
411
  relatedType: "product",
408
412
  createdAt: daysAgo(1),
409
413
  updatedAt: daysAgo(1),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "4.35.0",
3
+ "version": "4.35.1",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"