@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.
Files changed (204) hide show
  1. package/dist/.build-stamp.json +3 -3
  2. package/dist/_internal/server/features/checkout/actions.d.ts +80 -0
  3. package/dist/_internal/server/features/checkout/actions.js +542 -26
  4. package/dist/_internal/server/features/invoicing/allocate.d.ts +63 -0
  5. package/dist/_internal/server/features/invoicing/allocate.js +162 -0
  6. package/dist/_internal/server/features/invoicing/credit-note.d.ts +57 -0
  7. package/dist/_internal/server/features/invoicing/credit-note.js +126 -0
  8. package/dist/_internal/server/features/invoicing/gstr1.d.ts +74 -0
  9. package/dist/_internal/server/features/invoicing/gstr1.js +251 -0
  10. package/dist/_internal/server/features/invoicing/issue.d.ts +96 -0
  11. package/dist/_internal/server/features/invoicing/issue.js +202 -0
  12. package/dist/_internal/server/features/orders/actions.d.ts +8 -1
  13. package/dist/_internal/server/features/orders/actions.js +81 -8
  14. package/dist/_internal/server/features/orders/adapters.js +33 -0
  15. package/dist/_internal/server/features/orders/cancellation-refund.d.ts +15 -9
  16. package/dist/_internal/server/features/orders/cancellation-refund.js +15 -9
  17. package/dist/_internal/server/features/points/award.js +46 -4
  18. package/dist/_internal/server/features/refunds/actions.js +26 -0
  19. package/dist/_internal/server/features/shipping/courier-block.d.ts +69 -0
  20. package/dist/_internal/server/features/shipping/courier-block.js +239 -0
  21. package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
  22. package/dist/_internal/server/features/site-settings/adapters.js +15 -0
  23. package/dist/_internal/server/features/stores/adapters.d.ts +2 -2
  24. package/dist/_internal/server/features/stores/adapters.js +51 -0
  25. package/dist/_internal/server/functions/scheduled.js +38 -1
  26. package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +9 -2
  27. package/dist/_internal/server/jobs/core/dailyStatusDigest.d.ts +4 -0
  28. package/dist/_internal/server/jobs/core/dailyStatusDigest.js +19 -2
  29. package/dist/_internal/server/jobs/core/index.d.ts +2 -1
  30. package/dist/_internal/server/jobs/core/index.js +2 -1
  31. package/dist/_internal/server/jobs/core/onJobCreated.d.ts +30 -7
  32. package/dist/_internal/server/jobs/core/onJobCreated.js +78 -45
  33. package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +7 -0
  34. package/dist/_internal/server/jobs/core/onOrderStatusChange.js +62 -0
  35. package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +33 -0
  36. package/dist/_internal/server/jobs/core/runJobOnce.d.ts +48 -0
  37. package/dist/_internal/server/jobs/core/runJobOnce.js +195 -0
  38. package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +10 -2
  39. package/dist/_internal/server/jobs/handlers/onOrderStatusChange.js +17 -0
  40. package/dist/_internal/server/notifications/channel-health.d.ts +26 -3
  41. package/dist/_internal/shared/actions/action-registry.js +28 -0
  42. package/dist/_internal/shared/checkout/coupon-discount.d.ts +43 -1
  43. package/dist/_internal/shared/checkout/coupon-discount.js +26 -1
  44. package/dist/_internal/shared/checkout/order-math.d.ts +145 -0
  45. package/dist/_internal/shared/checkout/order-math.js +173 -0
  46. package/dist/_internal/shared/checkout/refundable-base.d.ts +25 -0
  47. package/dist/_internal/shared/checkout/refundable-base.js +13 -1
  48. package/dist/_internal/shared/checkout/split.d.ts +87 -0
  49. package/dist/_internal/shared/checkout/split.js +100 -0
  50. package/dist/_internal/shared/content/disclaimers.d.ts +88 -0
  51. package/dist/_internal/shared/content/disclaimers.js +217 -0
  52. package/dist/_internal/shared/content/published-values.d.ts +88 -0
  53. package/dist/_internal/shared/content/published-values.js +198 -0
  54. package/dist/_internal/shared/features/admin/integrations.d.ts +109 -0
  55. package/dist/_internal/shared/features/admin/integrations.js +253 -0
  56. package/dist/_internal/shared/features/orders/dispute-reasons.d.ts +113 -0
  57. package/dist/_internal/shared/features/orders/dispute-reasons.js +181 -0
  58. package/dist/_internal/shared/features/orders/refund-copy.d.ts +1 -1
  59. package/dist/_internal/shared/features/orders/refund-copy.js +18 -1
  60. package/dist/_internal/shared/features/points/accepts.d.ts +36 -0
  61. package/dist/_internal/shared/features/points/accepts.js +43 -0
  62. package/dist/_internal/shared/features/shipping/shared-purchase.d.ts +91 -0
  63. package/dist/_internal/shared/features/shipping/shared-purchase.js +108 -0
  64. package/dist/_internal/shared/features/site-settings/types.d.ts +11 -0
  65. package/dist/_internal/shared/fees/calculator.d.ts +33 -1
  66. package/dist/_internal/shared/fees/calculator.js +32 -2
  67. package/dist/client.d.ts +11 -0
  68. package/dist/client.js +30 -0
  69. package/dist/constants/api-endpoints.d.ts +42 -0
  70. package/dist/constants/api-endpoints.js +14 -0
  71. package/dist/constants/field-names.d.ts +1 -0
  72. package/dist/constants/field-names.js +5 -0
  73. package/dist/contracts/index.d.ts +1 -1
  74. package/dist/contracts/shipping.d.ts +45 -0
  75. package/dist/contracts/shipping.js +15 -0
  76. package/dist/features/about/components/AboutView.js +1 -1
  77. package/dist/features/account/components/BuyerAuctionsGuideView.js +4 -1
  78. package/dist/features/account/components/UserOrdersView.js +19 -33
  79. package/dist/features/admin/components/AdminCouponEditorView.js +17 -0
  80. package/dist/features/admin/components/AdminInvoicingView.d.ts +2 -0
  81. package/dist/features/admin/components/AdminInvoicingView.js +65 -0
  82. package/dist/features/admin/components/AdminOrderEditorView.d.ts +18 -1
  83. package/dist/features/admin/components/AdminOrderEditorView.js +11 -2
  84. package/dist/features/admin/components/AdminOrdersGuideView.js +23 -2
  85. package/dist/features/admin/components/AdminOrdersView.js +67 -5
  86. package/dist/features/admin/components/OwnerQuickActions.js +24 -2
  87. package/dist/features/admin/constants/filter-tabs.d.ts +6 -0
  88. package/dist/features/admin/constants/filter-tabs.js +6 -0
  89. package/dist/features/admin/constants/site-settings-tabs.d.ts +1 -1
  90. package/dist/features/admin/hooks/useAdminListingData.js +16 -0
  91. package/dist/features/admin/schemas/admin-user-form.d.ts +2 -2
  92. package/dist/features/admin/schemas/firestore.d.ts +30 -0
  93. package/dist/features/admin/schemas/firestore.js +5 -0
  94. package/dist/features/audit-log/schemas/firestore.d.ts +30 -0
  95. package/dist/features/audit-log/schemas/firestore.js +15 -0
  96. package/dist/features/auth/permissions/constants.js +13 -0
  97. package/dist/features/cart/components/CartPriceBreakdown.d.ts +13 -0
  98. package/dist/features/cart/components/CartPriceBreakdown.js +6 -2
  99. package/dist/features/cart/components/PointsTenderPicker.d.ts +61 -0
  100. package/dist/features/cart/components/PointsTenderPicker.js +15 -0
  101. package/dist/features/cart/components/StoreAddonsPicker.js +2 -1
  102. package/dist/features/cart/components/index.d.ts +2 -0
  103. package/dist/features/cart/components/index.js +1 -0
  104. package/dist/features/cart/repository/cart.repository.d.ts +19 -0
  105. package/dist/features/cart/repository/cart.repository.js +27 -0
  106. package/dist/features/cart/schemas/firestore.d.ts +92 -0
  107. package/dist/features/contact/email.d.ts +27 -10
  108. package/dist/features/contact/email.js +20 -3
  109. package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +38 -0
  110. package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +80 -0
  111. package/dist/features/dispute-reasons/schemas/firestore.d.ts +64 -0
  112. package/dist/features/dispute-reasons/schemas/firestore.js +40 -0
  113. package/dist/features/faq/components/FAQAccordion.d.ts +13 -1
  114. package/dist/features/faq/components/FAQAccordion.js +29 -4
  115. package/dist/features/invoicing/schemas/firestore.d.ts +112 -0
  116. package/dist/features/invoicing/schemas/firestore.js +45 -0
  117. package/dist/features/invoicing/utils/financial-year.d.ts +43 -0
  118. package/dist/features/invoicing/utils/financial-year.js +53 -0
  119. package/dist/features/jobs/actions/enqueue-job.js +18 -1
  120. package/dist/features/jobs/repository/jobs.repository.d.ts +62 -1
  121. package/dist/features/jobs/repository/jobs.repository.js +84 -1
  122. package/dist/features/jobs/schemas/firestore.d.ts +80 -0
  123. package/dist/features/jobs/schemas/firestore.js +37 -0
  124. package/dist/features/lottery/schemas/zod.d.ts +6 -6
  125. package/dist/features/media/MediaImage.js +22 -1
  126. package/dist/features/media/upload/ImageUpload.js +1 -1
  127. package/dist/features/obligations/constants/policies.js +24 -1
  128. package/dist/features/orders/components/OrderPaymentSummary.js +10 -1
  129. package/dist/features/orders/components/OrderProvenanceCard.d.ts +8 -0
  130. package/dist/features/orders/components/OrderProvenanceCard.js +185 -0
  131. package/dist/features/orders/components/RaiseDisputeForm.d.ts +14 -0
  132. package/dist/features/orders/components/RaiseDisputeForm.js +111 -0
  133. package/dist/features/orders/components/RefundRequestView.js +19 -1
  134. package/dist/features/orders/components/index.d.ts +4 -0
  135. package/dist/features/orders/components/index.js +2 -0
  136. package/dist/features/orders/constants/order-lane-tabs.d.ts +69 -0
  137. package/dist/features/orders/constants/order-lane-tabs.js +88 -0
  138. package/dist/features/orders/repository/orders.repository.d.ts +57 -0
  139. package/dist/features/orders/repository/orders.repository.js +145 -0
  140. package/dist/features/orders/schemas/firestore.d.ts +196 -1
  141. package/dist/features/orders/schemas/index.d.ts +54 -6
  142. package/dist/features/orders/schemas/index.js +12 -0
  143. package/dist/features/orders/types/index.d.ts +26 -0
  144. package/dist/features/orders/utils/courier-block.d.ts +25 -0
  145. package/dist/features/orders/utils/courier-block.js +23 -0
  146. package/dist/features/payment-cases/schemas/firestore.d.ts +26 -1
  147. package/dist/features/payment-cases/schemas/firestore.js +1 -0
  148. package/dist/features/products/components/PrizeDrawDetailPageView.js +2 -2
  149. package/dist/features/products/components/ProductDetailPageView.d.ts +17 -1
  150. package/dist/features/products/components/ProductDetailPageView.js +2 -2
  151. package/dist/features/products/schemas/product-features.validators.d.ts +10 -10
  152. package/dist/features/promotions/actions/coupon-stacking.d.ts +30 -3
  153. package/dist/features/promotions/actions/coupon-stacking.js +21 -2
  154. package/dist/features/promotions/components/CouponCard.js +13 -0
  155. package/dist/features/promotions/constants/coupon-help.d.ts +2 -2
  156. package/dist/features/promotions/constants/coupon-help.js +9 -1
  157. package/dist/features/promotions/schemas/firestore.d.ts +24 -0
  158. package/dist/features/promotions/schemas/firestore.js +4 -0
  159. package/dist/features/promotions/types/index.d.ts +19 -1
  160. package/dist/features/scams/components/ScamProfileView.js +2 -2
  161. package/dist/features/scams/components/ScamRegistryView.js +2 -2
  162. package/dist/features/search/action-index/tab-entries.js +1 -1
  163. package/dist/features/seller/actions/seller-actions.js +33 -3
  164. package/dist/features/seller/components/SellerOrdersView.js +129 -27
  165. package/dist/features/seller/hooks/useSellerListingData.js +25 -0
  166. package/dist/features/shipments/schemas/validation.d.ts +4 -4
  167. package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +43 -0
  168. package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +90 -0
  169. package/dist/features/shipping-boxes/schemas/firestore.d.ts +82 -0
  170. package/dist/features/shipping-boxes/schemas/firestore.js +53 -0
  171. package/dist/features/store-extensions/schemas/report-create-form.d.ts +2 -2
  172. package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +2 -2
  173. package/dist/features/stores/components/StoreOrdersGuideView.js +6 -1
  174. package/dist/features/stores/schemas/firestore.d.ts +71 -0
  175. package/dist/features/stores/types/index.d.ts +19 -0
  176. package/dist/features/support/schemas/index.d.ts +6 -6
  177. package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +1 -1
  178. package/dist/index.d.ts +6 -1
  179. package/dist/index.js +14 -1
  180. package/dist/next/routing/route-map.d.ts +20 -0
  181. package/dist/next/routing/route-map.js +10 -0
  182. package/dist/providers/shipping-shiprocket/index.d.ts +27 -1
  183. package/dist/providers/shipping-shiprocket/index.js +84 -1
  184. package/dist/schemas/registry.d.ts +47 -18
  185. package/dist/schemas/sieve.d.ts +2 -2
  186. package/dist/server-entry.d.ts +2 -2
  187. package/dist/server-entry.js +8 -1
  188. package/dist/server.d.ts +10 -1
  189. package/dist/server.js +39 -1
  190. package/dist/styles.css +64 -80
  191. package/dist/tailwind-utilities.css +1 -1
  192. package/dist/tokens/themes/default-dark.js +1 -0
  193. package/dist/tokens/themes/default-light.js +1 -0
  194. package/dist/tokens/themes/required.d.ts +1 -1
  195. package/dist/tokens/themes/required.js +1 -0
  196. package/dist/tokens/tokens.css +15 -0
  197. package/dist/ui/components/Disclaimer.d.ts +19 -0
  198. package/dist/ui/components/Disclaimer.js +74 -0
  199. package/dist/ui/components/tabs-overflow.js +64 -11
  200. package/dist/ui/index.d.ts +2 -0
  201. package/dist/ui/index.js +6 -0
  202. package/dist/ui/rich-text/RichText.style.css +48 -79
  203. package/package.json +1 -1
  204. 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
