@mohasinac/appkit 4.23.0 → 4.24.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.
Files changed (47) hide show
  1. package/dist/_internal/server/features/bundles/actions.js +7 -0
  2. package/dist/_internal/server/features/checkout/actions.js +24 -16
  3. package/dist/_internal/shared/actions/action-registry.js +41 -0
  4. package/dist/_internal/shared/checkout/order-math.d.ts +23 -5
  5. package/dist/_internal/shared/checkout/order-math.js +29 -6
  6. package/dist/_internal/shared/checkout/rules/_limits.d.ts +12 -2
  7. package/dist/_internal/shared/checkout/rules/_limits.js +12 -2
  8. package/dist/_internal/shared/features/cart/config.d.ts +2 -1
  9. package/dist/_internal/shared/features/cart/config.js +2 -1
  10. package/dist/_internal/shared/features/cart/schema.d.ts +6 -6
  11. package/dist/_internal/shared/features/notifications/action-url.d.ts +47 -0
  12. package/dist/_internal/shared/features/notifications/action-url.js +111 -0
  13. package/dist/constants/notification-types.d.ts +38 -10
  14. package/dist/constants/notification-types.js +39 -28
  15. package/dist/features/account/types/index.d.ts +1 -10
  16. package/dist/features/admin/actions/notification-actions.d.ts +12 -0
  17. package/dist/features/admin/actions/notification-actions.js +149 -4
  18. package/dist/features/admin/schemas/firestore.d.ts +14 -3
  19. package/dist/features/admin/schemas/firestore.js +79 -0
  20. package/dist/features/cart/actions/cart-actions.d.ts +16 -0
  21. package/dist/features/cart/actions/cart-actions.js +26 -12
  22. package/dist/features/cart/components/CartDrawer.js +2 -1
  23. package/dist/features/cart/schemas/firestore.d.ts +132 -8
  24. package/dist/features/cart/schemas/firestore.js +22 -0
  25. package/dist/features/email/index.d.ts +1 -1
  26. package/dist/features/email/index.js +5 -1
  27. package/dist/features/email/notification-templates.d.ts +35 -0
  28. package/dist/features/email/notification-templates.js +96 -0
  29. package/dist/features/orders/schemas/index.d.ts +2 -2
  30. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +30 -0
  31. package/dist/index.d.ts +3 -3
  32. package/dist/index.js +3 -1
  33. package/dist/schemas/registry.d.ts +2 -2
  34. package/dist/seed/factories/notification.factory.d.ts +2 -1
  35. package/dist/server-entry.d.ts +1 -0
  36. package/dist/server-entry.js +5 -0
  37. package/dist/server.d.ts +5 -1
  38. package/dist/server.js +10 -1
  39. package/dist/styles.css +58 -1
  40. package/dist/tailwind-utilities.css +1 -1
  41. package/dist/ui/components/QuantityStepper.d.ts +38 -0
  42. package/dist/ui/components/QuantityStepper.js +20 -0
  43. package/dist/ui/components/QuantityStepper.style.css +56 -0
  44. package/dist/ui/components/index.style.css +1 -0
  45. package/dist/ui/index.d.ts +2 -0
  46. package/dist/ui/index.js +1 -0
  47. package/package.json +1 -1
@@ -6,6 +6,53 @@
6
6
  */
7
7
  import type { BaseDocument } from "../../../_internal/shared/types/base-document";
8
8
  import type { ListingType } from "../../products/types/index";
