@mohasinac/appkit 4.62.0 → 4.62.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.
- package/dist/.build-stamp.json +3 -3
- package/dist/_internal/server/features/checkout/actions.d.ts +80 -0
- package/dist/_internal/server/features/checkout/actions.js +542 -26
- package/dist/_internal/server/features/invoicing/allocate.d.ts +63 -0
- package/dist/_internal/server/features/invoicing/allocate.js +162 -0
- package/dist/_internal/server/features/invoicing/credit-note.d.ts +57 -0
- package/dist/_internal/server/features/invoicing/credit-note.js +126 -0
- package/dist/_internal/server/features/invoicing/gstr1.d.ts +74 -0
- package/dist/_internal/server/features/invoicing/gstr1.js +251 -0
- package/dist/_internal/server/features/invoicing/issue.d.ts +96 -0
- package/dist/_internal/server/features/invoicing/issue.js +202 -0
- package/dist/_internal/server/features/orders/actions.d.ts +8 -1
- package/dist/_internal/server/features/orders/actions.js +81 -8
- package/dist/_internal/server/features/orders/adapters.js +33 -0
- package/dist/_internal/server/features/orders/cancellation-refund.d.ts +15 -9
- package/dist/_internal/server/features/orders/cancellation-refund.js +15 -9
- package/dist/_internal/server/features/points/award.js +46 -4
- package/dist/_internal/server/features/refunds/actions.js +26 -0
- package/dist/_internal/server/features/shipping/courier-block.d.ts +69 -0
- package/dist/_internal/server/features/shipping/courier-block.js +239 -0
- package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
- package/dist/_internal/server/features/site-settings/adapters.js +15 -0
- package/dist/_internal/server/features/stores/adapters.d.ts +2 -2
- package/dist/_internal/server/features/stores/adapters.js +51 -0
- package/dist/_internal/server/functions/scheduled.js +38 -1
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +9 -2
- package/dist/_internal/server/jobs/core/dailyStatusDigest.d.ts +4 -0
- package/dist/_internal/server/jobs/core/dailyStatusDigest.js +19 -2
- package/dist/_internal/server/jobs/core/index.d.ts +2 -1
- package/dist/_internal/server/jobs/core/index.js +2 -1
- package/dist/_internal/server/jobs/core/onJobCreated.d.ts +30 -7
- package/dist/_internal/server/jobs/core/onJobCreated.js +78 -45
- package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +7 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +62 -0
- package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +33 -0
- package/dist/_internal/server/jobs/core/runJobOnce.d.ts +48 -0
- package/dist/_internal/server/jobs/core/runJobOnce.js +195 -0
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +10 -2
- package/dist/_internal/server/jobs/handlers/onOrderStatusChange.js +17 -0
- package/dist/_internal/server/notifications/channel-health.d.ts +26 -3
- package/dist/_internal/shared/actions/action-registry.js +28 -0
- package/dist/_internal/shared/checkout/coupon-discount.d.ts +43 -1
- package/dist/_internal/shared/checkout/coupon-discount.js +26 -1
- package/dist/_internal/shared/checkout/order-math.d.ts +145 -0
- package/dist/_internal/shared/checkout/order-math.js +173 -0
- package/dist/_internal/shared/checkout/refundable-base.d.ts +25 -0
- package/dist/_internal/shared/checkout/refundable-base.js +13 -1
- package/dist/_internal/shared/checkout/split.d.ts +87 -0
- package/dist/_internal/shared/checkout/split.js +100 -0
- package/dist/_internal/shared/content/disclaimers.d.ts +88 -0
- package/dist/_internal/shared/content/disclaimers.js +217 -0
- package/dist/_internal/shared/content/published-values.d.ts +88 -0
- package/dist/_internal/shared/content/published-values.js +198 -0
- package/dist/_internal/shared/features/admin/integrations.d.ts +109 -0
- package/dist/_internal/shared/features/admin/integrations.js +253 -0
- package/dist/_internal/shared/features/orders/dispute-reasons.d.ts +113 -0
- package/dist/_internal/shared/features/orders/dispute-reasons.js +181 -0
- package/dist/_internal/shared/features/orders/refund-copy.d.ts +1 -1
- package/dist/_internal/shared/features/orders/refund-copy.js +18 -1
- package/dist/_internal/shared/features/points/accepts.d.ts +36 -0
- package/dist/_internal/shared/features/points/accepts.js +43 -0
- package/dist/_internal/shared/features/shipping/shared-purchase.d.ts +91 -0
- package/dist/_internal/shared/features/shipping/shared-purchase.js +108 -0
- package/dist/_internal/shared/features/site-settings/types.d.ts +11 -0
- package/dist/_internal/shared/fees/calculator.d.ts +33 -1
- package/dist/_internal/shared/fees/calculator.js +32 -2
- package/dist/client.d.ts +11 -0
- package/dist/client.js +30 -0
- package/dist/constants/api-endpoints.d.ts +42 -0
- package/dist/constants/api-endpoints.js +14 -0
- package/dist/constants/field-names.d.ts +1 -0
- package/dist/constants/field-names.js +5 -0
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/shipping.d.ts +45 -0
- package/dist/contracts/shipping.js +15 -0
- package/dist/features/about/components/AboutView.js +1 -1
- package/dist/features/account/components/BuyerAuctionsGuideView.js +4 -1
- package/dist/features/account/components/UserOrdersView.js +19 -33
- package/dist/features/admin/components/AdminCouponEditorView.js +17 -0
- package/dist/features/admin/components/AdminInvoicingView.d.ts +2 -0
- package/dist/features/admin/components/AdminInvoicingView.js +65 -0
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +18 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +11 -2
- package/dist/features/admin/components/AdminOrdersGuideView.js +23 -2
- package/dist/features/admin/components/AdminOrdersView.js +67 -5
- package/dist/features/admin/components/OwnerQuickActions.js +24 -2
- package/dist/features/admin/constants/filter-tabs.d.ts +6 -0
- package/dist/features/admin/constants/filter-tabs.js +6 -0
- package/dist/features/admin/constants/site-settings-tabs.d.ts +1 -1
- package/dist/features/admin/hooks/useAdminListingData.js +16 -0
- package/dist/features/admin/schemas/admin-user-form.d.ts +2 -2
- package/dist/features/admin/schemas/firestore.d.ts +30 -0
- package/dist/features/admin/schemas/firestore.js +5 -0
- package/dist/features/audit-log/schemas/firestore.d.ts +30 -0
- package/dist/features/audit-log/schemas/firestore.js +15 -0
- package/dist/features/auth/permissions/constants.js +13 -0
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +13 -0
- package/dist/features/cart/components/CartPriceBreakdown.js +6 -2
- package/dist/features/cart/components/PointsTenderPicker.d.ts +61 -0
- package/dist/features/cart/components/PointsTenderPicker.js +15 -0
- package/dist/features/cart/components/StoreAddonsPicker.js +2 -1
- package/dist/features/cart/components/index.d.ts +2 -0
- package/dist/features/cart/components/index.js +1 -0
- package/dist/features/cart/repository/cart.repository.d.ts +19 -0
- package/dist/features/cart/repository/cart.repository.js +27 -0
- package/dist/features/cart/schemas/firestore.d.ts +92 -0
- package/dist/features/contact/email.d.ts +27 -10
- package/dist/features/contact/email.js +20 -3
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +38 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +80 -0
- package/dist/features/dispute-reasons/schemas/firestore.d.ts +64 -0
- package/dist/features/dispute-reasons/schemas/firestore.js +40 -0
- package/dist/features/faq/components/FAQAccordion.d.ts +13 -1
- package/dist/features/faq/components/FAQAccordion.js +29 -4
- package/dist/features/invoicing/schemas/firestore.d.ts +112 -0
- package/dist/features/invoicing/schemas/firestore.js +45 -0
- package/dist/features/invoicing/utils/financial-year.d.ts +43 -0
- package/dist/features/invoicing/utils/financial-year.js +53 -0
- package/dist/features/jobs/actions/enqueue-job.js +18 -1
- package/dist/features/jobs/repository/jobs.repository.d.ts +62 -1
- package/dist/features/jobs/repository/jobs.repository.js +84 -1
- package/dist/features/jobs/schemas/firestore.d.ts +80 -0
- package/dist/features/jobs/schemas/firestore.js +37 -0
- package/dist/features/lottery/schemas/zod.d.ts +6 -6
- package/dist/features/media/MediaImage.js +22 -1
- package/dist/features/media/upload/ImageUpload.js +1 -1
- package/dist/features/obligations/constants/policies.js +24 -1
- package/dist/features/orders/components/OrderPaymentSummary.js +10 -1
- package/dist/features/orders/components/OrderProvenanceCard.d.ts +8 -0
- package/dist/features/orders/components/OrderProvenanceCard.js +185 -0
- package/dist/features/orders/components/RaiseDisputeForm.d.ts +14 -0
- package/dist/features/orders/components/RaiseDisputeForm.js +111 -0
- package/dist/features/orders/components/RefundRequestView.js +19 -1
- package/dist/features/orders/components/index.d.ts +4 -0
- package/dist/features/orders/components/index.js +2 -0
- package/dist/features/orders/constants/order-lane-tabs.d.ts +69 -0
- package/dist/features/orders/constants/order-lane-tabs.js +88 -0
- package/dist/features/orders/repository/orders.repository.d.ts +57 -0
- package/dist/features/orders/repository/orders.repository.js +145 -0
- package/dist/features/orders/schemas/firestore.d.ts +196 -1
- package/dist/features/orders/schemas/index.d.ts +54 -6
- package/dist/features/orders/schemas/index.js +12 -0
- package/dist/features/orders/types/index.d.ts +26 -0
- package/dist/features/orders/utils/courier-block.d.ts +25 -0
- package/dist/features/orders/utils/courier-block.js +23 -0
- package/dist/features/payment-cases/schemas/firestore.d.ts +26 -1
- package/dist/features/payment-cases/schemas/firestore.js +1 -0
- package/dist/features/products/components/PrizeDrawDetailPageView.js +2 -2
- package/dist/features/products/components/ProductDetailPageView.d.ts +17 -1
- package/dist/features/products/components/ProductDetailPageView.js +2 -2
- package/dist/features/products/schemas/product-features.validators.d.ts +10 -10
- package/dist/features/promotions/actions/coupon-stacking.d.ts +30 -3
- package/dist/features/promotions/actions/coupon-stacking.js +21 -2
- package/dist/features/promotions/components/CouponCard.js +13 -0
- package/dist/features/promotions/constants/coupon-help.d.ts +2 -2
- package/dist/features/promotions/constants/coupon-help.js +9 -1
- package/dist/features/promotions/schemas/firestore.d.ts +24 -0
- package/dist/features/promotions/schemas/firestore.js +4 -0
- package/dist/features/promotions/types/index.d.ts +19 -1
- package/dist/features/scams/components/ScamProfileView.js +2 -2
- package/dist/features/scams/components/ScamRegistryView.js +2 -2
- package/dist/features/search/action-index/tab-entries.js +1 -1
- package/dist/features/seller/actions/seller-actions.js +33 -3
- package/dist/features/seller/components/SellerOrdersView.js +129 -27
- package/dist/features/seller/hooks/useSellerListingData.js +25 -0
- package/dist/features/shipments/schemas/validation.d.ts +4 -4
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +43 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +90 -0
- package/dist/features/shipping-boxes/schemas/firestore.d.ts +82 -0
- package/dist/features/shipping-boxes/schemas/firestore.js +53 -0
- package/dist/features/store-extensions/schemas/report-create-form.d.ts +2 -2
- package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +2 -2
- package/dist/features/stores/components/StoreOrdersGuideView.js +6 -1
- package/dist/features/stores/schemas/firestore.d.ts +71 -0
- package/dist/features/stores/types/index.d.ts +19 -0
- package/dist/features/support/schemas/index.d.ts +6 -6
- package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +14 -1
- package/dist/next/routing/route-map.d.ts +20 -0
- package/dist/next/routing/route-map.js +10 -0
- package/dist/providers/shipping-shiprocket/index.d.ts +27 -1
- package/dist/providers/shipping-shiprocket/index.js +84 -1
- package/dist/schemas/registry.d.ts +47 -18
- package/dist/schemas/sieve.d.ts +2 -2
- package/dist/server-entry.d.ts +2 -2
- package/dist/server-entry.js +8 -1
- package/dist/server.d.ts +10 -1
- package/dist/server.js +39 -1
- package/dist/styles.css +64 -80
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/themes/default-dark.js +1 -0
- package/dist/tokens/themes/default-light.js +1 -0
- package/dist/tokens/themes/required.d.ts +1 -1
- package/dist/tokens/themes/required.js +1 -0
- package/dist/tokens/tokens.css +15 -0
- package/dist/ui/components/Disclaimer.d.ts +19 -0
- package/dist/ui/components/Disclaimer.js +74 -0
- package/dist/ui/components/tabs-overflow.js +64 -11
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +6 -0
- package/dist/ui/rich-text/RichText.style.css +48 -79
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +1831 -1822
|
@@ -18,7 +18,11 @@ export const COUPON_HELP = {
|
|
|
18
18
|
stackingRules: [
|
|
19
19
|
"One store coupon per store. If your cart has items from three stores, you can apply each of those stores' coupons at the same time.",
|
|
20
20
|
"Plus one platform-wide LetItRip coupon, on top of any store coupons.",
|
|
21
|
-
|
|
21
|
+
// D51's third bucket, stated for the buyer. Without this line the rule
|
|
22
|
+
// above reads as "one platform coupon" and a buyer with a points offer
|
|
23
|
+
// applied would reasonably think it was using up that slot.
|
|
24
|
+
"Plus one points offer, which stacks with everything else — it earns you points rather than discounting, so it never takes the place of a discount coupon.",
|
|
25
|
+
"A second coupon for the same store, a second platform-wide coupon, or a second points offer will be declined — remove the first one to swap it.",
|
|
22
26
|
],
|
|
23
27
|
/** How the discount is actually distributed. */
|
|
24
28
|
applicationHeading: "Where each discount lands",
|
|
@@ -26,6 +30,10 @@ export const COUPON_HELP = {
|
|
|
26
30
|
"A store coupon only discounts that store's items.",
|
|
27
31
|
"A platform-wide coupon is spread proportionally across every store in your cart.",
|
|
28
32
|
"Orders are placed per store, so each store's order shows its own share of the discount.",
|
|
33
|
+
// Stated because the ORDER of operations is what a buyer would otherwise
|
|
34
|
+
// guess wrong — points are earned on what you actually paid, so a discount
|
|
35
|
+
// reduces the points as well as the price.
|
|
36
|
+
"A points offer earns points on what you pay after any discounts, so the two interact rather than ignoring each other.",
|
|
29
37
|
],
|
|
30
38
|
/** Reuses the failure reasons already written for the buyer shopping guide. */
|
|
31
39
|
failureHeading: "Why a coupon might not apply",
|
|
@@ -72,6 +72,29 @@ export interface CouponDocument extends BaseDocument {
|
|
|
72
72
|
discount: DiscountConfig;
|
|
73
73
|
bxgy?: BXGYConfig;
|
|
74
74
|
tiers?: TieredDiscount[];
|
|
75
|
+
/**
|
|
76
|
+
* D51 — percentage of the amount PAID that a `points_earn` coupon awards as
|
|
77
|
+
* points. Meaningless, and ignored, on every other type.
|
|
78
|
+
*
|
|
79
|
+
* ## 🛑 Why this is not `discount.value`
|
|
80
|
+
*
|
|
81
|
+
* `discount` is read by `computeGroupCouponDiscount` to reduce the order.
|
|
82
|
+
* Overloading its `value` to mean "percent awarded as points" would make a
|
|
83
|
+
* points coupon discount the order by the same number it was supposed to
|
|
84
|
+
* credit — the exact overloading that `bxgy` and `tiers` are separate fields
|
|
85
|
+
* to avoid.
|
|
86
|
+
*
|
|
87
|
+
* `discount` stays REQUIRED on the interface because every existing coupon
|
|
88
|
+
* has one and widening it to optional would ripple through ~30 readers; a
|
|
89
|
+
* points coupon carries `{ value: 0 }`. The protection that matters is in
|
|
90
|
+
* the discount loop, which skips on TYPE rather than trusting that zero
|
|
91
|
+
* (`coupon-discount.ts`) — bad data cannot bypass a type check.
|
|
92
|
+
*
|
|
93
|
+
* The base is the post-discount amount actually paid in money, which is
|
|
94
|
+
* `POINTS_EARN_PERCENT`'s existing rule; this field exists so a campaign can
|
|
95
|
+
* raise it (the festival double event) without editing a constant.
|
|
96
|
+
*/
|
|
97
|
+
pointsPercent?: number;
|
|
75
98
|
usage: UsageConfig;
|
|
76
99
|
validity: ValidityConfig;
|
|
77
100
|
restrictions: RestrictionsConfig;
|
|
@@ -143,6 +166,7 @@ export declare const COUPON_FIELDS: {
|
|
|
143
166
|
readonly FIXED: CouponType;
|
|
144
167
|
readonly FREE_SHIPPING: CouponType;
|
|
145
168
|
readonly BUY_X_GET_Y: CouponType;
|
|
169
|
+
readonly POINTS_EARN: CouponType;
|
|
146
170
|
};
|
|
147
171
|
readonly SCOPE_VALUES: {
|
|
148
172
|
readonly ADMIN: "admin";
|
|
@@ -48,6 +48,7 @@ export const COUPON_FIELDS = {
|
|
|
48
48
|
FIXED: "fixed",
|
|
49
49
|
FREE_SHIPPING: "free_shipping",
|
|
50
50
|
BUY_X_GET_Y: "buy_x_get_y",
|
|
51
|
+
POINTS_EARN: "points_earn",
|
|
51
52
|
},
|
|
52
53
|
SCOPE_VALUES: {
|
|
53
54
|
ADMIN: "admin",
|
|
@@ -59,6 +60,9 @@ export const COUPON_TYPE_LABELS = {
|
|
|
59
60
|
fixed: "Fixed Amount Discount",
|
|
60
61
|
free_shipping: "Free Shipping",
|
|
61
62
|
buy_x_get_y: "Buy X Get Y",
|
|
63
|
+
// Named for what the buyer gets, not for the mechanism. "Points earn" is
|
|
64
|
+
// our word for it; "Points back" is what appears on their balance.
|
|
65
|
+
points_earn: "Points Back",
|
|
62
66
|
};
|
|
63
67
|
export const DEFAULT_COUPON_DATA = {
|
|
64
68
|
usage: { currentUsage: 0 },
|
|
@@ -1,4 +1,22 @@
|
|
|
1
|
-
export type CouponType = "percentage" | "fixed" | "free_shipping" | "buy_x_get_y"
|
|
1
|
+
export type CouponType = "percentage" | "fixed" | "free_shipping" | "buy_x_get_y"
|
|
2
|
+
/**
|
|
3
|
+
* D51 — a coupon that awards POINTS rather than discounting the order.
|
|
4
|
+
*
|
|
5
|
+
* 🛑 It carries `pointsPercent`, not a discount value, and it must never
|
|
6
|
+
* reach the discount arithmetic: a `points_earn` coupon with a `discount`
|
|
7
|
+
* field read by `computeGroupCouponDiscount` would silently discount the
|
|
8
|
+
* order as well as award points.
|
|
9
|
+
*
|
|
10
|
+
* It exists because `RIPLI05` is a PERMANENT, site-wide coupon, and
|
|
11
|
+
* `detectCouponConflict` permits exactly one admin coupon site-wide — so
|
|
12
|
+
* putting it in the admin slot would make every other platform coupon
|
|
13
|
+
* unusable for as long as it exists. It gets its own stacking bucket.
|
|
14
|
+
*
|
|
15
|
+
* Adding it here is a compile error at `COUPON_TYPE_LABELS`, which is a
|
|
16
|
+
* `Record<CouponType, string>` — that is the point of declaring the union
|
|
17
|
+
* first.
|
|
18
|
+
*/
|
|
19
|
+
| "points_earn";
|
|
2
20
|
export type CouponScope = "admin" | "seller";
|
|
3
21
|
export interface CouponItem {
|
|
4
22
|
id: string;
|
|
@@ -4,7 +4,7 @@ import { formatCurrency } from "../../../utils/number.formatter.js";
|
|
|
4
4
|
const CLS_OUTLINE_BTN = "appkit-button appkit-button--outline appkit-button--md flex w-full items-center gap-[var(--appkit-space-2)]";
|
|
5
5
|
import Link from "next/link.js";
|
|
6
6
|
import { Shield, Phone, Wallet, Mail, AlertOctagon, Flag, Eye, Users, Calendar, ExternalLink, FileText, MessageSquare, Link2, } from "lucide-react";
|
|
7
|
-
import { Alert, Anchor, Badge, Breadcrumb, Card, CardBody, CardHeader, Container, Div, EmptyState, Grid, Heading, Main, Row, Section, Span, Stack, Text, } from "../../../ui/index.js";
|
|
7
|
+
import { Alert, Anchor, Badge, Breadcrumb, Card, CardBody, CardHeader, Container, Disclaimer, Div, EmptyState, Grid, Heading, Main, Row, Section, Span, Stack, Text, } from "../../../ui/index.js";
|
|
8
8
|
import { MediaImage } from "../../media/MediaImage.js";
|
|
9
9
|
import { SCAM_PLATFORM_LABELS, SCAMMER_STATUS_LABELS, SOCIAL_PLATFORM_LABELS, CONTEST_TYPE_LABELS, } from "../schemas/firestore.js";
|
|
10
10
|
import { SCAM_TYPE_LABELS, getScamType } from "../constants/scam-types.js";
|
|
@@ -32,7 +32,7 @@ function statusVariant(status) {
|
|
|
32
32
|
}
|
|
33
33
|
// ─── Sub-components ───────────────────────────────────────────────────────────
|
|
34
34
|
function ScammerHeaderCard({ scammer }) {
|
|
35
|
-
return (_jsx(Card, { variant: "elevated", padding: "lg", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Row, { justify: "between", gap: "md", align: "start", wrap: true, children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 1, weight: "bold", size: "2xl", children: scammer.displayNames[0] }), scammer.displayNames.length > 1 && (_jsxs(Text, { variant: "secondary", size: "sm", children: ["Also known as: ", scammer.displayNames.slice(1).join(", ")] }))] }), _jsx(Badge, { variant: statusVariant(scammer.status), children: SCAMMER_STATUS_LABELS[scammer.status] ?? scammer.status })] }), _jsxs(Row, { gap: "xs", wrap: true, children: [_jsx(Badge, { variant: "warning", children: SCAM_TYPE_LABELS[scammer.scamType] ?? scammer.scamType }), _jsxs(Badge, { variant: "default", children: ["via ", SCAM_PLATFORM_LABELS[scammer.scamPlatform] ?? scammer.scamPlatform] }), scammer.tags.includes("repeat_offender") && (_jsx(Badge, { variant: "danger", children: "Repeat Offender" }))] }), _jsxs(Row, { gap: "md", wrap: true, children: [_jsxs(Text, { variant: "secondary", layout: "flex", gap: "1.5", size: "sm", children: [_jsx(Eye, { className: "h-4 w-4" }), scammer.views, " views"] }), _jsxs(Text, { variant: "secondary", layout: "flex", gap: "1.5", size: "sm", children: [_jsx(Users, { className: "h-4 w-4" }), scammer.incidentCount + 1, " victim", scammer.incidentCount !== 0 ? "s" : "", " reported"] }), _jsxs(Text, { variant: "secondary", layout: "flex", gap: "1.5", size: "sm", children: [_jsx(Calendar, { className: "h-4 w-4" }), "Reported ", formatDate(scammer.createdAt)] }), scammer.amountLost ? (_jsxs(Text, { className: "text-[color:var(--appkit-color-danger,theme(colors.red.600))]", size: "sm", weight: "medium", children: [formatCurrency(scammer.amountLost), " lost (primary incident)"] })) : null] })] }) }));
|
|
35
|
+
return (_jsx(Card, { variant: "elevated", padding: "lg", children: _jsxs(Stack, { gap: "md", children: [_jsx(Disclaimer, { id: "scam-registry" }), _jsxs(Row, { justify: "between", gap: "md", align: "start", wrap: true, children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 1, weight: "bold", size: "2xl", children: scammer.displayNames[0] }), scammer.displayNames.length > 1 && (_jsxs(Text, { variant: "secondary", size: "sm", children: ["Also known as: ", scammer.displayNames.slice(1).join(", ")] }))] }), _jsx(Badge, { variant: statusVariant(scammer.status), children: SCAMMER_STATUS_LABELS[scammer.status] ?? scammer.status })] }), _jsxs(Row, { gap: "xs", wrap: true, children: [_jsx(Badge, { variant: "warning", children: SCAM_TYPE_LABELS[scammer.scamType] ?? scammer.scamType }), _jsxs(Badge, { variant: "default", children: ["via ", SCAM_PLATFORM_LABELS[scammer.scamPlatform] ?? scammer.scamPlatform] }), scammer.tags.includes("repeat_offender") && (_jsx(Badge, { variant: "danger", children: "Repeat Offender" }))] }), _jsxs(Row, { gap: "md", wrap: true, children: [_jsxs(Text, { variant: "secondary", layout: "flex", gap: "1.5", size: "sm", children: [_jsx(Eye, { className: "h-4 w-4" }), scammer.views, " views"] }), _jsxs(Text, { variant: "secondary", layout: "flex", gap: "1.5", size: "sm", children: [_jsx(Users, { className: "h-4 w-4" }), scammer.incidentCount + 1, " victim", scammer.incidentCount !== 0 ? "s" : "", " reported"] }), _jsxs(Text, { variant: "secondary", layout: "flex", gap: "1.5", size: "sm", children: [_jsx(Calendar, { className: "h-4 w-4" }), "Reported ", formatDate(scammer.createdAt)] }), scammer.amountLost ? (_jsxs(Text, { className: "text-[color:var(--appkit-color-danger,theme(colors.red.600))]", size: "sm", weight: "medium", children: [formatCurrency(scammer.amountLost), " lost (primary incident)"] })) : null] })] }) }));
|
|
36
36
|
}
|
|
37
37
|
function ScammerIncidentsSection({ incidents }) {
|
|
38
38
|
return (_jsxs(Stack, { gap: "sm", children: [_jsx(Row, { justify: "between", align: "center", children: _jsxs(Heading, { level: 2, size: "base", weight: "semibold", children: ["Additional Incidents", incidents.length > 0 && ` (${incidents.length})`] }) }), incidents.length === 0 ? (_jsx(EmptyState, { icon: _jsx(FileText, { className: "h-8 w-8" }), title: "No additional incidents yet", description: "Other victims' verified reports linked to this profile will appear here." })) : (_jsx(Stack, { gap: "sm", children: incidents.map((inc) => (_jsx(Card, { variant: "outlined", padding: "md", children: _jsxs(Stack, { gap: "xs", children: [_jsxs(Row, { justify: "between", align: "start", gap: "sm", wrap: true, children: [_jsxs(Row, { gap: "xs", wrap: true, children: [_jsx(Badge, { variant: "warning", children: SCAM_TYPE_LABELS[inc.scamType] ?? inc.scamType }), _jsxs(Badge, { variant: "default", children: ["via ", SCAM_PLATFORM_LABELS[inc.scamPlatform] ?? inc.scamPlatform] })] }), _jsx(Text, { variant: "secondary", size: "xs", children: formatDate(inc.createdAt) })] }), inc.itemInvolved && (_jsxs(Text, { variant: "secondary", size: "xs", children: ["Item: ", inc.itemInvolved] })), inc.amountLost ? (_jsxs(Text, { className: "text-[color:var(--appkit-color-danger,theme(colors.red.600))]", size: "xs", weight: "medium", children: [formatCurrency(inc.amountLost), " lost"] })) : null, _jsx(Text, { variant: "secondary", className: "leading-relaxed", size: "sm", children: inc.description.length > 200
|
|
@@ -7,7 +7,7 @@ const CLS_ICON_SM = "h-3 w-3 shrink-0";
|
|
|
7
7
|
const CLS_ROW_CHEVRON = "h-4 w-4 shrink-0 transition group-hover:text-error text-[var(--appkit-color-text-faint)]";
|
|
8
8
|
const CLS_PILL_LINK = "inline-flex items-center gap-[var(--appkit-space-1)] rounded-full bg-error-surface dark:bg-error-surface px-[var(--appkit-space-2-5)] py-[var(--appkit-space-0-5)] text-[length:var(--appkit-text-xs)] font-medium text-error dark:text-error hover:bg-error-surface transition-colors";
|
|
9
9
|
import { Shield, Phone, Wallet, Mail, ChevronRight, Search } from "lucide-react";
|
|
10
|
-
import { Button, Container, Div, Form, Heading, Main, Section, Select, Text, Grid, Stack, Row, Card, Badge, Alert, EmptyState, Input, } from "../../../ui/index.js";
|
|
10
|
+
import { Button, Container, Disclaimer, Div, Form, Heading, Main, Section, Select, Text, Grid, Stack, Row, Card, Badge, Alert, EmptyState, Input, } from "../../../ui/index.js";
|
|
11
11
|
import { listVerifiedScammers } from "../actions/scam-actions.js";
|
|
12
12
|
import { SCAM_PLATFORM_LABELS } from "../schemas/firestore.js";
|
|
13
13
|
import { SCAM_TYPES, SCAM_TYPE_LABELS } from "../constants/scam-types.js";
|
|
@@ -61,7 +61,7 @@ export async function ScamRegistryView({ searchParams = {} }) {
|
|
|
61
61
|
const qs = p.toString();
|
|
62
62
|
return `/scams${qs ? `?${qs}` : ""}`;
|
|
63
63
|
}
|
|
64
|
-
return (_jsxs(Main, { children: [_jsx(Div, { className: "border-b appkit-alert--warning", rounded: "none", children: _jsx(Container, { padding: "y-xs", size: "xl", className: "", children: _jsxs(Alert, { variant: "warning", compact: true, children: ["All profiles on this page have been verified by our moderation team. If you recognise a scammer not listed here,", " ", _jsx(Link, { href: reportHref, className: "font-medium underline hover:no-underline", children: "report them" }), "."] }) }) }), _jsx(Section, { padding: "y-2xl", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "xl", children: [_jsxs(Row, { justify: "between", gap: "md", align: "end", wrap: true, children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 1, weight: "semibold", size: "3xl", children: "Scam Registry" }), _jsx(Text, { variant: "secondary", size: "sm", children: "
|
|
64
|
+
return (_jsxs(Main, { children: [_jsx(Div, { className: "border-b appkit-alert--warning", rounded: "none", children: _jsx(Container, { padding: "y-xs", size: "xl", className: "", children: _jsxs(Alert, { variant: "warning", compact: true, children: ["All profiles on this page have been verified by our moderation team. If you recognise a scammer not listed here,", " ", _jsx(Link, { href: reportHref, className: "font-medium underline hover:no-underline", children: "report them" }), "."] }) }) }), _jsx(Section, { padding: "y-2xl", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "xl", children: [_jsxs(Row, { justify: "between", gap: "md", align: "end", wrap: true, children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 1, weight: "semibold", size: "3xl", children: "Scam Registry" }), _jsx(Text, { variant: "secondary", size: "sm", children: "Reports of scams in India's collectibles community, submitted by buyers and sellers. Search by name, phone, or UPI." })] }), _jsxs(Link, { href: reportHref, className: "appkit-button appkit-button--danger appkit-button--md shrink-0", children: [_jsx(Shield, { className: "h-4 w-4" }), "Report a Scammer"] })] }), _jsx(Disclaimer, { id: "scam-registry" }), _jsx(Form, { method: "GET", children: _jsxs(Row, { wrap: true, gap: "sm", children: [_jsx(Div, { className: "flex-1 min-w-48", children: _jsx(Input, { type: "search", name: "q", defaultValue: query, placeholder: "Search name, phone, UPI ID, or email\u2026", icon: _jsx(Search, { className: "h-4 w-4" }) }) }), _jsx(Select, { name: "scamType", defaultValue: scamType, options: SCAM_TYPES.map((t) => ({ value: t.id, label: t.label })), placeholder: "All scam types" }), _jsx(Select, { name: "sort", defaultValue: sort, options: SORT_OPTIONS.map((o) => ({ value: o.value, label: o.label })) }), _jsx(Button, { type: "submit", variant: "primary", size: "sm", children: "Search" }), (query || scamType || sort !== "-createdAt") && (_jsx(Link, { href: ROUTES.PUBLIC.SCAMS, className: "rounded-lg border border-[var(--appkit-color-border)] px-[var(--appkit-space-4)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] text-[var(--appkit-color-text-muted)] hover:bg-surface-hover transition-colors", children: "Clear" }))] }) }), scamType && SCAM_TYPE_LABELS[scamType] && (_jsxs(Row, { gap: "sm", align: "center", children: [_jsx(Text, { variant: "secondary", size: "xs", children: "Filtering by:" }), _jsxs(Link, { href: buildHref({ scamType: "" }), className: CLS_PILL_LINK, children: [SCAM_TYPE_LABELS[scamType], _jsx(Span, { "aria-hidden": "true", children: "\u00D7" })] })] })), result.items.length === 0 ? (_jsx(EmptyState, { icon: _jsx(Shield, { className: "h-12 w-12" }), title: query || scamType
|
|
65
65
|
? "No verified scammers matched your filters"
|
|
66
66
|
: "No verified scammers in the registry yet", description: query || scamType
|
|
67
67
|
? "Try a different search or clear your filters."
|
|
@@ -103,7 +103,7 @@ export const CONSOLIDATED_TAB_SURFACES = [
|
|
|
103
103
|
tab: "navigation",
|
|
104
104
|
portal: "admin",
|
|
105
105
|
sectionPath: "Admin › System › Permissions Toggles",
|
|
106
|
-
requiredPermission: "settings:write",
|
|
106
|
+
requiredPermission: "admin:settings:write",
|
|
107
107
|
},
|
|
108
108
|
];
|
|
109
109
|
export function deriveConsolidatedTabEntries(hrefs) {
|
|
@@ -28,7 +28,7 @@ import { couponsRepository } from "../../promotions/repository/coupons.repositor
|
|
|
28
28
|
import { generateStoreSlug } from "../../stores/schemas/firestore.js";
|
|
29
29
|
import { siteSettingsRepository } from "../../admin/repository/site-settings.repository.js";
|
|
30
30
|
import { computePayoutDeduction } from "../../../_internal/shared/fees/calculator.js";
|
|
31
|
-
import { ordersTaxableValue, ordersShippingReimbursement, } from "../../../_internal/shared/checkout/order-math.js";
|
|
31
|
+
import { ordersTaxableValue, ordersShippingReimbursement, ordersPointsBorneByStore, } from "../../../_internal/shared/checkout/order-math.js";
|
|
32
32
|
import { getDefaultCurrency } from "../../../core/baseline-resolver.js";
|
|
33
33
|
import { finalizeStagedMediaUrl, finalizeStagedMediaField, finalizeStagedMediaArray, } from "../../media/finalize.js";
|
|
34
34
|
import { StoreStatusValues } from "../../stores/schemas/firestore.js";
|
|
@@ -249,7 +249,19 @@ async function computeSellerEarnings(sellerId) {
|
|
|
249
249
|
* alone.
|
|
250
250
|
*/
|
|
251
251
|
const shippingReimbursement = ordersShippingReimbursement(eligibleOrders);
|
|
252
|
-
|
|
252
|
+
/*
|
|
253
|
+
* D46/D47 — the points programme, which the STORE funds at both ends
|
|
254
|
+
* (decisions 3 and 17): points issued on these orders plus points spent on
|
|
255
|
+
* them. Deducted outside the commission and gateway bases, for the inverse
|
|
256
|
+
* of postage's reason — the seller agreed to fund a discount, not to pay us a
|
|
257
|
+
* commission on the discount they funded.
|
|
258
|
+
*
|
|
259
|
+
* 🛑 Passed at all FOUR call sites, exactly like `shippingReimbursement`
|
|
260
|
+
* above. `ordersPointsFunded` existed with zero consumers before this, which
|
|
261
|
+
* meant the platform had silently been funding the earn.
|
|
262
|
+
*/
|
|
263
|
+
const pointsFunded = ordersPointsBorneByStore(eligibleOrders);
|
|
264
|
+
const deduction = computePayoutDeduction(grossAmount, siteSettings.commissions, shippingReimbursement, pointsFunded);
|
|
253
265
|
return {
|
|
254
266
|
eligibleOrders,
|
|
255
267
|
grossAmount,
|
|
@@ -257,6 +269,22 @@ async function computeSellerEarnings(sellerId) {
|
|
|
257
269
|
gatewayFee: deduction.gatewayFee,
|
|
258
270
|
gstAmount: deduction.gstOnFee,
|
|
259
271
|
netAmount: deduction.netAmount,
|
|
272
|
+
/*
|
|
273
|
+
* Surfaced so a seller can see WHY a payout is short of
|
|
274
|
+
* `gross − fees`. Two terms now move `netAmount` without appearing in
|
|
275
|
+
* `totalDeduction` — postage up, points down — and a net figure that
|
|
276
|
+
* cannot be reconstructed from the figures beside it reads as an error in
|
|
277
|
+
* our arithmetic rather than as the two things the seller agreed to.
|
|
278
|
+
*
|
|
279
|
+
* 🛑 Returned rather than written to `PayoutDocument`, matching
|
|
280
|
+
* `shippingReimbursement`, which is also not on that schema. One of these
|
|
281
|
+
* should probably be persisted eventually — a settled payout cannot be
|
|
282
|
+
* re-explained from a preview — but adding a document field with no reader
|
|
283
|
+
* is the pattern this very function just finished closing, so it waits for
|
|
284
|
+
* the admin surface that would read it.
|
|
285
|
+
*/
|
|
286
|
+
shippingReimbursement: deduction.shippingReimbursement,
|
|
287
|
+
pointsFunded: deduction.pointsFunded,
|
|
260
288
|
productCount: products.length,
|
|
261
289
|
commissions: siteSettings.commissions,
|
|
262
290
|
};
|
|
@@ -359,7 +387,9 @@ export async function bulkSellerOrder(userId, userRole, userDisplayName, userEma
|
|
|
359
387
|
* scheduled job pay differently for the same orders. */
|
|
360
388
|
const grossAmount = ordersTaxableValue(eligible);
|
|
361
389
|
const bulkSiteSettings = await siteSettingsRepository.getSingleton();
|
|
362
|
-
const bulkDeduction = computePayoutDeduction(grossAmount, bulkSiteSettings.commissions, ordersShippingReimbursement(eligible)
|
|
390
|
+
const bulkDeduction = computePayoutDeduction(grossAmount, bulkSiteSettings.commissions, ordersShippingReimbursement(eligible),
|
|
391
|
+
// The store-funded points, both ends — see the preview path above.
|
|
392
|
+
ordersPointsBorneByStore(eligible));
|
|
363
393
|
const payoutDoc = await payoutRepository.createPayout({
|
|
364
394
|
storeId: userId,
|
|
365
395
|
sellerName: userDisplayName,
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { formatPhysicalLocation } from "../../../utils/physical-location.js";
|
|
4
4
|
import { OrderAddonBadges } from "../../orders/components/OrderAddonBadges.js";
|
|
5
|
+
import { OrderProvenanceCard } from "../../orders/components/OrderProvenanceCard.js";
|
|
6
|
+
import { groupOrderItemsByLine } from "../../orders/utils/bundle-grouping.js";
|
|
5
7
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
8
|
+
// The defining module, not a barrel (Root Cause #18). Maps a server error CODE
|
|
9
|
+
// to user-facing copy; it can never return server text, which is the point.
|
|
10
|
+
import { toUserMessage } from "../../../errors/error-display-map.js";
|
|
6
11
|
import { Row, SIEVE_OP, sieveFilter } from "@mohasinac/appkit/client";
|
|
7
12
|
import { sortBy } from "@mohasinac/appkit/client";
|
|
8
13
|
import React, { useState, useCallback } from "react";
|
|
@@ -12,6 +17,7 @@ import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
|
12
17
|
import { Badge, Button, Div, FilterChipGroup, SideDrawer, Span, Stack, Text, useToast } from "../../../ui/index.js";
|
|
13
18
|
import { SELLER_ORDER_STATUS_TABS } from "../../admin/constants/filter-tabs.js";
|
|
14
19
|
import { useOrderScope } from "../../orders/components/OrderScopeTabs.js";
|
|
20
|
+
import { ORDER_LANE_TABS } from "../../orders/constants/order-lane-tabs.js";
|
|
15
21
|
import { OrderStatusValues } from "../../orders/schemas/firestore.js";
|
|
16
22
|
import { isManualPaymentMethod } from "../../orders/constants/payment-window.js";
|
|
17
23
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
|
|
@@ -60,6 +66,14 @@ const UPDATE_STATUS_OPTIONS = [
|
|
|
60
66
|
{ value: "delivered", label: "Delivered" },
|
|
61
67
|
{ value: "cancelled", label: "Cancelled" },
|
|
62
68
|
];
|
|
69
|
+
/**
|
|
70
|
+
* One item row. Extracted verbatim when grouping landed so the single and
|
|
71
|
+
* grouped paths cannot render differently — two copies of this markup is how
|
|
72
|
+
* the field-name bug below would come back on one path only.
|
|
73
|
+
*/
|
|
74
|
+
function sellerOrderItemRow(item, key) {
|
|
75
|
+
return (_jsxs(Row, { paddingY: "y-xs-tall", padding: "x-sm", align: "center", justify: "between", gap: "3", children: [_jsxs(Row, { align: "center", gap: "sm", className: "min-w-0", children: [_jsx(Div, { className: "h-10 w-10 shrink-0", rounded: "md", overflow: "hidden", children: _jsx(MediaImage, { src: item.image, alt: item.title ?? "Order item", size: "thumbnail" }) }), _jsxs(Div, { className: "min-w-0", children: [_jsx(Text, { size: "sm", className: "truncate", weight: "medium", children: item.productTitle ?? item.title ?? item.productId ?? "Item" }), _jsxs(Text, { size: "xs", className: "text-[var(--appkit-color-text-muted)]", children: ["Qty: ", item.quantity ?? 1] })] })] }), _jsx(Text, { size: "sm", className: "shrink-0", weight: "medium", children: toCurrency(item.totalPrice ?? item.unitPrice ?? item.price ?? 0) })] }, key));
|
|
76
|
+
}
|
|
63
77
|
/**
|
|
64
78
|
* Manual-payment state for the seller's read-only badge. Derived from the same
|
|
65
79
|
* two fields the admin queue uses (`paymentProofUrl` + `paymentReviewOutcome`)
|
|
@@ -198,8 +212,33 @@ export function SellerOrderDetailPanel({ orderId, apiBase, onClose, }) {
|
|
|
198
212
|
body: JSON.stringify(payload),
|
|
199
213
|
});
|
|
200
214
|
if (!res.ok) {
|
|
201
|
-
const d = await res.json().catch(() => ({}));
|
|
202
|
-
|
|
215
|
+
const d = (await res.json().catch(() => ({})));
|
|
216
|
+
/*
|
|
217
|
+
* 🛑 FIELD ERRORS GO ON THE FIELD. The server already sends them.
|
|
218
|
+
*
|
|
219
|
+
* `mapToHttpError` answers a rejected body with
|
|
220
|
+
* `{ status: 400, code: VALIDATION_FAILED, message: "Validation
|
|
221
|
+
* failed", issues }` — the per-field detail is ON THE WIRE. This
|
|
222
|
+
* handler used to throw `d.error` and render it in the banner below,
|
|
223
|
+
* so the seller saw a bare "Validation failed" with nothing marked and
|
|
224
|
+
* no way to tell WHICH field was wrong. Rule #9 §6.
|
|
225
|
+
*
|
|
226
|
+
* `applyZodIssues` keys by the full dotted path, so a nested field
|
|
227
|
+
* lands on itself rather than collapsing onto `issue.path[0]`.
|
|
228
|
+
*/
|
|
229
|
+
if (Array.isArray(d.issues) && d.issues.length > 0) {
|
|
230
|
+
applyZodIssues(d.issues, form.setFieldError);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
/*
|
|
234
|
+
* 🛑 NEVER the server's own message, and this is not hypothetical.
|
|
235
|
+
* `d.error` is server text; rendering it is how a Node
|
|
236
|
+
* `MODULE_NOT_FOUND` with a `/var/task/.next/...` require stack was
|
|
237
|
+
* once shown inside a buyer's "Place your bid" modal (Root Cause #86).
|
|
238
|
+
* `toUserMessage` resolves through `errors.codes.*`, terminates at
|
|
239
|
+
* UNKNOWN, and cannot return server internals.
|
|
240
|
+
*/
|
|
241
|
+
throw new Error(toUserMessage(d.code));
|
|
203
242
|
}
|
|
204
243
|
const updated = await res.json();
|
|
205
244
|
const next = (updated?.data ?? updated);
|
|
@@ -217,8 +256,19 @@ export function SellerOrderDetailPanel({ orderId, apiBase, onClose, }) {
|
|
|
217
256
|
});
|
|
218
257
|
}
|
|
219
258
|
catch (err) {
|
|
220
|
-
|
|
221
|
-
|
|
259
|
+
/*
|
|
260
|
+
* `normalizeError`'s RESULT, not an `as Error` cast. The cast asserted a
|
|
261
|
+
* shape nothing checked, and the only reason it happened to work is that
|
|
262
|
+
* every throw above is one we construct — a rejected promise from `fetch`
|
|
263
|
+
* or a JSON parse failure is not an `Error` by that assertion's logic,
|
|
264
|
+
* it just happens to be one at runtime.
|
|
265
|
+
*
|
|
266
|
+
* What reaches the banner is now bounded: either `toUserMessage(code)`
|
|
267
|
+
* from the throw above (mapped copy, never server text) or a browser-level
|
|
268
|
+
* network message. Field-level problems never get here at all — they were
|
|
269
|
+
* routed to the fields by `applyZodIssues`.
|
|
270
|
+
*/
|
|
271
|
+
setSaveError(normalizeError(err).message);
|
|
222
272
|
}
|
|
223
273
|
finally {
|
|
224
274
|
setSaving(false);
|
|
@@ -249,21 +299,21 @@ export function SellerOrderDetailPanel({ orderId, apiBase, onClose, }) {
|
|
|
249
299
|
const addrLine = typeof rawAddr === "string" && rawAddr.trim()
|
|
250
300
|
? rawAddr.trim()
|
|
251
301
|
: [addr.addressLine1, addr.city, addr.state, addr.pincode].filter(Boolean).join(", ");
|
|
252
|
-
return (_jsxs(_Fragment, { children: [loading && (_jsx(Row, { align: "center", justify: "center", padding: "y-4xl", children: _jsx(Div, { className: "h-6 w-6 animate-spin border-2 border-[var(--appkit-color-primary)] border-t-transparent", rounded: "full" }) })), fetchError && (_jsx(Div, { textSize: "sm", className: "mx-4 mt-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "lg", children: fetchError })), order && !loading && (
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
302
|
+
return (_jsxs(_Fragment, { children: [loading && (_jsx(Row, { align: "center", justify: "center", padding: "y-4xl", children: _jsx(Div, { className: "h-6 w-6 animate-spin border-2 border-[var(--appkit-color-primary)] border-t-transparent", rounded: "full" }) })), fetchError && (_jsx(Div, { textSize: "sm", className: "mx-4 mt-4 border border-error/20", color: "error", surface: "danger-surface", padding: "inline", rounded: "lg", children: fetchError })), order && !loading && (_jsx(Stack, { gap: "none", children: _jsxs(Stack, { className: `flex-1 ${__O.yAuto}`, gap: "5", padding: "md", children: [_jsxs(Row, { align: "center", justify: "between", children: [_jsx(Badge, { variant: STATUS_BADGE_VARIANT[order.status ?? ""] ?? "default", children: order.status ?? "Unknown" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-muted)]", children: toRelativeDate(order.createdAt) })] }), _jsx(OrderProvenanceCard, { sourceContext: order.sourceContext, currency: order.currency }), (order.items ?? []).length > 0 && (_jsxs(Div, { children: [_jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text)] mb-2", weight: "semibold", children: "Items" }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)] divide-[var(--appkit-color-border)] border border-[var(--appkit-color-border)]", rounded: "lg", children: groupOrderItemsByLine(order.items ?? []).map((group, gi) => group.kind === "single" ? (sellerOrderItemRow(group.item, `s-${group.index}`)) : (_jsxs(Div, { children: [_jsxs(Row, { paddingY: "y-xs", padding: "x-sm", align: "center", gap: "sm", surface: "muted", children: [_jsx(Text, { size: "xs", weight: "semibold", children: group.groupTitle ?? "Grouped purchase" }), _jsx(Text, { size: "xs", className: "text-[var(--appkit-color-text-muted)]", children: `${group.memberCount} ${group.memberCount === 1 ? "item" : "items"} · one line` })] }), _jsx(Div, { className: "pl-4", children: group.items.map(({ item, index }) => sellerOrderItemRow(item, `m-${index}`)) })] }, `g-${gi}`))) })] })), _jsx(OrderAddonBadges, { order: order, variant: "detail" }), order.giftWrapAddon && (_jsxs(Div, { className: "border border-[var(--appkit-color-primary-200)] dark:border-[var(--appkit-color-primary-800)]", surface: "subtle", padding: "inline", rounded: "lg", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "\uD83C\uDF81 Gift wrap requested" }), order.giftWrapMessage && (_jsxs(Text, { size: "sm", className: "mt-1 text-[var(--appkit-color-text-muted)]", children: ["\u201C", order.giftWrapMessage, "\u201D"] }))] })), _jsxs(Row, { surface: "muted", padding: "inline", align: "center", justify: "between", rounded: "lg", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Total" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-primary)]", weight: "bold", children: toCurrency(order.totalAmount ?? order.totalPrice ?? 0) })] }), addrLine && (_jsxs(Div, { children: [_jsx(Text, { size: "sm", className: "mb-1", weight: "semibold", children: "Shipping address" }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-muted)]", children: [String(addr.fullName ?? ""), addrLine].filter(Boolean).join(" · ") })] })), order.paymentMethod && (_jsxs(Div, { children: [_jsxs(Row, { align: "center", justify: "between", className: "mb-1", gap: "3", children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Payment" }), isManualPaymentMethod(order.paymentMethod) && (_jsx(Badge, { variant: sellerPaymentBadge(order).variant, children: sellerPaymentBadge(order).label }))] }), _jsx(Text, { size: "sm", className: "text-[var(--appkit-color-text-muted)]", transform: "capitalize", children: order.paymentMethod }), isManualPaymentMethod(order.paymentMethod) && order.paymentTransactionId && (_jsxs(Text, { size: "xs", color: "muted", className: "mt-1", children: ["UTR: ", order.paymentTransactionId] }))] })), order.emiEnabled && (_jsxs(Div, { className: "border-t border-[var(--appkit-color-border)]", padding: "t-md", children: [_jsxs(Row, { align: "center", justify: "between", className: "mb-2", children: [_jsxs(Text, { size: "sm", weight: "semibold", children: ["EMI plan ", order.emiTenureMonths ? `· ${order.emiTenureMonths} months` : ""] }), _jsx(Badge, { variant: order.emiComplete ? "success" : "warning", children: order.emiComplete ? "Fully paid" : "In progress" })] }), _jsxs(Row, { align: "center", justify: "between", className: "mb-2", children: [_jsx(Text, { size: "xs", color: "muted", children: "Token collected" }), _jsx(Text, { size: "xs", weight: "medium", children: toCurrency(order.emiTokenAmount ?? 0) })] }), _jsxs(Row, { align: "center", justify: "between", className: "mb-3", children: [_jsx(Text, { size: "xs", color: "muted", children: "Remaining balance" }), _jsx(Text, { size: "xs", weight: "medium", children: toCurrency(order.emiRemainingBalance ?? 0) })] }), _jsx(Div, { className: "divide-y divide-[var(--appkit-color-border)] border border-[var(--appkit-color-border)]", rounded: "lg", children: (order.emiInstallments ?? []).map((inst) => (_jsxs(Row, { paddingY: "y-xs-tall", padding: "x-sm", align: "center", justify: "between", gap: "3", children: [_jsxs(Div, { className: "min-w-0", children: [_jsxs(Text, { size: "sm", weight: "medium", children: ["Installment ", inst.index, " \u00B7 ", toCurrency(inst.amount)] }), _jsx(Text, { size: "xs", color: "muted", children: inst.status === "paid" && inst.paidAt
|
|
303
|
+
? `Paid ${toRelativeDate(inst.paidAt)}`
|
|
304
|
+
: inst.dueDate
|
|
305
|
+
? `Due ${toRelativeDate(inst.dueDate)}`
|
|
306
|
+
: "" })] }), _jsxs(Row, { align: "center", gap: "sm", className: "shrink-0", children: [_jsx(Badge, { variant: EMI_INSTALLMENT_BADGE_VARIANT[inst.status] ?? "default", children: inst.status }), inst.status !== "paid" && (_jsx(Button, { action: ACTIONS.STORE["mark-installment-paid"], size: "sm", variant: "outline", isLoading: markingPaidIndex === inst.index, disabled: markingPaidIndex !== null, onClick: () => handleMarkInstallmentPaid(inst.index), children: "Mark paid" }))] })] }, inst.index))) }), emiError && (_jsx(Div, { textSize: "xs", className: "mt-2 border border-error/20", color: "error", surface: "danger-surface", padding: "inlineSm", rounded: "lg", children: emiError }))] })), _jsxs(Stack, { className: "border-t border-[var(--appkit-color-border)]", padding: "t-md", gap: "3", children: [_jsxs(FormShellContext.Provider, { value: form.shellCtx, children: [_jsx(FormErrorSummary, {}), _jsx(SectionForm, { sections: sections, values: draft, onChange: (partial) => setDraft((d) => ({ ...d, ...partial })), onSubmit: () => {
|
|
307
|
+
// SectionForm scrolls to the first erroring section and then
|
|
308
|
+
// submits regardless, so the guard lives here.
|
|
309
|
+
form.clearErrors();
|
|
310
|
+
const parsed = sellerOrderUpdateSchema.safeParse(visibleValues(sellerOrderUpdateSchema, draft));
|
|
311
|
+
if (!parsed.success) {
|
|
312
|
+
applyZodIssues(parsed.error.issues, form.setFieldError);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
void handleSave();
|
|
316
|
+
}, onValidationChange: () => form.validate(draft), schema: sellerOrderUpdateSchema, openIds: nav.openIds, onOpenChange: nav.setOpenIds, submitLabel: "Save", cancelLabel: "Close", onCancel: onClose, isLoading: saving })] }), saveError && (_jsx(Div, { textSize: "xs", className: "border border-error/20", color: "error", surface: "danger-surface", padding: "inlineSm", rounded: "lg", children: saveError }))] })] }) }))] }));
|
|
267
317
|
}
|
|
268
318
|
function OrderDetailDrawer({ orderId, apiBase, onClose, }) {
|
|
269
319
|
return (_jsx(SideDrawer, { isOpen: true, title: `Order ${orderId}`, onClose: onClose, children: _jsx(SellerOrderDetailPanel, { orderId: orderId, apiBase: apiBase, onClose: onClose }) }));
|
|
@@ -343,9 +393,7 @@ export function SellerOrdersView({ orderDetailApiBase = SELLER_ENDPOINTS.ORDERS,
|
|
|
343
393
|
{
|
|
344
394
|
key: "primary",
|
|
345
395
|
header: "Order",
|
|
346
|
-
render: (row) => (_jsxs(Row, { gap: "sm", align: "center", className: "min-w-0", children: [_jsx(Div, { className: "h-10 w-10 shrink-0", rounded: "md", overflow: "hidden", children: _jsx(MediaImage, { src: row.itemImage, alt: row.itemTitle ?? "Order item", size: "thumbnail" }) }), _jsxs(Stack, { gap: "none", className: "min-w-0", children: [_jsx(Text, { className: "truncate", size: "sm", weight: "semibold", children: row.
|
|
347
|
-
? `${row.itemTitle}${row.itemCount > 1 ? ` +${row.itemCount - 1} more` : ""}`
|
|
348
|
-
: row.primary }), _jsxs(Text, { size: "xs", color: "muted", children: [row.buyerName, " \u00B7 ", row.itemCount, " item", row.itemCount !== 1 ? "s" : ""] })] })] })),
|
|
396
|
+
render: (row) => (_jsxs(Row, { gap: "sm", align: "center", className: "min-w-0", children: [_jsx(Div, { className: "h-10 w-10 shrink-0", rounded: "md", overflow: "hidden", children: _jsx(MediaImage, { src: row.itemImage, alt: row.itemTitle ?? "Order item", size: "thumbnail" }) }), _jsxs(Stack, { gap: "none", className: "min-w-0", children: [_jsx(Text, { className: "truncate", size: "sm", weight: "semibold", children: row.primary }), _jsxs(Text, { size: "xs", color: "muted", children: [row.buyerName, " \u00B7 ", row.itemCount, " item", row.itemCount !== 1 ? "s" : ""] })] })] })),
|
|
349
397
|
},
|
|
350
398
|
{
|
|
351
399
|
key: "totalAmount",
|
|
@@ -392,7 +440,7 @@ export function SellerOrdersView({ orderDetailApiBase = SELLER_ENDPOINTS.ORDERS,
|
|
|
392
440
|
commit: "debounce",
|
|
393
441
|
},
|
|
394
442
|
emptyLabel: "No orders yet",
|
|
395
|
-
filterKeys: ["status"],
|
|
443
|
+
filterKeys: ["status", "orderType"],
|
|
396
444
|
defaultSort: DEFAULT_SORT,
|
|
397
445
|
queryKey: ["seller", "orders", "listing"],
|
|
398
446
|
endpoint: SELLER_ENDPOINTS.ORDERS,
|
|
@@ -425,10 +473,45 @@ export function SellerOrdersView({ orderDetailApiBase = SELLER_ENDPOINTS.ORDERS,
|
|
|
425
473
|
* below as `itemTitle`.
|
|
426
474
|
*/
|
|
427
475
|
primary: (() => {
|
|
428
|
-
|
|
476
|
+
/*
|
|
477
|
+
* Counted in LINES, not item rows, and a grouped line is named by
|
|
478
|
+
* its own title.
|
|
479
|
+
*
|
|
480
|
+
* This was `items[0].productTitle` + `itemsArr.length - 1`, so a
|
|
481
|
+
* four-member bundle read "Dranzer S +3 more" — an arbitrary
|
|
482
|
+
* member, implying four separate purchases. Grouping through the
|
|
483
|
+
* same helper the drawer now uses makes it "Starter Bundle", and a
|
|
484
|
+
* bundle beside one standalone item "+1 more".
|
|
485
|
+
*/
|
|
486
|
+
const groups = groupOrderItemsByLine(itemsArr
|
|
487
|
+
.filter((entry) => Boolean(entry) && typeof entry === "object")
|
|
488
|
+
/*
|
|
489
|
+
* Narrowed, not cast: `Record<string, unknown>` does NOT
|
|
490
|
+
* satisfy `OrderItemForLineGrouping` — its `groupSlug` is
|
|
491
|
+
* `unknown`, which is not assignable to `string | undefined`.
|
|
492
|
+
* A cast would compile and then group on whatever junk a bad
|
|
493
|
+
* document carried.
|
|
494
|
+
*/
|
|
495
|
+
.map((e) => ({
|
|
496
|
+
productTitle: typeof e.productTitle === "string" ? e.productTitle : undefined,
|
|
497
|
+
groupSlug: typeof e.groupSlug === "string" ? e.groupSlug : undefined,
|
|
498
|
+
groupTitle: typeof e.groupTitle === "string" ? e.groupTitle : undefined,
|
|
499
|
+
bundleCategorySlug: typeof e.bundleCategorySlug === "string" ? e.bundleCategorySlug : undefined,
|
|
500
|
+
bundleProductIds: Array.isArray(e.bundleProductIds)
|
|
501
|
+
? e.bundleProductIds.filter((v) => typeof v === "string")
|
|
502
|
+
: undefined,
|
|
503
|
+
})));
|
|
504
|
+
const head = groups[0];
|
|
505
|
+
const extra = Math.max(0, groups.length - 1);
|
|
506
|
+
const title = head === undefined
|
|
507
|
+
? typeof firstItem.productTitle === "string"
|
|
508
|
+
? firstItem.productTitle
|
|
509
|
+
: ""
|
|
510
|
+
: head.kind === "bundle"
|
|
511
|
+
? (head.groupTitle ?? "Grouped purchase")
|
|
512
|
+
: (head.item.productTitle ?? "");
|
|
429
513
|
if (!title)
|
|
430
514
|
return `Order ${toStringValue(item.id, "-")}`;
|
|
431
|
-
const extra = itemsArr.length - 1;
|
|
432
515
|
return extra > 0 ? `${title} +${extra} more` : title;
|
|
433
516
|
})(),
|
|
434
517
|
/*
|
|
@@ -456,10 +539,29 @@ export function SellerOrdersView({ orderDetailApiBase = SELLER_ENDPOINTS.ORDERS,
|
|
|
456
539
|
// The scope narrows; the status chip drills down inside it. Both are
|
|
457
540
|
// emitted — an explicit status always wins, since picking "Delivered"
|
|
458
541
|
// while sitting on Active should show delivered orders, not nothing.
|
|
459
|
-
|
|
542
|
+
/*
|
|
543
|
+
* Two independent axes, joined. The lane answers "how was this won", the
|
|
544
|
+
* status answers "where is it now" — a seller asking "which of my auction
|
|
545
|
+
* wins are still unshipped" should not have to choose between them.
|
|
546
|
+
*
|
|
547
|
+
* 🛑 `orderType` is a registered SELLER_SIEVE_FIELD as of this change. It
|
|
548
|
+
* was not, and the failure would have been invisible: `throwExceptions` is
|
|
549
|
+
* false, so an unallowlisted field drops its clause and the chip would have
|
|
550
|
+
* returned the seller's whole order list while looking like it filtered
|
|
551
|
+
* (Root Cause #62). `standard` is stripped from the query server-side and
|
|
552
|
+
* refined in memory — see `/api/store/orders`.
|
|
553
|
+
*/
|
|
554
|
+
buildFilters: (state) => [
|
|
555
|
+
state.status && state.status !== "All"
|
|
556
|
+
? sieveFilter("status", SIEVE_OP.EQ, state.status)
|
|
557
|
+
: null,
|
|
558
|
+
state.orderType ? sieveFilter("orderType", SIEVE_OP.EQ, state.orderType) : null,
|
|
559
|
+
]
|
|
560
|
+
.filter(Boolean)
|
|
561
|
+
.join(",") || undefined,
|
|
460
562
|
buildExtraParams: () => orderScope.extraParams,
|
|
461
563
|
renderAboveContent: orderScope.renderAboveContent,
|
|
462
|
-
renderFilterPanel: ({ pendingFilters, setPendingFilters }) => (_jsx(FilterChipGroup, { label: "Status", tabs: STATUS_OPTIONS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })) })),
|
|
564
|
+
renderFilterPanel: ({ pendingFilters, setPendingFilters }) => (_jsxs(_Fragment, { children: [_jsx(FilterChipGroup, { label: "Status", tabs: STATUS_OPTIONS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })) }), _jsx(FilterChipGroup, { label: "Type", tabs: ORDER_LANE_TABS, value: pendingFilters.orderType ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, orderType: id })) })] })),
|
|
463
565
|
buildBulkActions: (selection) => {
|
|
464
566
|
const handlePrintPackingSlips = () => {
|
|
465
567
|
const ids = selection.selectedIds.join(",");
|
|
@@ -1,10 +1,35 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { formatCurrency } from "../../../utils/number.formatter.js";
|
|
3
|
+
// The defining module, not a barrel (Root Cause #18). `hasEncPrefix` is a pure
|
|
4
|
+
// string test with no crypto in its import chain, so it is client-safe.
|
|
5
|
+
import { hasEncPrefix } from "../../../security/pii-mask.js";
|
|
3
6
|
export function toRecordArray(value) {
|
|
4
7
|
return Array.isArray(value) ? value.filter(Boolean) : [];
|
|
5
8
|
}
|
|
6
9
|
export function toStringValue(value, fallback = "-") {
|
|
7
10
|
if (typeof value === "string") {
|
|
11
|
+
/*
|
|
12
|
+
* 🛑 Ciphertext is not a display value, and this is the last line of
|
|
13
|
+
* defence rather than the fix.
|
|
14
|
+
*
|
|
15
|
+
* `OrderRepository.mapDoc` deliberately does not decrypt, so any route
|
|
16
|
+
* that forgets `revealOrders`/`maskedOrders` hands this function a stored
|
|
17
|
+
* `enc:v1:…` blob — and it is a `string`, so it rendered verbatim.
|
|
18
|
+
* Measured live in the seller's order list, where the row subtitle read
|
|
19
|
+
* `enc:v1:8plr3ZzKBqejPPti:MCZsrfOidLkSL6EogbxlCzU=:…` where the buyer's
|
|
20
|
+
* name belongs.
|
|
21
|
+
*
|
|
22
|
+
* This is a RECURRING class, not one slip: eight separate files in appkit
|
|
23
|
+
* carry a comment recording the same leak, and one of them had already
|
|
24
|
+
* baked `enc:v1:…` into 383 notification bodies. The route fix is the real
|
|
25
|
+
* repair; this makes the NEXT one degrade to "-" instead of garbage.
|
|
26
|
+
*
|
|
27
|
+
* `hasEncPrefix` rather than `isPiiEncrypted`: its own docstring says this
|
|
28
|
+
* is the check the display masks want, because a settings-encrypted or
|
|
29
|
+
* malformed envelope is exactly as unshowable as a PII one.
|
|
30
|
+
*/
|
|
31
|
+
if (hasEncPrefix(value))
|
|
32
|
+
return fallback;
|
|
8
33
|
return value.trim() || fallback;
|
|
9
34
|
}
|
|
10
35
|
if (typeof value === "number" || typeof value === "boolean") {
|
|
@@ -15,7 +15,7 @@ export declare const createShipmentSchema: z.ZodObject<{
|
|
|
15
15
|
laborRatePerHour: z.ZodOptional<z.ZodNumber>;
|
|
16
16
|
notes: z.ZodOptional<z.ZodString>;
|
|
17
17
|
}, "strip", z.ZodTypeAny, {
|
|
18
|
-
status: "received" | "completed" | "processing" | "cancelled" | "
|
|
18
|
+
status: "received" | "completed" | "processing" | "cancelled" | "in_transit" | "ordered" | "planning" | "customs";
|
|
19
19
|
shipmentNumber: string;
|
|
20
20
|
supplierName: string;
|
|
21
21
|
customsTotal: number;
|
|
@@ -29,7 +29,7 @@ export declare const createShipmentSchema: z.ZodObject<{
|
|
|
29
29
|
laborHoursSpent?: number | undefined;
|
|
30
30
|
laborRatePerHour?: number | undefined;
|
|
31
31
|
}, {
|
|
32
|
-
status: "received" | "completed" | "processing" | "cancelled" | "
|
|
32
|
+
status: "received" | "completed" | "processing" | "cancelled" | "in_transit" | "ordered" | "planning" | "customs";
|
|
33
33
|
shipmentNumber: string;
|
|
34
34
|
supplierName: string;
|
|
35
35
|
customsTotal: number;
|
|
@@ -58,7 +58,7 @@ export declare const updateShipmentSchema: z.ZodObject<{
|
|
|
58
58
|
laborRatePerHour: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
|
|
59
59
|
notes: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
60
60
|
}, "strip", z.ZodTypeAny, {
|
|
61
|
-
status?: "received" | "completed" | "processing" | "cancelled" | "
|
|
61
|
+
status?: "received" | "completed" | "processing" | "cancelled" | "in_transit" | "ordered" | "planning" | "customs" | undefined;
|
|
62
62
|
trackingNumber?: string | undefined;
|
|
63
63
|
notes?: string | undefined;
|
|
64
64
|
carrier?: string | undefined;
|
|
@@ -72,7 +72,7 @@ export declare const updateShipmentSchema: z.ZodObject<{
|
|
|
72
72
|
laborHoursSpent?: number | undefined;
|
|
73
73
|
laborRatePerHour?: number | undefined;
|
|
74
74
|
}, {
|
|
75
|
-
status?: "received" | "completed" | "processing" | "cancelled" | "
|
|
75
|
+
status?: "received" | "completed" | "processing" | "cancelled" | "in_transit" | "ordered" | "planning" | "customs" | undefined;
|
|
76
76
|
trackingNumber?: string | undefined;
|
|
77
77
|
notes?: string | undefined;
|
|
78
78
|
carrier?: string | undefined;
|