@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
@@ -129,6 +129,25 @@ export declare class CartRepository extends BaseRepository<CartDocument> {
129
129
  * and keeping both shapes around invites readers that handle only one.
130
130
  */
131
131
  setStoreAddons(userId: string, storeId: string, addons: CartStoreAddons | null): Promise<void>;
132
+ /**
133
+ * Opt the cart in or out of paying with points (D46/D47, decision 24).
134
+ *
135
+ * 🛑 A BOOLEAN, and the schema deliberately has no numeric sibling. Opt in
136
+ * and the server applies as many points as the 50% cap allows; whatever
137
+ * cannot be applied stays in the balance for a later order. A client-supplied
138
+ * amount is a client-supplied discount, which is the whole reason
139
+ * `storeAddons` is the source of truth for add-on fees rather than a request
140
+ * body.
141
+ *
142
+ * Written with the same `set(..., { merge: true })` as its neighbours so a
143
+ * concurrent add-on or selection write is not clobbered.
144
+ *
145
+ * 🛑 Opting OUT deletes the field rather than writing `false` — absent and
146
+ * `false` mean the same thing to every reader (`cart.useAllPoints === true`),
147
+ * and keeping both shapes invites a reader that handles only one. Same
148
+ * reasoning as `setStoreAddons` directly above.
149
+ */
150
+ setUseAllPoints(userId: string, useAllPoints: boolean): Promise<void>;
132
151
  /**
133
152
  * Cloud Functions: return refs of stale carts not updated within TTL.
134
153
  */
@@ -590,6 +590,33 @@ export class CartRepository extends BaseRepository {
590
590
  updatedAt: new Date(),
591
591
  }, { merge: true });
592
592
  }
593
+ /**
594
+ * Opt the cart in or out of paying with points (D46/D47, decision 24).
595
+ *
596
+ * 🛑 A BOOLEAN, and the schema deliberately has no numeric sibling. Opt in
597
+ * and the server applies as many points as the 50% cap allows; whatever
598
+ * cannot be applied stays in the balance for a later order. A client-supplied
599
+ * amount is a client-supplied discount, which is the whole reason
600
+ * `storeAddons` is the source of truth for add-on fees rather than a request
601
+ * body.
602
+ *
603
+ * Written with the same `set(..., { merge: true })` as its neighbours so a
604
+ * concurrent add-on or selection write is not clobbered.
605
+ *
606
+ * 🛑 Opting OUT deletes the field rather than writing `false` — absent and
607
+ * `false` mean the same thing to every reader (`cart.useAllPoints === true`),
608
+ * and keeping both shapes invites a reader that handles only one. Same
609
+ * reasoning as `setStoreAddons` directly above.
610
+ */
611
+ async setUseAllPoints(userId, useAllPoints) {
612
+ await this.db
613
+ .collection(this.collection)
614
+ .doc(userId)
615
+ .set({
616
+ useAllPoints: useAllPoints ? true : FieldValue.delete(),
617
+ updatedAt: new Date(),
618
+ }, { merge: true });
619
+ }
593
620
  /*
594
621
  * `pruneStoreAddons(userId, keepStoreIds)` used to live here. It had zero
595
622
  * callers from the day it was written, and `pruneForItems` now does the same
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import type { BaseDocument } from "../../../_internal/shared/types/base-document";
8
8
  import type { ListingType } from "../../products/types/index";
9
+ import type { CouponType } from "../../promotions/types/index";
9
10
  /**
10
11
  * What KIND of thing one cart line represents.
11
12
  *
@@ -57,6 +58,33 @@ export interface CartLineMember {
57
58
  gstRate?: 0 | 5 | 12 | 18 | 28;
58
59
  hsnCode?: string;
59
60
  listingType?: ListingType;
61
+ /**
62
+ * How the coordinator expressed this member's share of the line.
63
+ *
64
+ * Absent means `"amount"` — every line written before splits existed, so no
65
+ * migration and no behaviour change until a coordinator picks percentages.
66
+ *
67
+ * 🛑 The PERCENTAGE is the stored fact, not the resolved rupees. Percentages
68
+ * apply to the LANDED cost (goods + shipping + fees), so a member's figure
69
+ * MOVES when shipping is re-quoted — storing the computed amount instead
70
+ * would freeze a number that no longer matches the agreed share. The
71
+ * computed figures are snapshotted at COMMIT, which is where the
72
+ * coordinator's agreement actually becomes binding (D61).
73
+ *
74
+ * `resolveSplit` (`_internal/shared/checkout/split.ts`) is the only thing
75
+ * that turns these into money.
76
+ */
77
+ splitMode?: "amount" | "percent";
78
+ /** 0–100, read only when `splitMode === "percent"`. */
79
+ percent?: number;
80
+ /**
81
+ * The member's agreed contribution in decimal rupees, when typed directly.
82
+ *
83
+ * 🛑 NOT the same as `unitPrice`. `unitPrice` is what the ITEM costs;
84
+ * this is what this PERSON agreed to put in, and the two differ by design
85
+ * the moment a split is uneven or the total exceeds the landed cost.
86
+ */
87
+ contribution?: number;
60
88
  }
