@mohasinac/appkit 4.63.4 → 4.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/dist/.build-stamp.json +3 -3
  2. package/dist/_internal/server/features/categories/adapters.d.ts +1 -1
  3. package/dist/_internal/server/features/categories/adapters.js +13 -0
  4. package/dist/_internal/server/features/checkout/actions.d.ts +2 -0
  5. package/dist/_internal/server/features/checkout/actions.js +22 -5
  6. package/dist/_internal/server/features/events/like-actions.d.ts +25 -0
  7. package/dist/_internal/server/features/events/like-actions.js +67 -0
  8. package/dist/_internal/server/features/events/likes.d.ts +65 -0
  9. package/dist/_internal/server/features/events/likes.js +146 -0
  10. package/dist/_internal/server/features/orders/adapters.js +3 -1
  11. package/dist/_internal/server/features/seo/index.d.ts +3 -0
  12. package/dist/_internal/server/features/seo/index.js +2 -0
  13. package/dist/_internal/server/features/seo/product-detail-types.d.ts +1 -0
  14. package/dist/_internal/server/features/seo/product-detail-types.js +26 -0
  15. package/dist/_internal/server/features/seo/robots.js +20 -1
  16. package/dist/_internal/server/features/seo/sitemap.js +125 -27
  17. package/dist/_internal/server/features/seo/static-params.d.ts +107 -0
  18. package/dist/_internal/server/features/seo/static-params.js +228 -0
  19. package/dist/_internal/server/features/seo/taxonomy-links.d.ts +44 -0
  20. package/dist/_internal/server/features/seo/taxonomy-links.js +85 -0
  21. package/dist/_internal/server/features/site-settings/adapters.js +20 -2
  22. package/dist/_internal/server/features/stores/adapters.d.ts +2 -2
  23. package/dist/_internal/server/features/stores/adapters.js +88 -1
  24. package/dist/_internal/server/functions/scheduled.d.ts +16 -1
  25. package/dist/_internal/server/functions/scheduled.js +23 -1
  26. package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +5 -2
  27. package/dist/_internal/server/jobs/core/jobRunners.js +5 -0
  28. package/dist/_internal/server/jobs/core/onOrderCreate.js +104 -10
  29. package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +2 -0
  30. package/dist/_internal/server/jobs/core/onOrderStatusChange.js +1 -0
  31. package/dist/_internal/server/jobs/core/onScamReportCreate.js +1 -0
  32. package/dist/_internal/server/jobs/core/onScamReportRejected.js +1 -0
  33. package/dist/_internal/server/jobs/core/onScamReportVerified.js +1 -0
  34. package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +1 -0
  35. package/dist/_internal/server/jobs/core/pendingOrderTimeout.js +1 -0
  36. package/dist/_internal/server/jobs/core/sellerStandingRollup.d.ts +63 -0
  37. package/dist/_internal/server/jobs/core/sellerStandingRollup.js +412 -0
  38. package/dist/_internal/server/jobs/core/whatsappNotify.d.ts +35 -0
  39. package/dist/_internal/server/jobs/core/whatsappNotify.js +140 -0
  40. package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
  41. package/dist/_internal/server/jobs/handlers/index.js +1 -0
  42. package/dist/_internal/server/jobs/handlers/sellerStandingRollup.d.ts +12 -0
  43. package/dist/_internal/server/jobs/handlers/sellerStandingRollup.js +14 -0
  44. package/dist/_internal/server/notifications/channel-health.d.ts +8 -6
  45. package/dist/_internal/server/notifications/channel-health.js +2 -2
  46. package/dist/_internal/server/notifications/send-guard.d.ts +1 -1
  47. package/dist/_internal/server/notifications/send-guard.js +4 -2
  48. package/dist/_internal/shared/actions/action-registry.js +89 -0
  49. package/dist/_internal/shared/checkout/order-math.d.ts +1 -1
  50. package/dist/_internal/shared/checkout/order-math.js +1 -1
  51. package/dist/_internal/shared/features/checkout/config.d.ts +2 -0
  52. package/dist/_internal/shared/features/checkout/config.js +2 -0
  53. package/dist/_internal/shared/features/messaging/config.d.ts +5 -7
  54. package/dist/_internal/shared/features/messaging/config.js +4 -1
  55. package/dist/_internal/shared/features/seller-standing/config.d.ts +130 -0
  56. package/dist/_internal/shared/features/seller-standing/config.js +153 -0
  57. package/dist/_internal/shared/features/site-settings/fees.d.ts +2 -0
  58. package/dist/_internal/shared/features/site-settings/fees.js +2 -0
  59. package/dist/_internal/shared/features/site-settings/types.d.ts +27 -2
  60. package/dist/_internal/shared/fees/calculator.d.ts +8 -0
  61. package/dist/_internal/shared/fees/calculator.js +7 -0
  62. package/dist/cli/index.js +2 -0
  63. package/dist/client.d.ts +2 -2
  64. package/dist/client.js +1 -1
  65. package/dist/constants/api-endpoints.d.ts +39 -0
  66. package/dist/constants/api-endpoints.js +16 -0
  67. package/dist/constants/notification-types.d.ts +1 -1
  68. package/dist/constants/notification-types.js +1 -1
  69. package/dist/core/integration-keys.d.ts +5 -0
  70. package/dist/core/integration-keys.js +10 -0
  71. package/dist/features/about/components/FeesView.js +6 -0
  72. package/dist/features/about/components/HelpPageView.js +9 -1
  73. package/dist/features/account/components/BecomeSellerView.d.ts +15 -3
  74. package/dist/features/account/components/BecomeSellerView.js +2 -2
  75. package/dist/features/account/components/BuyerShoppingGuideView.js +1 -1
  76. package/dist/features/account/components/NotificationPreferencesPanel.js +4 -3
  77. package/dist/features/account/types/index.d.ts +2 -0
  78. package/dist/features/admin/actions/notification-actions.d.ts +17 -2
  79. package/dist/features/admin/actions/notification-actions.js +154 -9
  80. package/dist/features/admin/components/AdminOrderEditorView.d.ts +1 -1
  81. package/dist/features/admin/components/AdminOrdersView.js +2 -0
  82. package/dist/features/admin/components/AdminSiteSettingsView.js +66 -6
  83. package/dist/features/admin/components/AdminStoreEditorView.d.ts +12 -1
  84. package/dist/features/admin/components/AdminStoreEditorView.js +31 -2
  85. package/dist/features/admin/components/AdminStoresGuideView.js +1 -1
  86. package/dist/features/admin/components/AdminStoresView.js +59 -2
  87. package/dist/features/admin/components/AdminTrustGuideView.js +10 -0
  88. package/dist/features/admin/constants/site-settings-tabs.d.ts +12 -7
  89. package/dist/features/admin/constants/site-settings-tabs.js +12 -6
  90. package/dist/features/admin/repository/site-settings.repository.js +1 -1
  91. package/dist/features/admin/schemas/admin-editor-forms.d.ts +10 -0
  92. package/dist/features/admin/schemas/admin-editor-forms.js +34 -0
  93. package/dist/features/admin/schemas/admin-user-form.d.ts +61 -0
  94. package/dist/features/admin/schemas/admin-user-form.js +44 -3
  95. package/dist/features/admin/schemas/firestore.d.ts +73 -0
  96. package/dist/features/admin/schemas/firestore.js +8 -3
  97. package/dist/features/auth/checkout-value-otp.d.ts +1 -1
  98. package/dist/features/auth/checkout-value-otp.js +1 -1
  99. package/dist/features/auth/permissions/constants.d.ts +2 -2
  100. package/dist/features/auth/permissions/constants.js +2 -1
  101. package/dist/features/blog/components/BlogIndexPageView.js +2 -2
  102. package/dist/features/blog/components/BlogListView.js +2 -1
  103. package/dist/features/blog/components/BlogPostView.js +2 -1
  104. package/dist/features/blog/constants/category-badge.d.ts +33 -0
  105. package/dist/features/blog/constants/category-badge.js +40 -0
  106. package/dist/features/cart/components/CartPriceBreakdown.d.ts +2 -0
  107. package/dist/features/cart/components/CartPriceBreakdown.js +1 -1
  108. package/dist/features/cart/components/StoreAddonsPicker.d.ts +5 -2
  109. package/dist/features/cart/components/StoreAddonsPicker.js +5 -4
  110. package/dist/features/cart/repository/cart.repository.js +2 -1
  111. package/dist/features/cart/schemas/firestore.d.ts +2 -1
  112. package/dist/features/categories/components/TaxonomyLinkIndex.d.ts +39 -0
  113. package/dist/features/categories/components/TaxonomyLinkIndex.js +11 -0
  114. package/dist/features/categories/schemas/firestore.d.ts +25 -0
  115. package/dist/features/categories/utils/load-category-options.d.ts +33 -0
  116. package/dist/features/categories/utils/load-category-options.js +50 -0
  117. package/dist/features/checkout/actions/checkout-value-otp-actions.d.ts +5 -10
  118. package/dist/features/checkout/actions/checkout-value-otp-actions.js +88 -2
  119. package/dist/features/events/components/EventCard.js +4 -12
  120. package/dist/features/events/components/EventInsights.d.ts +27 -0
  121. package/dist/features/events/components/EventInsights.js +10 -0
  122. package/dist/features/events/components/EventLikeButton.d.ts +21 -0
  123. package/dist/features/events/components/EventLikeButton.js +59 -0
  124. package/dist/features/events/components/EventLikersRail.d.ts +23 -0
  125. package/dist/features/events/components/EventLikersRail.js +54 -0
  126. package/dist/features/events/components/EventTypeBadge.d.ts +26 -0
  127. package/dist/features/events/components/EventTypeBadge.js +7 -0
  128. package/dist/features/events/components/EventsListPageView.js +2 -2
  129. package/dist/features/events/components/SpinWheel.d.ts +41 -0
  130. package/dist/features/events/components/SpinWheel.js +128 -0
  131. package/dist/features/events/components/SpinWheelView.js +36 -15
  132. package/dist/features/events/constants/event-type-meta.d.ts +54 -0
  133. package/dist/features/events/constants/event-type-meta.js +109 -0
  134. package/dist/features/events/repository/event-likes.repository.d.ts +57 -0
  135. package/dist/features/events/repository/event-likes.repository.js +151 -0
  136. package/dist/features/events/schemas/firestore.d.ts +111 -0
  137. package/dist/features/events/schemas/likes.d.ts +81 -0
  138. package/dist/features/events/schemas/likes.js +59 -0
  139. package/dist/features/events/types/index.d.ts +7 -0
  140. package/dist/features/events/utils/carousel-slide.d.ts +42 -0
  141. package/dist/features/events/utils/carousel-slide.js +174 -0
  142. package/dist/features/events/utils/event-charts.d.ts +65 -0
  143. package/dist/features/events/utils/event-charts.js +172 -0
  144. package/dist/features/faq/constants/help-guides.js +9 -0
  145. package/dist/features/faq/schemas/firestore.js +1 -0
  146. package/dist/features/faq/types/index.d.ts +19 -0
  147. package/dist/features/homepage/components/BrandsSection.js +25 -1
  148. package/dist/features/homepage/components/HeroCarousel.js +47 -10
  149. package/dist/features/homepage/lib/section-renderer.js +1 -1
  150. package/dist/features/homepage/types/index.d.ts +7 -0
  151. package/dist/features/orders/actions/refund-actions.js +1 -0
  152. package/dist/features/orders/components/OrderAddonBadges.d.ts +3 -1
  153. package/dist/features/orders/components/OrderAddonBadges.js +4 -3
  154. package/dist/features/orders/repository/orders.repository.d.ts +8 -0
  155. package/dist/features/orders/repository/orders.repository.js +2 -0
  156. package/dist/features/orders/schemas/firestore.d.ts +4 -0
  157. package/dist/features/orders/types/index.d.ts +5 -2
  158. package/dist/features/search/action-index/settings-entries.js +9 -0
  159. package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +6 -1
  160. package/dist/features/store-extensions/repository/store-extensions.repositories.js +10 -1
  161. package/dist/features/store-extensions/schemas/firestore.d.ts +17 -1
  162. package/dist/features/store-extensions/schemas/firestore.js +10 -0
  163. package/dist/features/stores/components/InteractiveStoreCard.js +2 -1
  164. package/dist/features/stores/components/SellerStandingPanel.d.ts +31 -0
  165. package/dist/features/stores/components/SellerStandingPanel.js +25 -0
  166. package/dist/features/stores/components/SellerTierBadge.d.ts +57 -0
  167. package/dist/features/stores/components/SellerTierBadge.js +19 -0
  168. package/dist/features/stores/components/StoreCapabilitiesGuideView.js +33 -2
  169. package/dist/features/stores/components/StoreCreateForm.d.ts +46 -0
  170. package/dist/features/stores/components/StoreCreateForm.js +107 -0
  171. package/dist/features/stores/components/StoreGuideHubView.js +18 -5
  172. package/dist/features/stores/components/StoreHeader.js +2 -1
  173. package/dist/features/stores/components/StoreSettingsGuideView.js +6 -2
  174. package/dist/features/stores/repository/store.repository.d.ts +23 -1
  175. package/dist/features/stores/repository/store.repository.js +52 -1
  176. package/dist/features/stores/schemas/firestore.d.ts +76 -7
  177. package/dist/features/stores/schemas/firestore.js +21 -6
  178. package/dist/features/stores/schemas/index.d.ts +88 -2
  179. package/dist/features/stores/schemas/index.js +14 -0
  180. package/dist/features/stores/schemas/store-create.d.ts +47 -0
  181. package/dist/features/stores/schemas/store-create.js +86 -0
  182. package/dist/features/stores/types/index.d.ts +42 -0
  183. package/dist/features/whatsapp-bot/api/send-status/route.d.ts +12 -0
  184. package/dist/features/whatsapp-bot/api/send-status/route.js +38 -0
  185. package/dist/features/whatsapp-bot/api/webhook/route.d.ts +12 -0
  186. package/dist/features/whatsapp-bot/api/webhook/route.js +43 -0
  187. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.d.ts +57 -0
  188. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +76 -0
  189. package/dist/features/whatsapp-bot/components/WhatsAppChatButton.d.ts +9 -0
  190. package/dist/features/whatsapp-bot/components/WhatsAppChatButton.js +7 -0
  191. package/dist/features/whatsapp-bot/components/index.d.ts +3 -0
  192. package/dist/features/whatsapp-bot/components/index.js +2 -0
  193. package/dist/features/whatsapp-bot/helpers/whatsapp.d.ts +52 -0
  194. package/dist/features/whatsapp-bot/helpers/whatsapp.js +288 -0
  195. package/dist/features/whatsapp-bot/index.d.ts +3 -0
  196. package/dist/features/whatsapp-bot/index.js +3 -0
  197. package/dist/features/whatsapp-bot/manifest.d.ts +2 -0
  198. package/dist/features/whatsapp-bot/manifest.js +13 -0
  199. package/dist/features/whatsapp-bot/messages/en.json +7 -0
  200. package/dist/features/whatsapp-bot/server.d.ts +7 -0
  201. package/dist/features/whatsapp-bot/server.js +7 -0
  202. package/dist/features/whatsapp-bot/types/index.d.ts +126 -0
  203. package/dist/features/whatsapp-bot/types/index.js +1 -0
  204. package/dist/index.d.ts +23 -6
  205. package/dist/index.js +38 -12
  206. package/dist/next/routing/route-map.d.ts +6 -1
  207. package/dist/next/routing/route-map.js +14 -0
  208. package/dist/providers/db-firebase/base.repository.d.ts +1 -1
  209. package/dist/providers/db-firebase/base.repository.js +1 -1
  210. package/dist/repositories/index.d.ts +1 -1
  211. package/dist/repositories/index.js +1 -1
  212. package/dist/schemas/registry.d.ts +115 -0
  213. package/dist/schemas/registry.js +2 -1
  214. package/dist/security/pii-encrypt.js +1 -1
  215. package/dist/server.d.ts +15 -2
  216. package/dist/server.js +32 -2
  217. package/dist/styles.css +36 -1
  218. package/dist/tailwind-utilities.css +1 -1
  219. package/dist/ui/components/HorizontalScroller.js +15 -2
  220. package/dist/ui/components/NavigationLoader.d.ts +14 -1
  221. package/dist/ui/components/NavigationLoader.js +79 -30
  222. package/dist/ui/components/PageHeroHeading.d.ts +40 -0
  223. package/dist/ui/components/PageHeroHeading.js +15 -0
  224. package/dist/ui/components/Typography.d.ts +3 -3
  225. package/dist/ui/components/Typography.js +59 -31
  226. package/dist/ui/components/Typography.style.css +35 -0
  227. package/dist/ui/index.d.ts +2 -0
  228. package/dist/ui/index.js +1 -0
  229. package/package.json +1 -1
  230. package/scripts/seed-cli.mjs +4 -2
  231. package/dist/features/orders/utils/order-announcement-message.d.ts +0 -18
  232. package/dist/features/orders/utils/order-announcement-message.js +0 -16
