@mohasinac/appkit 4.23.0 → 4.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/server/features/bundles/actions.js +7 -0
- package/dist/_internal/server/features/checkout/actions.js +30 -21
- package/dist/_internal/server/features/checkout/bundle-expansion.d.ts +4 -6
- package/dist/_internal/server/features/checkout/bundle-expansion.js +19 -17
- package/dist/_internal/server/features/checkout/stock-restore.js +6 -5
- package/dist/_internal/server/features/products/group-members.d.ts +55 -0
- package/dist/_internal/server/features/products/group-members.js +41 -0
- package/dist/_internal/shared/actions/action-registry.js +41 -0
- package/dist/_internal/shared/checkout/line-members.d.ts +60 -0
- package/dist/_internal/shared/checkout/line-members.js +68 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +23 -5
- package/dist/_internal/shared/checkout/order-math.js +29 -6
- package/dist/_internal/shared/checkout/rules/_limits.d.ts +12 -2
- package/dist/_internal/shared/checkout/rules/_limits.js +12 -2
- package/dist/_internal/shared/checkout/rules/_registry.js +8 -2
- package/dist/_internal/shared/features/cart/config.d.ts +2 -1
- package/dist/_internal/shared/features/cart/config.js +2 -1
- package/dist/_internal/shared/features/cart/schema.d.ts +6 -6
- package/dist/_internal/shared/features/notifications/action-url.d.ts +47 -0
- package/dist/_internal/shared/features/notifications/action-url.js +111 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +1 -0
- package/dist/constants/api-endpoints.d.ts +27 -0
- package/dist/constants/api-endpoints.js +9 -0
- package/dist/constants/notification-types.d.ts +38 -10
- package/dist/constants/notification-types.js +39 -28
- package/dist/features/account/types/index.d.ts +1 -10
- package/dist/features/admin/actions/notification-actions.d.ts +12 -0
- package/dist/features/admin/actions/notification-actions.js +149 -4
- package/dist/features/admin/schemas/firestore.d.ts +24 -29
- package/dist/features/admin/schemas/firestore.js +89 -26
- package/dist/features/cart/actions/cart-actions.d.ts +47 -1
- package/dist/features/cart/actions/cart-actions.js +189 -12
- package/dist/features/cart/components/CartDrawer.js +2 -1
- package/dist/features/cart/repository/cart.repository.d.ts +33 -1
- package/dist/features/cart/repository/cart.repository.js +146 -2
- package/dist/features/cart/schemas/firestore.d.ts +132 -8
- package/dist/features/cart/schemas/firestore.js +22 -0
- package/dist/features/email/index.d.ts +1 -1
- package/dist/features/email/index.js +5 -1
- package/dist/features/email/notification-templates.d.ts +35 -0
- package/dist/features/email/notification-templates.js +96 -0
- package/dist/features/orders/schemas/index.d.ts +2 -2
- package/dist/features/products/components/GroupMemberPicker.d.ts +46 -0
- package/dist/features/products/components/GroupMemberPicker.js +108 -0
- package/dist/features/products/components/ShowGroupSection.js +7 -9
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +30 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +7 -1
- package/dist/schemas/registry.d.ts +2 -2
- package/dist/seed/factories/notification.factory.d.ts +2 -1
- package/dist/seed/notifications-seed-data.js +225 -0
- package/dist/seed/payouts-seed-data.js +47 -0
- package/dist/seed/scammers-seed-data.js +34 -0
- package/dist/seed/stores-seed-data.js +34 -0
- package/dist/seed/support-tickets-seed-data.js +34 -0
- package/dist/server-entry.d.ts +1 -0
- package/dist/server-entry.js +5 -0
- package/dist/server.d.ts +5 -1
- package/dist/server.js +10 -1
- package/dist/styles.css +58 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/QuantityStepper.d.ts +38 -0
- package/dist/ui/components/QuantityStepper.js +20 -0
- package/dist/ui/components/QuantityStepper.style.css +56 -0
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +1 -0
- package/package.json +1 -1
|
@@ -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 {
|
|
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?: {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GroupMemberPicker — pick which members of a group you want, and how many of
|
|
3
|
+
* each, then add the whole selection as ONE cart line.
|
|
4
|
+
*
|
|
5
|
+
* This is the "pick as you wish" half of the grouped-purchase model. Bundles
|
|
6
|
+
* are the other half and deliberately do NOT use this: a bundle is priced as a
|
|
7
|
+
* whole and is all-or-nothing, so its members render read-only and the only
|
|
8
|
+
* stepper is the bundle's own copy count.
|
|
9
|
+
*
|
|
10
|
+
* Two mount points, one component: the "Part of: …" modal/drawer on a product
|
|
11
|
+
* detail page, and the public `/groups/{slug}` page.
|
|
12
|
+
*
|
|
13
|
+
* The component talks to the API directly rather than taking an `onAdd` server
|
|
14
|
+
* action prop. That keeps `ShowGroupSection`'s `renderGroupSection` slot
|
|
15
|
+
* signature unchanged, which in turn leaves both detail-page views and both
|
|
16
|
+
* consumer pages untouched.
|
|
17
|
+
*/
|
|
18
|
+
import React from "react";
|
|
19
|
+
import type { ListingType } from "../types";
|
|
20
|
+
export interface GroupPickerMember {
|
|
21
|
+
id: string;
|
|
22
|
+
title: string;
|
|
23
|
+
price: number;
|
|
24
|
+
currency?: string;
|
|
25
|
+
images?: string[];
|
|
26
|
+
image?: string;
|
|
27
|
+
slug?: string;
|
|
28
|
+
listingType?: ListingType;
|
|
29
|
+
isGroupParent?: boolean;
|
|
30
|
+
condition?: string;
|
|
31
|
+
availableQuantity?: number;
|
|
32
|
+
storeId?: string;
|
|
33
|
+
storeName?: string;
|
|
34
|
+
isSold?: boolean;
|
|
35
|
+
status?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface GroupMemberPickerProps {
|
|
38
|
+
groupId: string;
|
|
39
|
+
groupSource: "product-group" | "grouped-listing";
|
|
40
|
+
members: GroupPickerMember[];
|
|
41
|
+
/** Called after a successful add so the host can close its overlay. */
|
|
42
|
+
onAdded?: () => void;
|
|
43
|
+
/** Called when the API reports the caller isn't signed in. */
|
|
44
|
+
onAuthRequired?: () => void;
|
|
45
|
+
}
|
|
46
|
+
export declare function GroupMemberPicker({ groupId, groupSource, members, onAdded, onAuthRequired, }: GroupMemberPickerProps): React.JSX.Element;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* GroupMemberPicker — pick which members of a group you want, and how many of
|
|
5
|
+
* each, then add the whole selection as ONE cart line.
|
|
6
|
+
*
|
|
7
|
+
* This is the "pick as you wish" half of the grouped-purchase model. Bundles
|
|
8
|
+
* are the other half and deliberately do NOT use this: a bundle is priced as a
|
|
9
|
+
* whole and is all-or-nothing, so its members render read-only and the only
|
|
10
|
+
* stepper is the bundle's own copy count.
|
|
11
|
+
*
|
|
12
|
+
* Two mount points, one component: the "Part of: …" modal/drawer on a product
|
|
13
|
+
* detail page, and the public `/groups/{slug}` page.
|
|
14
|
+
*
|
|
15
|
+
* The component talks to the API directly rather than taking an `onAdd` server
|
|
16
|
+
* action prop. That keeps `ShowGroupSection`'s `renderGroupSection` slot
|
|
17
|
+
* signature unchanged, which in turn leaves both detail-page views and both
|
|
18
|
+
* consumer pages untouched.
|
|
19
|
+
*/
|
|
20
|
+
import { useMemo, useState } from "react";
|
|
21
|
+
import { Alert, Button, Div, Row, Span, Stack, Table, Tbody, Td, Text, Th, Thead, Tr, } from "../../../ui";
|
|
22
|
+
import { QuantityStepper } from "../../../ui/components/QuantityStepper";
|
|
23
|
+
import { MediaImage } from "../../media/MediaImage";
|
|
24
|
+
import { formatCurrency } from "../../../utils/number.formatter";
|
|
25
|
+
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
26
|
+
import { CART_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
27
|
+
import { normalizeError } from "../../../errors/normalize";
|
|
28
|
+
import { pluginFor } from "../../../_internal/shared/listing-types/_registry";
|
|
29
|
+
import { normalizeListingType } from "../utils/listing-type";
|
|
30
|
+
const __O = { xAuto: "overflow-x-auto", hidden: "overflow-hidden" };
|
|
31
|
+
function memberImage(m) {
|
|
32
|
+
return m.image ?? m.images?.[0] ?? "";
|
|
33
|
+
}
|
|
34
|
+
function memberHref(m) {
|
|
35
|
+
return pluginFor(normalizeListingType(m)).detailRoute(m.slug ?? m.id);
|
|
36
|
+
}
|
|
37
|
+
/** A member the buyer cannot choose, and the reason to show instead of a stepper. */
|
|
38
|
+
function blockedReason(m) {
|
|
39
|
+
if (m.isSold)
|
|
40
|
+
return "Sold";
|
|
41
|
+
if (m.status && m.status !== "published")
|
|
42
|
+
return "Unavailable";
|
|
43
|
+
if ((m.availableQuantity ?? 0) <= 0)
|
|
44
|
+
return "Out of stock";
|
|
45
|
+
// `cartLine: "blocked"` is how a classified / live listing says "contact the
|
|
46
|
+
// seller instead" — those can never join a cart line.
|
|
47
|
+
if (pluginFor(normalizeListingType(m)).cartLine === "blocked")
|
|
48
|
+
return "Not purchasable here";
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
export function GroupMemberPicker({ groupId, groupSource, members, onAdded, onAuthRequired, }) {
|
|
52
|
+
const [qty, setQty] = useState({});
|
|
53
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
54
|
+
const [error, setError] = useState(null);
|
|
55
|
+
// A group whose members span sellers cannot become one cart line: storeId is
|
|
56
|
+
// the order-splitting key and the key per-store add-ons, coupons, shipping and
|
|
57
|
+
// payout all hang off. Rather than offer a button that always fails, the
|
|
58
|
+
// picker stands down and the table stays read-only.
|
|
59
|
+
const storeIds = useMemo(() => new Set(members.map((m) => m.storeId).filter(Boolean)), [members]);
|
|
60
|
+
const isCrossStore = storeIds.size > 1;
|
|
61
|
+
const selected = useMemo(() => members
|
|
62
|
+
.map((m) => ({ member: m, quantity: qty[m.id] ?? 0 }))
|
|
63
|
+
.filter((s) => s.quantity > 0), [members, qty]);
|
|
64
|
+
const selectedTotal = selected.reduce((sum, s) => sum + s.member.price * s.quantity, 0);
|
|
65
|
+
const selectedUnits = selected.reduce((sum, s) => sum + s.quantity, 0);
|
|
66
|
+
const currency = members[0]?.currency ?? "INR";
|
|
67
|
+
const handleAdd = async () => {
|
|
68
|
+
setError(null);
|
|
69
|
+
setIsSaving(true);
|
|
70
|
+
try {
|
|
71
|
+
const res = await fetch(CART_ENDPOINTS.GROUP, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: { "Content-Type": "application/json" },
|
|
74
|
+
body: JSON.stringify({
|
|
75
|
+
groupId,
|
|
76
|
+
groupSource,
|
|
77
|
+
// Ids and quantities only. Prices and titles are re-resolved on the
|
|
78
|
+
// server — anything else would be a price-manipulation hole.
|
|
79
|
+
members: selected.map((s) => ({ productId: s.member.id, quantity: s.quantity })),
|
|
80
|
+
}),
|
|
81
|
+
});
|
|
82
|
+
if (res.status === 401) {
|
|
83
|
+
onAuthRequired?.();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const json = (await res.json());
|
|
87
|
+
if (!res.ok || json.success === false) {
|
|
88
|
+
setError(json.error ?? "Couldn't add those items to your cart.");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
setQty({});
|
|
92
|
+
onAdded?.();
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
void normalizeError(err);
|
|
96
|
+
setError("Couldn't reach the server. Please try again.");
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
setIsSaving(false);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
return (_jsxs(Stack, { gap: "sm", children: [isCrossStore && (_jsx(Alert, { variant: "info", children: "These items are sold by different sellers, so they can't be combined into one cart line. Open an item to buy it on its own." })), _jsx(Div, { className: __O.xAuto, children: _jsxs(Table, { className: "text-left min-w-[440px]", children: [_jsx(Thead, { children: _jsxs(Tr, { border: "default", children: [_jsx(Th, { paddingY: "sm", paddingSide: "pr-sm", color: "muted", size: "xs", weight: "semibold", children: "Image" }), _jsx(Th, { paddingY: "sm", paddingSide: "pr-sm", color: "muted", size: "xs", weight: "semibold", children: "Name" }), _jsx(Th, { paddingY: "sm", paddingSide: "pr-sm", color: "muted", size: "xs", weight: "semibold", children: "Price" }), !isCrossStore && (_jsx(Th, { paddingY: "sm", paddingSide: "pr-sm", color: "muted", size: "xs", weight: "semibold", children: "Qty" })), _jsx(Th, { paddingY: "sm", paddingSide: "pb-sm", color: "muted", size: "xs", weight: "semibold" })] }) }), _jsx(Tbody, { children: members.map((m) => {
|
|
103
|
+
const blocked = blockedReason(m);
|
|
104
|
+
return (_jsxs(Tr, { className: "last:border-0", border: "subtle", children: [_jsx(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: _jsx(Div, { className: `w-10 h-10 ${__O.hidden}`, rounded: "full", border: "default", children: _jsx(MediaImage, { src: memberImage(m), alt: m.title, size: "thumbnail" }) }) }), _jsxs(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: [_jsx(Text, { className: "line-clamp-2", color: "primary", size: "sm", weight: "medium", children: m.title }), m.isGroupParent && (_jsx(Span, { weight: "semibold", className: "text-[10px] text-[var(--appkit-color-primary)]", children: "Parent" }))] }), _jsx(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: _jsx(Text, { size: "sm", color: "muted", children: formatCurrency(m.price, m.currency ?? currency) }) }), !isCrossStore && (_jsx(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: blocked ? (_jsx(Span, { size: "xs", color: "muted", surface: "muted", rounded: "default", padding: "pill-2xs", children: blocked })) : (_jsx(QuantityStepper, { value: qty[m.id] ?? 0, onChange: (next) => setQty((prev) => ({ ...prev, [m.id]: next })), min: 0, max: m.availableQuantity, ariaLabel: `Quantity for ${m.title}`, decrementLabel: ACTIONS.CART["decrease-quantity"].ariaLabel, incrementLabel: ACTIONS.CART["increase-quantity"].ariaLabel })) })), _jsx(Td, { padding: "xs-tall", children: _jsx("a", { href: memberHref(m), className: "text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-primary)] hover:underline", children: "View \u2192" }) })] }, m.id));
|
|
105
|
+
}) })] }) }), !isCrossStore && (_jsxs(_Fragment, { children: [error && _jsx(Alert, { variant: "error", children: error }), _jsxs(Row, { justify: "between", align: "center", gap: "sm", className: "min-w-0", children: [_jsx(Text, { size: "sm", color: "muted", children: selectedUnits > 0
|
|
106
|
+
? `${selectedUnits} item${selectedUnits === 1 ? "" : "s"} · ${formatCurrency(selectedTotal, currency)}`
|
|
107
|
+
: "Nothing selected yet" }), _jsx(Button, { action: ACTIONS.CART["add-group-to-cart"], disabled: selectedUnits === 0, isLoading: isSaving, onClick: handleAdd })] })] }))] }));
|
|
108
|
+
}
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
|
-
import { Div, Row, Span,
|
|
5
|
+
import { Div, Row, Span, Text, Modal, SideDrawer, Button } from "../../../ui";
|
|
6
6
|
import { MediaImage } from "../../media/MediaImage";
|
|
7
|
+
import { GroupMemberPicker } from "./GroupMemberPicker";
|
|
7
8
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
8
9
|
import { normalizeListingType } from "../utils/listing-type";
|
|
9
10
|
import { pluginFor } from "../../../_internal/shared/listing-types/_registry";
|
|
@@ -24,12 +25,6 @@ function MemberThumb({ member, isCurrent }) {
|
|
|
24
25
|
? "border-[var(--appkit-color-primary)] ring-2 ring-[var(--appkit-color-primary)]/30"
|
|
25
26
|
: "border-[var(--appkit-color-border)] group-hover:border-[var(--appkit-color-primary)]"}`, children: [_jsx(MediaImage, { src: image, alt: member.title, size: "card" }), member.isGroupParent && (_jsx(Span, { color: "inverse", rounded: "sm", className: "absolute bottom-0 right-0 bg-[var(--appkit-color-primary)] text-[8px] leading-none px-[0.25rem] py-[0.125rem]", children: "Set" }))] }), _jsx(Text, { className: "text-[10px] leading-tight line-clamp-2 w-full", color: "muted", align: "start", children: member.title }), _jsx(Text, { className: "text-[10px]", color: "primary", weight: "semibold", children: price })] }));
|
|
26
27
|
}
|
|
27
|
-
function GroupTableRow({ member }) {
|
|
28
|
-
const href = memberHref(member);
|
|
29
|
-
const price = formatCurrency(member.price, member.currency ?? "INR");
|
|
30
|
-
const image = member.images?.[0] ?? "";
|
|
31
|
-
return (_jsxs(Tr, { className: "last:border-0", border: "subtle", children: [_jsx(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: _jsx(Div, { className: `w-10 h-10 ${__O.hidden}`, rounded: "full", border: "default", children: _jsx(MediaImage, { src: image, alt: member.title, size: "thumbnail" }) }) }), _jsxs(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: [_jsx(Text, { className: "line-clamp-2", color: "primary", size: "sm", weight: "medium", children: member.title }), member.isGroupParent && (_jsx(Span, { weight: "semibold", className: "text-[10px] text-[var(--appkit-color-primary)]", children: "Parent" }))] }), _jsx(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: _jsx(Text, { size: "sm", color: "muted", children: price }) }), _jsx(Td, { paddingSide: "pr-sm", padding: "xs-tall", children: _jsx(Text, { size: "xs", transform: "capitalize", color: "muted", children: member.condition ?? "—" }) }), _jsx(Td, { padding: "xs-tall", children: _jsx(Link, { href: href, className: "text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-primary)] hover:underline", children: "View \u2192" }) })] }));
|
|
32
|
-
}
|
|
33
28
|
export function ShowGroupSection({ groupId, currentSlug, isParent, groupTitle }) {
|
|
34
29
|
const [open, setOpen] = useState(false);
|
|
35
30
|
const [members, setMembers] = useState([]);
|
|
@@ -50,11 +45,14 @@ export function ShowGroupSection({ groupId, currentSlug, isParent, groupTitle })
|
|
|
50
45
|
const label = groupTitle ?? "Product group";
|
|
51
46
|
const parentLabel = isParent ? `Parts in this group: ${label}` : `Part of: ${label}`;
|
|
52
47
|
const useDrawer = members.length >= 5;
|
|
53
|
-
|
|
48
|
+
// The overlay is now a PICKER, not a read-only table: pick how many of each
|
|
49
|
+
// member you want and add the whole selection as one cart line. The picker
|
|
50
|
+
// stands itself down (read-only, no CTA) when the group spans sellers.
|
|
51
|
+
const tableContent = (_jsx(GroupMemberPicker, { groupId: groupId, groupSource: "product-group", members: members, onAdded: () => setShowAll(false), onAuthRequired: () => setShowAll(false) }));
|
|
54
52
|
return (_jsxs(_Fragment, { children: [_jsxs(Div, { className: `${__O.hidden}`, surface: "muted", rounded: "xl", border: "default", children: [_jsxs(Row, { role: "button", tabIndex: 0, onClick: () => setOpen((v) => !v), onKeyDown: (e) => { if (e.key === "Enter" || e.key === " ") {
|
|
55
53
|
e.preventDefault();
|
|
56
54
|
setOpen((v) => !v);
|
|
57
|
-
} }, justify: "between", paddingX: "x-md", paddingY: "y-sm", className: "w-full cursor-pointer text-left hover:bg-[var(--appkit-color-surface-elevated)]/70 transition-colors", "aria-expanded": open, children: [_jsxs(Row, { align: "center", gap: "xs", children: [_jsx(Span, { size: "xs", className: "mr-1", color: "faint", children: open ? "▼" : "▶" }), _jsx(Text, { size: "sm", weight: "medium", color: "primary", children: parentLabel }), _jsx(Span, { size: "xs", className: "ml-1", rounded: "full", padding: "pill-xs", surface: "subtle", color: "muted", children: members.length })] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); setShowAll(true); }, className: "text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-primary)] hover:underline ml-3 flex-shrink-0", children: "
|
|
55
|
+
} }, justify: "between", paddingX: "x-md", paddingY: "y-sm", className: "w-full cursor-pointer text-left hover:bg-[var(--appkit-color-surface-elevated)]/70 transition-colors", "aria-expanded": open, children: [_jsxs(Row, { align: "center", gap: "xs", children: [_jsx(Span, { size: "xs", className: "mr-1", color: "faint", children: open ? "▼" : "▶" }), _jsx(Text, { size: "sm", weight: "medium", color: "primary", children: parentLabel }), _jsx(Span, { size: "xs", className: "ml-1", rounded: "full", padding: "pill-xs", surface: "subtle", color: "muted", children: members.length })] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: (e) => { e.stopPropagation(); setShowAll(true); }, className: "text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-primary)] hover:underline ml-3 flex-shrink-0", children: "Pick items \u2192" })] }), open && (_jsx(Div, { className: `pb-[1rem] pt-[0.25rem] ${__O.xAuto}`, padding: "x-md", children: _jsx(Row, { gap: "3", className: "min-w-0", children: [...members]
|
|
58
56
|
.sort((a, b) => (b.isGroupParent ? 1 : 0) - (a.isGroupParent ? 1 : 0))
|
|
59
57
|
.map((m) => (_jsx(MemberThumb, { member: m, isCurrent: m.slug === currentSlug || m.id === currentSlug }, m.id))) }) }))] }), useDrawer ? (_jsx(SideDrawer, { isOpen: showAll, onClose: () => setShowAll(false), title: label, children: tableContent })) : (_jsx(Modal, { open: showAll, onClose: () => setShowAll(false), title: label, children: tableContent }))] }));
|
|
60
58
|
}
|
|
@@ -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 "./
|
|
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,8 @@ 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, addGroupLineToCart, updateCartGroupMembers, GROUP_LINE_MAX_MEMBERS, type AddGroupLineToCartInput, } from "./features/cart/server";
|
|
1555
|
+
export { toPublicGroupMember, toPublicGroupMembers, type PublicGroupMember, } from "./_internal/server/features/products/group-members";
|
|
1555
1556
|
export { clearCart } from "./features/cart/server";
|
|
1556
1557
|
export { getCart } from "./features/cart/server";
|
|
1557
1558
|
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,12 @@ 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
|
-
|
|
2914
|
+
// assertCanAddNewItems - the lane gate every add-to-cart entry point must run.
|
|
2915
|
+
// addGroupLineToCart - add a buyer-assembled group selection as ONE line.
|
|
2916
|
+
export { addItemToCart, assertCanAddNewItems, addGroupLineToCart, updateCartGroupMembers, GROUP_LINE_MAX_MEMBERS, } from "./features/cart/server";
|
|
2917
|
+
// Public allow-list projection for a product-group member — see
|
|
2918
|
+
// § "Public Data Projections".
|
|
2919
|
+
export { toPublicGroupMember, toPublicGroupMembers, } from "./_internal/server/features/products/group-members";
|
|
2914
2920
|
// [SERVER-ONLY]-Server-only â€" uses Node.js, Next.js server internals, or third-party server SDKs (auth, email, payment, shipping).
|
|
2915
2921
|
// clearCart - Shared export for clear cart.
|
|
2916
2922
|
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
|
-
|
|
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;
|