9
+ /**
10
+ * What KIND of thing one cart line represents.
11
+ *
12
+ * - absent — an ordinary single-product line.
13
+ * - `"bundle"` — a fixed, all-or-nothing set priced as a whole
14
+ * (`categoryType:"bundle"` row on `categories`). Members are
15
+ * not pickable; the only stepper is the line-level one and it
16
+ * means COPIES OF THE BUNDLE.
17
+ * - `"group"` — a buyer-assembled selection out of a product group
18
+ * (`product.groupId`) or a `groupedListings` doc. Members ARE
19
+ * pickable; there is no line-level stepper.
20
+ */
21
+ export type CartLineKind = "bundle" | "group";
22
+ /** Which collection a `"group"` line's members were picked from. */
23
+ export type CartGroupSource = "product-group" | "grouped-listing";
24
+ /**
25
+ * One member product inside a multi-member cart line.
26
+ *
27
+ * 🛑 THE QUANTITY INVARIANT — every reader and writer depends on it:
28
+ *
29
+ * `CartItemDocument.quantity` = copies of the whole selection
30
+ * `CartLineMember.quantity` = units of THIS member per copy
31
+ * demand for a product = item.quantity × member.quantity
32
+ *
33
+ * A `"group"` line pins `item.quantity` to 1, which is what lets `unitPriceFor`
34
+ * keep returning a UNIT price that every existing call site multiplies by
35
+ * `item.quantity` without double-counting. A `"bundle"` line is the mirror
36
+ * image: `item.quantity` is the editable one and member quantities are frozen
37
+ * at add time.
38
+ *
39
+ * Prices/titles/images are SNAPSHOTS taken server-side at add time. They are
40
+ * never accepted from the client — see `POST /api/cart/group`, which takes ids
41
+ * and quantities only.
42
+ */
43
+ export interface CartLineMember {
44
+ productId: string;
45
+ /** Units of this member per copy of the line. See the invariant above. */
46
+ quantity: number;
47
+ /** Snapshot of the member's own price at add time, in decimal rupees. */
48
+ unitPrice: number;
49
+ title: string;
50
+ image?: string;
51
+ /** Snapshot for per-member GST proration at checkout (`lineTaxComponentsFor`). */
52
+ gstRate?: number;
53
+ hsnCode?: string;
54
+ listingType?: ListingType;
55
+ }
9
56
  export interface CartItemDocument {
10
57
  itemId: string;
11
58
  productId: string;
@@ -64,16 +111,47 @@ export interface CartItemDocument {
64
111
  /**
65
112
  * SB-UNI-4 2026-05-13 — bundle identifier when this cart line represents a
66
113
  * bundle (categoryType:"bundle" row on the categories collection). When set,
67
- * `productId` points at the bundle category's id, `price` is the locked
68
- * bundlePrice, and `bundleProductIds` snapshots the member product
69
- * ids at add-to-cart time. Order-side fan-out into per-product OrderItem
70
- * entries lands in S-SBUNI-5 (checkout-side stock decrement + per-store
71
- * grouping); until then the foundation is here but BundleDetailView keeps
72
- * the "Add to cart coming soon" notice up.
114
+ * `productId` points at the bundle category's id and `price` is the locked
115
+ * bundlePrice.
116
+ *
117
+ * This is an IDENTITY, not a mirror — it is what the order-side collapse key
118
+ * (`groupOrderItemsByLine`) groups on, and it keeps being written.
73
119
  */
74
120
  bundleCategorySlug?: string;
75
- /** Snapshot of `bundle.bundleProductIds` at add-to-cart time. */
121
+ /**
122
+ * @deprecated Legacy carts only. Superseded by `groupMembers`, which can
123
+ * express per-member quantities where a bare id array cannot.
124
+ *
125
+ * Do NOT write this from new code and do NOT read it directly — a
126
+ * hand-maintained mirror alongside `groupMembers` is exactly the drift
127
+ * Recurrent Root Cause #42 describes. Read through `getCartLineMembers()`
128
+ * (`_internal/server/features/checkout/bundle-expansion.ts`), which falls
129
+ * back to these ids at quantity 1 so lines written before `groupMembers`
130
+ * existed keep behaving byte-identically.
131
+ */
76
132
  bundleProductIds?: string[];
133
+ /**
134
+ * Discriminator for a multi-member line. Absent on an ordinary
135
+ * single-product line. See `CartLineKind` for the two behaviours.
136
+ */
137
+ lineKind?: CartLineKind;
138
+ /** Which collection the members came from. `"group"` lines only. */
139
+ groupSource?: CartGroupSource;
140
+ /** `product.groupId` | `groupedListing.id` | the bundle category's id. */
141
+ groupId?: string;
142
+ /** Slug for the line's detail href. Bundles use `bundleCategorySlug` instead. */
143
+ groupSlug?: string;
144
+ /** Display snapshot of the group/bundle name. */
145
+ groupTitle?: string;
146
+ /**
147
+ * The members of a multi-member line, AUTHORITATIVE over `bundleProductIds`.
148
+ * See `CartLineMember` for the quantity invariant.
149
+ *
150
+ * `price` on this document is derived from this array by the repository
151
+ * (`deriveLinePrice`) at every write, never set by a caller — so no caller
152
+ * can forget to keep the two in step.
153
+ */
154
+ groupMembers?: CartLineMember[];
77
155
  /**
78
156
  * S-SBUNI-RULES 2026-05-13 — buyer's chosen shipping provider for this
79
157
  * item, snapshotted at cart time. Locked at checkout; the per-tab
@@ -149,10 +227,32 @@ export declare const CART_FIELDS: {
149
227
  readonly STORE_ID: "storeId";
150
228
  readonly STORE_NAME: "storeName";
151
229
  readonly LISTING_TYPE: "listingType";
230
+ readonly IS_OFFER: "isOffer";
231
+ readonly OFFER_ID: "offerId";
232
+ readonly IS_AUCTION_WIN: "isAuctionWin";
233
+ readonly AUCTION_ID: "auctionId";
234
+ readonly BID_ID: "bidId";
235
+ readonly IS_BUYOUT: "isBuyout";
236
+ readonly LOCKED_PRICE: "lockedPrice";
237
+ readonly CHECKOUT_DEADLINE: "checkoutDeadline";
238
+ readonly LOCKED: "locked";
239
+ readonly BUNDLE_CATEGORY_SLUG: "bundleCategorySlug";
240
+ /** @deprecated legacy read path — see CartItemDocument.bundleProductIds */
241
+ readonly BUNDLE_PRODUCT_IDS: "bundleProductIds";
242
+ readonly LINE_KIND: "lineKind";
243
+ readonly GROUP_SOURCE: "groupSource";
244
+ readonly GROUP_ID: "groupId";
245
+ readonly GROUP_SLUG: "groupSlug";
246
+ readonly GROUP_TITLE: "groupTitle";
247
+ readonly GROUP_MEMBERS: "groupMembers";
248
+ readonly CHOSEN_SHIPPING_PROVIDER_ID: "chosenShippingProviderId";
249
+ readonly CHOSEN_SHIPPING_FEE: "chosenShippingFee";
152
250
  readonly ADDED_AT: "addedAt";
153
251
  readonly UPDATED_AT: "updatedAt";
154
252
  };
155
253
  };