@@ -16,6 +16,32 @@ export class StoreRepository extends BaseRepository {
16
16
  constructor() {
17
17
  super(STORE_COLLECTION);
18
18
  }
19
+ mapDoc(snap) {
20
+ return super.mapDoc(snap);
21
+ }
22
+ /**
23
+ * LIST reads never carry the WhatsApp token — not even encrypted.
24
+ *
25
+ * `mapDoc` above decrypts it, and `listStores(model, activeOnly)` backs the
26
+ * PUBLIC `/stores` page. Once `sieveQuery` started routing list rows through
27
+ * `mapDoc` (it previously mapped documents itself and skipped every
28
+ * override), that decrypt would have put a plaintext Meta OAuth bearer token
29
+ * on a public listing — the exact shape of Root Cause #70.
30
+ *
31
+ * So the list path deliberately does NOT call `decryptSecrets`, and drops
32
+ * `accessToken` outright: a list has no legitimate use for it, and a value
33
+ * that is never in the payload cannot be leaked by a projection someone
34
+ * forgets to write.
35
+ *
36
+ * Detail reads are unchanged — `findBySlug`/`findById` still decrypt, so the
37
+ * WhatsApp settings and catalog-sync routes keep working exactly as before.
38
+ * Removing the decrypt from `mapDoc` itself is a separate, riskier change
39
+ * that CLAUDE.md explicitly defers; this closes the public-exposure half
40
+ * without touching the encryption round-trip at all.
41
+ */
42
+ async update(id, data) {
43
+ return super.update(id, data);
44
+ }
19
45
  /**
20
46
  * Create a new store.
21
47
  * The document ID is set to storeSlug for easy URL-based lookups.
@@ -123,6 +149,29 @@ export class StoreRepository extends BaseRepository {
123
149
  // Auto-set isPublic when approved
124
150
  ...(status === StoreStatusValues.ACTIVE && { isPublic: true }),
125
151
  ...(status !== StoreStatusValues.ACTIVE && { isPublic: false }),
152
+ /*
153
+ * 🛑 LEAVING `active` REVOKES TRUST IMMEDIATELY — and this cannot
154
+ * wait for the monthly recompute.
155
+ *
156
+ * `trustedSeller` means the store is paid at SHIPPING rather than at
157
+ * delivery, so a suspended store that kept the flag would go on
158
+ * being paid before parcels land for up to 31 days, on a decision
159
+ * the suspension has just reversed. `sellerStandingRollup` caches
160
+ * its verdict monthly; this is the one place that cache must not be
161
+ * trusted.
162
+ *
163
+ * Both fields are tracked, so the clear earns its own timeline
164
+ * entry alongside the status change. Re-granting is NOT automatic on
165
+ * re-activation: the next month-end run decides, which is the
166
+ * correct direction for a flag that moves money.
167
+ *
168
+ * `extra` is spread AFTER, so an admin explicitly setting a tier in
169
+ * the same request still wins — the override stays authoritative.
170
+ */
171
+ ...(status !== StoreStatusValues.ACTIVE && {
172
+ trustedSeller: false,
173
+ sellerTier: "seller",
174
+ }),
126
175
  ...extra,
127
176
  updatedAt: new Date(),
128
177
  }, ctx, "setStatus"));