61
89
  export interface CartItemDocument {
62
90
  itemId: string;
@@ -183,6 +211,26 @@ export interface CartItemDocument {
183
211
  chosenShippingProviderId?: string;
184
212
  /** Fee in decimal rupees for the chosen provider, snapshotted at cart time. */
185
213
  chosenShippingFee?: number;
214
+ /**
215
+ * ── S5a: which address THIS line ships to ────────────────────────────────
216
+ *
217
+ * One payer, N destinations. Absent means "the order's own shipping
218
+ * address", which is every cart that predates this and every ordinary
219
+ * single-destination order — so nothing changes for them.
220
+ *
221
+ * 🛑 An `addresses/{id}` REFERENCE, never a copied address. Copying would
222
+ * put a name, a phone number and a street address onto the cart document,
223
+ * which is a PII store with no encryption, no `*_PII_FIELDS` entry and no
224
+ * retention story — and carts are swept by a TTL job that is not written to
225
+ * handle personal data. The id resolves server-side at checkout, where the
226
+ * address is already being read and decrypted.
227
+ *
228
+ * 🛑 It must belong to the BUYER. An arbitrary `addresses` id would let a
229
+ * caller address a parcel to any stored address on the platform, which is
230
+ * both a delivery hijack and a way to read whether an id exists. Checked
231
+ * server-side at the write, not trusted from the client.
232
+ */
233
+ destinationAddressId?: string;
186
234
  addedAt: Date;
187
235
  updatedAt: Date;
188
236
  }
@@ -196,6 +244,28 @@ export interface CartAppliedCoupon {
196
244
  storeId?: string;
197
245
  /** Item IDs (CartItemDocument.itemId) this coupon was calculated against */
198
246
  applicableItemIds?: string[];
247
+ /**
248
+ * The coupon's type, snapshotted at apply time (D51).
249
+ *
250
+ * 🛑 Needed because `detectCouponConflict` buckets on TYPE, not scope: a
251
+ * `points_earn` coupon is admin-scoped like every platform coupon but
252
+ * competes for its own slot, so without this field on the stored entry the
253
+ * detector could not tell a points coupon already in the cart from an
254
+ * ordinary platform one, and `RIPLI05` would block every other admin coupon
255
+ * — the exact defect the third bucket exists to prevent.
256
+ *
257
+ * Absent means "a discount coupon", which is what every cart written before
258
+ * this field holds, and is the reading that preserves the previous
259
+ * behaviour exactly.
260
+ *
261
+ * 🛑 Snapshotted deliberately, unlike the restrictions: a coupon's TYPE
262
+ * decides which slot it occupies, so re-reading it live would mean an admin
263
+ * editing a coupon's type could retroactively make a live cart's
264
+ * combination illegal. The money is still re-validated against the live
265
+ * coupon at placement (`revalidateAppliedCoupons`), which is where a changed
266
+ * discount has to be caught.
267
+ */
268
+ type?: CouponType;
199
269
  }
200
270
  /**
201
271
  * Paid add-ons the buyer opted into, for ONE store.
@@ -239,6 +309,28 @@ export interface CartDocument extends BaseDocument {
239
309
  * rather than a flag someone has to remember to check.
240
310
  */
241
311
  storeAddons?: Record<string, CartStoreAddons>;
312
+ /**
313
+ * The buyer has opted to spend points on this checkout.
314
+ *
315
+ * ## 🛑 A BOOLEAN, and the amount is never stored or sent
316
+ *
317
+ * Owner's rule: *"if use point it is all and then the balance is returned if
318
+ * excess"* — one opt-in, all-or-nothing. Ticked, the checkout applies as many
319
+ * points as the 50% cap allows; whatever cannot be applied stays in the
320
+ * balance. The buyer never types a number.
321
+ *
322
+ * So this is the ONLY thing the client may say about points. The amount is
323
+ * derived server-side from the live balance and
324
+ * `POINTS_MAX_REDEMPTION_PERCENT` at the moment the order is priced — the
325
+ * same rule that makes `storeAddons` above the source of truth rather than a
326
+ * request body, and for the same reason: a client-supplied money figure is a
327
+ * client-supplied discount.
328
+ *
329
+ * 🛑 This replaces a deleted `pointsToRedeem?: number` on the checkout input,
330
+ * whose handler debited the buyer AFTER the gateway had captured full price
331
+ * (D46). A number was the wrong shape as well as the wrong place.
332
+ */
333
+ useAllPoints?: boolean;
242
334
  }