254
+ export declare const CART_LINE_KIND_VALUES: readonly ["bundle", "group"];
255
+ export declare const CART_GROUP_SOURCE_VALUES: readonly ["product-group", "grouped-listing"];
156
256
  export declare const DEFAULT_CART_DATA: Partial<CartDocument>;
157
257
  export type CartCreateInput = Omit<CartDocument, "id" | "createdAt" | "updatedAt">;
158
258
  export type AddToCartInput = {
@@ -178,12 +278,36 @@ export type AddToCartInput = {
178
278
  locked?: boolean;
179
279
  /** SB-UNI-4 2026-05-13 — bundle identifier when the line is a bundle. */
180
280
  bundleCategorySlug?: string;
181
- /** Snapshot of bundle members at add-to-cart time. */
281
+ /** @deprecated Legacy only — write `groupMembers` instead. */
182
282
  bundleProductIds?: string[];
283
+ lineKind?: CartLineKind;
284
+ groupSource?: CartGroupSource;
285
+ groupId?: string;
286
+ groupSlug?: string;
287
+ groupTitle?: string;
288
+ groupMembers?: CartLineMember[];
289
+ };
290
+ /**
291
+ * Input for creating a multi-member line. `price` is deliberately absent — the
292
+ * repository derives it from `groupMembers` so the two can never disagree.
293
+ */
294
+ export type AddGroupLineInput = Omit<AddToCartInput, "price" | "groupMembers"> & {
295
+ groupMembers: CartLineMember[];
183
296
  };
184
297
  export type UpdateCartItemInput = {
185
298
  quantity: number;
186
299
  };
300
+ /**
301
+ * Body of a per-member edit. The WHOLE array is sent, not a delta: the cart is
302
+ * a single Firestore document written with `set()`, so N per-member writes
303
+ * would be both a Rule #6 cost and a real lost-update race on a double-click.
304
+ */
305
+ export type UpdateCartGroupMembersInput = {
306
+ groupMembers: Array<{
307
+ productId: string;
308
+ quantity: number;
309
+ }>;
310
+ };
187
311
  export declare const cartQueryHelpers: {
188
312
  readonly byUserId: (userId: string) => {
189
313
  field: "userId";
@@ -23,10 +23,32 @@ export const CART_FIELDS = {
23
23
  STORE_ID: "storeId",
24
24
  STORE_NAME: "storeName",
25
25
  LISTING_TYPE: "listingType",
26
+ IS_OFFER: "isOffer",
27
+ OFFER_ID: "offerId",
28
+ IS_AUCTION_WIN: "isAuctionWin",
29
+ AUCTION_ID: "auctionId",
30
+ BID_ID: "bidId",
31
+ IS_BUYOUT: "isBuyout",
32
+ LOCKED_PRICE: "lockedPrice",
33
+ CHECKOUT_DEADLINE: "checkoutDeadline",
34
+ LOCKED: "locked",
35
+ BUNDLE_CATEGORY_SLUG: "bundleCategorySlug",
36
+ /** @deprecated legacy read path — see CartItemDocument.bundleProductIds */
37
+ BUNDLE_PRODUCT_IDS: "bundleProductIds",
38
+ LINE_KIND: "lineKind",
39
+ GROUP_SOURCE: "groupSource",
40
+ GROUP_ID: "groupId",
41
+ GROUP_SLUG: "groupSlug",
42
+ GROUP_TITLE: "groupTitle",
43
+ GROUP_MEMBERS: "groupMembers",
44
+ CHOSEN_SHIPPING_PROVIDER_ID: "chosenShippingProviderId",
45
+ CHOSEN_SHIPPING_FEE: "chosenShippingFee",
26
46
  ADDED_AT: "addedAt",
27
47
  UPDATED_AT: "updatedAt",
28
48
  },
29
49
  };
50
+ export const CART_LINE_KIND_VALUES = ["bundle", "group"];
51
+ export const CART_GROUP_SOURCE_VALUES = ["product-group", "grouped-listing"];
30
52
  export const DEFAULT_CART_DATA = {
31
53
  items: [],
32
54
  };
@@ -5,5 +5,5 @@
5
5
  * `renderToStaticMarkup`. They emit table-based, inline-styled markup that
6
6
  * email clients render correctly.
7
7
  */
8
- export { EmailButton, EmailColumn, EmailContainer, EmailDivider, EmailDoc, EmailFooter, EmailHeader, EmailImage, EmailLink, EmailRow, } from "./primitives";
8
+ export { EmailBold, EmailButton, EmailColumn, EmailContainer, EmailDivider, EmailDoc, EmailFooter, EmailHeader, EmailImage, EmailLink, EmailRow, } from "./primitives";
9
9
  export type { EmailButtonProps, EmailColumnProps, EmailContainerProps, EmailDividerProps, EmailDocProps, EmailFooterProps, EmailHeaderProps, EmailImageProps, EmailLinkProps, EmailRowProps, EmailTone, } from "./primitives";
@@ -5,4 +5,8 @@
5
5
  * `renderToStaticMarkup`. They emit table-based, inline-styled markup that
6
6
  * email clients render correctly.
7
7
  */
8
- export { EmailButton, EmailColumn, EmailContainer, EmailDivider, EmailDoc, EmailFooter, EmailHeader, EmailImage, EmailLink, EmailRow, } from "./primitives";
8
+ export {
9
+ // Was MISSING from this barrel, so both consumers reached past it into
10
+ // ./primitives directly — a workaround that reads as intentional and
11
+ // quietly says "the barrel is not the public surface after all".
12
+ EmailBold, EmailButton, EmailColumn, EmailContainer, EmailDivider, EmailDoc, EmailFooter, EmailHeader, EmailImage, EmailLink, EmailRow, } from "./primitives";
@@ -0,0 +1,35 @@
1
+ import "server-only";
2
+ import type { NotificationType } from "../admin/schemas/firestore";
3
+ export interface NotificationEmailContext {
4
+ title: string;
5
+ message: string;
6
+ siteName: string;
7
+ /** Absolute URL the CTA opens. Absent → the email renders without a button. */
8
+ actionUrl?: string;
9
+ }
10
+ export interface NotificationTemplateDef {
11
+ /** CTA label. Absent → no button, for a type with nowhere to go. */
12
+ cta?: string;
13
+ /**
14
+ * One line of framing above the message, so the reader knows what KIND of
15
+ * mail this is before reading it. Deliberately not the message itself —
16
+ * duplicating it is worse than omitting it.
17
+ */
18
+ lead: string;
19
+ /** Terminal-negative types render in the danger tone. */
20
+ tone?: "default" | "danger" | "success";
21
+ }
22
+ /**
23
+ * 🛑 Every notification type, no exceptions.
24
+ *
25
+ * When adding a type, the compiler will ask for an entry here. Answer three
26
+ * questions: what does the reader need to know before the message (`lead`),
27
+ * where are they going (`cta`, or none if nowhere), and is this good news.
28
+ */
29
+ export declare const NOTIFICATION_EMAIL_TEMPLATES: Record<NotificationType, NotificationTemplateDef>;
30
+ /**
31
+ * Render one notification as an email.
32
+ *
33
+ * Returns the full HTML document, ready for `emailHtml`.
34
+ */
35
+ export declare function renderNotificationEmail(type: NotificationType, ctx: NotificationEmailContext): string;
@@ -0,0 +1,96 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import "server-only";
3
+ import { EmailBold, EmailButton, EmailContainer, EmailDivider, EmailDoc, EmailFooter, EmailHeader, EmailRow, } from "./primitives";
4
+ /**
5
+ * A real email per notification type.
6
+ *
7
+ * ## What this replaces
8
+ *
9
+ * `emailHtml` has existed on `SendNotificationInput` since the channel was
10
+ * built, and **0 of 40** call sites passed it — so every one of the 28
11
+ * notification types fell through to `html: \`<p>${message}</p>\``. A bare
12
+ * paragraph, no subject context, no branding, nothing to click. That is the
13
+ * reported "emails are sent but I can't understand what is being sent".
14
+ *
15
+ * ## Why a Record, not a lookup with a fallback
16
+ *
17
+ * `Record<NotificationType, TemplateDef>` — a new notification type cannot
18
+ * compile without an email. A `Partial` with a default would have let the
19
+ * next type silently go back to `<p>{message}</p>`, which is how the feature
20
+ * got here in the first place.
21
+ *
22
+ * ## The two structural constraints
23
+ *
24
+ * 1. **`renderToStaticMarkup` must be a lazy `require`.** A top-level
25
+ * `import` from "react-dom/server" is flagged by Next's static tracer as
26
+ * soon as any module in the re-export chain is reachable from a Server
27
+ * Component — and this one is, via `@mohasinac/appkit/server`. `require()`
28
+ * is opaque to that analyser, so the chain stops here. Same reason
29
+ * `features/contact/email.tsx` does it.
30
+ * 2. **Never a raw `<table>`.** The `Email*` primitives emit the table-based,
31
+ * inline-styled markup email clients actually render; hand-rolled markup
32
+ * is what `audit-email-raw-html` blocks.
33
+ */
34
+ // See constraint (1) above — this MUST stay a lazy require.
35
+ function renderToStaticMarkup(el) {
36
+ const m = require("react-dom/server");
37
+ return m.renderToStaticMarkup(el);
38
+ }
39
+ /**
40
+ * 🛑 Every notification type, no exceptions.
41
+ *
42
+ * When adding a type, the compiler will ask for an entry here. Answer three
43
+ * questions: what does the reader need to know before the message (`lead`),
44
+ * where are they going (`cta`, or none if nowhere), and is this good news.
45
+ */
46
+ export const NOTIFICATION_EMAIL_TEMPLATES = {
47
+ order_placed: { lead: "We've received your order.", cta: "View order", tone: "success" },
48
+ order_confirmed: { lead: "Your order is confirmed and being prepared.", cta: "View order", tone: "success" },
49
+ order_shipped: { lead: "Your order is on its way.", cta: "Track order", tone: "success" },
50
+ order_delivered: { lead: "Your order has been delivered.", cta: "View order", tone: "success" },
51
+ order_cancelled: { lead: "Your order has been cancelled.", cta: "View order", tone: "danger" },
52
+ refund_initiated: { lead: "A refund is on its way back to you.", cta: "View order" },
53
+ payment_review: { lead: "There's an update on your payment.", cta: "View payment" },
54
+ emi_installment_due_soon: { lead: "An EMI installment is due soon.", cta: "View order" },
55
+ emi_installment_overdue: { lead: "An EMI installment is overdue.", cta: "View order", tone: "danger" },
56
+ bid_placed: { lead: "Your bid has been placed.", cta: "View bids" },
57
+ bid_outbid: { lead: "Someone has outbid you.", cta: "View bids", tone: "danger" },
58
+ bid_won: { lead: "You won the auction.", cta: "Complete checkout", tone: "success" },
59
+ bid_lost: { lead: "This auction closed without you.", cta: "View bids" },
60
+ auction_ended: { lead: "Your auction has ended.", cta: "View auction" },
61
+ offer_received: { lead: "A buyer has made you an offer.", cta: "Review offer" },
62
+ offer_responded: { lead: "The seller has responded to your offer.", cta: "View offer" },
63
+ offer_counter_accepted: { lead: "Your counter-offer was accepted.", cta: "Complete checkout", tone: "success" },
64
+ offer_expired: { lead: "An offer has expired.", cta: "View offers" },
65
+ prize_won: { lead: "You've won a prize.", cta: "View prize", tone: "success" },
66
+ prize_reveal_expired: { lead: "A prize reveal window has closed.", cta: "View prize draws" },
67
+ review_approved: { lead: "A review has been published.", cta: "Read review" },
68
+ review_replied: { lead: "Someone replied to your review.", cta: "Read reply" },
69
+ product_available: { lead: "Something you were watching is back in stock.", cta: "View listing" },
70
+ catalogue_images_stale: { lead: "Your catalogue photos need refreshing.", cta: "Update photos" },
71
+ welcome: { lead: "Welcome aboard.", cta: "Go to your account", tone: "success" },
72
+ account_action: { lead: "There's been a change to your account.", cta: "View account" },
73
+ // No CTA: a promotion's destination is whatever the copy names, and a
74
+ // generic "View" button on a marketing mail is worse than none.
75
+ promotion: { lead: "An offer you might like." },
76
+ // No CTA for the same reason — a system announcement has no record behind
77
+ // it, and `resolveNotificationActionUrl` correctly returns undefined.
78
+ system: { lead: "A message from the team." },
79
+ };
80
+ /**
81
+ * Render one notification as an email.
82
+ *
83
+ * Returns the full HTML document, ready for `emailHtml`.
84
+ */
85
+ export function renderNotificationEmail(type, ctx) {
86
+ const def = NOTIFICATION_EMAIL_TEMPLATES[type];
87
+ /*
88
+ * `EmailButton` takes a narrower tone set than `EmailTone` — brand / accent
89
+ * / neutral only, because a button is a call to action and there is no
90
+ * useful "danger" button. So the template's tone drives the BUTTON's
91
+ * emphasis, not its colour: bad news gets the neutral button ("View order"
92
+ * on a cancellation should not shout), good news gets the brand one.
93
+ */
94
+ const buttonTone = def.tone === "danger" ? "neutral" : "brand";
95
+ return `<!DOCTYPE html>${renderToStaticMarkup(_jsx(EmailDoc, { title: ctx.title, children: _jsxs(EmailContainer, { children: [_jsx(EmailHeader, { brandName: ctx.siteName }), _jsxs(EmailRow, { children: [_jsx(EmailBold, { children: ctx.title }), _jsx("br", {}), _jsx("br", {}), def.lead] }), _jsx(EmailDivider, {}), _jsx(EmailRow, { children: ctx.message }), def.cta && ctx.actionUrl ? (_jsx(EmailRow, { children: _jsx(EmailButton, { href: ctx.actionUrl, tone: buttonTone, children: def.cta }) })) : null, _jsx(EmailFooter, { copyright: `© ${ctx.siteName}` })] }) }))}`;
96
+ }
@@ -612,6 +612,7 @@ export declare const orderFirestoreSchema: z.ZodObject<{
612
612
  shelf: string;
613
613
  bin: string;
614
614
  } | undefined;
615
+ offerId?: string | undefined;
615
616
  orderType?: string | undefined;
616
617
  imageUrls?: string[] | undefined;
617
618
  refundFeeDeducted?: number | undefined;
@@ -649,7 +650,6 @@ export declare const orderFirestoreSchema: z.ZodObject<{
649
650
  }[] | undefined;
650
651
  emiRemainingBalance?: number | undefined;
651
652
  emiComplete?: boolean | undefined;
652
- offerId?: string | undefined;
653
653
  isNonRefundable?: boolean | undefined;
654
654
  paymentBatchId?: string | undefined;
655
655
  refunds?: {
@@ -760,6 +760,7 @@ export declare const orderFirestoreSchema: z.ZodObject<{
760
760
  shelf: string;
761
761
  bin: string;
762
762
  } | undefined;
763
+ offerId?: string | undefined;
763
764
  orderType?: string | undefined;
764
765
  imageUrls?: string[] | undefined;
765
766
  refundFeeDeducted?: number | undefined;
@@ -797,7 +798,6 @@ export declare const orderFirestoreSchema: z.ZodObject<{
797
798
  }[] | undefined;
798
799
  emiRemainingBalance?: number | undefined;
799
800
  emiComplete?: boolean | undefined;
800
- offerId?: string | undefined;
801
801
  isNonRefundable?: boolean | undefined;
802
802
  paymentBatchId?: string | undefined;
803
803
  refunds?: {
@@ -1156,6 +1156,36 @@ const rawTesterChecklistItems = [
1156
1156
  description: "The fix had to cover the update path as well as create — a create-only fix would mean the listing saved correctly and then lost its per-type block the first time anyone edited anything else (Root Cause #39).",
1157
1157
  href: "/store/classified",
1158
1158
  },
1159
+ {
1160
+ key: "notification-email-is-a-real-email",
1161
+ label: "A notification email has a heading, a lead line and a button — not one bare paragraph",
1162
+ description: "`emailHtml` existed on the input since the channel was built and 0 of 40 call sites passed it, so all 28 notification types shipped `<p>{message}</p>`. Trigger any notification that emails (place an order as a buyer). The mail must carry the site header, a bold title, a one-line lead saying what kind of mail it is, the message, and a CTA button.",
1163
+ href: "/user/notifications",
1164
+ },
1165
+ {
1166
+ key: "notification-click-lands-on-the-right-page",
1167
+ label: "Clicking a notification opens the record it is about, in YOUR portal",
1168
+ description: "actionUrl was set by 5 of 40 writers, so 35 notifications had nothing to click. It is now resolved centrally from relatedType + relatedId + audience. Check that an order notification opens /user/orders/view/... for the buyer and /store/orders/.../view for the seller — the SAME event, two destinations. A bid or offer lands on the list, deliberately: no per-record page exists in any role yet.",
1169
+ href: "/user/notifications",
1170
+ },
1171
+ {
1172
+ key: "admin-can-filter-every-notification-type",
1173
+ label: "The admin notifications filter offers all 28 types, and no imaginary ones",
1174
+ description: "The chips came from a hand-written 9-value list while the document held 27 — so 18 real types were unfilterable and two chips (`review_posted`, `payout_processed`) matched zero rows because no notification has ever had those values. The list is derived now. Confirm `offer_received` and `payment_review` are both present.",
1175
+ href: "/admin/notifications",
1176
+ },
1177
+ {
1178
+ key: "admin-can-allowlist-every-type-per-channel",
1179
+ label: "Site Settings can allow-list offer and payment-review notifications for email",
1180
+ description: "Same 9-value list fed the per-channel allow-list, so an admin literally could not allow-list `offer_received` or `payment_review` — the toggle did not exist. Open Site Settings → Notifications and confirm every type appears in the per-channel type picker.",
1181
+ href: "/admin/site",
1182
+ },
1183
+ {
1184
+ key: "notification-optout-is-honoured-for-emi-and-payment",
1185
+ label: "Turning off order updates actually stops EMI and payment-review emails",
1186
+ description: "`typeToPrefsKey` was a Partial map missing `emi_installment_due_soon`, `emi_installment_overdue` and `payment_review` — and a missing key meant the opt-out check never ran, so the user's setting was silently ignored while reading as honoured. Turn off Order updates in /user/settings, then trigger a payment-review notification and confirm no email arrives.",
1187
+ href: "/user/settings",
1188
+ },
1159
1189
  {
1160
1190
  key: "ticket-resolution-timestamp-stamped",
1161
1191
  label: "Resolving a support ticket records WHEN it was resolved",
package/dist/index.d.ts CHANGED
@@ -678,7 +678,8 @@ export { groupedListingsSeedData } from "./seed/index";
678
678
  export type { CouponDiscountType } from "./seed/index";
679
679
  export type { FirestoreIndexConfig } from "./seed/index";
680
680
  export type { HomepageSectionType } from "./seed/index";
681
- export type { NotificationType } from "./seed/index";
681
+ export type { NotificationType } from "./features/admin/schemas/firestore";
682
+ export { NOTIFICATION_TYPE_VALUES } from "./features/admin/schemas/firestore";
682
683
  export type { PayoutStatus } from "./seed/index";
683
684
  export type { SeedAddressDocument } from "./seed/index";
684
685
  export type { SeedBaseOrderDocument } from "./seed/index";
@@ -1047,7 +1048,6 @@ export type { UserAccountHubViewProps } from "./features/account/index";
1047
1048
  export type { UserAddress } from "./features/account/index";
1048
1049
  export type { UserAddressesViewLabels } from "./features/account/index";
1049
1050
  export type { UserAddressesViewProps } from "./features/account/index";
1050
- export type { UserNotification } from "./features/account/index";
1051
1051
  export type { UserNotificationsViewLabels } from "./features/account/index";
1052
1052
  export type { UserNotificationsViewProps } from "./features/account/index";
1053
1053
  export type { UserOffer } from "./features/account/index";
@@ -1551,7 +1551,7 @@ export { updateBlogPostSchema } from "./features/blog/server";
1551
1551
  export type { BlogPostDetailResponse } from "./features/blog/server";
1552
1552
  export type { CreateBlogPostInput } from "./features/blog/server";
1553
1553
  export type { UpdateBlogPostInput } from "./features/blog/server";
1554
- export { addItemToCart } from "./features/cart/server";
1554
+ export { addItemToCart, assertCanAddNewItems } from "./features/cart/server";
1555
1555
  export { clearCart } from "./features/cart/server";
1556
1556
  export { getCart } from "./features/cart/server";
1557
1557
  export { mergeGuestCart } from "./features/cart/server";
package/dist/index.js CHANGED
@@ -1395,6 +1395,7 @@ export { conversationsSeedData } from "./seed/index";
1395
1395
  // SB-UNI-B â€" sublistingCategoriesSeedData merged into categoriesSeedData with categoryType:"sublisting".
1396
1396
  // groupedListingsSeedData - Model for grouped listings seed data.
1397
1397
  export { groupedListingsSeedData } from "./seed/index";
1398
+ export { NOTIFICATION_TYPE_VALUES } from "./features/admin/schemas/firestore";
1398
1399
  // ./monitoring/index
1399
1400
  // [UTIL]-Pure utility â€" no framework or runtime dependency; safe to import from any environment.
1400
1401
  // ANALYTICS_EVENTS - Constant used across modules.
@@ -2910,7 +2911,8 @@ export { updateBlogPostSchema } from "./features/blog/server";
2910
2911
  // ./features/cart/server
2911
2912
  // [SERVER-ONLY]-Server-only â€" uses Node.js, Next.js server internals, or third-party server SDKs (auth, email, payment, shipping).
2912
2913
  // addItemToCart - Helper for add item to cart.
2913
- export { addItemToCart } from "./features/cart/server";
2914
+ // assertCanAddNewItems - the lane gate every add-to-cart entry point must run.
2915
+ export { addItemToCart, assertCanAddNewItems } from "./features/cart/server";
2914
2916
  // [SERVER-ONLY]-Server-only â€" uses Node.js, Next.js server internals, or third-party server SDKs (auth, email, payment, shipping).
2915
2917
  // clearCart - Shared export for clear cart.
2916
2918
  export { clearCart } from "./features/cart/server";
@@ -2658,6 +2658,7 @@ export declare const SCHEMAS: {
2658
2658
  shelf: string;
2659
2659
  bin: string;
2660
2660
  } | undefined;
2661
+ offerId?: string | undefined;
2661
2662
  orderType?: string | undefined;
2662
2663
  imageUrls?: string[] | undefined;
2663
2664
  refundFeeDeducted?: number | undefined;
@@ -2695,7 +2696,6 @@ export declare const SCHEMAS: {
2695
2696
  }[] | undefined;
2696
2697
  emiRemainingBalance?: number | undefined;
2697
2698
  emiComplete?: boolean | undefined;
2698
- offerId?: string | undefined;
2699
2699
  isNonRefundable?: boolean | undefined;
2700
2700
  paymentBatchId?: string | undefined;
2701
2701
  refunds?: {
@@ -2806,6 +2806,7 @@ export declare const SCHEMAS: {
2806
2806
  shelf: string;
2807
2807
  bin: string;
2808
2808
  } | undefined;
2809
+ offerId?: string | undefined;
2809
2810
  orderType?: string | undefined;
2810
2811
  imageUrls?: string[] | undefined;
2811
2812
  refundFeeDeducted?: number | undefined;
@@ -2843,7 +2844,6 @@ export declare const SCHEMAS: {
2843
2844
  }[] | undefined;
2844
2845
  emiRemainingBalance?: number | undefined;
2845
2846
  emiComplete?: boolean | undefined;
2846
- offerId?: string | undefined;
2847
2847
  isNonRefundable?: boolean | undefined;
2848
2848
  paymentBatchId?: string | undefined;
2849
2849
  refunds?: {
@@ -1,4 +1,5 @@
1
- export type NotificationType = "order_placed" | "order_shipped" | "order_delivered" | "order_cancelled" | "bid_outbid" | "bid_won" | "review_received" | "payout_completed" | "system";
1
+ import type { NotificationType } from "../../features/admin/schemas/firestore";
2
+ export type { NotificationType };
2
3
  export interface SeedNotificationDocument {
3
4
  id: string;
4
5
  userId: string;
@@ -67,6 +67,7 @@ export { getSearchResults, searchAction, type SearchQuery, } from "./_internal/s
67
67
  export { getCategoryForDetail, listRootCategories, listFeaturedCategories, listMenuCategories, getCategoryTree, listSitemapCategories, CATEGORIES_PAGE_SIZE, CATEGORIES_ROOT_TIER, CATEGORIES_MAX_DEPTH, CATEGORIES_SITEMAP_LIMIT, CATEGORIES_FEATURED_LIMIT, CATEGORIES_MENU_LIMIT, } from "./_internal/server/features/categories/index";
68
68
  export { getGroupedListingForDetail, getGroupedListingWithItems, listGroupedListings, listFeaturedGroupedListings, listSitemapGroupedListings, getGroupsForProduct, getGroupsWithItemsForProduct, GROUPED_LISTINGS_PAGE_SIZE, GROUPED_LISTINGS_FEATURED_LIMIT, GROUPED_LISTINGS_SITEMAP_LIMIT, type GroupedListingWithItems, type ListGroupedListingsParams, type SitemapGroupedListing, } from "./_internal/server/features/grouped/index";
69
69
  export { createCheckoutOrderAction, attachPaymentAction, verifyAndPlaceRazorpayOrderAction, previewCheckoutPricing, resolveShippingCost, formatShippingAddress, CHECKOUT_DEFAULT_COMMISSIONS, CHECKOUT_PAYMENT_METHODS, type CreateCheckoutOrderInput, type VerifyAndPlaceRazorpayOrderInput, type CheckoutOrderResult, type CheckoutPaymentMethod, type CheckoutPricingPreviewInput, type CheckoutPricingPreview, } from "./_internal/server/features/checkout/index";
70
+ export { unitPriceFor, lineTotalFor, computePreOrderDepositAmount, } from "./_internal/shared/checkout/order-math";
70
71
  export { processRefundAction, type ProcessRefundInput, } from "./_internal/server/features/refunds/actions";
71
72
  export { createPaymentIntentAction, verifyPaymentSignatureAction, resolvePaymentFee, PAYMENTS_DEFAULT_PLATFORM_FEE_PERCENT, PAYMENTS_DEFAULT_GST_PERCENT, PAYMENTS_RECEIPT_PREFIX, type CreatePaymentIntentInput, type CreatePaymentIntentResult, type VerifyPaymentSignatureInput, type ResolvedPaymentFee, } from "./_internal/server/features/payments/index";
72
73
  export { getSublistingCategoryForDetail } from "./_internal/server/features/sublisting-categories/index";
@@ -97,6 +97,11 @@ export { getGroupedListingForDetail, getGroupedListingWithItems, listGroupedList
97
97
  // `.listByType("bundle", opts)` / `.findById(id)` directly.
98
98
  // S4: checkout actions
99
99
  export { createCheckoutOrderAction, attachPaymentAction, verifyAndPlaceRazorpayOrderAction, previewCheckoutPricing, resolveShippingCost, formatShippingAddress, CHECKOUT_DEFAULT_COMMISSIONS, CHECKOUT_PAYMENT_METHODS, } from "./_internal/server/features/checkout/index";
100
+ // The one definition of "what does this cart line cost". Exported because the
101
+ // consumer's Razorpay create-order route needs it: that route used to hand-roll
102
+ // the rule and reproduced only the bundle branch, so it captured the LIST price
103
+ // for an accepted offer. Pure math — no Firestore, no server-only imports.
104
+ export { unitPriceFor, lineTotalFor, computePreOrderDepositAmount, } from "./_internal/shared/checkout/order-math";
100
105
  // S-SBUNI-RULES: refund action
101
106
  export { processRefundAction, } from "./_internal/server/features/refunds/actions";
102
107
  // S4: payments actions
package/dist/server.d.ts CHANGED
@@ -231,7 +231,7 @@ export { getLatestBlogPosts } from "./features/blog/server";
231
231
  export { listBlogPosts } from "./features/blog/server";
232
232
  export { updateBlogPost } from "./features/blog/server";
233
233
  export { updateBlogPostSchema } from "./features/blog/server";
234
- export { addItemToCart } from "./features/cart/server";
234
+ export { addItemToCart, assertCanAddNewItems } from "./features/cart/server";
235
235
  export { clearCart } from "./features/cart/server";
236
236
  export { getCart } from "./features/cart/server";
237
237
  export { mergeGuestCart } from "./features/cart/server";
@@ -573,3 +573,7 @@ export { toClientLotterySlot, toClientLotteryConfig } from "./_internal/server/f
573
573
  export { buildLotteryMetadata } from "./_internal/server/features/lottery/metadata";
574
574
  export { submitLotteryPullAction, flagLotteryEntryAction, reopenLotterySlotAction, createLotteryEventAction, updateLotteryEventAction, setProductLotteryModeAction, } from "./_internal/server/features/lottery/actions";
575
575
  export { lotteryEntryRepository } from "./features/lottery/repository/lottery-entry.repository";
576
+ export { renderNotificationEmail, NOTIFICATION_EMAIL_TEMPLATES, } from "./features/email/notification-templates";
577
+ export type { NotificationTemplateDef, NotificationEmailContext, } from "./features/email/notification-templates";
578
+ export { resolveNotificationActionUrl, } from "./_internal/shared/features/notifications/action-url";
579
+ export type { NotificationAudience } from "./_internal/shared/features/notifications/action-url";
package/dist/server.js CHANGED
@@ -658,7 +658,8 @@ export { updateBlogPost } from "./features/blog/server";
658
658
  export { updateBlogPostSchema } from "./features/blog/server";
659
659
  // [SERVER-ONLY]-Server-only — uses Node.js, Next.js server internals, or third-party server SDKs (auth, email, payment, shipping).
660
660
  // addItemToCart - Helper for add item to cart.
661
- export { addItemToCart } from "./features/cart/server";
661
+ // assertCanAddNewItems - the lane gate every add-to-cart entry point must run.
662
+ export { addItemToCart, assertCanAddNewItems } from "./features/cart/server";
662
663
  // [SERVER-ONLY]-Server-only — uses Node.js, Next.js server internals, or third-party server SDKs (auth, email, payment, shipping).
663
664
  // clearCart - Shared export for clear cart.
664
665
  export { clearCart } from "./features/cart/server";
@@ -1444,3 +1445,11 @@ export { toClientLotterySlot, toClientLotteryConfig } from "./_internal/server/f
1444
1445
  export { buildLotteryMetadata } from "./_internal/server/features/lottery/metadata";
1445
1446
  export { submitLotteryPullAction, flagLotteryEntryAction, reopenLotterySlotAction, createLotteryEventAction, updateLotteryEventAction, setProductLotteryModeAction, } from "./_internal/server/features/lottery/actions";
1446
1447
  export { lotteryEntryRepository } from "./features/lottery/repository/lottery-entry.repository";
1448
+ /*
1449
+ * Notification email templates + the shared actionUrl resolver.
1450
+ *
1451
+ * Server-only: the templates call `renderToStaticMarkup` through a lazy
1452
+ * `require`, which must never end up reachable from a client chunk.
1453
+ */
1454
+ export { renderNotificationEmail, NOTIFICATION_EMAIL_TEMPLATES, } from "./features/email/notification-templates";
1455
+ export { resolveNotificationActionUrl, } from "./_internal/shared/features/notifications/action-url";