- "A second coupon for the same store, or a second platform-wide coupon, will be declined — remove the first one to swap it.",
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: "Verified scammers active in India's collectibles community. 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(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
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
- const deduction = computePayoutDeduction(grossAmount, siteSettings.commissions, shippingReimbursement);
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
- throw new Error(d?.error ?? "Failed to update order");
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
- void normalizeError(err);
221
- setSaveError(err.message);
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 && (_jsxs(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) })] }), (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: (order.items ?? []).map((item, i) => (_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) })] }, i))) })] })), _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
253
- ? `Paid ${toRelativeDate(inst.paidAt)}`
254
- : inst.dueDate
255
- ? `Due ${toRelativeDate(inst.dueDate)}`
256
- : "" })] }), _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: () => {
257
- // SectionForm scrolls to the first erroring section and then
258
- // submits regardless, so the guard lives here.
259
- form.clearErrors();
260
- const parsed = sellerOrderUpdateSchema.safeParse(visibleValues(sellerOrderUpdateSchema, draft));
261
- if (!parsed.success) {
262
- applyZodIssues(parsed.error.issues, form.setFieldError);
263
- return;
264
- }
265
- void handleSave();
266
- }, 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 }))] })] }), _jsxs(Row, { border: "top", paddingY: "y-sm-tall", padding: "x-md", align: "center", justify: "end", gap: "3", children: [_jsx(Button, { variant: "outline", onClick: onClose, disabled: saving, children: "Close" }), _jsx(Button, { onClick: handleSave, isLoading: saving, disabled: saving, children: "Save" })] })] }))] }));
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.itemTitle
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
- const title = typeof firstItem.productTitle === "string" ? firstItem.productTitle : "";
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
- buildFilters: (state) => (state.status && state.status !== "All" ? sieveFilter("status", SIEVE_OP.EQ, state.status) : undefined),
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" | "ordered" | "in_transit" | "planning" | "customs";
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" | "ordered" | "in_transit" | "planning" | "customs";
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" | "ordered" | "in_transit" | "planning" | "customs" | undefined;
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" | "ordered" | "in_transit" | "planning" | "customs" | undefined;
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;