@mohasinac/appkit 4.43.0 → 4.44.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/_internal/server/features/categories/adapters.d.ts +47 -0
- package/dist/_internal/server/features/categories/adapters.js +235 -0
- package/dist/_internal/server/features/checkout/actions.js +161 -22
- package/dist/_internal/server/features/checkout/expand-order-items.js +34 -0
- package/dist/_internal/server/features/orders/adapters.js +21 -0
- package/dist/_internal/server/features/refunds/actions.js +1 -1
- package/dist/_internal/server/features/seo/sitemap.js +54 -15
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +6 -0
- package/dist/_internal/server/functions/scheduled.d.ts +4 -2
- package/dist/_internal/server/functions/scheduled.js +64 -6
- package/dist/_internal/server/jobs/core/adminAnalytics.d.ts +0 -1
- package/dist/_internal/server/jobs/core/adminAnalytics.js +9 -1
- package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.d.ts +47 -0
- package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +468 -0
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +14 -1
- package/dist/_internal/server/jobs/core/countersReconcile.js +24 -8
- package/dist/_internal/server/jobs/core/onCategoryWrite.js +54 -0
- package/dist/_internal/server/jobs/core/priceIndexRollup.d.ts +47 -0
- package/dist/_internal/server/jobs/core/priceIndexRollup.js +234 -0
- package/dist/_internal/server/jobs/core/{testerSandboxCleanup.d.ts → testDataCleanup.d.ts} +3 -3
- package/dist/_internal/server/jobs/core/{testerSandboxCleanup.js → testDataCleanup.js} +3 -3
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +5 -1
- package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.js +2 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +3 -1
- package/dist/_internal/server/jobs/handlers/index.js +3 -1
- package/dist/_internal/server/jobs/handlers/messages.d.ts +5 -2
- package/dist/_internal/server/jobs/handlers/messages.js +5 -2
- package/dist/_internal/server/jobs/handlers/priceIndexRollup.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/priceIndexRollup.js +2 -0
- package/dist/_internal/server/jobs/handlers/testDataCleanup.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/testDataCleanup.js +2 -0
- package/dist/_internal/shared/actions/action-registry.js +34 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +113 -3
- package/dist/_internal/shared/checkout/order-math.js +194 -7
- package/dist/_internal/shared/features/categories/price-guidance.d.ts +110 -0
- package/dist/_internal/shared/features/categories/price-guidance.js +136 -0
- package/dist/_internal/shared/features/site-settings/gst-registration.d.ts +90 -0
- package/dist/_internal/shared/features/site-settings/gst-registration.js +126 -0
- package/dist/_internal/shared/fees/calculator.d.ts +105 -0
- package/dist/_internal/shared/fees/calculator.js +143 -0
- package/dist/_internal/shared/listing-types/art/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/classified/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/digital-code/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/live/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/standard/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/stickers/config.d.ts +0 -6
- package/dist/_internal/shared/templating/placeholders.d.ts +70 -0
- package/dist/_internal/shared/templating/placeholders.js +73 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +1 -0
- package/dist/constants/api-endpoints.d.ts +42 -0
- package/dist/constants/api-endpoints.js +14 -0
- package/dist/features/admin/actions/admin-actions.d.ts +1 -1
- package/dist/features/admin/actions/admin-actions.js +1 -1
- package/dist/features/admin/components/AdminCategoryEditorView.js +269 -3
- package/dist/features/admin/components/AdminProductEditorView.js +8 -1
- package/dist/features/admin/components/AdminProductsView.js +78 -7
- package/dist/features/admin/components/AdminSiteSettingsView.js +20 -1
- package/dist/features/admin/components/AdminTaxCodeEditorView.d.ts +10 -0
- package/dist/features/admin/components/AdminTaxCodeEditorView.js +160 -0
- package/dist/features/admin/components/AdminTaxCodesView.d.ts +13 -0
- package/dist/features/admin/components/AdminTaxCodesView.js +86 -0
- package/dist/features/admin/components/AdminTesterChecklistView.js +1 -1
- package/dist/features/admin/components/analytics/AdminTopProductsTable.js +1 -1
- package/dist/features/auth/schemas/firestore.d.ts +0 -1
- package/dist/features/blog/schemas/firestore.d.ts +2 -2
- package/dist/features/catalogue/components/CatalogueItemEditorView.js +14 -3
- package/dist/features/categories/api/[id]/description-template/route.d.ts +45 -0
- package/dist/features/categories/api/[id]/description-template/route.js +252 -0
- package/dist/features/categories/api/route.js +156 -11
- package/dist/features/categories/components/CategoryDetailPageView.js +43 -10
- package/dist/features/categories/components/CategoryTemplatePicker.d.ts +65 -0
- package/dist/features/categories/components/CategoryTemplatePicker.js +170 -0
- package/dist/features/categories/constants/listing-categories.d.ts +17 -0
- package/dist/features/categories/constants/listing-categories.js +53 -0
- package/dist/features/categories/hooks/useCategoryTree.js +28 -2
- package/dist/features/categories/repository/categories.repository.d.ts +62 -11
- package/dist/features/categories/repository/categories.repository.js +178 -35
- package/dist/features/categories/repository/price-index.repository.d.ts +115 -0
- package/dist/features/categories/repository/price-index.repository.js +85 -0
- package/dist/features/categories/schemas/category-content.d.ts +206 -0
- package/dist/features/categories/schemas/category-content.js +28 -0
- package/dist/features/categories/schemas/firestore.d.ts +74 -5
- package/dist/features/categories/schemas/firestore.js +74 -3
- package/dist/features/categories/schemas/index.d.ts +1047 -0
- package/dist/features/categories/schemas/index.js +96 -0
- package/dist/features/categories/server.d.ts +1 -0
- package/dist/features/categories/server.js +7 -0
- package/dist/features/categories/types/index.d.ts +9 -0
- package/dist/features/events/schemas/firestore.d.ts +2 -2
- package/dist/features/faq/schemas/firestore.d.ts +2 -1
- package/dist/features/faq/schemas/firestore.js +20 -8
- package/dist/features/orders/schemas/firestore.d.ts +70 -0
- package/dist/features/orders/types/index.d.ts +55 -0
- package/dist/features/products/columns/index.d.ts +0 -1
- package/dist/features/products/columns/index.js +5 -1
- package/dist/features/products/components/ProductFilters.d.ts +0 -9
- package/dist/features/products/components/ProductForm.d.ts +27 -1
- package/dist/features/products/components/ProductForm.js +26 -12
- package/dist/features/products/constants/sieve.d.ts +0 -6
- package/dist/features/products/constants/sieve.js +35 -2
- package/dist/features/products/repository/products.repository.d.ts +115 -1
- package/dist/features/products/repository/products.repository.js +491 -39
- package/dist/features/products/schemas/firestore.d.ts +65 -5
- package/dist/features/products/schemas/firestore.js +9 -0
- package/dist/features/products/schemas/product-features.d.ts +140 -2
- package/dist/features/products/schemas/product-features.js +62 -1
- package/dist/features/promotions/repository/coupons.repository.js +6 -1
- package/dist/features/promotions/schemas/firestore.d.ts +44 -1
- package/dist/features/promotions/schemas/firestore.js +76 -2
- package/dist/features/seller/actions/seller-actions.js +17 -2
- package/dist/features/seller/components/PrintCenterView.d.ts +3 -2
- package/dist/features/seller/components/PrintCenterView.js +3 -2
- package/dist/features/seller/components/SellerAnalyticsAlertsView.js +21 -11
- package/dist/features/seller/components/SellerOrdersView.js +6 -1
- package/dist/features/seller/components/SellerPayoutSettingsView.js +19 -3
- package/dist/features/seller/components/SellerProductShell.d.ts +50 -2
- package/dist/features/seller/components/SellerProductShell.js +101 -9
- package/dist/features/seller/components/SellerProductsFilterDrawer.d.ts +13 -1
- package/dist/features/seller/components/SellerProductsFilterDrawer.js +2 -2
- package/dist/features/seller/components/SellerProductsView.js +108 -3
- package/dist/features/seller/types/index.d.ts +17 -0
- package/dist/features/seller/utils/product-draft-mapping.d.ts +13 -0
- package/dist/features/seller/utils/product-draft-mapping.js +17 -0
- package/dist/features/store-extensions/constants/alert-metrics.d.ts +86 -0
- package/dist/features/store-extensions/constants/alert-metrics.js +142 -0
- package/dist/features/store-extensions/schemas/firestore.d.ts +19 -1
- package/dist/features/stores/schemas/firestore.d.ts +31 -2
- package/dist/features/tax-codes/repository/tax-codes.repository.d.ts +55 -0
- package/dist/features/tax-codes/repository/tax-codes.repository.js +131 -0
- package/dist/features/tax-codes/schemas/firestore.d.ts +125 -0
- package/dist/features/tax-codes/schemas/firestore.js +82 -0
- package/dist/features/tax-codes/schemas/tax-code-form.d.ts +63 -0
- package/dist/features/tax-codes/schemas/tax-code-form.js +152 -0
- package/dist/features/tax-codes/schemas/tax-codes.validators.d.ts +107 -0
- package/dist/features/tax-codes/schemas/tax-codes.validators.js +85 -0
- package/dist/features/tester/actions/index.d.ts +0 -1
- package/dist/features/tester/actions/index.js +0 -1
- package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +1 -1
- package/dist/features/tester/components/AdminTesterFeedbackListView.js +1 -1
- package/dist/features/tester/components/index.d.ts +0 -4
- package/dist/features/tester/components/index.js +0 -2
- package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +1 -20
- package/dist/features/tester/repository/tester-checklist-item.repository.js +0 -52
- package/dist/features/tester/schemas/firestore.d.ts +0 -6
- package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.js +3 -3
- package/dist/features/tester/seed-data/authored/admin__site-system.js +4 -5
- package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js +2 -2
- package/dist/features/tester/seed-data/authored/index.js +0 -2
- package/dist/features/tester/seed-data/authored/page-wiring__reachability.js +0 -19
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +8 -27
- package/dist/index.d.ts +21 -7
- package/dist/index.js +53 -7
- package/dist/next/routing/route-map.d.ts +2 -4
- package/dist/next/routing/route-map.js +1 -2
- package/dist/repositories/index.d.ts +1 -0
- package/dist/repositories/index.js +1 -0
- package/dist/schemas/registry.d.ts +506 -0
- package/dist/seed/_helpers/brand-rows.d.ts +35 -0
- package/dist/seed/_helpers/brand-rows.js +576 -0
- package/dist/seed/_helpers/category-content.d.ts +39 -0
- package/dist/seed/_helpers/category-content.js +634 -0
- package/dist/seed/_helpers/category-forest.d.ts +2 -0
- package/dist/seed/_helpers/category-forest.js +585 -243
- package/dist/seed/_helpers/category-models.d.ts +5 -0
- package/dist/seed/_helpers/category-models.js +1364 -0
- package/dist/seed/_helpers/category-tree.js +17 -1
- package/dist/seed/_helpers/character-features.d.ts +10 -0
- package/dist/seed/_helpers/character-features.js +118 -0
- package/dist/seed/_helpers/search-txt-wrappers.d.ts +10 -0
- package/dist/seed/_helpers/search-txt-wrappers.js +12 -1
- package/dist/seed/categories-seed-data.js +147 -169
- package/dist/seed/index.d.ts +1 -0
- package/dist/seed/index.js +1 -0
- package/dist/seed/manifest.d.ts +1 -1
- package/dist/seed/manifest.js +5 -0
- package/dist/seed/product-features-seed-data.d.ts +1 -8
- package/dist/seed/product-features-seed-data.js +358 -154
- package/dist/seed/products-art-seed-data.js +16 -4
- package/dist/seed/products-auctions-seed-data.js +6 -6
- package/dist/seed/products-classifieds-seed-data.js +17 -7
- package/dist/seed/products-digital-codes-seed-data.js +12 -12
- package/dist/seed/products-preorders-seed-data.js +8 -8
- package/dist/seed/products-prize-draws-seed-data.js +25 -8
- package/dist/seed/products-standard-seed-data.js +10 -60
- package/dist/seed/products-stickers-seed-data.js +6 -6
- package/dist/seed/site-settings-seed-data.js +24 -0
- package/dist/seed/store-extensions-seed-data.js +10 -4
- package/dist/seed/tax-codes-seed-data.d.ts +34 -0
- package/dist/seed/tax-codes-seed-data.js +100 -0
- package/dist/seed/test-data-ttl.d.ts +29 -0
- package/dist/seed/test-data-ttl.js +31 -0
- package/dist/server-entry.d.ts +1 -1
- package/dist/server-entry.js +6 -1
- package/dist/server.d.ts +2 -1
- package/dist/server.js +5 -2
- package/dist/styles.css +40 -0
- package/dist/ui/components/PaginatedSelect.d.ts +14 -0
- package/dist/ui/components/PaginatedSelect.js +0 -0
- package/dist/ui/components/PaginatedSelect.style.css +40 -0
- package/dist/utils/physical-location.d.ts +23 -0
- package/dist/utils/physical-location.js +52 -0
- package/dist/utils/search-txt-builders.d.ts +20 -0
- package/dist/utils/search-txt-builders.js +36 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +4 -1
- package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.d.ts +0 -2
- package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.js +0 -2
- package/dist/features/tester/actions/leaderboard-actions.d.ts +0 -3
- package/dist/features/tester/actions/leaderboard-actions.js +0 -5
- package/dist/features/tester/components/BugHunterLeaderboardView.d.ts +0 -23
- package/dist/features/tester/components/BugHunterLeaderboardView.js +0 -18
- package/dist/features/tester/components/TesterHubView.d.ts +0 -5
- package/dist/features/tester/components/TesterHubView.js +0 -110
- package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.d.ts +0 -2
- package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.js +0 -78
- package/dist/features/tester/seed-data/tester-ttl.d.ts +0 -4
- package/dist/features/tester/seed-data/tester-ttl.js +0 -6
|
@@ -124,13 +124,37 @@ export interface ProductPrintMeta {
|
|
|
124
124
|
/** Limited-edition print run size; omitted for open editions. */
|
|
125
125
|
editionSize?: number;
|
|
126
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Licensed manufacture vs. an unbranded 1st copy vs. not established.
|
|
129
|
+
*
|
|
130
|
+
* 🛑 THERE IS NO "fake" VALUE, and that is a product decision rather than an
|
|
131
|
+
* omission. A poor-quality counterfeit — BAS's own buying guide names cadmium
|
|
132
|
+
* and lead in the metal parts — is not a listing state we offer. It is a
|
|
133
|
+
* report, and it belongs in the scam registry, which already has 27 scam
|
|
134
|
+
* types and an evidence model.
|
|
135
|
+
*
|
|
136
|
+
* "reproduction" is the midfake tier: largely Chinese, no manufacturer's
|
|
137
|
+
* marque, generally decent quality, and some WBO formats permit certain repro
|
|
138
|
+
* parts — so it is sellable WHEN LABELLED. "unverified" exists because the
|
|
139
|
+
* alternative is far worse: without it, an honest seller who does not know
|
|
140
|
+
* would have to pick "original", and silence would be indistinguishable from
|
|
141
|
+
* a genuine claim.
|
|
142
|
+
*
|
|
143
|
+
* 🛑 And the buyer-facing framing to design against is NOT "fake". The
|
|
144
|
+
* counterfeit seller in the crawl makes no authenticity claim at all — zero
|
|
145
|
+
* occurrences of original/Takara/authentic/replica/fake across 1,277 products
|
|
146
|
+
* — and sells itself as an "aftermarket brand" with "upgraded components".
|
|
147
|
+
* "Aftermarket" and "custom" are the words our buyers will be sold, so the
|
|
148
|
+
* help copy has to answer that, not a word nobody uses.
|
|
149
|
+
*/
|
|
150
|
+
export type ProductAuthenticity = "original" | "reproduction" | "unverified";
|
|
127
151
|
export interface ProductDocument extends BaseDocument {
|
|
128
152
|
title: string;
|
|
129
153
|
description: string;
|
|
130
154
|
slug?: string;
|
|
131
|
-
/**
|
|
155
|
+
/** Test-data flag — set on shared admin-seeded test products; swept by testDataCleanup. */
|
|
132
156
|
isTestData?: boolean;
|
|
133
|
-
/** When isTestData, the cutoff after which
|
|
157
|
+
/** When isTestData, the cutoff after which testDataCleanup deletes this doc (cascades to bids). */
|
|
134
158
|
testDataExpiresAt?: Date;
|
|
135
159
|
seoTitle?: string;
|
|
136
160
|
seoDescription?: string;
|
|
@@ -251,10 +275,46 @@ export interface ProductDocument extends BaseDocument {
|
|
|
251
275
|
isSold?: boolean;
|
|
252
276
|
/** When true, an EMI order for this product ships as soon as it's confirmed instead of waiting for every installment to be paid. Default false. */
|
|
253
277
|
allowShipBeforeEmiComplete?: boolean;
|
|
254
|
-
/**
|
|
278
|
+
/**
|
|
279
|
+
* P-8 GST — buyer-facing tax rate on this product (%).
|
|
280
|
+
*
|
|
281
|
+
* 🛑 `undefined` and `0` MEAN DIFFERENT THINGS. Zero is a deliberate
|
|
282
|
+
* exemption (live plants and animals); undefined is "nobody has said".
|
|
283
|
+
* So the test is `== null`, NEVER `input.gstRate || default`, which would
|
|
284
|
+
* silently overwrite every exempt row with the default rate. And there is
|
|
285
|
+
* deliberately no entry for this in `DEFAULT_PRODUCT_DATA` — a default
|
|
286
|
+
* there makes `undefined` unreachable in new rows while existing rows
|
|
287
|
+
* still lack it, which is the `finalSale` trap one field over.
|
|
288
|
+
*
|
|
289
|
+
* Written by `deriveTaxonomy` from the category's `taxCodeId`, but the
|
|
290
|
+
* SCALAR is what every reader and the order-item snapshot use: a tax-code
|
|
291
|
+
* edit must change future derivations, never retroactively re-rate an
|
|
292
|
+
* invoice already issued.
|
|
293
|
+
*/
|
|
255
294
|
gstRate?: 0 | 5 | 12 | 18 | 28;
|
|
256
|
-
/** P-8 GST —
|
|
295
|
+
/** P-8 GST — HSN code for GST-compliant invoices. Resolved alongside `gstRate`. */
|
|
257
296
|
hsnCode?: string;
|
|
297
|
+
/**
|
|
298
|
+
* WHO MADE IT — distinct from `condition` (how worn) and from `brand`
|
|
299
|
+
* (whose marque is on it, which a reproduction has none of). The two axes
|
|
300
|
+
* are independent: an original can be broken and a reproduction can be
|
|
301
|
+
* mint.
|
|
302
|
+
*
|
|
303
|
+
* 🛑 This is BOTH a field and a feature, deliberately, and it is the only
|
|
304
|
+
* place in this schema where that duplication is justified. Structurally
|
|
305
|
+
* it is a classifier and belongs in the `authenticity` FeatureGroup — but
|
|
306
|
+
* a feature can be OMITTED and nothing notices. A mislabelled
|
|
307
|
+
* reproduction is a refund; a mislabelled counterfeit is a child handling
|
|
308
|
+
* leaded metal. The enum makes the question unskippable and writes the
|
|
309
|
+
* matching feature, so the facet, the badge and search all read one
|
|
310
|
+
* vocabulary.
|
|
311
|
+
*
|
|
312
|
+
* Validated by the competitor crawl: worldhobbyshop ships a nav category
|
|
313
|
+
* called "MidFake Beyblades" and an FAQ promising that authenticity and
|
|
314
|
+
* condition are both disclosed per listing. Hence "Midfake" as the LABEL
|
|
315
|
+
* on the reproduction tier — it is the word buyers search.
|
|
316
|
+
*/
|
|
317
|
+
authenticity?: ProductAuthenticity;
|
|
258
318
|
/** Print-specific metadata for "art" / "stickers" listings — printed-only physical goods. Optional on every listing type; only populated for art/stickers. */
|
|
259
319
|
printMeta?: ProductPrintMeta;
|
|
260
320
|
promotionEndDate?: Date;
|
|
@@ -423,7 +483,7 @@ export declare const PRODUCT_COLLECTION: "products";
|
|
|
423
483
|
export declare const PRODUCT_INDEXED_FIELDS: readonly ["storeId", "status", "category", "featured", "listingType", "isPromoted", "isOnSale", "isSold", "searchTxt", "createdAt", "barcodeId"];
|
|
424
484
|
export declare const DEFAULT_PRODUCT_DATA: Partial<ProductDocument>;
|
|
425
485
|
export declare const PRODUCT_PUBLIC_FIELDS: readonly ["allowOffers", "minOfferPercent", "id", "title", "description", "category", "subcategory", "brand", "price", "currency", "stockQuantity", "availableQuantity", "images", "status", "storeName", "featured", "tags", "specifications", "features", "shippingInfo", "returnPolicy", "finalSale", "listingType", "auctionEndDate", "startingBid", "currentBid", "bidCount", "reservePrice", "buyNowPrice", "minBidIncrement", "autoExtendable", "auctionExtensionMinutes", "auctionShippingPaidBy", "bidsHaveStarted", "grading", "card", "classified", "digitalCode", "liveItem", "catalogProductId", "preOrderDeliveryDate", "preOrderDepositPercent", "preOrderDepositAmount", "preOrderMaxQuantity", "preOrderCurrentCount", "preOrderProductionStatus", "preOrderCancellable", "condition", "insurance", "insuranceCost", "shippingPaidBy", "isPromoted", "isOnSale", "isSold", "slug", "seoTitle", "seoDescription", "seoKeywords", "viewCount", "customFields", "customSections", "sublistingCategoryId", "groupId", "isGroupParent", "groupParentSlug", "groupChildSlugs", "groupTitle", "createdAt"];
|
|
426
|
-
export declare const PRODUCT_UPDATABLE_FIELDS: readonly ["title", "description", "category", "subcategory", "brand", "price", "stockQuantity", "images", "status", "tags", "specifications", "features", "shippingInfo", "returnPolicy", "finalSale", "pickupAddressId", "condition", "insurance", "shippingPaidBy", "autoExtendable", "auctionExtensionMinutes", "auctionShippingPaidBy", "reservePrice", "buyNowPrice", "minBidIncrement", "bidsHaveStarted", "grading", "card", "classified", "digitalCode", "liveItem", "catalogProductId", "listingType", "preOrderDeliveryDate", "preOrderDepositPercent", "preOrderDepositAmount", "preOrderMaxQuantity", "preOrderProductionStatus", "preOrderCancellable", "isOnSale", "isSold", "allowShipBeforeEmiComplete", "printMeta", "seoTitle", "seoDescription", "seoKeywords", "customFields", "customSections", "sublistingCategoryId", "groupId", "isGroupParent", "groupParentSlug", "groupChildSlugs", "groupTitle", "barcodeId"];
|
|
486
|
+
export declare const PRODUCT_UPDATABLE_FIELDS: readonly ["title", "description", "category", "subcategory", "brand", "price", "stockQuantity", "images", "status", "tags", "specifications", "features", "shippingInfo", "returnPolicy", "finalSale", "pickupAddressId", "condition", "insurance", "shippingPaidBy", "autoExtendable", "auctionExtensionMinutes", "auctionShippingPaidBy", "reservePrice", "buyNowPrice", "minBidIncrement", "bidsHaveStarted", "grading", "card", "classified", "digitalCode", "liveItem", "catalogProductId", "listingType", "preOrderDeliveryDate", "preOrderDepositPercent", "preOrderDepositAmount", "preOrderMaxQuantity", "preOrderProductionStatus", "preOrderCancellable", "isOnSale", "isSold", "allowShipBeforeEmiComplete", "gstRate", "hsnCode", "authenticity", "printMeta", "seoTitle", "seoDescription", "seoKeywords", "customFields", "customSections", "sublistingCategoryId", "groupId", "isGroupParent", "groupParentSlug", "groupChildSlugs", "groupTitle", "barcodeId"];
|
|
427
487
|
export type ProductCreateInput = Omit<ProductDocument, "id" | "createdAt" | "updatedAt" | "availableQuantity" | "bidCount" | "currentBid" | "auctionOriginalEndDate">;
|
|
428
488
|
export type ProductUpdateInput = Partial<Pick<ProductDocument, (typeof PRODUCT_UPDATABLE_FIELDS)[number]>>;
|
|
429
489
|
export type ProductAdminUpdateInput = Partial<Omit<ProductDocument, "id" | "createdAt">>;
|
|
@@ -173,6 +173,15 @@ export const PRODUCT_UPDATABLE_FIELDS = [
|
|
|
173
173
|
"isOnSale",
|
|
174
174
|
"isSold",
|
|
175
175
|
"allowShipBeforeEmiComplete",
|
|
176
|
+
/*
|
|
177
|
+
* B3 — these three were MISSING, so the fields existed on the document
|
|
178
|
+
* and were not type-legal to update: ProductUpdateInput is derived from
|
|
179
|
+
* this list, so an admin or seller form could accept a GST rate and the
|
|
180
|
+
* repository would refuse to write it.
|
|
181
|
+
*/
|
|
182
|
+
"gstRate",
|
|
183
|
+
"hsnCode",
|
|
184
|
+
"authenticity",
|
|
176
185
|
"printMeta",
|
|
177
186
|
"seoTitle",
|
|
178
187
|
"seoDescription",
|
|
@@ -17,7 +17,22 @@
|
|
|
17
17
|
export declare const PRODUCT_FEATURES_COLLECTION: "productFeatures";
|
|
18
18
|
export declare const PRODUCT_FEATURE_PREFIX: "feature-";
|
|
19
19
|
export declare const MAX_STORE_CUSTOM_FEATURES = 20;
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Hard cap on `product.features[].length`.
|
|
22
|
+
*
|
|
23
|
+
* 🛑 RAISED 10 → 24 (B3). Ten was sized for *badges* — a handful of
|
|
24
|
+
* "Free Shipping"-style trust marks. This collection is now the platform's one
|
|
25
|
+
* controlled VOCABULARY (`tags` is retired), and a single realistic listing
|
|
26
|
+
* spends six before anything optional: a reproduction, new-in-box,
|
|
27
|
+
* japan-import, attack-type, right-spin, limited-release bey. A TCG single
|
|
28
|
+
* adds language + format + grade on top.
|
|
29
|
+
*
|
|
30
|
+
* 24 rather than unbounded because `features[]` is `array-contains`-indexed and
|
|
31
|
+
* feeds `buildProductSearchTxt`, which truncates at 600 tokens SILENTLY — an
|
|
32
|
+
* unbounded feature list would quietly evict real title/description tokens
|
|
33
|
+
* from the search index.
|
|
34
|
+
*/
|
|
35
|
+
export declare const MAX_FEATURES_PER_PRODUCT = 24;
|
|
21
36
|
export type ProductFeatureScope = "platform" | "store";
|
|
22
37
|
export type ProductFeatureCategory = "shipping" | "seller" | "condition" | "platform" | "auction" | "preorder" | "custom";
|
|
23
38
|
export type ProductFeatureProductType = "product" | "auction" | "preorder" | "prize-draw" | "classified" | "digital-code" | "live" | "art" | "stickers" | "all";
|
|
@@ -27,6 +42,90 @@ export type ProductFeatureProductType = "product" | "auction" | "preorder" | "pr
|
|
|
27
42
|
*/
|
|
28
43
|
export type ProductFeatureIcon = string;
|
|
29
44
|
export declare function isFeatureIconPath(icon: string): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* The vocabulary's namespace.
|
|
47
|
+
*
|
|
48
|
+
* 🛑 NOT the same axis as `category` above, which groups features by the
|
|
49
|
+
* SURFACE that shows them (shipping/seller/platform/auction/preorder) and is
|
|
50
|
+
* what drives badge placement. `group` namespaces what a feature MEANS, and
|
|
51
|
+
* drives the filter drawer: one facet section per group, rather than one flat
|
|
52
|
+
* 60-value checkbox list.
|
|
53
|
+
*
|
|
54
|
+
* Namespacing is load-bearing because **the same word means different things
|
|
55
|
+
* per vertical**: "sealed" on a TCG booster box is a factory seal, on a bey it
|
|
56
|
+
* is an unopened blister; "premium" is a Hot Wheels sub-line, not a quality
|
|
57
|
+
* claim. A single flat vocabulary would have to pick one meaning.
|
|
58
|
+
*
|
|
59
|
+
* Values derived from the measured competitor crawl — see
|
|
60
|
+
* `docs/research/competitor-crawl-2026-10/vocabulary.md`, which records the
|
|
61
|
+
* `n` behind each group.
|
|
62
|
+
*/
|
|
63
|
+
export type FeatureGroup =
|
|
64
|
+
/** How worn / how packaged. Independent of `authenticity`. */
|
|
65
|
+
"condition"
|
|
66
|
+
/** Who made it. Mirrors `ProductDocument.authenticity`; see the note there. */
|
|
67
|
+
| "authenticity"
|
|
68
|
+
/** Regular / limited / early / random-booster / RLC — a real price multiplier. */
|
|
69
|
+
| "release"
|
|
70
|
+
/** Beyblade battle type: attack · defense · stamina · balance. */
|
|
71
|
+
| "type"
|
|
72
|
+
/** right-spin · left-spin · dual-spin. */
|
|
73
|
+
| "spin"
|
|
74
|
+
/** Merchandising state: clearance, on-sale, best-seller. */
|
|
75
|
+
| "commercial"
|
|
76
|
+
/** Where it came from: japan-import · imported · domestic. */
|
|
77
|
+
| "sourcing"
|
|
78
|
+
/** Hot Wheels chase variants, incl. the India-only blister card. */
|
|
79
|
+
| "chase"
|
|
80
|
+
/** PSA/BGS/CGC grade. Pairs with the `sublisting` + `itemCode` axis. */
|
|
81
|
+
| "grade"
|
|
82
|
+
/** TCG print language. */
|
|
83
|
+
| "tcg-lang"
|
|
84
|
+
/** TCG product format — booster box vs single differs ~20× in price. */
|
|
85
|
+
| "tcg-format"
|
|
86
|
+
/** Figure scale, finish and packaging state. */
|
|
87
|
+
| "figure"
|
|
88
|
+
/** WBO / limited format legality. */
|
|
89
|
+
| "tournament"
|
|
90
|
+
/**
|
|
91
|
+
* The CHARACTER the item is of, or belongs to.
|
|
92
|
+
*
|
|
93
|
+
* ONE axis across every vertical here, which is the point: a bey's anime
|
|
94
|
+
* blader (Dragoon G -> Tyson Granger), a comic figure's hero (a Marvel
|
|
95
|
+
* Legends Spider-Man -> Spider-Man), an anime figure's character
|
|
96
|
+
* (Nendoroid Luffy -> Monkey D. Luffy), a card's franchise face. A
|
|
97
|
+
* collector who wants "everything Spider-Man" wants the figure, the card
|
|
98
|
+
* and the die-cast together, and no category tree can give them that —
|
|
99
|
+
* each of those three lives under a different root.
|
|
100
|
+
*
|
|
101
|
+
* 93 bladers ship with B5, GENERATED from the corpus. Figure and card
|
|
102
|
+
* characters join in B6, as the same group with the same mechanics; only
|
|
103
|
+
* the slug prefix differs (`blader-` vs `character-`), so a URL says which
|
|
104
|
+
* kind of name it is.
|
|
105
|
+
*
|
|
106
|
+
* 🛑 A FEATURE and never a tree node, for the reason §4.4 gives: a
|
|
107
|
+
* character crosses every generation. Tyson Granger owns nine beys spanning
|
|
108
|
+
* Spin Gear, Magnacore, Engine Gear and HMS, so a `category-tyson-granger`
|
|
109
|
+
* node would either duplicate all nine under a second parent or break the
|
|
110
|
+
* one-chain-per-product invariant that lets a category page match on its
|
|
111
|
+
* own id alone.
|
|
112
|
+
*
|
|
113
|
+
* As a feature it is clickable from any listing surface
|
|
114
|
+
* (`?features=feature-blader-tyson-granger`), it faceted-filters, it is
|
|
115
|
+
* indexed into `searchTxt` so "Tyson" finds Dragoon, and it composes with
|
|
116
|
+
* the generation tree rather than competing with it — browse Beyblade
|
|
117
|
+
* Original, then narrow to Tyson's.
|
|
118
|
+
*/
|
|
119
|
+
| "character";
|
|
120
|
+
export declare const FEATURE_GROUP_VALUES: readonly ["condition", "authenticity", "release", "type", "spin", "commercial", "sourcing", "chase", "grade", "tcg-lang", "tcg-format", "figure", "tournament", "character"];
|
|
121
|
+
/**
|
|
122
|
+
* Human-facing section headings for the filter drawer, one per group.
|
|
123
|
+
*
|
|
124
|
+
* A `Record<FeatureGroup, string>` on purpose: a fourteenth group cannot
|
|
125
|
+
* compile without a label, which is the failure mode Root Cause #61 describes
|
|
126
|
+
* (ten hand-written enumerations of one union, nine of which drifted).
|
|
127
|
+
*/
|
|
128
|
+
export declare const FEATURE_GROUP_LABEL: Record<FeatureGroup, string>;
|
|
30
129
|
export interface ProductFeatureDocument {
|
|
31
130
|
id: string;
|
|
32
131
|
slug: string;
|
|
@@ -37,17 +136,48 @@ export interface ProductFeatureDocument {
|
|
|
37
136
|
/** CSS variable token key, e.g. "--appkit-color-primary". Optional — defaults to neutral. */
|
|
38
137
|
iconColor?: string;
|
|
39
138
|
category: ProductFeatureCategory;
|
|
139
|
+
/**
|
|
140
|
+
* The vocabulary namespace — see `FeatureGroup`. Optional only so the 10
|
|
141
|
+
* pre-B3 rows keep parsing; the seed sets it on every row and the admin form
|
|
142
|
+
* requires it. An ungrouped feature renders in no facet section.
|
|
143
|
+
*/
|
|
144
|
+
group?: FeatureGroup;
|
|
40
145
|
scope: ProductFeatureScope;
|
|
41
146
|
productTypes: ProductFeatureProductType[];
|
|
42
147
|
/** Required when scope === "store". */
|
|
43
148
|
storeId?: string;
|
|
44
149
|
isActive: boolean;
|
|
45
150
|
displayOrder: number;
|
|
151
|
+
/**
|
|
152
|
+
* Alternative spellings that should FIND this feature.
|
|
153
|
+
*
|
|
154
|
+
* Three sources, all measured — see
|
|
155
|
+
* `docs/research/competitor-crawl-2026-10/counterfeit-signals.md`:
|
|
156
|
+
* · part-code pairs, so "rubber flat" finds `RF` and `RF` finds "rubber flat"
|
|
157
|
+
* · JP / romaji names
|
|
158
|
+
* · 🛑 the counterfeit seller's systematic MISSPELLINGS (`Pegasis`,
|
|
159
|
+
* `Jupitar`, `Fireblase`). A counterfeiter's trademark-evasion spelling
|
|
160
|
+
* is exactly what a real buyer mistypes, so indexing them makes our
|
|
161
|
+
* search tolerant of real typos at zero query-time cost.
|
|
162
|
+
*
|
|
163
|
+
* 🛑 Budgeted, not unbounded: these fold into `searchTxt`, which truncates at
|
|
164
|
+
* 600 tokens SILENTLY, so an over-long alias list evicts tokens nobody chose
|
|
165
|
+
* to drop.
|
|
166
|
+
*/
|
|
167
|
+
aliases?: string[];
|
|
168
|
+
/**
|
|
169
|
+
* Edge-n-gram search index for this collection, so the picker can search at
|
|
170
|
+
* all. 🛑 Must be built from the LABEL and `aliases`, never from the slug:
|
|
171
|
+
* `feature-nib` tokenises to `feature`/`nib`, so the label "New in Box" is
|
|
172
|
+
* unfindable, which is the same defect `buildProductSearchTxt` has today for
|
|
173
|
+
* `features[]` (it indexes the raw ids).
|
|
174
|
+
*/
|
|
175
|
+
searchTxt?: string[];
|
|
46
176
|
createdAt: Date;
|
|
47
177
|
updatedAt: Date;
|
|
48
178
|
}
|
|
49
179
|
export type ProductFeatureCreateInput = Omit<ProductFeatureDocument, "id" | "slug" | "createdAt" | "updatedAt">;
|
|
50
|
-
export type ProductFeatureUpdateInput = Partial<Pick<ProductFeatureDocument, "label" | "description" | "icon" | "iconColor" | "category" | "productTypes" | "isActive" | "displayOrder">>;
|
|
180
|
+
export type ProductFeatureUpdateInput = Partial<Pick<ProductFeatureDocument, "label" | "description" | "icon" | "iconColor" | "category" | "group" | "aliases" | "productTypes" | "isActive" | "displayOrder">>;
|
|
51
181
|
export declare const PRODUCT_FEATURE_SIEVE_FIELDS: {
|
|
52
182
|
readonly label: {
|
|
53
183
|
readonly canFilter: true;
|
|
@@ -69,6 +199,14 @@ export declare const PRODUCT_FEATURE_SIEVE_FIELDS: {
|
|
|
69
199
|
readonly canFilter: true;
|
|
70
200
|
readonly canSort: false;
|
|
71
201
|
};
|
|
202
|
+
readonly group: {
|
|
203
|
+
readonly canFilter: true;
|
|
204
|
+
readonly canSort: false;
|
|
205
|
+
};
|
|
206
|
+
readonly searchTxt: {
|
|
207
|
+
readonly canFilter: true;
|
|
208
|
+
readonly canSort: false;
|
|
209
|
+
};
|
|
72
210
|
readonly isActive: {
|
|
73
211
|
readonly canFilter: true;
|
|
74
212
|
readonly canSort: false;
|
|
@@ -17,16 +17,77 @@
|
|
|
17
17
|
export const PRODUCT_FEATURES_COLLECTION = "productFeatures";
|
|
18
18
|
export const PRODUCT_FEATURE_PREFIX = "feature-";
|
|
19
19
|
export const MAX_STORE_CUSTOM_FEATURES = 20;
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Hard cap on `product.features[].length`.
|
|
22
|
+
*
|
|
23
|
+
* 🛑 RAISED 10 → 24 (B3). Ten was sized for *badges* — a handful of
|
|
24
|
+
* "Free Shipping"-style trust marks. This collection is now the platform's one
|
|
25
|
+
* controlled VOCABULARY (`tags` is retired), and a single realistic listing
|
|
26
|
+
* spends six before anything optional: a reproduction, new-in-box,
|
|
27
|
+
* japan-import, attack-type, right-spin, limited-release bey. A TCG single
|
|
28
|
+
* adds language + format + grade on top.
|
|
29
|
+
*
|
|
30
|
+
* 24 rather than unbounded because `features[]` is `array-contains`-indexed and
|
|
31
|
+
* feeds `buildProductSearchTxt`, which truncates at 600 tokens SILENTLY — an
|
|
32
|
+
* unbounded feature list would quietly evict real title/description tokens
|
|
33
|
+
* from the search index.
|
|
34
|
+
*/
|
|
35
|
+
export const MAX_FEATURES_PER_PRODUCT = 24;
|
|
21
36
|
export function isFeatureIconPath(icon) {
|
|
22
37
|
return icon.trimStart().startsWith("M ") || icon.trimStart().startsWith("m ");
|
|
23
38
|
}
|
|
39
|
+
export const FEATURE_GROUP_VALUES = [
|
|
40
|
+
"condition",
|
|
41
|
+
"authenticity",
|
|
42
|
+
"release",
|
|
43
|
+
"type",
|
|
44
|
+
"spin",
|
|
45
|
+
"commercial",
|
|
46
|
+
"sourcing",
|
|
47
|
+
"chase",
|
|
48
|
+
"grade",
|
|
49
|
+
"tcg-lang",
|
|
50
|
+
"tcg-format",
|
|
51
|
+
"figure",
|
|
52
|
+
"tournament",
|
|
53
|
+
"character",
|
|
54
|
+
];
|
|
55
|
+
/**
|
|
56
|
+
* Human-facing section headings for the filter drawer, one per group.
|
|
57
|
+
*
|
|
58
|
+
* A `Record<FeatureGroup, string>` on purpose: a fourteenth group cannot
|
|
59
|
+
* compile without a label, which is the failure mode Root Cause #61 describes
|
|
60
|
+
* (ten hand-written enumerations of one union, nine of which drifted).
|
|
61
|
+
*/
|
|
62
|
+
export const FEATURE_GROUP_LABEL = {
|
|
63
|
+
condition: "Condition",
|
|
64
|
+
authenticity: "Authenticity",
|
|
65
|
+
release: "Release type",
|
|
66
|
+
type: "Battle type",
|
|
67
|
+
spin: "Spin direction",
|
|
68
|
+
commercial: "Offers",
|
|
69
|
+
sourcing: "Sourcing",
|
|
70
|
+
chase: "Chase & rarity",
|
|
71
|
+
grade: "Grading",
|
|
72
|
+
"tcg-lang": "Card language",
|
|
73
|
+
"tcg-format": "Card format",
|
|
74
|
+
figure: "Figure details",
|
|
75
|
+
tournament: "Tournament legality",
|
|
76
|
+
character: "Blader & character",
|
|
77
|
+
};
|
|
24
78
|
export const PRODUCT_FEATURE_SIEVE_FIELDS = {
|
|
25
79
|
label: { canFilter: true, canSort: true },
|
|
26
80
|
slug: { canFilter: true, canSort: false },
|
|
27
81
|
scope: { canFilter: true, canSort: false },
|
|
28
82
|
storeId: { canFilter: true, canSort: false },
|
|
29
83
|
category: { canFilter: true, canSort: false },
|
|
84
|
+
group: { canFilter: true, canSort: false },
|
|
85
|
+
/*
|
|
86
|
+
* The picker's search. `array-contains` on an edge-n-gram index, the same
|
|
87
|
+
* shape every other searchable collection uses — a flat Checkbox grid is
|
|
88
|
+
* fine for 10 rows and unusable at ~60.
|
|
89
|
+
*/
|
|
90
|
+
searchTxt: { canFilter: true, canSort: false },
|
|
30
91
|
isActive: { canFilter: true, canSort: false },
|
|
31
92
|
displayOrder: { canFilter: false, canSort: true },
|
|
32
93
|
createdAt: { canFilter: false, canSort: true },
|
|
@@ -458,7 +458,12 @@ export class CouponsRepository extends BaseRepository {
|
|
|
458
458
|
message: "Minimum purchase requirement not met",
|
|
459
459
|
};
|
|
460
460
|
}
|
|
461
|
-
|
|
461
|
+
/*
|
|
462
|
+
* `eligible` is passed through, not just its subtotal — `buy_x_get_y`
|
|
463
|
+
* needs the individual unit prices to know which units are free, and
|
|
464
|
+
* without them it silently returned 0 for every BOGO coupon ever created.
|
|
465
|
+
*/
|
|
466
|
+
const discountAmount = calculateDiscount(coupon, eligibleSubtotal, eligible);
|
|
462
467
|
return {
|
|
463
468
|
valid: true,
|
|
464
469
|
coupon,
|
|
@@ -163,5 +163,48 @@ export declare function sellerCouponCodePrefix(storeSlug: string): string;
|
|
|
163
163
|
export declare function buildSellerCouponCode(storeSlug: string, sellerCode: string): string;
|
|
164
164
|
export declare function isCouponValid(coupon: CouponDocument): boolean;
|
|
165
165
|
export declare function canUserUseCoupon(coupon: CouponDocument, userUsageCount: number): boolean;
|
|
166
|
-
|
|
166
|
+
/** One cart line, as much of it as a BOGO calculation needs. */
|
|
167
|
+
export interface DiscountableLine {
|
|
168
|
+
price: number;
|
|
169
|
+
quantity: number;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* "Buy X, get Y free" over the ELIGIBLE lines.
|
|
173
|
+
*
|
|
174
|
+
* 🛑 THE BUG THIS CLOSES: a BOGO coupon discounted ₹0.
|
|
175
|
+
*
|
|
176
|
+
* `calculateDiscount` had `case "buy_x_get_y": discountAmount = 0`, sharing a
|
|
177
|
+
* branch with `free_shipping`. That zero is CORRECT for free shipping — the
|
|
178
|
+
* waiver is honoured downstream as a shipping line, not as a discount — but
|
|
179
|
+
* `buy_x_get_y` had no second path anywhere: `coupon.bxgy` had **zero read
|
|
180
|
+
* sites in the repo**. So an admin could create and save a BOGO coupon,
|
|
181
|
+
* `validateCouponForCart` would answer "Coupon is valid", `CouponCard` would
|
|
182
|
+
* render a BOGO badge, and the buyer paid full price.
|
|
183
|
+
*
|
|
184
|
+
* WHY THIS NEEDS ITS OWN FUNCTION: BOGO cannot be computed from a total. Which
|
|
185
|
+
* units are free depends on individual unit prices, so the line items are
|
|
186
|
+
* required input. `calculateDiscount(coupon, total)` structurally could not
|
|
187
|
+
* express it, which is how the `= 0` came to look like a complete branch.
|
|
188
|
+
*
|
|
189
|
+
* THE RULE: units are grouped in blocks of `buyQuantity + getQuantity`, and in
|
|
190
|
+
* each complete block the CHEAPEST `getQuantity` units are free. Cheapest-free
|
|
191
|
+
* is the conventional retail reading of "buy 2 get 1 free" and the one a buyer
|
|
192
|
+
* expects to be charged; awarding the dearest would make the same promotion
|
|
193
|
+
* worth a different amount depending on basket order.
|
|
194
|
+
*
|
|
195
|
+
* 🛑 An INCOMPLETE block earns nothing. "Buy 2 get 1" on 2 units is a block of
|
|
196
|
+
* 2 against a required 3, so the discount is 0 — not a partial credit.
|
|
197
|
+
*/
|
|
198
|
+
export declare function calculateBxgyDiscount(bxgy: BXGYConfig | undefined, lines: readonly DiscountableLine[]): number;
|
|
199
|
+
export declare function calculateDiscount(coupon: CouponDocument, orderTotal: number,
|
|
200
|
+
/**
|
|
201
|
+
* The eligible lines, for the types that need them.
|
|
202
|
+
*
|
|
203
|
+
* Optional because one of the two callers — `validateCoupon(code, userId,
|
|
204
|
+
* orderTotal)` — only ever has a total. That caller gets 0 for a BOGO
|
|
205
|
+
* coupon, which is honest: the discount is genuinely unknowable without the
|
|
206
|
+
* basket. `validateCouponForCart` DOES hold the eligible lines (it already
|
|
207
|
+
* reduces them to `eligibleSubtotal`) and passes them.
|
|
208
|
+
*/
|
|
209
|
+
lines?: readonly DiscountableLine[]): number;
|
|
167
210
|
export declare function getTieredDiscount(tiers: TieredDiscount[] | undefined, orderTotal: number): number;
|
|
@@ -133,7 +133,75 @@ export function canUserUseCoupon(coupon, userUsageCount) {
|
|
|
133
133
|
return true;
|
|
134
134
|
return userUsageCount < coupon.usage.perUserLimit;
|
|
135
135
|
}
|
|
136
|
-
|
|
136
|
+
/**
|
|
137
|
+
* "Buy X, get Y free" over the ELIGIBLE lines.
|
|
138
|
+
*
|
|
139
|
+
* 🛑 THE BUG THIS CLOSES: a BOGO coupon discounted ₹0.
|
|
140
|
+
*
|
|
141
|
+
* `calculateDiscount` had `case "buy_x_get_y": discountAmount = 0`, sharing a
|
|
142
|
+
* branch with `free_shipping`. That zero is CORRECT for free shipping — the
|
|
143
|
+
* waiver is honoured downstream as a shipping line, not as a discount — but
|
|
144
|
+
* `buy_x_get_y` had no second path anywhere: `coupon.bxgy` had **zero read
|
|
145
|
+
* sites in the repo**. So an admin could create and save a BOGO coupon,
|
|
146
|
+
* `validateCouponForCart` would answer "Coupon is valid", `CouponCard` would
|
|
147
|
+
* render a BOGO badge, and the buyer paid full price.
|
|
148
|
+
*
|
|
149
|
+
* WHY THIS NEEDS ITS OWN FUNCTION: BOGO cannot be computed from a total. Which
|
|
150
|
+
* units are free depends on individual unit prices, so the line items are
|
|
151
|
+
* required input. `calculateDiscount(coupon, total)` structurally could not
|
|
152
|
+
* express it, which is how the `= 0` came to look like a complete branch.
|
|
153
|
+
*
|
|
154
|
+
* THE RULE: units are grouped in blocks of `buyQuantity + getQuantity`, and in
|
|
155
|
+
* each complete block the CHEAPEST `getQuantity` units are free. Cheapest-free
|
|
156
|
+
* is the conventional retail reading of "buy 2 get 1 free" and the one a buyer
|
|
157
|
+
* expects to be charged; awarding the dearest would make the same promotion
|
|
158
|
+
* worth a different amount depending on basket order.
|
|
159
|
+
*
|
|
160
|
+
* 🛑 An INCOMPLETE block earns nothing. "Buy 2 get 1" on 2 units is a block of
|
|
161
|
+
* 2 against a required 3, so the discount is 0 — not a partial credit.
|
|
162
|
+
*/
|
|
163
|
+
export function calculateBxgyDiscount(bxgy, lines) {
|
|
164
|
+
if (!bxgy)
|
|
165
|
+
return 0;
|
|
166
|
+
const buy = Math.floor(bxgy.buyQuantity);
|
|
167
|
+
const get = Math.floor(bxgy.getQuantity);
|
|
168
|
+
// A non-positive `get` awards nothing; a non-positive `buy` would make every
|
|
169
|
+
// unit free, which is a misconfiguration, not a promotion.
|
|
170
|
+
if (!Number.isFinite(buy) || !Number.isFinite(get) || buy <= 0 || get <= 0)
|
|
171
|
+
return 0;
|
|
172
|
+
// Expand to one entry per UNIT — a line of quantity 3 contributes 3 prices,
|
|
173
|
+
// because the free units are counted in units, not in lines.
|
|
174
|
+
const unitPrices = [];
|
|
175
|
+
for (const line of lines) {
|
|
176
|
+
const qty = Math.floor(line.quantity);
|
|
177
|
+
if (!Number.isFinite(qty) || qty <= 0)
|
|
178
|
+
continue;
|
|
179
|
+
if (!Number.isFinite(line.price) || line.price < 0)
|
|
180
|
+
continue;
|
|
181
|
+
for (let i = 0; i < qty; i++)
|
|
182
|
+
unitPrices.push(line.price);
|
|
183
|
+
}
|
|
184
|
+
const blockSize = buy + get;
|
|
185
|
+
const freeUnits = Math.floor(unitPrices.length / blockSize) * get;
|
|
186
|
+
if (freeUnits <= 0)
|
|
187
|
+
return 0;
|
|
188
|
+
unitPrices.sort((a, b) => a - b);
|
|
189
|
+
let discount = 0;
|
|
190
|
+
for (let i = 0; i < freeUnits; i++)
|
|
191
|
+
discount += unitPrices[i];
|
|
192
|
+
return discount;
|
|
193
|
+
}
|
|
194
|
+
export function calculateDiscount(coupon, orderTotal,
|
|
195
|
+
/**
|
|
196
|
+
* The eligible lines, for the types that need them.
|
|
197
|
+
*
|
|
198
|
+
* Optional because one of the two callers — `validateCoupon(code, userId,
|
|
199
|
+
* orderTotal)` — only ever has a total. That caller gets 0 for a BOGO
|
|
200
|
+
* coupon, which is honest: the discount is genuinely unknowable without the
|
|
201
|
+
* basket. `validateCouponForCart` DOES hold the eligible lines (it already
|
|
202
|
+
* reduces them to `eligibleSubtotal`) and passes them.
|
|
203
|
+
*/
|
|
204
|
+
lines) {
|
|
137
205
|
if (coupon.discount.minPurchase && orderTotal < coupon.discount.minPurchase)
|
|
138
206
|
return 0;
|
|
139
207
|
let discountAmount = 0;
|
|
@@ -149,8 +217,14 @@ export function calculateDiscount(coupon, orderTotal) {
|
|
|
149
217
|
case "fixed":
|
|
150
218
|
discountAmount = Math.min(coupon.discount.value, orderTotal);
|
|
151
219
|
break;
|
|
152
|
-
case "free_shipping":
|
|
153
220
|
case "buy_x_get_y":
|
|
221
|
+
// Needs the basket — see calculateBxgyDiscount. Capped at the total so a
|
|
222
|
+
// misconfigured coupon cannot discount more than is being bought.
|
|
223
|
+
discountAmount = Math.min(calculateBxgyDiscount(coupon.bxgy, lines ?? []), orderTotal);
|
|
224
|
+
break;
|
|
225
|
+
case "free_shipping":
|
|
226
|
+
// Genuinely 0 HERE. The waiver is applied as a shipping line downstream,
|
|
227
|
+
// not as a discount — this is the one type for which zero is the answer.
|
|
154
228
|
discountAmount = 0;
|
|
155
229
|
break;
|
|
156
230
|
}
|
|
@@ -26,6 +26,7 @@ import { couponsRepository } from "../../promotions/repository/coupons.repositor
|
|
|
26
26
|
import { generateStoreSlug } from "../../stores/schemas/firestore";
|
|
27
27
|
import { siteSettingsRepository } from "../../admin/repository/site-settings.repository";
|
|
28
28
|
import { computePayoutDeduction } from "../../../_internal/shared/fees/calculator";
|
|
29
|
+
import { ordersTaxableValue } from "../../../_internal/shared/checkout/order-math";
|
|
29
30
|
import { getDefaultCurrency } from "../../../core/baseline-resolver";
|
|
30
31
|
import { finalizeStagedMediaUrl, finalizeStagedMediaField, finalizeStagedMediaArray, } from "../../media/finalize";
|
|
31
32
|
import { StoreStatusValues } from "../../stores/schemas/firestore";
|
|
@@ -222,7 +223,18 @@ async function computeSellerEarnings(sellerId) {
|
|
|
222
223
|
}
|
|
223
224
|
const paidOutIds = await payoutRepository.getPaidOutOrderIds(sellerId);
|
|
224
225
|
const eligibleOrders = deliveredOrders.filter((o) => !paidOutIds.has(o.id));
|
|
225
|
-
|
|
226
|
+
/*
|
|
227
|
+
* The TAXABLE VALUE — see `orderTaxableValue`. This figure is what the
|
|
228
|
+
* seller sees as their earnings, so it must match what the two payout JOBS
|
|
229
|
+
* compute to the rupee; a seller shown one number and paid another is worse
|
|
230
|
+
* than either number being wrong on its own.
|
|
231
|
+
*
|
|
232
|
+
* Prices are tax-inclusive, so a seller listing at ₹999 @5% earns from a
|
|
233
|
+
* ₹951.43 taxable value — the ₹47.57 is GST we remit. A seller who would
|
|
234
|
+
* rather net ₹999 sets `pricesIncludeGst: false` and their typed price is
|
|
235
|
+
* grossed up at write time.
|
|
236
|
+
*/
|
|
237
|
+
const grossAmount = ordersTaxableValue(eligibleOrders);
|
|
226
238
|
const siteSettings = await siteSettingsRepository.getSingleton();
|
|
227
239
|
const deduction = computePayoutDeduction(grossAmount, siteSettings.commissions);
|
|
228
240
|
return {
|
|
@@ -319,7 +331,10 @@ export async function bulkSellerOrder(userId, userRole, userDisplayName, userEma
|
|
|
319
331
|
}
|
|
320
332
|
if (eligible.length === 0)
|
|
321
333
|
throw new ValidationError("No eligible orders found.");
|
|
322
|
-
|
|
334
|
+
/* The TAXABLE VALUE — see `orderTaxableValue`. Fourth and last of the four
|
|
335
|
+
* paths C0 converged; a half-migration means the manual button and the
|
|
336
|
+
* scheduled job pay differently for the same orders. */
|
|
337
|
+
const grossAmount = ordersTaxableValue(eligible);
|
|
323
338
|
const bulkSiteSettings = await siteSettingsRepository.getSingleton();
|
|
324
339
|
const bulkDeduction = computePayoutDeduction(grossAmount, bulkSiteSettings.commissions);
|
|
325
340
|
const payoutDoc = await payoutRepository.create({
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import { type PhysicalLocationParts } from "../../../utils/physical-location";
|
|
2
3
|
interface PrintCenterStore {
|
|
3
4
|
id: string;
|
|
4
5
|
storeName: string;
|
|
@@ -14,7 +15,7 @@ interface PrintCenterProduct {
|
|
|
14
15
|
listingType?: string;
|
|
15
16
|
condition?: string;
|
|
16
17
|
stockCount?: number;
|
|
17
|
-
physicalLocation?:
|
|
18
|
+
physicalLocation?: PhysicalLocationParts;
|
|
18
19
|
barcodeId?: string;
|
|
19
20
|
}
|
|
20
21
|
interface PrintCenterOrder {
|
|
@@ -29,7 +30,7 @@ interface PrintCenterOrder {
|
|
|
29
30
|
price: number;
|
|
30
31
|
barcodeId?: string;
|
|
31
32
|
}[];
|
|
32
|
-
physicalLocation?:
|
|
33
|
+
physicalLocation?: PhysicalLocationParts;
|
|
33
34
|
}
|
|
34
35
|
interface PrintCenterViewProps {
|
|
35
36
|
store?: PrintCenterStore | null;
|
|
@@ -7,6 +7,7 @@ import { Heading, Text } from "../../../ui/components/Typography";
|
|
|
7
7
|
import { Stack, Row } from "../../../ui/components/Layout";
|
|
8
8
|
import { Button } from "../../../ui/components/Button";
|
|
9
9
|
import { FieldCheckbox } from "../../../ui/forms/FieldCheckbox";
|
|
10
|
+
import { formatPhysicalLocation, } from "../../../utils/physical-location";
|
|
10
11
|
function ProductLabel({ product }) {
|
|
11
12
|
const svgRef = useRef(null);
|
|
12
13
|
useEffect(() => {
|
|
@@ -29,10 +30,10 @@ function ProductLabel({ product }) {
|
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
}, [product.barcodeId]);
|
|
32
|
-
return (_jsxs(Stack, { surface: "card", padding: "sm", rounded: "sm", gap: "xs", className: "break-inside-avoid text-center w-[200px]", children: [product.barcodeId ? (_jsx("svg", { ref: svgRef })) : (_jsx(Text, { size: "xs", color: "muted", children: "No barcode" })), _jsx(Text, { size: "xs", weight: "medium", truncate: 1, children: product.name }), _jsxs(Text, { size: "xs", color: "muted", children: ["\u20B9", product.price.toLocaleString("en-IN")] }), product.physicalLocation && (_jsx(Text, { size: "xs", color: "muted", children: product.physicalLocation }))] }));
|
|
33
|
+
return (_jsxs(Stack, { surface: "card", padding: "sm", rounded: "sm", gap: "xs", className: "break-inside-avoid text-center w-[200px]", children: [product.barcodeId ? (_jsx("svg", { ref: svgRef })) : (_jsx(Text, { size: "xs", color: "muted", children: "No barcode" })), _jsx(Text, { size: "xs", weight: "medium", truncate: 1, children: product.name }), _jsxs(Text, { size: "xs", color: "muted", children: ["\u20B9", product.price.toLocaleString("en-IN")] }), formatPhysicalLocation(product.physicalLocation) && (_jsx(Text, { size: "xs", color: "muted", children: formatPhysicalLocation(product.physicalLocation) }))] }));
|
|
33
34
|
}
|
|
34
35
|
function PackingSlip({ order, brandName }) {
|
|
35
|
-
return (_jsxs(Stack, { surface: "card", padding: "md", rounded: "sm", className: "break-inside-avoid mb-4 max-w-[480px]", children: [_jsxs(Row, { justify: "between", className: "mb-2", children: [_jsx(Heading, { level: 5, children: brandName }), _jsxs(Text, { size: "xs", color: "muted", children: ["#", order.id] })] }), _jsx(Text, { size: "xs", color: "muted", children: new Date(order.createdAt).toLocaleDateString("en-IN") }), order.buyerDisplayName && _jsx(Text, { size: "sm", weight: "medium", children: order.buyerDisplayName }), order.buyerCity && _jsx(Text, { size: "xs", color: "muted", children: order.buyerCity }), order.physicalLocation && (_jsxs(Text, { size: "xs", color: "muted", children: ["Staging: ", order.physicalLocation] })), _jsx(Stack, { border: "top", padding: "t-xs", className: "mt-3", children: order.items.map((item, i) => (_jsxs(Row, { justify: "between", padding: "y-2xs", className: "border-b last:border-b-0", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "xs", weight: "medium", children: item.productName }), item.barcodeId && _jsx(Text, { size: "xs", color: "muted", children: item.barcodeId })] }), _jsxs(Stack, { className: "text-right", children: [_jsxs(Text, { size: "xs", children: ["\u00D7", item.quantity] }), _jsxs(Text, { size: "xs", color: "muted", children: ["\u20B9", item.price.toLocaleString("en-IN")] })] })] }, i))) })] }));
|
|
36
|
+
return (_jsxs(Stack, { surface: "card", padding: "md", rounded: "sm", className: "break-inside-avoid mb-4 max-w-[480px]", children: [_jsxs(Row, { justify: "between", className: "mb-2", children: [_jsx(Heading, { level: 5, children: brandName }), _jsxs(Text, { size: "xs", color: "muted", children: ["#", order.id] })] }), _jsx(Text, { size: "xs", color: "muted", children: new Date(order.createdAt).toLocaleDateString("en-IN") }), order.buyerDisplayName && _jsx(Text, { size: "sm", weight: "medium", children: order.buyerDisplayName }), order.buyerCity && _jsx(Text, { size: "xs", color: "muted", children: order.buyerCity }), formatPhysicalLocation(order.physicalLocation) && (_jsxs(Text, { size: "xs", color: "muted", children: ["Staging: ", formatPhysicalLocation(order.physicalLocation)] })), _jsx(Stack, { border: "top", padding: "t-xs", className: "mt-3", children: order.items.map((item, i) => (_jsxs(Row, { justify: "between", padding: "y-2xs", className: "border-b last:border-b-0", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "xs", weight: "medium", children: item.productName }), item.barcodeId && _jsx(Text, { size: "xs", color: "muted", children: item.barcodeId })] }), _jsxs(Stack, { className: "text-right", children: [_jsxs(Text, { size: "xs", children: ["\u00D7", item.quantity] }), _jsxs(Text, { size: "xs", color: "muted", children: ["\u20B9", item.price.toLocaleString("en-IN")] })] })] }, i))) })] }));
|
|
36
37
|
}
|
|
37
38
|
export function PrintCenterView({ store, brandName = "LetItRip", initialProducts = [], initialOrders = [], }) {
|
|
38
39
|
const [tab, setTab] = useState("labels");
|