@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.
- package/dist/.build-stamp.json +3 -3
- package/dist/_internal/server/features/categories/adapters.d.ts +1 -1
- package/dist/_internal/server/features/categories/adapters.js +13 -0
- package/dist/_internal/server/features/checkout/actions.d.ts +2 -0
- package/dist/_internal/server/features/checkout/actions.js +22 -5
- package/dist/_internal/server/features/events/like-actions.d.ts +25 -0
- package/dist/_internal/server/features/events/like-actions.js +67 -0
- package/dist/_internal/server/features/events/likes.d.ts +65 -0
- package/dist/_internal/server/features/events/likes.js +146 -0
- package/dist/_internal/server/features/orders/adapters.js +3 -1
- package/dist/_internal/server/features/seo/index.d.ts +3 -0
- package/dist/_internal/server/features/seo/index.js +2 -0
- package/dist/_internal/server/features/seo/product-detail-types.d.ts +1 -0
- package/dist/_internal/server/features/seo/product-detail-types.js +26 -0
- package/dist/_internal/server/features/seo/robots.js +20 -1
- package/dist/_internal/server/features/seo/sitemap.js +125 -27
- package/dist/_internal/server/features/seo/static-params.d.ts +107 -0
- package/dist/_internal/server/features/seo/static-params.js +228 -0
- package/dist/_internal/server/features/seo/taxonomy-links.d.ts +44 -0
- package/dist/_internal/server/features/seo/taxonomy-links.js +85 -0
- package/dist/_internal/server/features/site-settings/adapters.js +20 -2
- package/dist/_internal/server/features/stores/adapters.d.ts +2 -2
- package/dist/_internal/server/features/stores/adapters.js +88 -1
- package/dist/_internal/server/functions/scheduled.d.ts +16 -1
- package/dist/_internal/server/functions/scheduled.js +23 -1
- package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +5 -2
- package/dist/_internal/server/jobs/core/jobRunners.js +5 -0
- package/dist/_internal/server/jobs/core/onOrderCreate.js +104 -10
- package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +2 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +1 -0
- package/dist/_internal/server/jobs/core/onScamReportCreate.js +1 -0
- package/dist/_internal/server/jobs/core/onScamReportRejected.js +1 -0
- package/dist/_internal/server/jobs/core/onScamReportVerified.js +1 -0
- package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +1 -0
- package/dist/_internal/server/jobs/core/pendingOrderTimeout.js +1 -0
- package/dist/_internal/server/jobs/core/sellerStandingRollup.d.ts +63 -0
- package/dist/_internal/server/jobs/core/sellerStandingRollup.js +412 -0
- package/dist/_internal/server/jobs/core/whatsappNotify.d.ts +35 -0
- package/dist/_internal/server/jobs/core/whatsappNotify.js +140 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
- package/dist/_internal/server/jobs/handlers/index.js +1 -0
- package/dist/_internal/server/jobs/handlers/sellerStandingRollup.d.ts +12 -0
- package/dist/_internal/server/jobs/handlers/sellerStandingRollup.js +14 -0
- package/dist/_internal/server/notifications/channel-health.d.ts +8 -6
- package/dist/_internal/server/notifications/channel-health.js +2 -2
- package/dist/_internal/server/notifications/send-guard.d.ts +1 -1
- package/dist/_internal/server/notifications/send-guard.js +4 -2
- package/dist/_internal/shared/actions/action-registry.js +89 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +1 -1
- package/dist/_internal/shared/checkout/order-math.js +1 -1
- package/dist/_internal/shared/features/checkout/config.d.ts +2 -0
- package/dist/_internal/shared/features/checkout/config.js +2 -0
- package/dist/_internal/shared/features/messaging/config.d.ts +5 -7
- package/dist/_internal/shared/features/messaging/config.js +4 -1
- package/dist/_internal/shared/features/seller-standing/config.d.ts +130 -0
- package/dist/_internal/shared/features/seller-standing/config.js +153 -0
- package/dist/_internal/shared/features/site-settings/fees.d.ts +2 -0
- package/dist/_internal/shared/features/site-settings/fees.js +2 -0
- package/dist/_internal/shared/features/site-settings/types.d.ts +27 -2
- package/dist/_internal/shared/fees/calculator.d.ts +8 -0
- package/dist/_internal/shared/fees/calculator.js +7 -0
- package/dist/cli/index.js +2 -0
- package/dist/client.d.ts +2 -2
- package/dist/client.js +1 -1
- package/dist/constants/api-endpoints.d.ts +39 -0
- package/dist/constants/api-endpoints.js +16 -0
- package/dist/constants/notification-types.d.ts +1 -1
- package/dist/constants/notification-types.js +1 -1
- package/dist/core/integration-keys.d.ts +5 -0
- package/dist/core/integration-keys.js +10 -0
- package/dist/features/about/components/FeesView.js +6 -0
- package/dist/features/about/components/HelpPageView.js +9 -1
- package/dist/features/account/components/BecomeSellerView.d.ts +15 -3
- package/dist/features/account/components/BecomeSellerView.js +2 -2
- package/dist/features/account/components/BuyerShoppingGuideView.js +1 -1
- package/dist/features/account/components/NotificationPreferencesPanel.js +4 -3
- package/dist/features/account/types/index.d.ts +2 -0
- package/dist/features/admin/actions/notification-actions.d.ts +17 -2
- package/dist/features/admin/actions/notification-actions.js +154 -9
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +1 -1
- package/dist/features/admin/components/AdminOrdersView.js +2 -0
- package/dist/features/admin/components/AdminSiteSettingsView.js +66 -6
- package/dist/features/admin/components/AdminStoreEditorView.d.ts +12 -1
- package/dist/features/admin/components/AdminStoreEditorView.js +31 -2
- package/dist/features/admin/components/AdminStoresGuideView.js +1 -1
- package/dist/features/admin/components/AdminStoresView.js +59 -2
- package/dist/features/admin/components/AdminTrustGuideView.js +10 -0
- package/dist/features/admin/constants/site-settings-tabs.d.ts +12 -7
- package/dist/features/admin/constants/site-settings-tabs.js +12 -6
- package/dist/features/admin/repository/site-settings.repository.js +1 -1
- package/dist/features/admin/schemas/admin-editor-forms.d.ts +10 -0
- package/dist/features/admin/schemas/admin-editor-forms.js +34 -0
- package/dist/features/admin/schemas/admin-user-form.d.ts +61 -0
- package/dist/features/admin/schemas/admin-user-form.js +44 -3
- package/dist/features/admin/schemas/firestore.d.ts +73 -0
- package/dist/features/admin/schemas/firestore.js +8 -3
- package/dist/features/auth/checkout-value-otp.d.ts +1 -1
- package/dist/features/auth/checkout-value-otp.js +1 -1
- package/dist/features/auth/permissions/constants.d.ts +2 -2
- package/dist/features/auth/permissions/constants.js +2 -1
- package/dist/features/blog/components/BlogIndexPageView.js +2 -2
- package/dist/features/blog/components/BlogListView.js +2 -1
- package/dist/features/blog/components/BlogPostView.js +2 -1
- package/dist/features/blog/constants/category-badge.d.ts +33 -0
- package/dist/features/blog/constants/category-badge.js +40 -0
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +2 -0
- package/dist/features/cart/components/CartPriceBreakdown.js +1 -1
- package/dist/features/cart/components/StoreAddonsPicker.d.ts +5 -2
- package/dist/features/cart/components/StoreAddonsPicker.js +5 -4
- package/dist/features/cart/repository/cart.repository.js +2 -1
- package/dist/features/cart/schemas/firestore.d.ts +2 -1
- package/dist/features/categories/components/TaxonomyLinkIndex.d.ts +39 -0
- package/dist/features/categories/components/TaxonomyLinkIndex.js +11 -0
- package/dist/features/categories/schemas/firestore.d.ts +25 -0
- package/dist/features/categories/utils/load-category-options.d.ts +33 -0
- package/dist/features/categories/utils/load-category-options.js +50 -0
- package/dist/features/checkout/actions/checkout-value-otp-actions.d.ts +5 -10
- package/dist/features/checkout/actions/checkout-value-otp-actions.js +88 -2
- package/dist/features/events/components/EventCard.js +4 -12
- package/dist/features/events/components/EventInsights.d.ts +27 -0
- package/dist/features/events/components/EventInsights.js +10 -0
- package/dist/features/events/components/EventLikeButton.d.ts +21 -0
- package/dist/features/events/components/EventLikeButton.js +59 -0
- package/dist/features/events/components/EventLikersRail.d.ts +23 -0
- package/dist/features/events/components/EventLikersRail.js +54 -0
- package/dist/features/events/components/EventTypeBadge.d.ts +26 -0
- package/dist/features/events/components/EventTypeBadge.js +7 -0
- package/dist/features/events/components/EventsListPageView.js +2 -2
- package/dist/features/events/components/SpinWheel.d.ts +41 -0
- package/dist/features/events/components/SpinWheel.js +128 -0
- package/dist/features/events/components/SpinWheelView.js +36 -15
- package/dist/features/events/constants/event-type-meta.d.ts +54 -0
- package/dist/features/events/constants/event-type-meta.js +109 -0
- package/dist/features/events/repository/event-likes.repository.d.ts +57 -0
- package/dist/features/events/repository/event-likes.repository.js +151 -0
- package/dist/features/events/schemas/firestore.d.ts +111 -0
- package/dist/features/events/schemas/likes.d.ts +81 -0
- package/dist/features/events/schemas/likes.js +59 -0
- package/dist/features/events/types/index.d.ts +7 -0
- package/dist/features/events/utils/carousel-slide.d.ts +42 -0
- package/dist/features/events/utils/carousel-slide.js +174 -0
- package/dist/features/events/utils/event-charts.d.ts +65 -0
- package/dist/features/events/utils/event-charts.js +172 -0
- package/dist/features/faq/constants/help-guides.js +9 -0
- package/dist/features/faq/schemas/firestore.js +1 -0
- package/dist/features/faq/types/index.d.ts +19 -0
- package/dist/features/homepage/components/BrandsSection.js +25 -1
- package/dist/features/homepage/components/HeroCarousel.js +47 -10
- package/dist/features/homepage/lib/section-renderer.js +1 -1
- package/dist/features/homepage/types/index.d.ts +7 -0
- package/dist/features/orders/actions/refund-actions.js +1 -0
- package/dist/features/orders/components/OrderAddonBadges.d.ts +3 -1
- package/dist/features/orders/components/OrderAddonBadges.js +4 -3
- package/dist/features/orders/repository/orders.repository.d.ts +8 -0
- package/dist/features/orders/repository/orders.repository.js +2 -0
- package/dist/features/orders/schemas/firestore.d.ts +4 -0
- package/dist/features/orders/types/index.d.ts +5 -2
- package/dist/features/search/action-index/settings-entries.js +9 -0
- package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +6 -1
- package/dist/features/store-extensions/repository/store-extensions.repositories.js +10 -1
- package/dist/features/store-extensions/schemas/firestore.d.ts +17 -1
- package/dist/features/store-extensions/schemas/firestore.js +10 -0
- package/dist/features/stores/components/InteractiveStoreCard.js +2 -1
- package/dist/features/stores/components/SellerStandingPanel.d.ts +31 -0
- package/dist/features/stores/components/SellerStandingPanel.js +25 -0
- package/dist/features/stores/components/SellerTierBadge.d.ts +57 -0
- package/dist/features/stores/components/SellerTierBadge.js +19 -0
- package/dist/features/stores/components/StoreCapabilitiesGuideView.js +33 -2
- package/dist/features/stores/components/StoreCreateForm.d.ts +46 -0
- package/dist/features/stores/components/StoreCreateForm.js +107 -0
- package/dist/features/stores/components/StoreGuideHubView.js +18 -5
- package/dist/features/stores/components/StoreHeader.js +2 -1
- package/dist/features/stores/components/StoreSettingsGuideView.js +6 -2
- package/dist/features/stores/repository/store.repository.d.ts +23 -1
- package/dist/features/stores/repository/store.repository.js +52 -1
- package/dist/features/stores/schemas/firestore.d.ts +76 -7
- package/dist/features/stores/schemas/firestore.js +21 -6
- package/dist/features/stores/schemas/index.d.ts +88 -2
- package/dist/features/stores/schemas/index.js +14 -0
- package/dist/features/stores/schemas/store-create.d.ts +47 -0
- package/dist/features/stores/schemas/store-create.js +86 -0
- package/dist/features/stores/types/index.d.ts +42 -0
- package/dist/features/whatsapp-bot/api/send-status/route.d.ts +12 -0
- package/dist/features/whatsapp-bot/api/send-status/route.js +38 -0
- package/dist/features/whatsapp-bot/api/webhook/route.d.ts +12 -0
- package/dist/features/whatsapp-bot/api/webhook/route.js +43 -0
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.d.ts +57 -0
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +76 -0
- package/dist/features/whatsapp-bot/components/WhatsAppChatButton.d.ts +9 -0
- package/dist/features/whatsapp-bot/components/WhatsAppChatButton.js +7 -0
- package/dist/features/whatsapp-bot/components/index.d.ts +3 -0
- package/dist/features/whatsapp-bot/components/index.js +2 -0
- package/dist/features/whatsapp-bot/helpers/whatsapp.d.ts +52 -0
- package/dist/features/whatsapp-bot/helpers/whatsapp.js +288 -0
- package/dist/features/whatsapp-bot/index.d.ts +3 -0
- package/dist/features/whatsapp-bot/index.js +3 -0
- package/dist/features/whatsapp-bot/manifest.d.ts +2 -0
- package/dist/features/whatsapp-bot/manifest.js +13 -0
- package/dist/features/whatsapp-bot/messages/en.json +7 -0
- package/dist/features/whatsapp-bot/server.d.ts +7 -0
- package/dist/features/whatsapp-bot/server.js +7 -0
- package/dist/features/whatsapp-bot/types/index.d.ts +126 -0
- package/dist/features/whatsapp-bot/types/index.js +1 -0
- package/dist/index.d.ts +23 -6
- package/dist/index.js +38 -12
- package/dist/next/routing/route-map.d.ts +6 -1
- package/dist/next/routing/route-map.js +14 -0
- package/dist/providers/db-firebase/base.repository.d.ts +1 -1
- package/dist/providers/db-firebase/base.repository.js +1 -1
- package/dist/repositories/index.d.ts +1 -1
- package/dist/repositories/index.js +1 -1
- package/dist/schemas/registry.d.ts +115 -0
- package/dist/schemas/registry.js +2 -1
- package/dist/security/pii-encrypt.js +1 -1
- package/dist/server.d.ts +15 -2
- package/dist/server.js +32 -2
- package/dist/styles.css +36 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/HorizontalScroller.js +15 -2
- package/dist/ui/components/NavigationLoader.d.ts +14 -1
- package/dist/ui/components/NavigationLoader.js +79 -30
- package/dist/ui/components/PageHeroHeading.d.ts +40 -0
- package/dist/ui/components/PageHeroHeading.js +15 -0
- package/dist/ui/components/Typography.d.ts +3 -3
- package/dist/ui/components/Typography.js +59 -31
- package/dist/ui/components/Typography.style.css +35 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +1 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +4 -2
- package/dist/features/orders/utils/order-announcement-message.d.ts +0 -18
- 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
|
|
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
|
|
374
|
-
*
|
|
375
|
-
*
|
|
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
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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>;
|