243
335
  export declare const CART_COLLECTION: "carts";
244
336
  export declare const CART_INDEXED_FIELDS: string[];
@@ -1,14 +1,9 @@
1
1
  import "server-only";
2
2
  import type { SendGuardContext } from "../../_internal/shared/features/messaging/config";
3
3
  import type { JsonValue } from "@mohasinac/appkit";
4
- type SendEmailOptions = Record<string, JsonValue> & {
5
- to: string | string[];
6
- subject: string;
4
+ import type { EmailOptions } from "../../contracts/email";
5
+ type SendEmailOptions = Omit<EmailOptions, "html"> & {
7
6
  html?: string;
8
- text?: string;
9
- from?: string;
10
- cc?: string | string[];
11
- replyTo?: string;
12
7
  };
13
8
  /**
14
9
  * Everything `sendEmail` needs beyond the message itself.
@@ -22,9 +17,9 @@ type SendEmailOptions = Record<string, JsonValue> & {
22
17
  * (CLAUDE.md Rule #22: make the wrong thing unrepresentable).
23
18
  *
24
19
  * Kept as a separate second parameter rather than a field on
25
- * `SendEmailOptions` because that type has a `Record<string, JsonValue>` index
26
- * signature — a structured object would not satisfy it, and widening the index
27
- * signature to accommodate one field would weaken every other field's typing.
20
+ * `SendEmailOptions`, which is now the provider's own `EmailOptions`. The
21
+ * guard is about who may send and why; it is not part of the message, and
22
+ * folding it in would put it on the wire to the provider.
28
23
  */
29
24
  export type SendEmailGuard = SendGuardContext;
30
25
  /**
@@ -59,6 +54,28 @@ export interface OrderConfirmationEmailParams {
59
54
  unitPrice: number;
60
55
  totalPrice: number;
61
56
  }>;
57
+ /**
58
+ * Phase 7 — the invoice PDF to attach, as STORED bytes.
59
+ *
60
+ * 🛑 Bytes, never a path and never a renderer. Two renders can disagree — a
61
+ * changed template, a different library version — and the buyer's copy must
62
+ * be byte-identical to ours. Letting this function render would be a second
63
+ * render by definition.
64
+ *
65
+ * 🛑 Optional, and absence must stay silent. The invoice is numbered on the
66
+ * PAID transition and its bytes are produced on first request, so a COD
67
+ * order confirmed before payment legitimately has none — and this email has
68
+ * always sent without an attachment. Failing or warning here would turn the
69
+ * normal case into a noisy one.
70
+ *
71
+ * Resend supports attachments already (`email-resend/provider.ts`), so this
72
+ * needs no new send, no new credential and no change to the 80/day ceiling.
73
+ * Only the payload is bigger.
74
+ */
75
+ invoicePdf?: {
76
+ filename: string;
77
+ content: Buffer;
78
+ };
62
79
  }