@@ -206,7 +255,9 @@ export class StoreRepository extends BaseRepository {
206
255
  options: { maxPageSize: win.maxPageSize(100) },
207
256
  fields: sieveFields,
208
257
  // Called DIRECTLY, not through `sieveQuery`, so the mapper has to be
209
- // passed by hand. With `activeOnly` this is the public /stores listing.
258
+ // passed by hand — and it is the list mapper, never `mapDoc`. With
259
+ // `activeOnly` this is the public /stores listing; `mapDoc` decrypts the
260
+ // WhatsApp bearer token and a list has no use for it.
210
261
  mapDoc: (snap) => this.mapDocForList(snap),
211
262
  });
212
263
  const searched = win.finish(result);
@@ -5,6 +5,7 @@
5
5
  * and helpers for the stores feature.
6
6
  */
7
7
  import type { StatusChangeEntry } from "../../../_internal/shared/history/types";
8
+ import type { SellerTier, SellerStandingCriterion } from "../../../_internal/shared/features/seller-standing/config";
8
9
  import type { StoreCapability } from "../../auth/permissions/constants";
9
10
  import type { BaseDocument } from "../../../_internal/shared/types/base-document";
10
11
  export type ShippingProviderType = "self-courier" | "store-pickup" | "custom";
@@ -107,6 +108,19 @@ export interface StoreDocument extends BaseDocument {
107
108
  adminNotes?: string;
108
109
  /** Admin "verified store" badge — distinct from `status`/`isPublic`. */
109
110
  isVerified?: boolean;
111
+ /**
112
+ * Does this store subscribe to WhatsApp order notifications?
113
+ *
114
+ * A monthly add-on priced by the admin (`commissions.whatsappNotifyMonthlyFee`),
115
+ * not a per-order charge: the platform owns one WhatsApp number, so the cost
116
+ * is standing and borne by the seller who benefits, rather than a line item
117
+ * a buyer opts into per order.
118
+ *
119
+ * 🛑 Absent means NOT subscribed, and the gate fails closed on it. Defaulting
120
+ * a missing flag to `true` would message customers on behalf of every seller
121
+ * who never signed up, and bill the platform for each template.
122
+ */
123
+ whatsappNotifySubscribed?: boolean;
110
124
  /**
111
125
  * Paid at SHIPPING rather than at delivery.
112
126
  *
@@ -128,6 +142,61 @@ export interface StoreDocument extends BaseDocument {
128
142
  * payout.
129
143
  */
130
144
  trustedSeller?: boolean;
145
+ /**
146
+ * The EARNED, PUBLIC tier.
147
+ *
148
+ * 🛑 Deliberately a second field rather than reusing `trustedSeller`, which
149
+ * means "paid at shipping" and is private by design (see directly above —
150
+ * "A store does not get to display that we trust it"). The monthly rollup
151
+ * writes BOTH together on a transition: `sellerTier` is what a buyer sees,
152
+ * `trustedSeller` is what the payout sweep reads. Collapsing them into one
153
+ * field would either leak the risk posture into the public projection or
154
+ * make the badge unrenderable.
155
+ *
156
+ * Absent means `"seller"`. There is no `"pending"` — a store that has not
157
+ * earned the tier simply has the base one.
158
+ */
159
+ sellerTier?: SellerTier;
160
+ /**
161
+ * The cached standing computation behind `sellerTier`.
162
+ *
163
+ * 🛑 This IS the cache. It is recomputed by `sellerStandingRollup` on the
164
+ * last day of each month and read straight off the store document by the
165
+ * seller's progress panel, the badge and the admin view — none of which
166
+ * recompute anything. `computedAt` / `nextComputeAt` are rendered to the
167
+ * seller, because without them a seller who shipped twenty orders this week
168
+ * will read an unchanged panel as broken rather than as monthly.
169
+ *
170
+ * Metrics only — no buyer uids, no addresses, nothing that could make this
171
+ * a PII store. The address criterion is counted from HMAC fingerprints and
172
+ * only the COUNT is kept.
173
+ */
174
+ standing?: {
175
+ computedAt: Date | string;
176
+ nextComputeAt?: Date | string;
177
+ windowDays: number;
178
+ deliveredOrders: number;
179
+ distinctBuyers: number;
180
+ distinctAddresses: number;
181
+ ratedOrders: number;
182
+ averageRating: number;
183
+ met: Record<SellerStandingCriterion, boolean>;
184
+ };
185
+ /**
186
+ * An admin set `sellerTier` by hand; the monthly rollup must not move it.
187
+ *
188
+ * 🛑 TOP-LEVEL, not a key inside `standing`, and the distinction is the
189
+ * point: `standing` is a MEASUREMENT — every field in it is recomputed from
190
+ * scratch each month — while this is a DECISION that must outlive the
191
+ * measurement. Nesting it meant a store could not be pinned without also
192
+ * fabricating a metrics block, which is why the platform store's seed could
193
+ * not express "trusted, don't touch" without inventing figures nobody had
194
+ * measured.
195
+ *
196
+ * PRIVATE. It tells a buyer nothing about the goods or the seller; it is a
197
+ * note about how we administer this record.
198
+ */
199
+ tierLockedByAdmin?: boolean;
131
200
  /** Reason recorded when `status` is set to "suspended". */
132
201
  suspensionReason?: string;
133
202
  stats?: {
@@ -368,14 +437,11 @@ export interface StoreDocument extends BaseDocument {
368
437
  * the store owner or the public, and `statusHistory` rides on the store
369
438
  * document the owner can read.
370
439
  */
371
- export declare const STORE_TRACKED_FIELDS: readonly ["status", "isPublic", "isVerified", "isFeatured", "suspensionReason", "capabilities"];
440
+ export declare const STORE_TRACKED_FIELDS: readonly ["status", "isPublic", "isVerified", "isFeatured", "suspensionReason", "capabilities", "sellerTier", "trustedSeller"];
372
441
  /**
373
- * PII on this document, for the `withHistory` scrub.
374
- *
375
- * `accessToken` stays listed although the WhatsApp integration that produced
376
- * one was removed 2026-10-20: a scrub list is cheap, and a future secret
377
- * reaching this document under that name should be scrubbed by default
378
- * rather than by someone remembering to add it.
442
+ * PII on this document, for `withHistory`'s scrub. `whatsappConfig.accessToken`
443
+ * is a live Meta credential; `mapDoc` decrypts it on EVERY read, so it is
444
+ * present in memory whenever a diff runs.
379
445
  */
380
446
  export declare const STORE_HISTORY_PII_FIELDS: readonly ["accessToken", "payoutDetails", "upiVpa", "accountNumber"];
381
447
  export type { StoreCapability } from "../../auth/permissions/constants";
@@ -408,6 +474,9 @@ export declare const STORE_FIELDS: {
408
474
  readonly IS_FEATURED: "isFeatured";
409
475
  readonly IS_VERIFIED: "isVerified";
410
476
  readonly TRUSTED_SELLER: "trustedSeller";
477
+ readonly SELLER_TIER: "sellerTier";
478
+ readonly STANDING: "standing";
479
+ readonly TIER_LOCKED_BY_ADMIN: "tierLockedByAdmin";
411
480
  readonly SUSPENSION_REASON: "suspensionReason";
412
481
  readonly CAPABILITIES: "capabilities";
413
482
  readonly STATUS_VALUES: {
@@ -31,14 +31,26 @@ export const STORE_TRACKED_FIELDS = [
31
31
  "isFeatured",
32
32
  "suspensionReason",
33
33
  "capabilities",
34
+ /*
35
+ * Both halves of the trust decision are tracked, and both matter for
36
+ * different reasons. `sellerTier` is what the seller and every buyer saw, so
37
+ * "when did I lose the badge" has an answer. `trustedSeller` moves MONEY —
38
+ * it is the flag that pays this store before delivery — so the timeline is
39
+ * the record of when the platform started and stopped carrying that risk,
40
+ * alongside the `STORE_TRUST_CHANGE` audit row that names who decided it.
41
+ *
42
+ * 🛑 `standing` is deliberately NOT tracked. It is recomputed in full every
43
+ * month, so every field inside it changes on every run; tracking it would
44
+ * write a diff of five numbers monthly and exhaust the 50-entry FIFO cap
45
+ * within four years while burying the two transitions anyone reads.
46
+ */
47
+ "sellerTier",
48
+ "trustedSeller",
34
49
  ];
35
50
  /**
36
- * PII on this document, for the `withHistory` scrub.
37
- *
38
- * `accessToken` stays listed although the WhatsApp integration that produced
39
- * one was removed 2026-10-20: a scrub list is cheap, and a future secret
40
- * reaching this document under that name should be scrubbed by default
41
- * rather than by someone remembering to add it.
51
+ * PII on this document, for `withHistory`'s scrub. `whatsappConfig.accessToken`
52
+ * is a live Meta credential; `mapDoc` decrypts it on EVERY read, so it is
53
+ * present in memory whenever a diff runs.
42
54
  */
43
55
  export const STORE_HISTORY_PII_FIELDS = [
44
56
  "accessToken",
@@ -75,6 +87,9 @@ export const STORE_FIELDS = {
75
87
  IS_FEATURED: "isFeatured",
76
88
  IS_VERIFIED: "isVerified",
77
89
  TRUSTED_SELLER: "trustedSeller",
90
+ SELLER_TIER: "sellerTier",
91
+ STANDING: "standing",
92
+ TIER_LOCKED_BY_ADMIN: "tierLockedByAdmin",
78
93
  SUSPENSION_REASON: "suspensionReason",
79
94
  CAPABILITIES: "capabilities",
80
95
  STATUS_VALUES: {
@@ -347,6 +347,60 @@ export declare const storeFirestoreSchema: z.ZodObject<{
347
347
  defaultProviderId: string;
348
348
  }>>;
349
349
  emiEnabled: z.ZodOptional<z.ZodBoolean>;
350
+ whatsappConfig: z.ZodOptional<z.ZodObject<{
351
+ phoneNumber: z.ZodOptional<z.ZodString>;
352
+ wabaId: z.ZodOptional<z.ZodString>;
353
+ catalogId: z.ZodOptional<z.ZodString>;
354
+ accessToken: z.ZodOptional<z.ZodString>;
355
+ catalogSyncEnabled: z.ZodBoolean;
356
+ lastCatalogSyncAt: z.ZodOptional<z.ZodUnion<[z.ZodDate, z.ZodString, z.ZodObject<{
357
+ toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
358
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
359
+ toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
360
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
361
+ toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
362
+ }, z.ZodTypeAny, "passthrough">>]>>;
363
+ lastSyncCount: z.ZodOptional<z.ZodNumber>;
364
+ lastSyncStatus: z.ZodOptional<z.ZodEnum<["success", "partial", "failed"]>>;
365
+ connected: z.ZodBoolean;
366
+ connectedAt: z.ZodOptional<z.ZodUnion<[z.ZodDate, z.ZodString, z.ZodObject<{
367
+ toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
368
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
369
+ toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
370
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
371
+ toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
372
+ }, z.ZodTypeAny, "passthrough">>]>>;
373
+ }, "strip", z.ZodTypeAny, {
374
+ catalogSyncEnabled: boolean;
375
+ connected: boolean;
376
+ phoneNumber?: string | undefined;
377
+ accessToken?: string | undefined;
378
+ wabaId?: string | undefined;
379
+ catalogId?: string | undefined;
380
+ lastCatalogSyncAt?: string | Date | z.objectOutputType<{
381
+ toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
382
+ }, z.ZodTypeAny, "passthrough"> | undefined;
383
+ lastSyncCount?: number | undefined;
384
+ lastSyncStatus?: "success" | "failed" | "partial" | undefined;
385
+ connectedAt?: string | Date | z.objectOutputType<{
386
+ toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
387
+ }, z.ZodTypeAny, "passthrough"> | undefined;
388
+ }, {
389
+ catalogSyncEnabled: boolean;
390
+ connected: boolean;
391
+ phoneNumber?: string | undefined;
392
+ accessToken?: string | undefined;
393
+ wabaId?: string | undefined;
394
+ catalogId?: string | undefined;
395
+ lastCatalogSyncAt?: string | Date | z.objectInputType<{
396
+ toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
397
+ }, z.ZodTypeAny, "passthrough"> | undefined;
398
+ lastSyncCount?: number | undefined;
399
+ lastSyncStatus?: "success" | "failed" | "partial" | undefined;
400
+ connectedAt?: string | Date | z.objectInputType<{
401
+ toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
402
+ }, z.ZodTypeAny, "passthrough"> | undefined;
403
+ }>>;
350
404
  }, "strip", z.ZodTypeAny, {
351
405
  status: "pending" | "active" | "rejected" | "suspended";
352
406
  id: string;
@@ -416,6 +470,22 @@ export declare const storeFirestoreSchema: z.ZodObject<{
416
470
  maxReviews?: number | undefined;
417
471
  minRating?: number | undefined;
418
472
  } | undefined;
473
+ whatsappConfig?: {
474
+ catalogSyncEnabled: boolean;
475
+ connected: boolean;
476
+ phoneNumber?: string | undefined;
477
+ accessToken?: string | undefined;
478
+ wabaId?: string | undefined;
479
+ catalogId?: string | undefined;
480
+ lastCatalogSyncAt?: string | Date | z.objectOutputType<{
481
+ toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
482
+ }, z.ZodTypeAny, "passthrough"> | undefined;
483
+ lastSyncCount?: number | undefined;
484
+ lastSyncStatus?: "success" | "failed" | "partial" | undefined;
485
+ connectedAt?: string | Date | z.objectOutputType<{
486
+ toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
487
+ }, z.ZodTypeAny, "passthrough"> | undefined;
488
+ } | undefined;
419
489
  }, {
420
490
  status: "pending" | "active" | "rejected" | "suspended";
421
491
  id: string;
@@ -485,6 +555,22 @@ export declare const storeFirestoreSchema: z.ZodObject<{
485
555
  maxReviews?: number | undefined;
486
556
  minRating?: number | undefined;
487
557
  } | undefined;
558
+ whatsappConfig?: {
559
+ catalogSyncEnabled: boolean;
560
+ connected: boolean;
561
+ phoneNumber?: string | undefined;
562
+ accessToken?: string | undefined;
563
+ wabaId?: string | undefined;
564
+ catalogId?: string | undefined;
565
+ lastCatalogSyncAt?: string | Date | z.objectInputType<{
566
+ toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
567
+ }, z.ZodTypeAny, "passthrough"> | undefined;
568
+ lastSyncCount?: number | undefined;
569
+ lastSyncStatus?: "success" | "failed" | "partial" | undefined;
570
+ connectedAt?: string | Date | z.objectInputType<{
571
+ toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
572
+ }, z.ZodTypeAny, "passthrough"> | undefined;
573
+ } | undefined;
488
574
  }>;
489
575
  export type StoreFromSchema = z.infer<typeof storeFirestoreSchema>;
490
576
  /** Zod schema for store status — use instead of inline `z.enum(["pending",...])`. */
@@ -530,10 +616,10 @@ export declare const storeListItemSchema: z.ZodObject<{
530
616
  storeDescription?: string | undefined;
531
617
  storeLogoURL?: string | undefined;
532
618
  storeBannerURL?: string | undefined;
619
+ averageRating?: number | undefined;
533
620
  totalProducts?: number | undefined;
534
621
  itemsSold?: number | undefined;
535
622
  totalReviews?: number | undefined;
536
- averageRating?: number | undefined;
537
623
  }, {
538
624
  status: string;
539
625
  id: string;
@@ -546,10 +632,10 @@ export declare const storeListItemSchema: z.ZodObject<{
546
632
  storeDescription?: string | undefined;
547
633
  storeLogoURL?: string | undefined;
548
634
  storeBannerURL?: string | undefined;
635
+ averageRating?: number | undefined;
549
636
  totalProducts?: number | undefined;
550
637
  itemsSold?: number | undefined;
551
638
  totalReviews?: number | undefined;
552
- averageRating?: number | undefined;
553
639
  }>;
554
640
  export declare const storeListParamsSchema: z.ZodObject<{
555
641
  category: z.ZodOptional<z.ZodString>;
@@ -92,6 +92,20 @@ export const storeFirestoreSchema = z.object({
92
92
  .optional(),
93
93
  shippingConfig: storeShippingConfigSchema.optional(),
94
94
  emiEnabled: z.boolean().optional(),
95
+ whatsappConfig: z
96
+ .object({
97
+ phoneNumber: z.string().optional(),
98
+ wabaId: z.string().optional(),
99
+ catalogId: z.string().optional(),
100
+ accessToken: z.string().optional(),
101
+ catalogSyncEnabled: z.boolean(),
102
+ lastCatalogSyncAt: firestoreDateSchema.optional(),
103
+ lastSyncCount: z.number().int().optional(),
104
+ lastSyncStatus: z.enum(["success", "partial", "failed"]).optional(),
105
+ connected: z.boolean(),
106
+ connectedAt: firestoreDateSchema.optional(),
107
+ })
108
+ .optional(),
95
109
  ...auditTimestampsShape,
96
110
  });
97
111
  /** Zod schema for store status — use instead of inline `z.enum(["pending",...])`. */
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The store-creation input contract — ONE definition, shared by the form and
3
+ * the server action that writes it.
4
+ *
5
+ * 🛑 Why this file exists at all. `createStoreAction` carried its own inline
6
+ * `z.object({ storeName, storeDescription, storeCategory })` while nothing
7
+ * rendered a form, so there was exactly one copy and no drift. The moment a
8
+ * form exists there are two places that must agree about "2–80 characters",
9
+ * and a client that validates more loosely than the server produces a form
10
+ * that submits and then fails with a message pointing at no field; a client
11
+ * that validates more strictly refuses input the server would have accepted.
12
+ * Both are Root Cause #75's shape. The action imports this; it does not
13
+ * restate it.
14
+ *
15
+ * The bounds are not new — they are lifted verbatim from the action's own
16
+ * schema so this is a move, not a retune.
17
+ */
18
+ import { z } from "zod";
19
+ /**
20
+ * 🛑 `.or(z.literal(""))` on both optional fields, matching the action.
21
+ *
22
+ * An untouched text input submits `""`, not `undefined`. A bare
23
+ * `.max(80).optional()` rejects it, so a seller who filled in only the
24
+ * required name would be refused for leaving an optional field alone — which
25
+ * reads as a broken form, because the field they are being blocked on is one
26
+ * they were told was optional.
27
+ */
28
+ export declare const storeCreateSchema: z.ZodObject<{
29
+ storeName: z.ZodString;
30
+ storeDescription: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
31
+ storeCategory: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
32
+ }, "strip", z.ZodTypeAny, {
33
+ storeName: string;
34
+ storeCategory?: string | undefined;
35
+ storeDescription?: string | undefined;
36
+ }, {
37
+ storeName: string;
38
+ storeCategory?: string | undefined;
39
+ storeDescription?: string | undefined;
40
+ }>;
41
+ export type StoreCreateInput = z.infer<typeof storeCreateSchema>;
42
+ /** Field names, so a form and an error path cannot disagree by typo. */
43
+ export declare const STORE_CREATE_FIELDS: {
44
+ readonly STORE_NAME: "storeName";
45
+ readonly STORE_DESCRIPTION: "storeDescription";
46
+ readonly STORE_CATEGORY: "storeCategory";
47
+ };
@@ -0,0 +1,86 @@
1
+ /**
2
+ * The store-creation input contract — ONE definition, shared by the form and
3
+ * the server action that writes it.
4
+ *
5
+ * 🛑 Why this file exists at all. `createStoreAction` carried its own inline
6
+ * `z.object({ storeName, storeDescription, storeCategory })` while nothing
7
+ * rendered a form, so there was exactly one copy and no drift. The moment a
8
+ * form exists there are two places that must agree about "2–80 characters",
9
+ * and a client that validates more loosely than the server produces a form
10
+ * that submits and then fails with a message pointing at no field; a client
11
+ * that validates more strictly refuses input the server would have accepted.
12
+ * Both are Root Cause #75's shape. The action imports this; it does not
13
+ * restate it.
14
+ *
15
+ * The bounds are not new — they are lifted verbatim from the action's own
16
+ * schema so this is a move, not a retune.
17
+ */
18
+ import { z } from "zod";
19
+ import { annotate } from "../../shell/field-ui-meta.js";
20
+ /**
21
+ * 🛑 `.or(z.literal(""))` on both optional fields, matching the action.
22
+ *
23
+ * An untouched text input submits `""`, not `undefined`. A bare
24
+ * `.max(80).optional()` rejects it, so a seller who filled in only the
25
+ * required name would be refused for leaving an optional field alone — which
26
+ * reads as a broken form, because the field they are being blocked on is one
27
+ * they were told was optional.
28
+ */
29
+ /*
30
+ * 🛑 ANNOTATED, and the annotations do not change what this parses.
31
+ *
32
+ * `annotate()` returns the same schema and registers its UI metadata in a side
33
+ * registry, so the server action validates exactly what it validated before.
34
+ * That is what lets ONE schema drive both the form's sections
35
+ * (`buildSectionsFromSchema`) and the server-side parse — the alternative is a
36
+ * second form-only schema, which is the drift this file exists to prevent.
37
+ *
38
+ * One section, because there is one: a store has a name, what it sells, and a
39
+ * category. `audit-form-sectionised` requires the fields to live in a section
40
+ * engine rather than a bare `<Form>`, which is also what gets this step the
41
+ * pinned mobile action bar every other form in the app has.
42
+ */
43
+ export const storeCreateSchema = z.object({
44
+ storeName: annotate(z
45
+ .string()
46
+ .min(2, "Store name must be at least 2 characters")
47
+ .max(80, "Store name must be 80 characters or fewer"), {
48
+ section: "store",
49
+ sectionLabel: "Your store",
50
+ sectionRequired: true,
51
+ quick: true,
52
+ order: 1,
53
+ row: "full",
54
+ label: "Store name",
55
+ help: "Buyers see this. It also forms your store's web address.",
56
+ }),
57
+ storeDescription: annotate(z
58
+ .string()
59
+ .max(10000, "Description must be 10,000 characters or fewer")
60
+ .optional()
61
+ .or(z.literal("")), {
62
+ section: "store",
63
+ order: 2,
64
+ row: "full",
65
+ kind: "textarea",
66
+ label: "What do you sell?",
67
+ help: "Optional. A short introduction for your storefront page.",
68
+ }),
69
+ storeCategory: annotate(z
70
+ .string()
71
+ .max(80, "Category must be 80 characters or fewer")
72
+ .optional()
73
+ .or(z.literal("")), {
74
+ section: "store",
75
+ order: 3,
76
+ row: "full",
77
+ label: "Main category",
78
+ help: "Optional. Helps buyers browsing that category find your store.",
79
+ }),
80
+ });
81
+ /** Field names, so a form and an error path cannot disagree by typo. */
82
+ export const STORE_CREATE_FIELDS = {
83
+ STORE_NAME: "storeName",
84
+ STORE_DESCRIPTION: "storeDescription",
85
+ STORE_CATEGORY: "storeCategory",
86
+ };
@@ -1,4 +1,5 @@
1
1
  import type { ListingType } from "../../products/types";
2
+ import type { SellerTier, SellerStandingCriterion } from "../../../_internal/shared/features/seller-standing/config";
2
3
  export interface StoreListItem {
3
4
  id: string;
4
5
  storeSlug: string;
@@ -43,8 +44,49 @@ export interface StoreListItem {
43
44
  */
44
45
  sharedPurchaseHandlingFee?: number;
45
46
  sharedPurchaseExtraShippingPerDestination?: number;
47
+ /**
48
+ * Seller trust, as a buyer sees it.
49
+ *
50
+ * 🛑 Neither is optional, for the same reason as the flags above: the
51
+ * projection resolves absence (`?? false` / `?? "seller"`), so no client has
52
+ * to decide what `undefined` means. A badge component testing `!== "seller"`
53
+ * on an undefined value would render a TRUSTED badge on a store that has
54
+ * earned nothing.
55
+ *
56
+ * 🛑 There is deliberately no `trustedSeller` here and there must never be.
57
+ * That field means "we pay this store before delivery" — our risk posture,
58
+ * not an endorsement — and it is in `PRIVATE_STORE_FIELDS` for that reason.
59
+ */
60
+ isVerified: boolean;
61
+ sellerTier: SellerTier;
62
+ /**
63
+ * The cached standing metrics behind `sellerTier`, when they have ever been
64
+ * computed.
65
+ *
66
+ * OPTIONAL, unlike the two above, and that asymmetry is the point: absent
67
+ * means "never measured", which the seller's panel must render differently
68
+ * from a measured zero. Defaulting it would turn "we have not looked yet"
69
+ * into a confident "0 of 25".
70
+ */
71
+ standing?: SellerStandingSnapshot;
46
72
  createdAt?: string;
47
73
  }
74
+ /**
75
+ * The public half of `StoreDocument.standing` — counts and a mean, nothing
76
+ * that identifies a buyer. The address criterion is counted from HMAC
77
+ * fingerprints and only the count is kept.
78
+ */
79
+ export interface SellerStandingSnapshot {
80
+ computedAt: Date | string;
81
+ nextComputeAt?: Date | string;
82
+ windowDays: number;
83
+ deliveredOrders: number;
84
+ distinctBuyers: number;
85
+ distinctAddresses: number;
86
+ ratedOrders: number;
87
+ averageRating: number;
88
+ met: Record<SellerStandingCriterion, boolean>;
89
+ }
48
90
  export interface StoreDetail extends StoreListItem {
49
91
  bio?: string;
50
92
  location?: string;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * feat-whatsapp-bot — POST /api/whatsapp/send-status
3
+ *
4
+ * WhatsApp message delivery status update.
5
+ *
6
+ * Consumer stub:
7
+ * ```ts
8
+ * export { whatsappSendStatusPOST as POST } from "@mohasinac/feat-whatsapp-bot";
9
+ * ```
10
+ */
11
+ import { NextResponse } from "next/server.js";
12
+ export declare function POST(request: Request): Promise<NextResponse>;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * feat-whatsapp-bot — POST /api/whatsapp/send-status
3
+ *
4
+ * WhatsApp message delivery status update.
5
+ *
6
+ * Consumer stub:
7
+ * ```ts
8
+ * export { whatsappSendStatusPOST as POST } from "@mohasinac/feat-whatsapp-bot";
9
+ * ```
10
+ */
11
+ import { NextResponse } from "next/server.js";
12
+ import { getProviders } from "../../../../contracts/index.js";
13
+ import { normalizeError } from "../../../../errors/normalize.js";
14
+ export async function POST(request) {
15
+ try {
16
+ const body = await request.json();
17
+ const { db } = getProviders();
18
+ if (!db)
19
+ return NextResponse.json({ success: false, error: "DB not configured" }, { status: 503 });
20
+ // Update message status in database
21
+ const repo = db.getRepository("whatsapp_message_status");
22
+ const updated = await repo.create({
23
+ ...body,
24
+ updatedAt: new Date(),
25
+ });
26
+ return NextResponse.json({
27
+ success: true,
28
+ data: updated,
29
+ });
30
+ }
31
+ catch (error) {
32
+ void normalizeError(error);
33
+ return NextResponse.json({
34
+ success: false,
35
+ error: error instanceof Error ? error.message : "Internal server error",
36
+ }, { status: 500 });
37
+ }
38
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * feat-whatsapp-bot — POST /api/whatsapp/webhook
3
+ *
4
+ * WhatsApp webhook receiver for incoming messages.
5
+ *
6
+ * Consumer stub:
7
+ * ```ts
8
+ * export { whatsappWebhookPOST as POST } from "@mohasinac/feat-whatsapp-bot";
9
+ * ```
10
+ */
11
+ import { NextResponse } from "next/server.js";
12
+ export declare function POST(request: Request): Promise<NextResponse>;