@mohasinac/appkit 4.53.1 → 4.54.0-next.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 +5 -5
- package/dist/_internal/client/theme/ThemeProvider.js +15 -1
- package/dist/_internal/server/features/auth/capabilities.js +7 -5
- package/dist/_internal/server/features/categories/price-guidance-resolve.d.ts +66 -0
- package/dist/_internal/server/features/categories/price-guidance-resolve.js +138 -0
- package/dist/_internal/server/features/checkout/supplier-identity.js +28 -6
- package/dist/_internal/server/features/price-index/market.d.ts +57 -0
- package/dist/_internal/server/features/price-index/market.js +120 -0
- package/dist/_internal/server/features/products/list-public.d.ts +12 -0
- package/dist/_internal/server/features/products/list-public.js +10 -1
- package/dist/_internal/server/features/products/listing-tab-counts.d.ts +25 -1
- package/dist/_internal/server/features/products/listing-tab-counts.js +184 -10
- package/dist/_internal/server/features/search/index-build/sources.js +43 -2
- package/dist/_internal/server/features/stores/tabs.d.ts +7 -2
- package/dist/_internal/server/features/stores/tabs.js +5 -3
- package/dist/_internal/server/jobs/core/jobRunners.js +0 -10
- package/dist/_internal/server/jobs/core/onOrderCreate.js +10 -104
- package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
- package/dist/_internal/shared/actions/action-registry.js +128 -0
- package/dist/_internal/shared/features/categories/bundle-copy.d.ts +1 -1
- package/dist/_internal/shared/features/site-settings/supplier-of-record.d.ts +56 -0
- package/dist/_internal/shared/features/site-settings/supplier-of-record.js +57 -0
- package/dist/_internal/shared/listing-types/tiers.d.ts +94 -0
- package/dist/_internal/shared/listing-types/tiers.js +150 -0
- package/dist/_internal/shared/search/match.js +28 -2
- package/dist/cli/index.js +0 -2
- package/dist/client.d.ts +21 -2
- package/dist/client.js +23 -4
- package/dist/constants/api-endpoints.d.ts +28 -0
- package/dist/constants/api-endpoints.js +14 -0
- package/dist/features/account/components/UserOrdersView.js +26 -7
- package/dist/features/addresses/utils/postal-autofill.d.ts +39 -0
- package/dist/features/addresses/utils/postal-autofill.js +54 -0
- package/dist/features/admin/components/AdminStoreEditorView.js +73 -34
- package/dist/features/admin/components/OwnerQuickActions.d.ts +29 -0
- package/dist/features/admin/components/OwnerQuickActions.js +142 -0
- package/dist/features/admin/schemas/admin-editor-forms.d.ts +1 -1
- package/dist/features/admin/schemas/admin-editor-forms.js +4 -1
- package/dist/features/auctions/components/AuctionDetailPageView.js +8 -7
- package/dist/features/auctions/components/MarketplaceAuctionCard.d.ts +11 -1
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +9 -2
- package/dist/features/auth/permissions/constants.d.ts +29 -2
- package/dist/features/auth/permissions/constants.js +68 -1
- package/dist/features/blog/components/BlogFeaturedCard.d.ts +11 -1
- package/dist/features/blog/components/BlogFeaturedCard.js +9 -2
- package/dist/features/cart/components/CartDrawer.js +3 -2
- package/dist/features/cart/components/CartGroupLineRow.js +2 -1
- package/dist/features/cart/hooks/useAddToCart.js +10 -5
- package/dist/features/cart/utils/pending-ops.d.ts +11 -1
- package/dist/features/cart/utils/pending-ops.js +14 -2
- package/dist/features/categories/api/[id]/price-guidance/route.js +1 -24
- package/dist/features/categories/components/BrandDetailPageView.js +15 -6
- package/dist/features/categories/components/BrandDetailTabs.d.ts +7 -1
- package/dist/features/categories/components/BrandDetailTabs.js +18 -3
- package/dist/features/categories/components/BundleCollage.js +3 -2
- package/dist/features/categories/components/BundleItemsPicker.js +3 -4
- package/dist/features/categories/components/CategoryDetailPageView.js +19 -8
- package/dist/features/categories/components/CategoryDetailTabs.d.ts +7 -1
- package/dist/features/categories/components/CategoryDetailTabs.js +29 -7
- package/dist/features/categories/components/CategoryGrid.d.ts +11 -1
- package/dist/features/categories/components/CategoryGrid.js +9 -2
- package/dist/features/checkout/actions/checkout-value-otp-actions.d.ts +10 -5
- package/dist/features/checkout/actions/checkout-value-otp-actions.js +2 -88
- package/dist/features/classified/components/ClassifiedDetailPageView.js +10 -1
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +10 -1
- package/dist/features/events/components/EventCard.d.ts +11 -1
- package/dist/features/events/components/EventCard.js +9 -2
- package/dist/features/layout/AppLayoutShell.js +2 -1
- package/dist/features/layout/BottomActions.d.ts +1 -0
- package/dist/features/layout/BottomActions.js +330 -0
- package/dist/features/layout/BottomChrome.d.ts +45 -0
- package/dist/features/layout/BottomChrome.js +5 -17
- package/dist/features/layout/SideActionDock.js +36 -13
- package/dist/features/layout/index.d.ts +2 -7
- package/dist/features/layout/index.js +7 -7
- package/dist/features/layout/useIsDesktop.d.ts +36 -0
- package/dist/features/layout/useIsDesktop.js +48 -0
- package/dist/features/live/components/LiveItemDetailPageView.js +10 -1
- package/dist/features/media/messages/en.json +67 -67
- package/dist/features/orders/components/YourPurchaseStrip.d.ts +24 -0
- package/dist/features/orders/components/YourPurchaseStrip.js +77 -0
- package/dist/features/orders/components/index.d.ts +2 -1
- package/dist/features/orders/components/index.js +1 -1
- package/dist/features/orders/utils/order-announcement-message.d.ts +18 -0
- package/dist/features/orders/utils/order-announcement-message.js +16 -0
- package/dist/features/orders/utils/payment-proof-message.d.ts +20 -0
- package/dist/features/orders/utils/payment-proof-message.js +17 -0
- package/dist/features/parts/schemas/firestore.d.ts +227 -0
- package/dist/features/parts/schemas/firestore.js +6 -6
- package/dist/features/pre-orders/components/MarketplacePreorderCard.d.ts +11 -1
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +10 -3
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +8 -7
- package/dist/features/price-index/components/PriceIndexToolView.d.ts +21 -0
- package/dist/features/price-index/components/PriceIndexToolView.js +230 -27
- package/dist/features/price-index/lookup-target.d.ts +48 -0
- package/dist/features/price-index/lookup-target.js +107 -0
- package/dist/features/products/api/[id]/route.js +8 -1
- package/dist/features/products/api/route.js +16 -4
- package/dist/features/products/components/AvailabilityScopeHint.d.ts +22 -0
- package/dist/features/products/components/AvailabilityScopeHint.js +27 -0
- package/dist/features/products/components/CompareOverlay.d.ts +2 -1
- package/dist/features/products/components/CompareOverlay.js +2 -1
- package/dist/features/products/components/GroupSettingsPanel.js +2 -1
- package/dist/features/products/components/InteractiveProductCard.d.ts +7 -1
- package/dist/features/products/components/InteractiveProductCard.js +16 -4
- package/dist/features/products/components/MarketplaceBundleCard.d.ts +11 -1
- package/dist/features/products/components/MarketplaceBundleCard.js +9 -2
- package/dist/features/products/components/MarketplacePrizeDrawCard.d.ts +11 -1
- package/dist/features/products/components/MarketplacePrizeDrawCard.js +9 -2
- package/dist/features/products/components/ProductDetailPageView.js +34 -4
- package/dist/features/products/components/ProductGrid.d.ts +14 -1
- package/dist/features/products/components/ProductGrid.js +22 -5
- package/dist/features/products/components/ProductsIndexListing.js +2 -2
- package/dist/features/products/components/SublistingCarouselSection.js +2 -1
- package/dist/features/products/repository/products.repository.d.ts +1 -0
- package/dist/features/products/repository/products.repository.js +5 -1
- package/dist/features/products/utils/group-member-image.d.ts +9 -2
- package/dist/features/products/utils/group-member-image.js +2 -1
- package/dist/features/reviews/api/[id]/route.js +83 -23
- package/dist/features/reviews/components/PurchaseReviewAffordance.d.ts +6 -0
- package/dist/features/reviews/components/PurchaseReviewAffordance.js +42 -0
- package/dist/features/reviews/components/ReviewFormDrawer.d.ts +34 -0
- package/dist/features/reviews/components/ReviewFormDrawer.js +127 -0
- package/dist/features/reviews/components/index.d.ts +4 -0
- package/dist/features/reviews/components/index.js +2 -0
- package/dist/features/seller/actions/seller-actions.d.ts +1 -1
- package/dist/features/seller/actions/seller-actions.js +49 -3
- package/dist/features/seller/components/SellerPayoutSettingsView.js +30 -2
- package/dist/features/seller/components/SellerProductShell.d.ts +17 -1
- package/dist/features/seller/components/SellerProductsView.js +3 -2
- package/dist/features/seller/hooks/useStoreListingEntitlements.d.ts +8 -0
- package/dist/features/seller/hooks/useStoreListingEntitlements.js +70 -0
- package/dist/features/stores/components/InteractiveStoreCard.d.ts +11 -1
- package/dist/features/stores/components/InteractiveStoreCard.js +9 -2
- package/dist/features/stores/components/StoreAuctionsListing.js +2 -1
- package/dist/features/stores/components/index.d.ts +0 -2
- package/dist/features/stores/components/index.js +0 -1
- package/dist/features/stores/schemas/index.d.ts +1 -1
- package/dist/features/stores/schemas/index.js +10 -1
- package/dist/features/tester/seed-data/authored/content-discovery__authenticity-checker.js +114 -2
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +415 -0
- package/dist/index.d.ts +8 -27
- package/dist/index.js +14 -35
- package/dist/next/routing/route-map.d.ts +2 -0
- package/dist/next/routing/route-map.js +9 -0
- package/dist/providers/shipping-shiprocket/index.d.ts +98 -0
- package/dist/providers/shipping-shiprocket/index.js +353 -0
- package/dist/schemas/registry.d.ts +2 -2
- package/dist/security/csp.d.ts +16 -1
- package/dist/security/csp.js +5 -3
- package/dist/seed/_helpers/category-forest.js +27 -0
- package/dist/seed/_helpers/category-parts-glossary-map.js +1816 -86
- package/dist/seed/_helpers/category-parts.d.ts +1 -1
- package/dist/seed/_helpers/category-parts.js +13412 -3020
- package/dist/seed/categories-seed-data.js +48 -6
- package/dist/seed/homepage-sections-seed-data.js +41 -28
- package/dist/seed/model-composition-seed-data.js +6 -8
- package/dist/seed/part-glossary/chunk0.d.ts +9 -0
- package/dist/seed/part-glossary/chunk0.js +35044 -0
- package/dist/seed/part-glossary/chunk1.d.ts +9 -0
- package/dist/seed/part-glossary/chunk1.js +39955 -0
- package/dist/seed/part-glossary/chunk2.d.ts +9 -0
- package/dist/seed/part-glossary/chunk2.js +50335 -0
- package/dist/seed/part-glossary/chunk3.d.ts +9 -0
- package/dist/seed/part-glossary/chunk3.js +52386 -0
- package/dist/seed/part-glossary/chunk4.d.ts +9 -0
- package/dist/seed/part-glossary/chunk4.js +50549 -0
- package/dist/seed/part-glossary-seed-data.d.ts +1 -1
- package/dist/seed/part-glossary-seed-data.js +23 -47483
- package/dist/seed/products-beyblade-x-seed-data.js +36 -1
- package/dist/server.d.ts +6 -8
- package/dist/server.js +16 -20
- package/dist/styles.css +516 -358
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/themes/index.d.ts +3 -1
- package/dist/tokens/themes/index.js +11 -1
- package/dist/tokens/themes/technical-dark.d.ts +30 -0
- package/dist/tokens/themes/technical-dark.js +103 -0
- package/dist/tokens/themes/technical-light.d.ts +22 -0
- package/dist/tokens/themes/technical-light.js +89 -0
- package/dist/tokens/tokens.css +155 -0
- package/dist/ui/components/GatedPrice.js +30 -3
- package/dist/ui/components/GatedPrice.style.css +22 -19
- package/dist/ui/components/QuickCreateModal.style.css +78 -78
- package/dist/ui/forms/FormShell.style.css +260 -260
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +133 -27
- package/dist/_internal/server/jobs/core/whatsappCatalogImport.d.ts +0 -27
- package/dist/_internal/server/jobs/core/whatsappCatalogImport.js +0 -172
- package/dist/_internal/server/jobs/core/whatsappNotify.d.ts +0 -35
- package/dist/_internal/server/jobs/core/whatsappNotify.js +0 -140
- package/dist/features/admin/components/AdminWhatsAppGuideView.d.ts +0 -2
- package/dist/features/admin/components/AdminWhatsAppGuideView.js +0 -35
- package/dist/features/orders/components/MarketplaceOrderCard.d.ts +0 -36
- package/dist/features/orders/components/MarketplaceOrderCard.js +0 -57
- package/dist/features/stores/components/StoreWhatsAppGuideView.d.ts +0 -3
- package/dist/features/stores/components/StoreWhatsAppGuideView.js +0 -12
- package/dist/features/whatsapp-bot/api/send-status/route.d.ts +0 -12
- package/dist/features/whatsapp-bot/api/send-status/route.js +0 -38
- package/dist/features/whatsapp-bot/api/webhook/route.d.ts +0 -12
- package/dist/features/whatsapp-bot/api/webhook/route.js +0 -43
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.d.ts +0 -6
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +0 -270
- package/dist/features/whatsapp-bot/components/WhatsAppChatButton.d.ts +0 -9
- package/dist/features/whatsapp-bot/components/WhatsAppChatButton.js +0 -7
- package/dist/features/whatsapp-bot/components/index.d.ts +0 -3
- package/dist/features/whatsapp-bot/components/index.js +0 -2
- package/dist/features/whatsapp-bot/helpers/payment-proof-message.d.ts +0 -14
- package/dist/features/whatsapp-bot/helpers/payment-proof-message.js +0 -17
- package/dist/features/whatsapp-bot/helpers/whatsapp.d.ts +0 -52
- package/dist/features/whatsapp-bot/helpers/whatsapp.js +0 -283
- package/dist/features/whatsapp-bot/index.d.ts +0 -3
- package/dist/features/whatsapp-bot/index.js +0 -3
- package/dist/features/whatsapp-bot/manifest.d.ts +0 -2
- package/dist/features/whatsapp-bot/manifest.js +0 -13
- package/dist/features/whatsapp-bot/messages/en.json +0 -7
- package/dist/features/whatsapp-bot/server.d.ts +0 -7
- package/dist/features/whatsapp-bot/server.js +0 -7
- package/dist/features/whatsapp-bot/types/index.d.ts +0 -126
- package/dist/features/whatsapp-bot/types/index.js +0 -1
package/scripts/seed-cli.mjs
CHANGED
|
@@ -387,6 +387,104 @@ const COLLECTION_MAP = {
|
|
|
387
387
|
* A root with no mapping yields NO franchise (living collectibles are not an
|
|
388
388
|
* IP) — see `FRANCHISE_BY_CATEGORY_ROOT`.
|
|
389
389
|
*/
|
|
390
|
+
/**
|
|
391
|
+
* The real, sellable catalogue — everything else in the product seed is demo
|
|
392
|
+
* furniture and is DROPPED HERE, before anything is written.
|
|
393
|
+
*
|
|
394
|
+
* ── 🛑 WHY A FILTER AND NOT A DELETION ────────────────────────────────────
|
|
395
|
+
*
|
|
396
|
+
* The fixtures stay in their files on purpose: they are the only coverage
|
|
397
|
+
* several features have (every `preOrderProductionStatus` value, a sold-out
|
|
398
|
+
* reservation, a depleted digital-code pool, an ended-unsold auction), and
|
|
399
|
+
* the tester checklist deep-links to them by id. Deleting them would silently
|
|
400
|
+
* remove that coverage. Not LOADING them is the part that was wanted.
|
|
401
|
+
*
|
|
402
|
+
* ── 🛑 WHY THIS HAD TO EXIST ──────────────────────────────────────────────
|
|
403
|
+
*
|
|
404
|
+
* `appkit-seed load` writes every registered row and `purge` deletes
|
|
405
|
+
* `seedPurpose: "test"` afterwards — so the demo listings came BACK on every
|
|
406
|
+
* reseed, and anyone who ran `load` to fix imagery silently republished 76
|
|
407
|
+
* fake products to the live storefront. That happened twice in one session.
|
|
408
|
+
* The owner's instruction was "delete them from the seed itself and make sure
|
|
409
|
+
* they do not return"; this is the point where that can be guaranteed, since
|
|
410
|
+
* every write path funnels through here.
|
|
411
|
+
*
|
|
412
|
+
* ── The discriminator is the STORE, not the listing type ──────────────────
|
|
413
|
+
*
|
|
414
|
+
* `store-beyblade-arena` is the fictional demo seller; `store-letitrip-
|
|
415
|
+
* official` is the real one. Keying on the store rather than on a per-file
|
|
416
|
+
* allow-list means a fixture added to ANY product seed file later is excluded
|
|
417
|
+
* automatically, which is the failure mode this is replacing — a list that
|
|
418
|
+
* has to be remembered is a list that drifts.
|
|
419
|
+
*
|
|
420
|
+
* Set `SEED_INCLUDE_DEMO=1` to load them anyway, for local demo work.
|
|
421
|
+
*/
|
|
422
|
+
const REAL_STORE_IDS = new Set(["store-letitrip-official"]);
|
|
423
|
+
|
|
424
|
+
function realListingsOnly(products) {
|
|
425
|
+
if (process.env.SEED_INCLUDE_DEMO === "1") {
|
|
426
|
+
console.log("▌ SEED_INCLUDE_DEMO=1 — demo listings INCLUDED");
|
|
427
|
+
return products;
|
|
428
|
+
}
|
|
429
|
+
/*
|
|
430
|
+
* 🛑 TWO rules, because the store alone is not enough. Six prize-draw
|
|
431
|
+
* fixtures live under the REAL store and are still demo furniture — their
|
|
432
|
+
* imagery is a 460-byte generated colour tile from /images/seed-tiles/,
|
|
433
|
+
* which is the giveaway: a listing nobody photographed is a listing nobody
|
|
434
|
+
* intends to sell. Measured before writing this: every one of the six, and
|
|
435
|
+
* nothing real, matches.
|
|
436
|
+
*/
|
|
437
|
+
const isPlaceholder = (p) => {
|
|
438
|
+
const urls = [p?.mainImage, ...(Array.isArray(p?.images) ? p.images : [])]
|
|
439
|
+
.map((v) => (typeof v === "string" ? v : v?.url))
|
|
440
|
+
.filter(Boolean);
|
|
441
|
+
return urls.length > 0 && urls.every((u) => String(u).includes("/images/seed-tiles/"));
|
|
442
|
+
};
|
|
443
|
+
const kept = products.filter((p) => REAL_STORE_IDS.has(p?.storeId) && !isPlaceholder(p));
|
|
444
|
+
const dropped = products.length - kept.length;
|
|
445
|
+
if (dropped > 0) {
|
|
446
|
+
console.log(` • ${dropped} demo listing(s) not loaded (set SEED_INCLUDE_DEMO=1 to include)`);
|
|
447
|
+
}
|
|
448
|
+
return kept;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Drop bundle categories whose members are not in the loaded catalogue.
|
|
453
|
+
*
|
|
454
|
+
* 🛑 A bundle is ALL-OR-NOTHING — `canAddToCart` buys every member together —
|
|
455
|
+
* so a bundle missing even one member is unsellable, and one missing ALL of
|
|
456
|
+
* them is a card that opens an empty page. Measured right after the demo
|
|
457
|
+
* listings stopped loading: of the five seeded bundles, three had zero
|
|
458
|
+
* surviving members and the other two had 1-of-4 and 2-of-3. All five were
|
|
459
|
+
* still `isActive: true`, and all five rendered on the HOMEPAGE as collage
|
|
460
|
+
* cards built from `display.mosaicImages` — which are baked at seed time from
|
|
461
|
+
* the members' own imagery, so they kept showing pictures of products that no
|
|
462
|
+
* longer exist. That is 92 placeholder-tile references on the homepage.
|
|
463
|
+
*
|
|
464
|
+
* Derived rather than hand-maintained for the same reason as the product
|
|
465
|
+
* filter: a bundle that becomes valid later starts loading again on its own,
|
|
466
|
+
* and one that breaks stops, with nobody having to remember either.
|
|
467
|
+
*
|
|
468
|
+
* Only `categoryType: "bundle"` rows are considered. Every other category is
|
|
469
|
+
* taxonomy and is never touched — rebuilding the tree is expensive and is
|
|
470
|
+
* explicitly not what this is for.
|
|
471
|
+
*/
|
|
472
|
+
function loadableBundlesOnly(categories, loadedProducts) {
|
|
473
|
+
const live = new Set(loadedProducts.map((p) => p?.id).filter(Boolean));
|
|
474
|
+
let dropped = 0;
|
|
475
|
+
const kept = categories.filter((c) => {
|
|
476
|
+
if (c?.categoryType !== "bundle") return true;
|
|
477
|
+
const ids = Array.isArray(c.bundleProductIds) ? c.bundleProductIds : [];
|
|
478
|
+
const ok = ids.length > 0 && ids.every((id) => live.has(id));
|
|
479
|
+
if (!ok) dropped++;
|
|
480
|
+
return ok;
|
|
481
|
+
});
|
|
482
|
+
if (dropped > 0) {
|
|
483
|
+
console.log(` • ${dropped} bundle(s) not loaded — members missing from the catalogue`);
|
|
484
|
+
}
|
|
485
|
+
return kept;
|
|
486
|
+
}
|
|
487
|
+
|
|
390
488
|
function withFranchise(products) {
|
|
391
489
|
let stamped = 0;
|
|
392
490
|
const out = products.map((p) => {
|
|
@@ -469,32 +567,13 @@ function withDerivedCategoryFields(products) {
|
|
|
469
567
|
return out;
|
|
470
568
|
}
|
|
471
569
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
* except the map that actually runs.
|
|
480
|
-
*/
|
|
481
|
-
payoutMethods: payoutMethodsSeedData,
|
|
482
|
-
shippingConfigs: shippingConfigsSeedData,
|
|
483
|
-
analyticsCards: analyticsCardsSeedData,
|
|
484
|
-
analyticsAlerts: analyticsAlertsSeedData,
|
|
485
|
-
storeCategories: storeCategoriesSeedData,
|
|
486
|
-
listingTemplates: listingTemplatesSeedData,
|
|
487
|
-
moderationQueue: moderationQueueSeedData,
|
|
488
|
-
reports: reportsSeedData,
|
|
489
|
-
itemRequests: itemRequestsSeedData,
|
|
490
|
-
storeGoogleConfig: storeGoogleConfigSeedData,
|
|
491
|
-
addresses: addressesSeedData,
|
|
492
|
-
storeAddresses: storeAddressesSeedData,
|
|
493
|
-
// SB-UNI-C: brands were merged into categories (categoryType:"brand") — no
|
|
494
|
-
// standalone brands collection/seed exists anymore.
|
|
495
|
-
categories: categoriesSeedData,
|
|
496
|
-
stores: storesSeedData,
|
|
497
|
-
products: withFranchise(withDerivedCategoryFields([
|
|
570
|
+
/**
|
|
571
|
+
* The real catalogue, computed ONCE so the bundle filter and the product
|
|
572
|
+
* loader cannot disagree about what "loaded" means. Deriving it twice is
|
|
573
|
+
* Root Cause #75's shape — two copies of one rule, one of which learns about
|
|
574
|
+
* a change and the other does not.
|
|
575
|
+
*/
|
|
576
|
+
const REAL_PRODUCTS = realListingsOnly([
|
|
498
577
|
...(productsStandardSeedData || []),
|
|
499
578
|
// Real, wiki-verified Beyblade X listings under store-letitrip-official —
|
|
500
579
|
// added 2026-10-19 for PhonePe merchant registration, which requires at
|
|
@@ -536,7 +615,34 @@ const SEED_DATA_MAP = {
|
|
|
536
615
|
* what should happen to a leaf that now has BOTH a real listing and a
|
|
537
616
|
* generated one.
|
|
538
617
|
*/
|
|
539
|
-
])
|
|
618
|
+
]);
|
|
619
|
+
|
|
620
|
+
const SEED_DATA_MAP = {
|
|
621
|
+
users: usersSeedData,
|
|
622
|
+
/*
|
|
623
|
+
* store-extensions. COLLECTION_MAP was only HALF the wiring: load() reads
|
|
624
|
+
* SEED_DATA_MAP and short-circuits on an empty array, so adding the ten
|
|
625
|
+
* collection names alone produced "no seed data, skipping" for all ten.
|
|
626
|
+
* Same shape as the art/stickers note further down -- added everywhere
|
|
627
|
+
* except the map that actually runs.
|
|
628
|
+
*/
|
|
629
|
+
payoutMethods: payoutMethodsSeedData,
|
|
630
|
+
shippingConfigs: shippingConfigsSeedData,
|
|
631
|
+
analyticsCards: analyticsCardsSeedData,
|
|
632
|
+
analyticsAlerts: analyticsAlertsSeedData,
|
|
633
|
+
storeCategories: storeCategoriesSeedData,
|
|
634
|
+
listingTemplates: listingTemplatesSeedData,
|
|
635
|
+
moderationQueue: moderationQueueSeedData,
|
|
636
|
+
reports: reportsSeedData,
|
|
637
|
+
itemRequests: itemRequestsSeedData,
|
|
638
|
+
storeGoogleConfig: storeGoogleConfigSeedData,
|
|
639
|
+
addresses: addressesSeedData,
|
|
640
|
+
storeAddresses: storeAddressesSeedData,
|
|
641
|
+
// SB-UNI-C: brands were merged into categories (categoryType:"brand") — no
|
|
642
|
+
// standalone brands collection/seed exists anymore.
|
|
643
|
+
categories: loadableBundlesOnly(categoriesSeedData, REAL_PRODUCTS),
|
|
644
|
+
stores: storesSeedData,
|
|
645
|
+
products: withFranchise(withDerivedCategoryFields(REAL_PRODUCTS)),
|
|
540
646
|
orders: ordersSeedData,
|
|
541
647
|
reviews: reviewsSeedData,
|
|
542
648
|
bids: bidsSeedData,
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Core: async WhatsApp Business Catalog import (`whatsappCatalogImport` job).
|
|
3
|
-
*
|
|
4
|
-
* `POST /api/store/whatsapp-settings/catalog-import` used to do this inline:
|
|
5
|
-
* one Meta fetch capped at `limit=250`, then a sequential per-item loop doing
|
|
6
|
-
* up to 2 `findBySlug` reads plus 1 `create` write EACH — a real 10s-timeout
|
|
7
|
-
* risk on Vercel Hobby (CLAUDE.md Rule #6) well before the cap was reached.
|
|
8
|
-
* It also declared `paging.next` in its response type and never followed it,
|
|
9
|
-
* so a catalog larger than 250 items silently imported only the first page.
|
|
10
|
-
*
|
|
11
|
-
* Running here instead buys the Function's 300s budget, which is what makes
|
|
12
|
-
* following `paging.next` safe rather than making the timeout worse.
|
|
13
|
-
*
|
|
14
|
-
* Two efficiency changes vs. the old route:
|
|
15
|
-
* - Existing-slug detection is ONE `findByStore` read into a Set, not 2 reads
|
|
16
|
-
* per item. A seller's WA catalog is populated by pushing their own store's
|
|
17
|
-
* products, so store-scoped lookup is equivalent to the old global
|
|
18
|
-
* `findBySlug` here while costing a single query.
|
|
19
|
-
* - That Set is updated as we create, so duplicate rows within one Meta
|
|
20
|
-
* response can't produce two products.
|
|
21
|
-
*/
|
|
22
|
-
import type { JobContext } from "../runtime/types";
|
|
23
|
-
import type { JobRunResult } from "./jobRunners";
|
|
24
|
-
export interface WhatsAppCatalogImportPayload {
|
|
25
|
-
storeSlug: string;
|
|
26
|
-
}
|
|
27
|
-
export declare function runWhatsAppCatalogImport(payload: WhatsAppCatalogImportPayload, ctx: JobContext): Promise<JobRunResult>;
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Core: async WhatsApp Business Catalog import (`whatsappCatalogImport` job).
|
|
3
|
-
*
|
|
4
|
-
* `POST /api/store/whatsapp-settings/catalog-import` used to do this inline:
|
|
5
|
-
* one Meta fetch capped at `limit=250`, then a sequential per-item loop doing
|
|
6
|
-
* up to 2 `findBySlug` reads plus 1 `create` write EACH — a real 10s-timeout
|
|
7
|
-
* risk on Vercel Hobby (CLAUDE.md Rule #6) well before the cap was reached.
|
|
8
|
-
* It also declared `paging.next` in its response type and never followed it,
|
|
9
|
-
* so a catalog larger than 250 items silently imported only the first page.
|
|
10
|
-
*
|
|
11
|
-
* Running here instead buys the Function's 300s budget, which is what makes
|
|
12
|
-
* following `paging.next` safe rather than making the timeout worse.
|
|
13
|
-
*
|
|
14
|
-
* Two efficiency changes vs. the old route:
|
|
15
|
-
* - Existing-slug detection is ONE `findByStore` read into a Set, not 2 reads
|
|
16
|
-
* per item. A seller's WA catalog is populated by pushing their own store's
|
|
17
|
-
* products, so store-scoped lookup is equivalent to the old global
|
|
18
|
-
* `findBySlug` here while costing a single query.
|
|
19
|
-
* - That Set is updated as we create, so duplicate rows within one Meta
|
|
20
|
-
* response can't produce two products.
|
|
21
|
-
*/
|
|
22
|
-
import { normalizeError } from "../../../../errors/normalize.js";
|
|
23
|
-
import { storeRepository, productRepository } from "../../../../repositories/index.js";
|
|
24
|
-
import { decryptPii } from "../../../../security/index.js";
|
|
25
|
-
const META_GRAPH_BASE = "https://graph.facebook.com/v20.0";
|
|
26
|
-
const SLUG_PREFIX = "product-";
|
|
27
|
-
const PAGE_SIZE = 250;
|
|
28
|
-
/**
|
|
29
|
-
* Defensive stop. 40 pages x 250 = 10,000 items, far beyond any realistic
|
|
30
|
-
* seller catalog — this exists so a malformed/looping `paging.next` from Meta
|
|
31
|
-
* can't spin until the Function's 300s ceiling kills the job mid-write.
|
|
32
|
-
*/
|
|
33
|
-
const MAX_PAGES = 40;
|
|
34
|
-
/** Meta returns "<amount> <currency>", e.g. "450.00 INR". */
|
|
35
|
-
function parseMetaPrice(raw) {
|
|
36
|
-
if (!raw)
|
|
37
|
-
return 0;
|
|
38
|
-
const digits = raw.replace(/[^0-9.]/g, "");
|
|
39
|
-
return Math.round((parseFloat(digits) || 0) * 100) / 100;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* The LetItRip slug a Meta item claims to correspond to, if any. Items we
|
|
43
|
-
* pushed carry it in `description` (see catalog-sync's field mapping) and/or
|
|
44
|
-
* `retailer_id`; items authored natively in Meta carry neither.
|
|
45
|
-
*/
|
|
46
|
-
function claimedSlug(item) {
|
|
47
|
-
const description = (item.description ?? "").trim();
|
|
48
|
-
if (description.startsWith(SLUG_PREFIX))
|
|
49
|
-
return description;
|
|
50
|
-
const retailerId = (item.retailer_id ?? "").trim();
|
|
51
|
-
if (retailerId.startsWith(SLUG_PREFIX))
|
|
52
|
-
return retailerId;
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
export async function runWhatsAppCatalogImport(payload, ctx) {
|
|
56
|
-
const { storeSlug } = payload;
|
|
57
|
-
const empty = { total: 0, succeeded: 0, skipped: 0, failed: 0 };
|
|
58
|
-
const store = await storeRepository.findBySlug(storeSlug);
|
|
59
|
-
if (!store) {
|
|
60
|
-
return {
|
|
61
|
-
summary: empty,
|
|
62
|
-
succeeded: [],
|
|
63
|
-
skipped: [],
|
|
64
|
-
failed: [{ id: storeSlug, reason: "Store not found" }],
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
const cfg = store.whatsappConfig;
|
|
68
|
-
const token = cfg?.accessToken ? decryptPii(cfg.accessToken) : null;
|
|
69
|
-
// Re-checked here even though the route gates on the same conditions — the
|
|
70
|
-
// job doc outlives the request, and the seller could disconnect in between.
|
|
71
|
-
if (!cfg?.connected || !cfg.catalogId || !token) {
|
|
72
|
-
return {
|
|
73
|
-
summary: empty,
|
|
74
|
-
succeeded: [],
|
|
75
|
-
skipped: [],
|
|
76
|
-
failed: [{ id: storeSlug, reason: "WhatsApp is not connected for this store" }],
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
const existing = await productRepository.findByStore(store.storeSlug);
|
|
80
|
-
const knownSlugs = new Set(existing.map((p) => p.slug).filter((s) => Boolean(s)));
|
|
81
|
-
const imported = [];
|
|
82
|
-
const skipped = [];
|
|
83
|
-
const failed = [];
|
|
84
|
-
let url = `${META_GRAPH_BASE}/${cfg.catalogId}/products` +
|
|
85
|
-
`?fields=name,price,currency,image_url,description,retailer_id&limit=${PAGE_SIZE}`;
|
|
86
|
-
let page = 0;
|
|
87
|
-
let truncated = false;
|
|
88
|
-
while (url) {
|
|
89
|
-
if (page >= MAX_PAGES) {
|
|
90
|
-
truncated = true;
|
|
91
|
-
ctx.logger.warn("whatsappCatalogImport: page cap reached — catalog truncated", {
|
|
92
|
-
storeSlug,
|
|
93
|
-
maxPages: MAX_PAGES,
|
|
94
|
-
importedSoFar: imported.length,
|
|
95
|
-
});
|
|
96
|
-
break;
|
|
97
|
-
}
|
|
98
|
-
page++;
|
|
99
|
-
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
100
|
-
if (!res.ok) {
|
|
101
|
-
const body = (await res.json().catch(() => ({})));
|
|
102
|
-
const reason = body.error?.message ?? res.statusText;
|
|
103
|
-
ctx.logger.error("whatsappCatalogImport: Meta fetch failed", null, { storeSlug, page, reason });
|
|
104
|
-
failed.push({ id: `page-${page}`, reason: `Meta fetch failed: ${reason}` });
|
|
105
|
-
break;
|
|
106
|
-
}
|
|
107
|
-
const body = (await res.json());
|
|
108
|
-
const items = body.data ?? [];
|
|
109
|
-
for (const item of items) {
|
|
110
|
-
const slug = claimedSlug(item);
|
|
111
|
-
if (slug && knownSlugs.has(slug)) {
|
|
112
|
-
skipped.push(item.id);
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
const input = {
|
|
116
|
-
title: item.name ?? `WhatsApp Import ${item.id}`,
|
|
117
|
-
description: "",
|
|
118
|
-
slug: "",
|
|
119
|
-
listingType: "standard",
|
|
120
|
-
status: "draft",
|
|
121
|
-
price: parseMetaPrice(item.price),
|
|
122
|
-
currency: "INR",
|
|
123
|
-
mainImage: item.image_url ?? "",
|
|
124
|
-
images: [],
|
|
125
|
-
stockQuantity: 1,
|
|
126
|
-
condition: "new",
|
|
127
|
-
storeId: store.id,
|
|
128
|
-
storeName: store.storeName,
|
|
129
|
-
tags: ["whatsapp-import"],
|
|
130
|
-
categorySlugs: [],
|
|
131
|
-
featured: false,
|
|
132
|
-
};
|
|
133
|
-
try {
|
|
134
|
-
const created = await productRepository.create(input);
|
|
135
|
-
// Guard against the same item appearing twice in one Meta response.
|
|
136
|
-
if (slug)
|
|
137
|
-
knownSlugs.add(slug);
|
|
138
|
-
if (created.slug)
|
|
139
|
-
knownSlugs.add(created.slug);
|
|
140
|
-
imported.push(created.id);
|
|
141
|
-
}
|
|
142
|
-
catch (err) {
|
|
143
|
-
void normalizeError(err);
|
|
144
|
-
failed.push({
|
|
145
|
-
id: item.id,
|
|
146
|
-
reason: err instanceof Error ? err.message : "Product create failed",
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
url = body.paging?.next;
|
|
151
|
-
}
|
|
152
|
-
ctx.logger.info("whatsappCatalogImport: complete", {
|
|
153
|
-
storeSlug,
|
|
154
|
-
pages: page,
|
|
155
|
-
imported: imported.length,
|
|
156
|
-
skipped: skipped.length,
|
|
157
|
-
failed: failed.length,
|
|
158
|
-
truncated,
|
|
159
|
-
});
|
|
160
|
-
return {
|
|
161
|
-
summary: {
|
|
162
|
-
total: imported.length + skipped.length + failed.length,
|
|
163
|
-
succeeded: imported.length,
|
|
164
|
-
skipped: skipped.length,
|
|
165
|
-
failed: failed.length,
|
|
166
|
-
},
|
|
167
|
-
succeeded: imported,
|
|
168
|
-
skipped,
|
|
169
|
-
failed,
|
|
170
|
-
data: { imported: imported.length, skipped: skipped.length, pages: page, truncated },
|
|
171
|
-
};
|
|
172
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Core: async WhatsApp notification dispatch (`whatsappNotify` job).
|
|
3
|
-
*
|
|
4
|
-
* `sendNotification()` (appkit/src/features/admin/actions/notification-actions.ts)
|
|
5
|
-
* no longer calls the Meta Cloud API synchronously in-request — it enqueues
|
|
6
|
-
* this job instead (CLAUDE.md Rule #6 async job primitive) and returns
|
|
7
|
-
* immediately. This runner does the actual send, with a bounded retry, and
|
|
8
|
-
* writes the outcome back onto `notifications/{notificationId}` so admins
|
|
9
|
-
* have visibility beyond the `jobs/{jobId}` doc itself.
|
|
10
|
-
*
|
|
11
|
-
* Retry note: `sendWhatsAppTemplateMessage`/`sendWhatsAppBusinessMessage`
|
|
12
|
-
* return a plain boolean (not a status code), so this runner cannot cleanly
|
|
13
|
-
* distinguish a transient 5xx from a terminal 4xx (bad token, unknown
|
|
14
|
-
* template) the way a status-code-aware client could. It retries any
|
|
15
|
-
* failure up to MAX_ATTEMPTS with a short backoff — an acceptable
|
|
16
|
-
* simplification since this runs off the critical path, well inside the
|
|
17
|
-
* Firebase Function's 300s budget, and a terminal error just burns two
|
|
18
|
-
* extra fast failures before giving up.
|
|
19
|
-
*/
|
|
20
|
-
import type { JobContext } from "../runtime/types";
|
|
21
|
-
import type { JobRunResult } from "./jobRunners";
|
|
22
|
-
export interface WhatsAppNotifyPayload {
|
|
23
|
-
toPhone: string;
|
|
24
|
-
title: string;
|
|
25
|
-
message: string;
|
|
26
|
-
type: string;
|
|
27
|
-
/** Approved Meta template name for this notification type, if configured. */
|
|
28
|
-
templateName?: string;
|
|
29
|
-
/** BCP-47 language code the template was approved in. Defaults to "en". */
|
|
30
|
-
templateLanguage?: string;
|
|
31
|
-
notificationId: string;
|
|
32
|
-
phoneNumberId: string;
|
|
33
|
-
accessToken: string;
|
|
34
|
-
}
|
|
35
|
-
export declare function runWhatsAppNotify(payload: WhatsAppNotifyPayload, ctx: JobContext): Promise<JobRunResult>;
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Core: async WhatsApp notification dispatch (`whatsappNotify` job).
|
|
3
|
-
*
|
|
4
|
-
* `sendNotification()` (appkit/src/features/admin/actions/notification-actions.ts)
|
|
5
|
-
* no longer calls the Meta Cloud API synchronously in-request — it enqueues
|
|
6
|
-
* this job instead (CLAUDE.md Rule #6 async job primitive) and returns
|
|
7
|
-
* immediately. This runner does the actual send, with a bounded retry, and
|
|
8
|
-
* writes the outcome back onto `notifications/{notificationId}` so admins
|
|
9
|
-
* have visibility beyond the `jobs/{jobId}` doc itself.
|
|
10
|
-
*
|
|
11
|
-
* Retry note: `sendWhatsAppTemplateMessage`/`sendWhatsAppBusinessMessage`
|
|
12
|
-
* return a plain boolean (not a status code), so this runner cannot cleanly
|
|
13
|
-
* distinguish a transient 5xx from a terminal 4xx (bad token, unknown
|
|
14
|
-
* template) the way a status-code-aware client could. It retries any
|
|
15
|
-
* failure up to MAX_ATTEMPTS with a short backoff — an acceptable
|
|
16
|
-
* simplification since this runs off the critical path, well inside the
|
|
17
|
-
* Firebase Function's 300s budget, and a terminal error just burns two
|
|
18
|
-
* extra fast failures before giving up.
|
|
19
|
-
*/
|
|
20
|
-
import { normalizeError } from "../../../../errors/normalize.js";
|
|
21
|
-
import { notificationRepository } from "../../../../features/admin/repository/notification.repository.js";
|
|
22
|
-
import { sendWhatsAppTemplateMessage, sendWhatsAppBusinessMessage, } from "../../../../features/whatsapp-bot/helpers/whatsapp.js";
|
|
23
|
-
import { isChannelHealthy, recordChannelOutcome } from "../../notifications/channel-health.js";
|
|
24
|
-
import { guardSend, logSuppressedSend } from "../../notifications/send-guard.js";
|
|
25
|
-
const MAX_ATTEMPTS = 3;
|
|
26
|
-
const RETRY_DELAY_MS = 1500;
|
|
27
|
-
function delay(ms) {
|
|
28
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
29
|
-
}
|
|
30
|
-
export async function runWhatsAppNotify(payload, ctx) {
|
|
31
|
-
const { toPhone, title, message, type, templateName, templateLanguage, notificationId, phoneNumberId, accessToken } = payload;
|
|
32
|
-
/*
|
|
33
|
-
* The messaging guard, applied HERE rather than inside
|
|
34
|
-
* `sendWhatsAppBusinessMessage` itself.
|
|
35
|
-
*
|
|
36
|
-
* 🛑 That placement is deliberate and must not be "improved" by pushing a
|
|
37
|
-
* required guard parameter down into the sender. `features/whatsapp-bot/helpers/whatsapp.ts`
|
|
38
|
-
* is re-exported from `appkit/src/index.ts` (via `isAdminNumber`), and
|
|
39
|
-
* `guardSend` reaches `firebase-admin` through the settings and budget
|
|
40
|
-
* repositories. A static import of it from that file would put
|
|
41
|
-
* `firebase-admin` in the main entry's import graph, which is precisely the
|
|
42
|
-
* Turbopack client-bundle trap in Root Cause #6/#24 — it builds fine locally
|
|
43
|
-
* on webpack and fails every page in production.
|
|
44
|
-
*
|
|
45
|
-
* So the senders stay pure and each of the three real call sites guards
|
|
46
|
-
* itself; `scripts/audit-unguarded-send.mjs` enforces that a file calling a
|
|
47
|
-
* WhatsApp sender also calls `guardSend`.
|
|
48
|
-
*
|
|
49
|
-
* Note this job is only ever reached for an email/WhatsApp-ELIGIBLE type —
|
|
50
|
-
* `sendNotification` returns before enqueuing otherwise — so this is the
|
|
51
|
-
* ceiling and kill-switch check, not the type check.
|
|
52
|
-
*/
|
|
53
|
-
const guard = {
|
|
54
|
-
channel: "whatsapp",
|
|
55
|
-
feature: type,
|
|
56
|
-
audience: "user",
|
|
57
|
-
};
|
|
58
|
-
const decision = await guardSend(guard);
|
|
59
|
-
if (!decision.allow) {
|
|
60
|
-
logSuppressedSend(guard, decision);
|
|
61
|
-
await notificationRepository
|
|
62
|
-
.update(notificationId, { whatsappStatus: "skipped" })
|
|
63
|
-
.catch((err) => {
|
|
64
|
-
void normalizeError(err);
|
|
65
|
-
});
|
|
66
|
-
return {
|
|
67
|
-
summary: { total: 1, succeeded: 0, skipped: 1, failed: 0 },
|
|
68
|
-
succeeded: [],
|
|
69
|
-
skipped: [notificationId],
|
|
70
|
-
failed: [],
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
if (!(await isChannelHealthy("whatsapp"))) {
|
|
74
|
-
ctx.logger.warn("whatsappNotify: WhatsApp channel circuit is open — skipping send", { notificationId });
|
|
75
|
-
await notificationRepository
|
|
76
|
-
.update(notificationId, { whatsappStatus: "skipped" })
|
|
77
|
-
.catch((err) => {
|
|
78
|
-
void normalizeError(err);
|
|
79
|
-
});
|
|
80
|
-
return {
|
|
81
|
-
summary: { total: 1, succeeded: 0, skipped: 1, failed: 0 },
|
|
82
|
-
succeeded: [],
|
|
83
|
-
skipped: [notificationId],
|
|
84
|
-
failed: [],
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
let sent = false;
|
|
88
|
-
let lastError;
|
|
89
|
-
for (let attempt = 1; attempt <= MAX_ATTEMPTS && !sent; attempt++) {
|
|
90
|
-
try {
|
|
91
|
-
sent = templateName
|
|
92
|
-
? await sendWhatsAppTemplateMessage({
|
|
93
|
-
toPhone,
|
|
94
|
-
phoneNumberId,
|
|
95
|
-
accessToken,
|
|
96
|
-
templateName,
|
|
97
|
-
languageCode: templateLanguage ?? "en",
|
|
98
|
-
bodyParams: [title, message],
|
|
99
|
-
})
|
|
100
|
-
: await sendWhatsAppBusinessMessage({
|
|
101
|
-
toPhone,
|
|
102
|
-
phoneNumberId,
|
|
103
|
-
accessToken,
|
|
104
|
-
message: `*${title}*\n${message}`,
|
|
105
|
-
});
|
|
106
|
-
if (!sent)
|
|
107
|
-
lastError = "Meta WhatsApp API returned a non-OK response";
|
|
108
|
-
}
|
|
109
|
-
catch (err) {
|
|
110
|
-
void normalizeError(err);
|
|
111
|
-
lastError = err instanceof Error ? err.message : String(err);
|
|
112
|
-
}
|
|
113
|
-
if (!sent && attempt < MAX_ATTEMPTS) {
|
|
114
|
-
ctx.logger.warn("whatsappNotify: send attempt failed, retrying", { notificationId, attempt, error: lastError ?? "" });
|
|
115
|
-
await delay(RETRY_DELAY_MS * attempt);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
if (!templateName) {
|
|
119
|
-
ctx.logger.warn("whatsappNotify: no approved template configured for this notification type — sent as free-form text, which Meta rejects outside the 24h customer-service window", { notificationId });
|
|
120
|
-
}
|
|
121
|
-
await recordChannelOutcome("whatsapp", sent);
|
|
122
|
-
await notificationRepository
|
|
123
|
-
.update(notificationId, { whatsappStatus: sent ? "sent" : "failed" })
|
|
124
|
-
.catch((err) => {
|
|
125
|
-
void normalizeError(err);
|
|
126
|
-
ctx.logger.warn("whatsappNotify: failed to write back whatsappStatus onto notification doc", {
|
|
127
|
-
notificationId,
|
|
128
|
-
error: err instanceof Error ? err.message : String(err),
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
if (!sent) {
|
|
132
|
-
throw new Error(lastError ?? "WhatsApp send failed after retries");
|
|
133
|
-
}
|
|
134
|
-
return {
|
|
135
|
-
summary: { total: 1, succeeded: 1, skipped: 0, failed: 0 },
|
|
136
|
-
succeeded: [notificationId],
|
|
137
|
-
skipped: [],
|
|
138
|
-
failed: [],
|
|
139
|
-
};
|
|
140
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { Building2, Smartphone, KeyRound, MessageSquare, Power, FlaskConical } from "lucide-react";
|
|
3
|
-
import { Alert, Code, Div, Heading, Li, Row, Section, Span, Stack, Table, Tbody, Td, Text, Th, Thead, Tr, Ul } from "../../../ui/index.js";
|
|
4
|
-
import { GC } from "../../_guide-cls.js";
|
|
5
|
-
export function AdminWhatsAppGuideView() {
|
|
6
|
-
return (_jsxs(Stack, { className: "max-w-3xl mx-auto", padding: "b-2xl", gap: "xl", children: [_jsxs(Section, { children: [_jsxs(Row, { className: "mb-2", align: "center", gap: "3", children: [_jsx(Row, { className: "flex-shrink-0 w-10 h-10 [background:linear-gradient(135deg,var(--appkit-color-primary-700)_0%,var(--appkit-color-cobalt)_100%)]", align: "center", justify: "center", rounded: "xl", children: _jsx(MessageSquare, { className: "w-5 h-5 text-white" }) }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)] tracking-widest", size: "sm", weight: "semibold", transform: "uppercase", children: "Admin Guide" })] }), _jsx(Heading, { level: 1, className: "text-[var(--appkit-color-text)] mb-2", mdSize: "3xl", size: "2xl", weight: "bold", children: "WhatsApp Integration" }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", children: "Connecting a real Meta WhatsApp Business Cloud API account so the platform-level order-notification addon and admin purchase announcements actually deliver. Do these steps outside the codebase, then paste the resulting values into Site Settings \u2192 WhatsApp." })] }), [
|
|
7
|
-
{
|
|
8
|
-
Icon: Building2, title: "Meta Business Manager & App",
|
|
9
|
-
content: (_jsxs(Ul, { className: GC.listMuted, children: [_jsxs(Li, { children: ["Create or verify a Business account at ", _jsx(Span, { weight: "bold", children: "business.facebook.com" }), ". Complete Business Verification (legal name, address, phone) \u2014 most message-template categories require a verified business before they'll be approved."] }), _jsxs(Li, { children: ["In ", _jsx(Span, { weight: "bold", children: "developers.facebook.com" }), ", create an App and add the ", _jsx(Span, { weight: "bold", children: "WhatsApp" }), " product. This gives you a test phone number for development immediately."] })] })),
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
Icon: Smartphone, title: "Register the Real Business Phone Number",
|
|
13
|
-
content: (_jsxs(Ul, { className: GC.listMuted, children: [_jsx(Li, { children: "WhatsApp Manager \u2192 Phone Numbers \u2192 Add phone number \u2192 verify via SMS or voice OTP." }), _jsx(Li, { children: "Complete Display Name review \u2014 Meta approves the shown business name before it goes live." })] })),
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
Icon: KeyRound, title: "System User & Permanent Access Token",
|
|
17
|
-
content: (_jsxs(_Fragment, { children: [_jsxs(Ul, { className: GC.listMuted, children: [_jsxs(Li, { children: ["Business Settings \u2192 Users \u2192 System Users \u2192 create a System User with ", _jsx(Span, { weight: "bold", children: "Admin" }), " role."] }), _jsxs(Li, { children: ["Assign it the WhatsApp Business Account asset with ", _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "whatsapp_business_messaging" }), " + ", _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "whatsapp_business_management" }), " permissions, then generate a token. System User tokens don't expire the way personal User tokens do, so this is the one to use in production."] })] }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)] mt-3", size: "sm", children: ["Copy the token into ", _jsx(Span, { weight: "bold", children: "Site Settings \u2192 WhatsApp \u2192 Cloud API System User Token" }), ", and the ", _jsx(Span, { weight: "bold", children: "Phone Number ID" }), " (WhatsApp Manager \u2192 API Setup) into ", _jsx(Span, { weight: "bold", children: "Site Settings \u2192 WhatsApp \u2192 Phone Number ID" }), "."] })] })),
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
Icon: MessageSquare, title: "Message Templates",
|
|
21
|
-
content: (_jsxs(_Fragment, { children: [_jsx(Text, { className: "text-[var(--appkit-color-text-muted)] mb-3", size: "sm", children: "Meta's Cloud API only allows free-form text messages within a 24-hour window after the customer has messaged the business first. Any business-initiated proactive notification \u2014 order placed, shipped, delivered, cancelled, refund initiated \u2014 needs a pre-approved Message Template." }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)] mb-3", size: "sm", children: ["WhatsApp Manager \u2192 Message Templates \u2192 create one template per notification type, category ", _jsx(Span, { weight: "bold", children: "UTILITY" }), " (transactional order updates get cheaper, faster approval than MARKETING):"] }), _jsx(Div, { overflow: "x-auto", children: _jsxs(Table, { size: "sm", children: [_jsx(Thead, { children: _jsxs(Tr, { className: "border-b border-[var(--appkit-color-border)]", children: [_jsx(Th, { align: "left", paddingSide: "pr-md", className: "text-[var(--appkit-color-text)]", padding: "xs-tall", weight: "semibold", children: "Notification type" }), _jsx(Th, { align: "left", className: "text-[var(--appkit-color-text)]", padding: "xs-tall", weight: "semibold", children: "Suggested template name" })] }) }), _jsx(Tbody, { size: "sm", color: "muted", children: [
|
|
22
|
-
["Order placed", "order_placed_update"],
|
|
23
|
-
["Order confirmed", "order_confirmed_update"],
|
|
24
|
-
["Order shipped", "order_shipped_update"],
|
|
25
|
-
["Order delivered", "order_delivered_update"],
|
|
26
|
-
["Order cancelled", "order_cancelled_update"],
|
|
27
|
-
["Refund initiated", "refund_initiated_update"],
|
|
28
|
-
].map(([type, name]) => (_jsxs(Tr, { className: "border-b border-[var(--appkit-color-border)]/50", children: [_jsx(Td, { paddingSide: "pr-md", padding: "xs-tall", children: type }), _jsx(Td, { padding: "xs-tall", children: _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: name }) })] }, type))) })] }) }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)] mt-3", size: "sm", children: ["Give each template two ", _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "{{1}}" }), "/", _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "{{2}}" }), " body variables \u2014 the runner substitutes the notification title and message text in that order. Submit for review; approval usually takes minutes to ~24 hours. Don't go live on a template until its status shows ", _jsx(Span, { weight: "bold", children: "APPROVED" }), "."] }), _jsxs(Text, { className: "text-[var(--appkit-color-text-muted)] mt-2", size: "sm", children: ["Once each template is approved, copy its exact template name (not the display label you gave it in Meta's UI) into ", _jsx(Span, { weight: "bold", children: "Site Settings \u2192 WhatsApp \u2192 [type] template name" }), ", and set the approved language code (e.g. ", _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "en" }), ") in ", _jsx(Span, { weight: "bold", children: "Template language code" }), "."] })] })),
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
Icon: Power, title: "Enable the Addon & Test",
|
|
32
|
-
content: (_jsxs(Ul, { className: GC.listMuted, children: [_jsxs(Li, { children: [_jsx(Span, { weight: "bold", children: "Site Settings \u2192 Fees \u2192 \"Offer the WhatsApp order-updates addon at checkout\"" }), " \u2014 turn this on once credentials + at least one template are configured. Set the fee amount (default \u20B910)."] }), _jsx(Li, { children: "Place a real test order with the addon checked. Confirm the buyer receives the templated WhatsApp message. Until real credentials + an approved template exist, the async delivery job will fail at the Meta API call with a clear credential/template error \u2014 that's expected, not a code bug." })] })),
|
|
33
|
-
},
|
|
34
|
-
].map(({ Icon, title, content }) => (_jsxs(Section, { overflow: "hidden", className: "border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", rounded: "2xl", children: [_jsxs(Row, { className: "border-b border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-2,var(--appkit-color-border))]/20", padding: "inlineLg", align: "center", gap: "3", children: [_jsx(Icon, { className: "w-5 h-5 text-[var(--appkit-color-primary)]" }), _jsx(Heading, { level: 2, size: "base", weight: "semibold", children: title })] }), _jsx(Div, { paddingY: "y-md-lg", padding: "x-lg", children: content })] }, title))), _jsx(Alert, { variant: "info", children: _jsxs(Row, { align: "center", gap: "sm", children: [_jsx(FlaskConical, { className: "w-4 h-4 flex-shrink-0" }), _jsx(Text, { size: "sm", children: "Store-level Meta Commerce Catalog sync (product catalog on WhatsApp) uses separate, per-store credentials \u2014 see the Seller Guide's \"WhatsApp Catalog Sync\" page (Store dashboard \u2192 Guides). It shares the same Meta Business Manager account but not the same access token. The inbound-reply webhook (buyer messages the business number within 24h) uses the free-text send path and doesn't need a template." })] }) })] }));
|
|
35
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
export interface MarketplaceOrderCardOrder {
|
|
2
|
-
id: string;
|
|
3
|
-
status: string;
|
|
4
|
-
orderDate: string | Date;
|
|
5
|
-
totalPrice: number;
|
|
6
|
-
currency?: string;
|
|
7
|
-
productId?: string;
|
|
8
|
-
productTitle?: string;
|
|
9
|
-
trackingNumber?: string;
|
|
10
|
-
items?: Array<unknown>;
|
|
11
|
-
}
|
|
12
|
-
export interface MarketplaceOrderCardLabels {
|
|
13
|
-
orderNumber: string;
|
|
14
|
-
placedOn: string;
|
|
15
|
-
items: string;
|
|
16
|
-
trackOrder: string;
|
|
17
|
-
writeReview: string;
|
|
18
|
-
viewOrder: string;
|
|
19
|
-
}
|
|
20
|
-
export interface MarketplaceOrderCardLinks {
|
|
21
|
-
detailHref: (order: MarketplaceOrderCardOrder) => string;
|
|
22
|
-
trackHref?: (order: MarketplaceOrderCardOrder) => string;
|
|
23
|
-
reviewHref?: (order: MarketplaceOrderCardOrder) => string;
|
|
24
|
-
}
|
|
25
|
-
export interface MarketplaceOrderCardProps {
|
|
26
|
-
order: MarketplaceOrderCardOrder;
|
|
27
|
-
links?: MarketplaceOrderCardLinks;
|
|
28
|
-
labels?: Partial<MarketplaceOrderCardLabels>;
|
|
29
|
-
className?: string;
|
|
30
|
-
variant?: "grid" | "list";
|
|
31
|
-
selectable?: boolean;
|
|
32
|
-
isSelected?: boolean;
|
|
33
|
-
onSelect?: (id: string, selected: boolean) => void;
|
|
34
|
-
onNavigate?: (href: string) => void;
|
|
35
|
-
}
|
|
36
|
-
export declare function MarketplaceOrderCard({ order, links, labels, className, variant, selectable, isSelected, onSelect, onNavigate, }: MarketplaceOrderCardProps): import("react").JSX.Element;
|