63
80
  export declare function sendOrderConfirmationEmail(params: OrderConfirmationEmailParams): Promise<{
64
81
  success: boolean;
@@ -124,9 +124,10 @@ export async function sendEmail(opts, guard) {
124
124
  ? opts.cc
125
125
  : undefined,
126
126
  replyTo: typeof opts.replyTo === "string" ? opts.replyTo : undefined,
127
- headers: typeof opts.headers === "object" && opts.headers
128
- ? opts.headers
129
- : undefined,
127
+ headers: opts.headers,
128
+ // Forwarded, not dropped — the provider has mapped these to Resend's
129
+ // `attachments` all along; nothing here ever handed them over.
130
+ attachments: opts.attachments,
130
131
  });
131
132
  void recordSendAttempt({
132
133
  to: opts.to,
@@ -184,6 +185,22 @@ export async function sendOrderConfirmationEmail(params) {
184
185
  to,
185
186
  subject: `Order Confirmed — ${orderId}`,
186
187
  html,
188
+ /*
189
+ * Phase 7 — the invoice, when there is one. The STORED bytes, handed
190
+ * in by the caller: see `invoicePdf` on the params type for why this
191
+ * function must not render its own.
192
+ */
193
+ ...(params.invoicePdf
194
+ ? {
195
+ attachments: [
196
+ {
197
+ filename: params.invoicePdf.filename,
198
+ content: params.invoicePdf.content,
199
+ contentType: "application/pdf",
200
+ },
201
+ ],
202
+ }
203
+ : {}),
187
204
  text: `Hi ${userName},\n\nYour order ${orderId} has been confirmed!\n\n${itemsText}\nTotal: ${formattedTotal}\nPayment: ${paymentLabel}\nShip to: ${shippingAddress ?? "-"}\n\nView your order: ${orderUrl}\n\n© ${currentYear()} ${siteName}`,
188
205
  }, {
189
206
  channel: "email",
@@ -0,0 +1,38 @@
1
+ import type { FirebaseSieveFields, FirebaseSieveResult, SieveModel } from "../../../providers/db-firebase";
2
+ import { BaseRepository } from "../../../providers/db-firebase";
3
+ import type { DisputeKind } from "../../../_internal/shared/features/orders/dispute-reasons";
4
+ import { type DisputeReasonDocument } from "../schemas/firestore";
5
+ export declare class DisputeReasonsRepository extends BaseRepository<DisputeReasonDocument> {
6
+ static readonly SIEVE_FIELDS: FirebaseSieveFields;
7
+ constructor();
8
+ /**
9
+ * The picker's list, scoped to one kind.
10
+ *
11
+ * 🛑 Scoped by KIND, not merged. A buyer reporting a damaged item must not be
12
+ * offered "it never arrived" — that reason belongs to the other gate, and
13
+ * picking it would produce a dispute the gate then refuses, which reads to
14
+ * the buyer as the form being broken rather than as the wrong choice.
15
+ */
16
+ listActiveByKind(kind: DisputeKind, model: SieveModel): Promise<FirebaseSieveResult<DisputeReasonDocument>>;
17
+ /**
18
+ * Add a reason. Staff only — enforced at the route, not here.
19
+ *
20
+ * 🛑 Keyed `dreason-<slug>`, so two agents cannot add the same wording twice.
21
+ * A colliding create returns the EXISTING row rather than overwriting it:
22
+ * that row has an author and may already be named on live disputes, and the
23
+ * second agent's intent ("there should be a reason for this") is satisfied by
24
+ * what exists. Same rule as the shipping-box presets.
25
+ */
26
+ createReason(input: {
27
+ label: string;
28
+ kind: DisputeKind;
29
+ createdByUid: string;
30
+ createdByName: string;
31
+ }): Promise<{
32
+ reason: DisputeReasonDocument;
33
+ created: boolean;
34
+ }>;
35
+ /** Soft-retire. See the schema for why this is never a delete. */
36
+ retire(id: string): Promise<void>;
37
+ }
38
+ export declare const disputeReasonsRepository: DisputeReasonsRepository;
@@ -0,0 +1,80 @@
1
+ import { BaseRepository } from "../../../providers/db-firebase/index.js";
2
+ import { slugify } from "../../../utils/string.formatter.js";
3
+ import { DISPUTE_REASON_COLLECTION, DISPUTE_REASON_FIELDS, DISPUTE_REASON_LABEL_MAX, } from "../schemas/firestore.js";
4
+ export class DisputeReasonsRepository extends BaseRepository {
5
+ constructor() {
6
+ super(DISPUTE_REASON_COLLECTION);
7
+ }
8
+ /**
9
+ * The picker's list, scoped to one kind.
10
+ *
11
+ * 🛑 Scoped by KIND, not merged. A buyer reporting a damaged item must not be
12
+ * offered "it never arrived" — that reason belongs to the other gate, and
13
+ * picking it would produce a dispute the gate then refuses, which reads to
14
+ * the buyer as the form being broken rather than as the wrong choice.
15
+ */
16
+ async listActiveByKind(kind, model) {
17
+ const baseQuery = this.getCollection()
18
+ .where(DISPUTE_REASON_FIELDS.IS_ACTIVE, "==", true)
19
+ .where(DISPUTE_REASON_FIELDS.KIND, "==", kind);
20
+ return this.sieveQuery(model, DisputeReasonsRepository.SIEVE_FIELDS, { baseQuery, defaultPageSize: 20, maxPageSize: 50 });
21
+ }
22
+ /**
23
+ * Add a reason. Staff only — enforced at the route, not here.
24
+ *
25
+ * 🛑 Keyed `dreason-<slug>`, so two agents cannot add the same wording twice.
26
+ * A colliding create returns the EXISTING row rather than overwriting it:
27
+ * that row has an author and may already be named on live disputes, and the
28
+ * second agent's intent ("there should be a reason for this") is satisfied by
29
+ * what exists. Same rule as the shipping-box presets.
30
+ */
31
+ async createReason(input) {
32
+ const label = input.label.trim().slice(0, DISPUTE_REASON_LABEL_MAX);
33
+ const id = `dreason-${slugify(label)}`;
34
+ const existing = await this.findById(id);
35
+ /*
36
+ * A retired row that someone re-adds is REACTIVATED rather than duplicated
37
+ * — otherwise the slug collides forever and the agent can never get their
38
+ * reason back. Reactivating keeps the original author, which is correct:
39
+ * they wrote the wording.
40
+ */
41
+ if (existing) {
42
+ if (!existing.isActive) {
43
+ await this.update(id, { isActive: true, updatedAt: new Date() });
44
+ return { reason: { ...existing, isActive: true }, created: false };
45
+ }
46
+ return { reason: existing, created: false };
47
+ }
48
+ const now = new Date();
49
+ const reason = {
50
+ id,
51
+ label,
52
+ kind: input.kind,
53
+ isActive: true,
54
+ createdByUid: input.createdByUid,
55
+ createdByName: input.createdByName,
56
+ createdAt: now,
57
+ updatedAt: now,
58
+ };
59
+ await this.createWithId(id, reason);
60
+ return { reason, created: true };
61
+ }
62
+ /** Soft-retire. See the schema for why this is never a delete. */
63
+ async retire(id) {
64
+ await this.update(id, { isActive: false, updatedAt: new Date() });
65
+ }
66
+ }
67
+ DisputeReasonsRepository.SIEVE_FIELDS = {
68
+ /*
69
+ * 🛑 `label` must be here because `PaginatedSelect` SEARCHES it. An
70
+ * unallowlisted searched field does not raise — `sieve.ts` sets
71
+ * `throwExceptions: false`, so `findField` returns undefined, the clause is
72
+ * silently dropped, and the picker returns the WHOLE list while looking
73
+ * exactly like it filtered (Root Cause #62).
74
+ */
75
+ label: { canFilter: true, canSort: true },
76
+ kind: { canFilter: true, canSort: false },
77
+ isActive: { canFilter: true, canSort: false },
78
+ createdByUid: { canFilter: true, canSort: false },
79
+ };
80
+ export const disputeReasonsRepository = new DisputeReasonsRepository();
@@ -0,0 +1,64 @@
1
+ /**
2
+ * `disputeReasons` — the staff-curated list a buyer picks from when raising a
3
+ * dispute.
4
+ *
5
+ * ## What this is NOT
6
+ *
7
+ * 🛑 It is not policy. Every row carries a `kind`, and the GATE lives on the
8
+ * kind — see `_internal/shared/features/orders/dispute-reasons.ts`. A row can
9
+ * never introduce a new kind of claim, because a gate cannot branch on a
10
+ * user-created string.
11
+ *
12
+ * So this list is for TRIAGE and ANALYTICS: "what are buyers actually
13
+ * reporting", answered without reading free text. Staff (admin + employees)
14
+ * add rows as real gaps appear; buyers select only.
15
+ *
16
+ * ## Why staff-creatable rather than a closed enum
17
+ *
18
+ * Its sibling `RETURN_REASON` *is* a closed enum, and deliberately so: each
19
+ * member there decides whether a final sale can refuse the return, which is a
20
+ * money consequence that must be reviewed in code. Nothing here decides money.
21
+ * A support agent who keeps seeing "the courier left it at the wrong flat"
22
+ * should be able to add it without a release.
23
+ */
24
+ import type { DisputeKind } from "../../../_internal/shared/features/orders/dispute-reasons";
25
+ export declare const DISPUTE_REASON_COLLECTION = "disputeReasons";
26
+ export interface DisputeReasonDocument {
27
+ /** `dreason-<slug of label>`, so the same reason cannot be added twice. */
28
+ id: string;
29
+ /** Buyer-facing. Shown verbatim in the picker. */
30
+ label: string;
31
+ /** Which gate this reason is subject to. Never free-form. */
32
+ kind: DisputeKind;
33
+ /**
34
+ * 🛑 Soft-retire, never delete.
35
+ *
36
+ * A past dispute names this reason. Deleting the row would orphan that
37
+ * record and take the author with it — and the order snapshots the LABEL
38
+ * precisely so history survives either way, but a deleted row still loses
39
+ * "who added this, and when" for anyone auditing the list itself.
40
+ */
41
+ isActive: boolean;
42
+ /** Display name, so a seller or agent can see whose wording this is. */
43
+ createdByName: string;
44
+ /** The uid, for moderation. Never rendered. */
45
+ createdByUid: string;
46
+ createdAt: Date;
47
+ updatedAt: Date;
48
+ }
49
+ export declare const DISPUTE_REASON_FIELDS: {
50
+ readonly ID: "id";
51
+ readonly LABEL: "label";
52
+ readonly KIND: "kind";
53
+ readonly IS_ACTIVE: "isActive";
54
+ readonly CREATED_BY_UID: "createdByUid";
55
+ readonly CREATED_AT: "createdAt";
56
+ };
57
+ /**
58
+ * Bounds on a label.
59
+ *
60
+ * Short on purpose: this is a picker option, not the buyer's explanation. The
61
+ * detail goes in the free-text field on the dispute itself, which is where a
62
+ * buyer should describe what actually happened.
63
+ */
64
+ export declare const DISPUTE_REASON_LABEL_MAX = 80;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * `disputeReasons` — the staff-curated list a buyer picks from when raising a
3
+ * dispute.
4
+ *
5
+ * ## What this is NOT
6
+ *
7
+ * 🛑 It is not policy. Every row carries a `kind`, and the GATE lives on the
8
+ * kind — see `_internal/shared/features/orders/dispute-reasons.ts`. A row can
9
+ * never introduce a new kind of claim, because a gate cannot branch on a
10
+ * user-created string.
11
+ *
12
+ * So this list is for TRIAGE and ANALYTICS: "what are buyers actually
13
+ * reporting", answered without reading free text. Staff (admin + employees)
14
+ * add rows as real gaps appear; buyers select only.
15
+ *
16
+ * ## Why staff-creatable rather than a closed enum
17
+ *
18
+ * Its sibling `RETURN_REASON` *is* a closed enum, and deliberately so: each
19
+ * member there decides whether a final sale can refuse the return, which is a
20
+ * money consequence that must be reviewed in code. Nothing here decides money.
21
+ * A support agent who keeps seeing "the courier left it at the wrong flat"
22
+ * should be able to add it without a release.
23
+ */
24
+ export const DISPUTE_REASON_COLLECTION = "disputeReasons";
25
+ export const DISPUTE_REASON_FIELDS = {
26
+ ID: "id",
27
+ LABEL: "label",
28
+ KIND: "kind",
29
+ IS_ACTIVE: "isActive",
30
+ CREATED_BY_UID: "createdByUid",
31
+ CREATED_AT: "createdAt",
32
+ };
33
+ /**
34
+ * Bounds on a label.
35
+ *
36
+ * Short on purpose: this is a picker option, not the buyer's explanation. The
37
+ * detail goes in the free-text field on the dispute itself, which is where a
38
+ * buyer should describe what actually happened.
39
+ */
40
+ export const DISPUTE_REASON_LABEL_MAX = 80;
@@ -1,5 +1,6 @@
1
1
  import React from "react";
2
2
  import type { FAQ, FAQCategory } from "../types";
3
+ import { type PublishedValues } from "../../../_internal/shared/content/published-values";
3
4
  export interface FAQAccordionProps {
4
5
  faqs: FAQ[];
5
6
  className?: string;
@@ -8,8 +9,19 @@ export interface FAQAccordionProps {
8
9
  labels?: {
9
10
  noResults?: string;
10
11
  };
12
+ /**
13
+ * Phase 9 — resolved published values, for `{{token}}` substitution in
14
+ * answers. Build them with `resolvePublishedValues(settings?.commissions)`
15
+ * at the page level and pass them down.
16
+ *
17
+ * 🛑 Passed in rather than read here. This component renders on the server
18
+ * and in the client, and a settings read inside a presentational component
19
+ * would both make it unusable in half the places it is needed and fire one
20
+ * read per accordion on a page that has several.
21
+ */
22
+ publishedValues?: PublishedValues;
11
23
  }
12
- export declare function FAQAccordion({ faqs, className, renderExpandedFooter, labels, }: FAQAccordionProps): React.JSX.Element;
24
+ export declare function FAQAccordion({ faqs, className, renderExpandedFooter, labels, publishedValues, }: FAQAccordionProps): React.JSX.Element;
13
25
  interface FAQCategoryTabsProps {
14
26
  categories: FAQCategory[];
15
27
  active?: FAQCategory | null;
@@ -2,16 +2,41 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState } from "react";
4
4
  import { Button, Div, RichText, Row, Span, Tabs, TabsList, TabsTrigger, Text } from "../../../ui/index.js";
5
+ /* Phase 9 — `publishedCopyKey`'s first reader. See the 🛑 at the render site. */
6
+ import { resolvePublishedCopy, resolvePublishedValues, } from "../../../_internal/shared/content/published-values.js";
5
7
  import { RecordOwnerQuickActions } from "../../admin/components/RecordOwnerQuickActions.js";
6
- function FAQAccordionItem({ faq, isOpen, onToggle, renderExpandedFooter, }) {
7
- return (_jsxs(Div, { className: "border-b border-neutral-200 last:border-0", children: [_jsxs(Button, { variant: "ghost", onClick: onToggle, "aria-expanded": isOpen, justify: "between", className: "w-full gap-[1rem] py-[1rem] text-left text-[0.875rem] font-[500] text-[var(--appkit-color-text)] dark:text-[var(--appkit-color-text)] transition hover:text-primary", children: [_jsx(Span, { children: faq.question }), _jsx(Span, { className: `flex-shrink-0 transition-transform ${isOpen ? "rotate-180" : ""}`, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M4 6l4 4 4-4", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }) })] }), isOpen && (_jsxs(Div, { textSize: "sm", className: "text-[var(--appkit-color-text-muted)]", padding: "b-md", children: [faq.tags && faq.tags.length > 0 && (_jsx(Row, { wrap: true, gap: "sm", className: "mb-3", children: faq.tags.map((tag) => (_jsx(Span, { className: "bg-[var(--appkit-color-surface)] py-[0.25rem] text-[var(--appkit-color-text-muted)]", rounded: "md", padding: "x-xs", size: "xs", children: tag }, tag))) })), faq.answer.format === "html" ? (_jsx(RichText, { html: faq.answer.text, className: "prose-sm" })) : (_jsx(Text, { className: "whitespace-pre-line", children: faq.answer.text })), _jsx(Row, { justify: "end", className: "mt-3", children: _jsx(RecordOwnerQuickActions, { resource: "FAQ", recordId: faq.id, placement: "detail", recordLabel: faq.question }) }), renderExpandedFooter?.(faq)] }))] }));
8
+ /**
9
+ * Code-derived values, resolved once at module scope.
10
+ *
11
+ * ## 🛑 Why a DEFAULT rather than a required prop
12
+ *
13
+ * `resolvePublishedValues()` with no settings argument is PURE over code
14
+ * constants — the obligation policies, `LANE_CHECKOUT_WINDOW_MS`,
15
+ * `POINTS_EARN_PERCENT`. It reads no Firestore and touches no settings, so
16
+ * calling it here costs one object at import and cannot leak anything.
17
+ *
18
+ * That matters because there are FOUR mount sites (the FAQ page, the
19
+ * searchable list, the category highlights section, and this file's own
20
+ * loop). Requiring the prop would mean threading it through four callers and
21
+ * their callers — and the one that got missed would render `{{…}}` to a
22
+ * visitor. A default that always resolves the code-derived numbers, with an
23
+ * override for the settings-driven money ones, gets every surface right
24
+ * immediately.
25
+ *
26
+ * A caller WITH settings in hand should still pass them: without them the
27
+ * money values fall back to the schema defaults, which is correct but is not
28
+ * the admin's current configuration.
29
+ */
30
+ const CODE_DERIVED_VALUES = resolvePublishedValues();
31
+ function FAQAccordionItem({ faq, isOpen, onToggle, renderExpandedFooter, publishedValues = CODE_DERIVED_VALUES, }) {
32
+ return (_jsxs(Div, { className: "border-b border-neutral-200 last:border-0", children: [_jsxs(Button, { variant: "ghost", onClick: onToggle, "aria-expanded": isOpen, justify: "between", className: "w-full gap-[1rem] py-[1rem] text-left text-[0.875rem] font-[500] text-[var(--appkit-color-text)] dark:text-[var(--appkit-color-text)] transition hover:text-primary", children: [_jsx(Span, { children: faq.question }), _jsx(Span, { className: `flex-shrink-0 transition-transform ${isOpen ? "rotate-180" : ""}`, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M4 6l4 4 4-4", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }) })] }), isOpen && (_jsxs(Div, { textSize: "sm", className: "text-[var(--appkit-color-text-muted)]", padding: "b-md", children: [faq.tags && faq.tags.length > 0 && (_jsx(Row, { wrap: true, gap: "sm", className: "mb-3", children: faq.tags.map((tag) => (_jsx(Span, { className: "bg-[var(--appkit-color-surface)] py-[0.25rem] text-[var(--appkit-color-text-muted)]", rounded: "md", padding: "x-xs", size: "xs", children: tag }, tag))) })), faq.answer.format === "html" ? (_jsx(RichText, { html: resolvePublishedCopy(faq.answer.text, publishedValues), className: "prose-sm" })) : (_jsx(Text, { className: "whitespace-pre-line", children: resolvePublishedCopy(faq.answer.text, publishedValues) })), _jsx(Row, { justify: "end", className: "mt-3", children: _jsx(RecordOwnerQuickActions, { resource: "FAQ", recordId: faq.id, placement: "detail", recordLabel: faq.question }) }), renderExpandedFooter?.(faq)] }))] }));
8
33
  }
9
- export function FAQAccordion({ faqs, className = "", renderExpandedFooter, labels, }) {
34
+ export function FAQAccordion({ faqs, className = "", renderExpandedFooter, labels, publishedValues, }) {
10
35
  const [openId, setOpenId] = useState(null);
11
36
  if (faqs.length === 0 && labels?.noResults) {
12
37
  return (_jsx(Div, { surface: "card", padding: "xl", className: "text-center", children: _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", children: labels.noResults }) }));
13
38
  }
14
- return (_jsx(Div, { surface: "card", className: `divide-y divide-neutral-100 divide-[var(--appkit-color-border)] ${className}`, padding: "x-md", children: faqs.map((faq) => (_jsx(FAQAccordionItem, { faq: faq, isOpen: openId === faq.id, onToggle: () => setOpenId(openId === faq.id ? null : faq.id), renderExpandedFooter: renderExpandedFooter }, faq.id))) }));
39
+ return (_jsx(Div, { surface: "card", className: `divide-y divide-neutral-100 divide-[var(--appkit-color-border)] ${className}`, padding: "x-md", children: faqs.map((faq) => (_jsx(FAQAccordionItem, { faq: faq, isOpen: openId === faq.id, onToggle: () => setOpenId(openId === faq.id ? null : faq.id), renderExpandedFooter: renderExpandedFooter, publishedValues: publishedValues }, faq.id))) }));
15
40
  }
16
41
  const ALL_TAB_VALUE = "__all__";
17
42
  export function FAQCategoryTabs({ categories, active, onSelect, labels = {}, }) {