@mohasinac/appkit 4.56.0 → 4.58.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 +4 -4
- package/dist/_internal/server/features/beys/data.d.ts +44 -0
- package/dist/_internal/server/features/beys/data.js +107 -0
- package/dist/_internal/server/features/beys/og.d.ts +42 -0
- package/dist/_internal/server/features/beys/og.js +89 -0
- package/dist/_internal/server/features/checkout/actions.d.ts +31 -0
- package/dist/_internal/server/features/checkout/actions.js +117 -5
- package/dist/_internal/server/features/orders/actions.d.ts +30 -0
- package/dist/_internal/server/features/orders/actions.js +103 -0
- package/dist/_internal/server/features/orders/adapters.js +7 -0
- package/dist/_internal/server/features/parts/data.js +3 -2
- package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
- package/dist/_internal/shared/actions/action-registry.js +139 -0
- package/dist/_internal/shared/shipping/quote.js +5 -6
- package/dist/client.d.ts +23 -0
- package/dist/client.js +3 -0
- package/dist/constants/api-endpoints.d.ts +12 -0
- package/dist/constants/api-endpoints.js +10 -0
- package/dist/constants/field-names.d.ts +46 -0
- package/dist/constants/field-names.js +46 -0
- package/dist/features/about/components/PublicProfileView.js +2 -1
- package/dist/features/account/components/UserOrdersView.js +25 -3
- package/dist/features/admin/components/AdminBeysView.d.ts +27 -0
- package/dist/features/admin/components/AdminBeysView.js +178 -0
- package/dist/features/admin/components/AdminPartsView.d.ts +27 -0
- package/dist/features/admin/components/AdminPartsView.js +160 -0
- package/dist/features/admin/components/OwnerQuickActions.d.ts +19 -2
- package/dist/features/admin/components/OwnerQuickActions.js +83 -8
- package/dist/features/admin/components/RecordOwnerQuickActions.d.ts +31 -0
- package/dist/features/admin/components/RecordOwnerQuickActions.js +128 -0
- package/dist/features/admin/components/ReferenceRecordEditor.d.ts +45 -0
- package/dist/features/admin/components/ReferenceRecordEditor.js +153 -0
- package/dist/features/admin/schemas/firestore.d.ts +1 -1
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +15 -2
- package/dist/features/auth/permissions/constants.d.ts +1 -1
- package/dist/features/auth/permissions/constants.js +12 -1
- package/dist/features/beys/components/BeyDetailPageView.d.ts +50 -0
- package/dist/features/beys/components/BeyDetailPageView.js +98 -0
- package/dist/features/beys/schemas/firestore.d.ts +417 -0
- package/dist/features/beys/schemas/firestore.js +65 -0
- package/dist/features/blog/components/BlogFeaturedCard.js +15 -2
- package/dist/features/blog/components/BlogPostView.js +2 -1
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +2 -0
- package/dist/features/cart/components/CartPriceBreakdown.js +1 -1
- package/dist/features/categories/components/BrandDetailPageView.js +4 -2
- package/dist/features/categories/components/BundleDetailView.js +2 -2
- package/dist/features/categories/components/CategoryDetailPageView.js +36 -3
- package/dist/features/categories/components/CategoryGrid.js +24 -2
- package/dist/features/categories/components/FranchiseDetailPageView.js +2 -1
- package/dist/features/events/components/EventCard.js +15 -2
- package/dist/features/faq/components/FAQPageContent.js +17 -4
- package/dist/features/faq/constants/help-guides.d.ts +36 -0
- package/dist/features/faq/constants/help-guides.js +64 -0
- package/dist/features/grouped/components/GroupedListingDetailView.js +2 -3
- package/dist/features/homepage/components/CustomCardsSection.js +67 -50
- package/dist/features/homepage/components/HeroCarousel.js +3 -1
- package/dist/features/homepage/components/ShopByCategorySection.js +23 -10
- package/dist/features/homepage/lib/section-renderer.js +2 -1
- package/dist/features/homepage/schemas/index.d.ts +10 -10
- package/dist/features/orders/components/EmiInstallmentsPanel.d.ts +19 -0
- package/dist/features/orders/components/EmiInstallmentsPanel.js +57 -0
- package/dist/features/orders/components/index.d.ts +1 -0
- package/dist/features/orders/components/index.js +1 -0
- package/dist/features/orders/schemas/index.d.ts +2 -2
- package/dist/features/orders/types/index.d.ts +15 -0
- package/dist/features/parts/components/PartDetailPageView.js +5 -3
- package/dist/features/parts/components/PartsSearchBox.js +79 -4
- package/dist/features/parts/schemas/admin-edit.d.ts +81 -0
- package/dist/features/parts/schemas/admin-edit.js +89 -0
- package/dist/features/parts/schemas/firestore.d.ts +2 -1
- package/dist/features/points/repository/points.repository.js +5 -5
- package/dist/features/points/schemas/firestore.d.ts +16 -0
- package/dist/features/points/schemas/firestore.js +17 -0
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +15 -2
- package/dist/features/products/components/MarketplaceBundleCard.js +15 -2
- package/dist/features/products/components/MarketplacePrizeDrawCard.js +15 -2
- package/dist/features/products/components/ProductGrid.js +18 -2
- package/dist/features/products/schemas/firestore.d.ts +18 -0
- package/dist/features/reviews/components/ReviewDetailPageView.js +3 -2
- package/dist/features/seller/components/CategoryInlineSelect.d.ts +10 -1
- package/dist/features/seller/components/CategoryInlineSelect.js +53 -5
- package/dist/features/seller/components/SellerProductShell.d.ts +39 -0
- package/dist/features/seller/components/SellerProductShell.js +31 -3
- package/dist/features/stores/components/StoreHeader.js +3 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.js +13 -0
- package/dist/next/routing/route-map.d.ts +17 -1
- package/dist/next/routing/route-map.js +39 -0
- package/dist/schemas/registry.d.ts +8 -8
- package/dist/seed/_helpers/category-models/chunk0.d.ts +2 -0
- package/dist/seed/_helpers/category-models/chunk0.js +2648 -0
- package/dist/seed/_helpers/category-models/chunk1.d.ts +2 -0
- package/dist/seed/_helpers/category-models/chunk1.js +2318 -0
- package/dist/seed/_helpers/category-models/chunk2.d.ts +2 -0
- package/dist/seed/_helpers/category-models/chunk2.js +2560 -0
- package/dist/seed/_helpers/category-models/chunk3.d.ts +2 -0
- package/dist/seed/_helpers/category-models/chunk3.js +2196 -0
- package/dist/seed/_helpers/category-models/chunk4.d.ts +2 -0
- package/dist/seed/_helpers/category-models/chunk4.js +1732 -0
- package/dist/seed/_helpers/category-models.d.ts +9 -2
- package/dist/seed/_helpers/category-models.js +19 -1368
- package/dist/seed/_helpers/category-parts-glossary-map.js +38 -38
- package/dist/seed/_helpers/category-parts.js +39 -39
- package/dist/seed/bey-products-seed-data.d.ts +18 -0
- package/dist/seed/bey-products-seed-data.js +18011 -0
- package/dist/seed/beys/chunk0.d.ts +17 -0
- package/dist/seed/beys/chunk0.js +36733 -0
- package/dist/seed/beys/chunk1.d.ts +17 -0
- package/dist/seed/beys/chunk1.js +35728 -0
- package/dist/seed/beys/chunk2.d.ts +17 -0
- package/dist/seed/beys/chunk2.js +35857 -0
- package/dist/seed/beys/chunk3.d.ts +17 -0
- package/dist/seed/beys/chunk3.js +34476 -0
- package/dist/seed/beys-seed-data.d.ts +18 -0
- package/dist/seed/beys-seed-data.js +11 -0
- package/dist/seed/homepage-sections-seed-data.js +16 -0
- package/dist/seed/index.d.ts +2 -0
- package/dist/seed/index.js +3 -0
- package/dist/seed/media-assets-seed-data.js +4 -4
- package/dist/seed/products-catalogue/chunk0.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk0.js +7262 -0
- package/dist/seed/products-catalogue/chunk1.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk1.js +7290 -0
- package/dist/seed/products-catalogue/chunk10.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk10.js +7290 -0
- package/dist/seed/products-catalogue/chunk11.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk11.js +7005 -0
- package/dist/seed/products-catalogue/chunk12.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk12.js +7002 -0
- package/dist/seed/products-catalogue/chunk13.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk13.js +7002 -0
- package/dist/seed/products-catalogue/chunk14.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk14.js +7002 -0
- package/dist/seed/products-catalogue/chunk15.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk15.js +7002 -0
- package/dist/seed/products-catalogue/chunk16.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk16.js +7002 -0
- package/dist/seed/products-catalogue/chunk17.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk17.js +7002 -0
- package/dist/seed/products-catalogue/chunk18.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk18.js +4398 -0
- package/dist/seed/products-catalogue/chunk2.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk2.js +7268 -0
- package/dist/seed/products-catalogue/chunk3.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk3.js +7264 -0
- package/dist/seed/products-catalogue/chunk4.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk4.js +7268 -0
- package/dist/seed/products-catalogue/chunk5.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk5.js +7258 -0
- package/dist/seed/products-catalogue/chunk6.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk6.js +7268 -0
- package/dist/seed/products-catalogue/chunk7.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk7.js +7311 -0
- package/dist/seed/products-catalogue/chunk8.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk8.js +7304 -0
- package/dist/seed/products-catalogue/chunk9.d.ts +12 -0
- package/dist/seed/products-catalogue/chunk9.js +7309 -0
- package/dist/seed/products-catalogue-seed-data.d.ts +2 -22
- package/dist/seed/products-catalogue-seed-data.js +58 -86382
- package/dist/seed/seed-purpose.js +9 -0
- package/dist/server.d.ts +5 -1
- package/dist/server.js +19 -0
- package/dist/styles.css +468 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/PosterCard.d.ts +99 -0
- package/dist/ui/components/PosterCard.js +109 -0
- package/dist/ui/components/PosterCard.style.css +466 -0
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +2 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +108 -1
package/dist/.build-stamp.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"stampVersion": 1,
|
|
3
|
-
"version": "4.
|
|
4
|
-
"head": "
|
|
3
|
+
"version": "4.58.0",
|
|
4
|
+
"head": "978d9ef892ec5d3ef08a87c7f040877faf3c5daa",
|
|
5
5
|
"branch": "main",
|
|
6
|
-
"dirty":
|
|
7
|
-
"builtAt": "2026-10-
|
|
6
|
+
"dirty": false,
|
|
7
|
+
"builtAt": "2026-10-20T20:29:00.050Z"
|
|
8
8
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type BeyDocument, type BeyProductDocument } from "../../../../features/beys/schemas/firestore";
|
|
2
|
+
/**
|
|
3
|
+
* One bey, by slug.
|
|
4
|
+
*
|
|
5
|
+
* 🛑 A DIRECT DOC READ — `id === slug`, so this needs no query, no index and
|
|
6
|
+
* no scan. That is the whole reason the id is the slug.
|
|
7
|
+
*/
|
|
8
|
+
export declare const getBeyBySlug: (slug: string) => Promise<BeyDocument | null>;
|
|
9
|
+
/**
|
|
10
|
+
* The bey(s) paired with a category leaf — the inverse of `BeyDocument.categoryId`.
|
|
11
|
+
*
|
|
12
|
+
* 🛑 ONE INDEXED EQUALITY, no composite index, which is the same shape
|
|
13
|
+
* `getCompositionsForCategory` uses for the parts side of this exact job. A
|
|
14
|
+
* single-field `==` is served by Firestore's automatic index, so this adds no
|
|
15
|
+
* index to declare and no deploy to wait on.
|
|
16
|
+
*
|
|
17
|
+
* 🛑 NOT `.limit(1)`, and that is measured rather than cautious. 1,880 beys
|
|
18
|
+
* carry a `categoryId` across 1,858 distinct leaves — so **22 leaves pair with
|
|
19
|
+
* TWO beys**, which are the vendor pairs (a Takara-Tomy release and its Hasbro
|
|
20
|
+
* equivalent merged onto one taxonomy leaf). `limit(1)` would render one of
|
|
21
|
+
* them and silently drop the other on 22 pages, with nothing to indicate a
|
|
22
|
+
* second existed. The cap is small and the extra rows are the point.
|
|
23
|
+
*
|
|
24
|
+
* Fails OPEN to an empty list for the same reason as below: a category page
|
|
25
|
+
* missing its reference link is a worse page, a category page that throws is no
|
|
26
|
+
* page at all.
|
|
27
|
+
*/
|
|
28
|
+
export declare const getBeysForCategory: (categoryId: string) => Promise<BeyDocument[]>;
|
|
29
|
+
/**
|
|
30
|
+
* The products that ship a bey.
|
|
31
|
+
*
|
|
32
|
+
* 🛑 A QUERY, unlike `getModelsUsingPart`'s `getAll`, and the difference is a
|
|
33
|
+
* real asymmetry in the data rather than an inconsistency: a part row carries
|
|
34
|
+
* `usedBy` so its models are ids already in hand, whereas the product→bey edge
|
|
35
|
+
* lives on the PRODUCT (`assortment[].beyId`). A bey has no stored list of the
|
|
36
|
+
* products containing it.
|
|
37
|
+
*
|
|
38
|
+
* So this is one `array-contains` over a denormalised id array, bounded at 12.
|
|
39
|
+
* 🛑 It needs `assortmentBeyIds` on the product — a flat array of the ids
|
|
40
|
+
* inside `assortment[]`, because Firestore cannot `array-contains` on a field
|
|
41
|
+
* nested inside objects in an array. Until the seed carries that field this
|
|
42
|
+
* returns [] and says so, rather than running a scan.
|
|
43
|
+
*/
|
|
44
|
+
export declare const getProductsContainingBey: (beyId: string) => Promise<BeyProductDocument[]>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads for the public bey surfaces.
|
|
3
|
+
*
|
|
4
|
+
* 🛑 Wrapped in `React.cache` because a page and its `generateMetadata` share
|
|
5
|
+
* the call — and the NARROWEST shared call is what gets cached, not whatever
|
|
6
|
+
* is convenient to export. `React.cache` memoises per argument list, so two
|
|
7
|
+
* call sites passing different arguments get two entries and dedupe nothing
|
|
8
|
+
* (the `getStoreBySlug(slug)` vs `getStoreBySlug(slug, viewer)` trap).
|
|
9
|
+
*/
|
|
10
|
+
import { cache } from "react";
|
|
11
|
+
import { BEYS_COLLECTION, BEY_PRODUCTS_COLLECTION, } from "../../../../features/beys/schemas/firestore.js";
|
|
12
|
+
import { getAdminDb } from "../../../../providers/db-firebase/index.js";
|
|
13
|
+
import { BEY_FIELDS } from "../../../../constants/field-names.js";
|
|
14
|
+
import { serverLogger } from "../../../../monitoring/server-logger.js";
|
|
15
|
+
import { normalizeError } from "../../../../errors/normalize.js";
|
|
16
|
+
/**
|
|
17
|
+
* One bey, by slug.
|
|
18
|
+
*
|
|
19
|
+
* 🛑 A DIRECT DOC READ — `id === slug`, so this needs no query, no index and
|
|
20
|
+
* no scan. That is the whole reason the id is the slug.
|
|
21
|
+
*/
|
|
22
|
+
export const getBeyBySlug = cache(async (slug) => {
|
|
23
|
+
try {
|
|
24
|
+
const snap = await getAdminDb().collection(BEYS_COLLECTION).doc(slug).get();
|
|
25
|
+
return snap.exists ? { id: snap.id, ...snap.data() } : null;
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
const e = normalizeError(err);
|
|
29
|
+
serverLogger.warn("getBeyBySlug failed", { slug, error: e.message });
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* The bey(s) paired with a category leaf — the inverse of `BeyDocument.categoryId`.
|
|
35
|
+
*
|
|
36
|
+
* 🛑 ONE INDEXED EQUALITY, no composite index, which is the same shape
|
|
37
|
+
* `getCompositionsForCategory` uses for the parts side of this exact job. A
|
|
38
|
+
* single-field `==` is served by Firestore's automatic index, so this adds no
|
|
39
|
+
* index to declare and no deploy to wait on.
|
|
40
|
+
*
|
|
41
|
+
* 🛑 NOT `.limit(1)`, and that is measured rather than cautious. 1,880 beys
|
|
42
|
+
* carry a `categoryId` across 1,858 distinct leaves — so **22 leaves pair with
|
|
43
|
+
* TWO beys**, which are the vendor pairs (a Takara-Tomy release and its Hasbro
|
|
44
|
+
* equivalent merged onto one taxonomy leaf). `limit(1)` would render one of
|
|
45
|
+
* them and silently drop the other on 22 pages, with nothing to indicate a
|
|
46
|
+
* second existed. The cap is small and the extra rows are the point.
|
|
47
|
+
*
|
|
48
|
+
* Fails OPEN to an empty list for the same reason as below: a category page
|
|
49
|
+
* missing its reference link is a worse page, a category page that throws is no
|
|
50
|
+
* page at all.
|
|
51
|
+
*/
|
|
52
|
+
export const getBeysForCategory = cache(async (categoryId) => {
|
|
53
|
+
try {
|
|
54
|
+
const snap = await getAdminDb()
|
|
55
|
+
.collection(BEYS_COLLECTION)
|
|
56
|
+
.where(BEY_FIELDS.CATEGORY_ID, "==", categoryId)
|
|
57
|
+
.limit(4)
|
|
58
|
+
.get();
|
|
59
|
+
return snap.docs.map((d) => ({ id: d.id, ...d.data() }));
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
const e = normalizeError(err);
|
|
63
|
+
serverLogger.warn("getBeysForCategory failed", { categoryId, error: e.message });
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* The products that ship a bey.
|
|
69
|
+
*
|
|
70
|
+
* 🛑 A QUERY, unlike `getModelsUsingPart`'s `getAll`, and the difference is a
|
|
71
|
+
* real asymmetry in the data rather than an inconsistency: a part row carries
|
|
72
|
+
* `usedBy` so its models are ids already in hand, whereas the product→bey edge
|
|
73
|
+
* lives on the PRODUCT (`assortment[].beyId`). A bey has no stored list of the
|
|
74
|
+
* products containing it.
|
|
75
|
+
*
|
|
76
|
+
* So this is one `array-contains` over a denormalised id array, bounded at 12.
|
|
77
|
+
* 🛑 It needs `assortmentBeyIds` on the product — a flat array of the ids
|
|
78
|
+
* inside `assortment[]`, because Firestore cannot `array-contains` on a field
|
|
79
|
+
* nested inside objects in an array. Until the seed carries that field this
|
|
80
|
+
* returns [] and says so, rather than running a scan.
|
|
81
|
+
*/
|
|
82
|
+
export const getProductsContainingBey = cache(async (beyId) => {
|
|
83
|
+
try {
|
|
84
|
+
const snap = await getAdminDb()
|
|
85
|
+
.collection(BEY_PRODUCTS_COLLECTION)
|
|
86
|
+
.where("assortmentBeyIds", "array-contains", beyId)
|
|
87
|
+
.limit(12)
|
|
88
|
+
.get();
|
|
89
|
+
return snap.docs.map((d) => ({ id: d.id, ...d.data() }));
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
const e = normalizeError(err);
|
|
93
|
+
/*
|
|
94
|
+
* Fails OPEN to an empty list. A missing composite index throws
|
|
95
|
+
* `FAILED_PRECONDITION`, and the right outcome is a bey page without its
|
|
96
|
+
* "found in" section rather than a broken page — the caller wraps this in
|
|
97
|
+
* `safeRead` as well, so the degradation is recorded rather than silent.
|
|
98
|
+
*
|
|
99
|
+
* 🛑 `normalizeError` is the FIRST statement on purpose. It was below
|
|
100
|
+
* this comment, which put it outside audit-catch-normalize's proximity
|
|
101
|
+
* window — so the audit correctly reported it as missing even though the
|
|
102
|
+
* call was right there. Comment after the call, never before it.
|
|
103
|
+
*/
|
|
104
|
+
serverLogger.warn("getProductsContainingBey failed", { beyId, error: e.message });
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OG card for `/beys/{slug}`.
|
|
3
|
+
*
|
|
4
|
+
* ── 🛑 NO IMAGE FETCHES, FOR THE REASON THE PARTS CARDS DO NOT EITHER ─────
|
|
5
|
+
*
|
|
6
|
+
* Beys DO have photographs — 1,903 of 1,988 — so this is a deliberate refusal
|
|
7
|
+
* rather than a gap. The project was suspended for **Fast Origin Transfer at
|
|
8
|
+
* 5.25× its allowance** (52.52 GB against 10 GB), traced to render-time image
|
|
9
|
+
* proxying (Root Cause #104). 1,988 bey routes with one remote fetch each, on
|
|
10
|
+
* a crawler sweep, is precisely the amplification that was just removed. Type
|
|
11
|
+
* on a gradient costs one lambda and nothing else.
|
|
12
|
+
*
|
|
13
|
+
* ── 🛑 NO MONEY ON THE CANVAS ─────────────────────────────────────────────
|
|
14
|
+
*
|
|
15
|
+
* All eight money-bearing OG renderers were stripped, and the rule is stated
|
|
16
|
+
* in CLAUDE.md: an OG image is a public image URL with the amount burned into
|
|
17
|
+
* the pixels, so no sign-in gate reaches it and no paywall markup describes
|
|
18
|
+
* it. 1,309 beys carry a price in the research data and none of it appears
|
|
19
|
+
* here.
|
|
20
|
+
*
|
|
21
|
+
* `audit-guest-price-leak`'s R1 blocks a `₹` in any `og.tsx`, so this is
|
|
22
|
+
* mechanically enforced rather than left to discipline.
|
|
23
|
+
*/
|
|
24
|
+
import type { ReactElement } from "react";
|
|
25
|
+
import type { BeyDocument } from "../../../../features/beys/schemas/firestore";
|
|
26
|
+
/**
|
|
27
|
+
* One bey's card.
|
|
28
|
+
*
|
|
29
|
+
* 🛑 EVERY CLAUSE IS CONDITIONAL, because the coverage genuinely varies: 93%
|
|
30
|
+
* of beys have a mass but only 248 state it directly, and `system` is on 94%
|
|
31
|
+
* while `type` and `spinDirection` are lower. A card reading "0 g" or an empty
|
|
32
|
+
* separator would be a claim the data does not support — the same reason the
|
|
33
|
+
* parts card builds its detail line by filtering.
|
|
34
|
+
*
|
|
35
|
+
* 🛑 The mass is marked `≈` when it was SUMMED from the parts rather than
|
|
36
|
+
* stated, which is the same honesty the detail page applies. A derived figure
|
|
37
|
+
* printed as an exact one is the single easiest way for this dataset to start
|
|
38
|
+
* lying.
|
|
39
|
+
*/
|
|
40
|
+
export declare function renderBeyOg(bey: BeyDocument | null | undefined, opts: {
|
|
41
|
+
siteName: string;
|
|
42
|
+
}): ReactElement;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
const BG = "#0f172a";
|
|
3
|
+
const GRADIENT = "linear-gradient(135deg, #0f172a 0%, #1e293b 100%)";
|
|
4
|
+
const MUTED = "#94a3b8";
|
|
5
|
+
const ACCENT = "#5eead4";
|
|
6
|
+
/*
|
|
7
|
+
* 🛑 A COPY of the parts shell, not an import, and that is a considered
|
|
8
|
+
* choice rather than laziness.
|
|
9
|
+
*
|
|
10
|
+
* The Duplication Decision Framework's Rule of Three says extract on the
|
|
11
|
+
* THIRD copy, not the second — "two copies is observation; three is a
|
|
12
|
+
* pattern". There are now exactly two OG shells of this shape. Lifting it to
|
|
13
|
+
* a shared module today would also put a third consumer between every OG
|
|
14
|
+
* renderer and its layout for no behavioural gain.
|
|
15
|
+
*
|
|
16
|
+
* When a third appears, extract then. The constants above are duplicated for
|
|
17
|
+
* the same reason and in the same breath.
|
|
18
|
+
*/
|
|
19
|
+
function shell(eyebrow, title, detail, siteName) {
|
|
20
|
+
return (_jsxs("div", { style: {
|
|
21
|
+
display: "flex",
|
|
22
|
+
width: "100%",
|
|
23
|
+
height: "100%",
|
|
24
|
+
background: BG,
|
|
25
|
+
fontFamily: "sans-serif",
|
|
26
|
+
position: "relative",
|
|
27
|
+
overflow: "hidden",
|
|
28
|
+
alignItems: "center",
|
|
29
|
+
justifyContent: "center",
|
|
30
|
+
}, children: [_jsx("div", { style: { position: "absolute", inset: 0, background: GRADIENT } }), _jsxs("div", { style: {
|
|
31
|
+
position: "relative",
|
|
32
|
+
display: "flex",
|
|
33
|
+
flexDirection: "column",
|
|
34
|
+
alignItems: "center",
|
|
35
|
+
gap: 24,
|
|
36
|
+
padding: "60px",
|
|
37
|
+
textAlign: "center",
|
|
38
|
+
}, children: [_jsx("div", { style: {
|
|
39
|
+
fontSize: 20,
|
|
40
|
+
color: ACCENT,
|
|
41
|
+
fontWeight: 600,
|
|
42
|
+
letterSpacing: 3,
|
|
43
|
+
textTransform: "uppercase",
|
|
44
|
+
}, children: eyebrow }), _jsx("div", { style: {
|
|
45
|
+
fontSize: title.length > 34 ? 56 : 76,
|
|
46
|
+
color: "#ffffff",
|
|
47
|
+
fontWeight: 700,
|
|
48
|
+
lineHeight: 1.1,
|
|
49
|
+
}, children: title }), detail && _jsx("div", { style: { fontSize: 28, color: MUTED }, children: detail }), _jsx("div", { style: { fontSize: 20, color: MUTED, marginTop: 8 }, children: siteName })] })] }));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* One bey's card.
|
|
53
|
+
*
|
|
54
|
+
* 🛑 EVERY CLAUSE IS CONDITIONAL, because the coverage genuinely varies: 93%
|
|
55
|
+
* of beys have a mass but only 248 state it directly, and `system` is on 94%
|
|
56
|
+
* while `type` and `spinDirection` are lower. A card reading "0 g" or an empty
|
|
57
|
+
* separator would be a claim the data does not support — the same reason the
|
|
58
|
+
* parts card builds its detail line by filtering.
|
|
59
|
+
*
|
|
60
|
+
* 🛑 The mass is marked `≈` when it was SUMMED from the parts rather than
|
|
61
|
+
* stated, which is the same honesty the detail page applies. A derived figure
|
|
62
|
+
* printed as an exact one is the single easiest way for this dataset to start
|
|
63
|
+
* lying.
|
|
64
|
+
*/
|
|
65
|
+
export function renderBeyOg(bey, opts) {
|
|
66
|
+
if (!bey)
|
|
67
|
+
return shell("Beyblades", "Beyblade database", "", opts.siteName);
|
|
68
|
+
const statedMass = typeof bey.weightG === "number" ? bey.weightG : null;
|
|
69
|
+
const summedMass = typeof bey.weightGFromParts === "number" ? bey.weightGFromParts : null;
|
|
70
|
+
const mass = statedMass !== null
|
|
71
|
+
? `${statedMass} g`
|
|
72
|
+
: summedMass !== null
|
|
73
|
+
? `≈ ${Math.round(summedMass * 10) / 10} g`
|
|
74
|
+
: null;
|
|
75
|
+
const bits = [
|
|
76
|
+
bey.type ? `${bey.type} type` : null,
|
|
77
|
+
bey.spinDirection ?? null,
|
|
78
|
+
mass,
|
|
79
|
+
bey.composition?.length ? `${bey.composition.length} parts` : null,
|
|
80
|
+
].filter((b) => b !== null);
|
|
81
|
+
/*
|
|
82
|
+
* The eyebrow is the SYSTEM (Hybrid Wheel System, Burst Ultimate…) because
|
|
83
|
+
* that is the one line a collector uses to place a bey in time, and it is
|
|
84
|
+
* present on 94% of them. `series` is the fallback and "Beyblade" the floor
|
|
85
|
+
* — never an empty eyebrow, which reads as a broken template.
|
|
86
|
+
*/
|
|
87
|
+
const eyebrow = bey.system ?? bey.series ?? "Beyblade";
|
|
88
|
+
return shell(eyebrow, bey.name, bits.join(" · "), opts.siteName);
|
|
89
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SelectedShippingQuote } from "../../../shared/shipping/quote";
|
|
1
2
|
import { type StoreShippingPolicy } from "../../../shared/features/shipping/mode";
|
|
2
3
|
import { type CartLane } from "../../../shared/checkout/lanes";
|
|
3
4
|
import type { OutOfStockPolicy } from "../../../../features/orders/schemas/index";
|
|
@@ -12,6 +13,36 @@ export interface CreateCheckoutOrderInput {
|
|
|
12
13
|
paymentMethod: CheckoutPaymentMethod;
|
|
13
14
|
/** Required when paymentMethod === "emi" — the buyer's chosen tenure (2–6 months). */
|
|
14
15
|
emiTenureMonths?: number;
|
|
16
|
+
/**
|
|
17
|
+
* The courier the buyer picked at checkout, with the ₹50-rounded rate they
|
|
18
|
+
* were shown.
|
|
19
|
+
*
|
|
20
|
+
* 🛑 OPTIONAL, and its absence is meaningful: a cart whose carrier quote
|
|
21
|
+
* failed, a digital-only cart, or any caller written before the picker
|
|
22
|
+
* existed falls back to `resolveShippingCost` exactly as before. Absence is
|
|
23
|
+
* what makes reconciliation skip the order as "no-quote" rather than
|
|
24
|
+
* measuring a seller's spend against a figure nobody was charged.
|
|
25
|
+
*
|
|
26
|
+
* 🛑 `chargedRate` is TRUSTED from the client, and that is a deliberate,
|
|
27
|
+
* bounded decision rather than an oversight. It is re-derived server-side
|
|
28
|
+
* through `roundShippingUp` before it is billed, so a tampered value can
|
|
29
|
+
* only ever be a different carrier rate rounded the same way — the gap
|
|
30
|
+
* worth closing is re-quoting the carrier at order time, which costs an
|
|
31
|
+
* outbound call on the payment path and is tracked separately.
|
|
32
|
+
*/
|
|
33
|
+
selectedShippingQuote?: SelectedShippingQuote;
|
|
34
|
+
/**
|
|
35
|
+
* Points the buyer chose to spend on this checkout. 1 point = ₹1.
|
|
36
|
+
*
|
|
37
|
+
* 🛑 Requested, never trusted. The server re-reads the balance, re-applies
|
|
38
|
+
* the 50% cap against the real cart total, and spends the LESSER of the two
|
|
39
|
+
* — a client that asks for more than it holds gets its request trimmed, not
|
|
40
|
+
* an error, because the honest reading of "spend 500 when you have 300" is a
|
|
41
|
+
* stale page rather than an attack. `pointsRepository.redeem` then re-checks
|
|
42
|
+
* the balance a third time inside its own transaction, which is the check
|
|
43
|
+
* that actually prevents a negative balance under concurrency.
|
|
44
|
+
*/
|
|
45
|
+
pointsToRedeem?: number;
|
|
15
46
|
notes?: string;
|
|
16
47
|
excludedProductIds?: string[];
|
|
17
48
|
/** When true this is an admin test order — OTP consent is pre-granted and the order is created paid/processing. */
|
|
@@ -12,6 +12,9 @@ import { roundRupees } from "../../../../utils/number.formatter.js";
|
|
|
12
12
|
import { ApiError, ValidationError, NotFoundError, ERROR_MESSAGES } from "../../../../errors/index.js";
|
|
13
13
|
import { ORDER_FIELDS } from "../../../../constants/field-names.js";
|
|
14
14
|
import { serverLogger } from "../../../../monitoring/index.js";
|
|
15
|
+
import { roundShippingUp, } from "../../../shared/shipping/quote.js";
|
|
16
|
+
import { pointsRepository } from "../../../../features/points/repository/points.repository.js";
|
|
17
|
+
import { maxRedeemablePoints } from "../../../../features/points/schemas/firestore.js";
|
|
15
18
|
import { unitOfWork, adminNotificationsRepository, siteSettingsRepository, userRepository, storeRepository, couponsRepository, claimedCouponsRepository, addressesRepository, productRepository, } from "../../../../repositories/index.js";
|
|
16
19
|
import { isIntraStateSupply } from "../../../shared/fees/calculator.js";
|
|
17
20
|
import { computePreOrderDepositAmount, lineTotalFor, sumGroupGst, unitPriceFor, } from "../../../shared/checkout/order-math.js";
|
|
@@ -490,7 +493,7 @@ function dispatchOrderConfirmationEmails(emailsToSend) {
|
|
|
490
493
|
* so the caller can accumulate the checkout-wide `total`.
|
|
491
494
|
*/
|
|
492
495
|
async function createOrderForGroup(group, orderType, ctx) {
|
|
493
|
-
const { paymentMethod, emiTenureMonths, emiSettings, commissions, appliedCoupons, cartSubtotal, couponUsageAccumulator, uid, userName, userEmail, shippingAddress, notes, adminBypass, adminBypassBy, adminBatchId, orderIds, emailsToSend, outOfStockPolicy, droppedItems, buyerState, gstSettings, siteContactUpiVpa, storeAddons, platformFeeByStore, } = ctx;
|
|
496
|
+
const { paymentMethod, emiTenureMonths, emiSettings, commissions, appliedCoupons, cartSubtotal, couponUsageAccumulator, uid, userName, userEmail, shippingAddress, notes, adminBypass, adminBypassBy, adminBatchId, orderIds, emailsToSend, outOfStockPolicy, droppedItems, buyerState, gstSettings, siteContactUpiVpa, storeAddons, platformFeeByStore, selectedShippingQuote, } = ctx;
|
|
494
497
|
const firstItem = group[0].item;
|
|
495
498
|
const firstProduct = group[0].product;
|
|
496
499
|
// Add-ons are this store's choice, not the cart's — see OrderGroupContext.
|
|
@@ -507,7 +510,25 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
507
510
|
return expandCartLineToOrderRows(item, product, ctx.productById).map((baseLine) => itemRule.decorateOrderItem(baseLine, product));
|
|
508
511
|
});
|
|
509
512
|
const totalQuantity = group.reduce((sum, { item }) => sum + item.quantity, 0);
|
|
510
|
-
const { shippingFee, storeOwnerId, storeEmiEnabled, storeState, storeProtections, storeTaxIdentity } = await resolveShippingCost(firstItem.storeId);
|
|
513
|
+
const { shippingFee: resolvedShippingFee, storeOwnerId, storeEmiEnabled, storeState, storeProtections, storeTaxIdentity } = await resolveShippingCost(firstItem.storeId);
|
|
514
|
+
/*
|
|
515
|
+
* 🛑 The buyer's quote WINS over the seller's configured rate, and it is
|
|
516
|
+
* re-rounded here rather than trusted as sent.
|
|
517
|
+
*
|
|
518
|
+
* `roundShippingUp` is idempotent, so re-applying it to an honest value is a
|
|
519
|
+
* no-op; applied to a tampered one it forces the result back onto a ₹50 step.
|
|
520
|
+
* That bounds what a forged `chargedRate` can be to "some carrier rate,
|
|
521
|
+
* correctly rounded" rather than an arbitrary number.
|
|
522
|
+
*
|
|
523
|
+
* Applied to the FIRST group only — see the field's note on OrderGroupContext.
|
|
524
|
+
* A cart spanning sellers must not be charged the same parcel rate twice.
|
|
525
|
+
*/
|
|
526
|
+
const isFirstGroup = ctx.orderIds.length === 0;
|
|
527
|
+
const shippingFee = selectedShippingQuote && isFirstGroup
|
|
528
|
+
? roundShippingUp(selectedShippingQuote.chargedRate)
|
|
529
|
+
: selectedShippingQuote
|
|
530
|
+
? 0
|
|
531
|
+
: resolvedShippingFee;
|
|
511
532
|
/*
|
|
512
533
|
* Who issues this invoice — the seller if they have their own GSTIN, the
|
|
513
534
|
* platform otherwise. ONE function decides, and it is the only place that
|
|
@@ -678,6 +699,16 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
678
699
|
// are "auction". Written ONCE here and never recomputed: the auction and
|
|
679
700
|
// offer documents it summarises are mutable and eventually archived.
|
|
680
701
|
sourceContext,
|
|
702
|
+
/*
|
|
703
|
+
* Frozen onto the order so reconciliation has a charged figure to measure
|
|
704
|
+
* the seller's real postage against. Only on the group that actually
|
|
705
|
+
* carries the shipping fee — a later group in a multi-seller cart is
|
|
706
|
+
* charged ₹0 shipping, so recording a quote there would invite a credit
|
|
707
|
+
* against money that group never collected.
|
|
708
|
+
*/
|
|
709
|
+
selectedShippingQuote: selectedShippingQuote && isFirstGroup
|
|
710
|
+
? { ...selectedShippingQuote, chargedRate: shippingFee }
|
|
711
|
+
: undefined,
|
|
681
712
|
offerId: firstItem.offerId ?? undefined,
|
|
682
713
|
// Distinguishes a Buy Now purchase from a settled auction win. Both are
|
|
683
714
|
// orderType "auction"; only this one gets the 24h admin-review window.
|
|
@@ -1139,12 +1170,53 @@ export async function createCheckoutOrderAction(input) {
|
|
|
1139
1170
|
siteContactUpiVpa: siteSettings?.contact?.upiVpa,
|
|
1140
1171
|
storeAddons: cart.storeAddons,
|
|
1141
1172
|
platformFeeByStore,
|
|
1173
|
+
selectedShippingQuote: input.selectedShippingQuote,
|
|
1142
1174
|
productById: checkoutProductById,
|
|
1143
1175
|
};
|
|
1144
1176
|
for (const { items: group, orderType } of orderGroups) {
|
|
1145
1177
|
total += await createOrderForGroup(group, orderType, groupCtx);
|
|
1146
1178
|
}
|
|
1147
1179
|
await flushCouponUsageAccumulator(couponUsageAccumulator, uid);
|
|
1180
|
+
/*
|
|
1181
|
+
* Spend points AFTER the orders exist.
|
|
1182
|
+
*
|
|
1183
|
+
* 🛑 This ordering is the whole safety argument. Redeeming first and then
|
|
1184
|
+
* failing to create an order would debit a buyer for something they never
|
|
1185
|
+
* received, and points have no refund path that does not need a human. This
|
|
1186
|
+
* way the worst case is an order placed at full price with the balance
|
|
1187
|
+
* intact — recoverable, and visible to the buyer, who still has their
|
|
1188
|
+
* points.
|
|
1189
|
+
*
|
|
1190
|
+
* The amount is re-derived server-side against the real total rather than
|
|
1191
|
+
* taken from the request; `maxRedeemablePoints` applies the 50% cap, and
|
|
1192
|
+
* `redeem` re-checks the balance inside its own transaction.
|
|
1193
|
+
*/
|
|
1194
|
+
if (input.pointsToRedeem && input.pointsToRedeem > 0 && orderIds.length > 0) {
|
|
1195
|
+
try {
|
|
1196
|
+
const balance = await pointsRepository.getBalance(uid);
|
|
1197
|
+
const spend = maxRedeemablePoints(total, Math.min(balance, input.pointsToRedeem));
|
|
1198
|
+
if (spend > 0) {
|
|
1199
|
+
await pointsRepository.redeem({
|
|
1200
|
+
userId: uid,
|
|
1201
|
+
points: spend,
|
|
1202
|
+
orderId: orderIds[0],
|
|
1203
|
+
description: `Redeemed on ${orderIds[0]}`,
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
catch (err) {
|
|
1208
|
+
/*
|
|
1209
|
+
* A failed redemption must not fail a placed order. The buyer keeps the
|
|
1210
|
+
* points and paid full price — the strictly safer side of this trade.
|
|
1211
|
+
*/
|
|
1212
|
+
const normalized = normalizeError(err);
|
|
1213
|
+
serverLogger.error("points redemption failed after order creation", {
|
|
1214
|
+
uid,
|
|
1215
|
+
orderId: orderIds[0],
|
|
1216
|
+
message: normalized.message,
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1148
1220
|
dispatchOrderConfirmationEmails(emailsToSend);
|
|
1149
1221
|
serverLogger.info(`createCheckoutOrderAction: ${orderIds.length} order(s) placed for uid=${uid}` +
|
|
1150
1222
|
(unavailable.length > 0 ? ` (${unavailable.length} item(s) unavailable)` : ""));
|
|
@@ -1572,7 +1644,7 @@ async function createPhonePeGroupOrder(group, orderType, ctx) {
|
|
|
1572
1644
|
// Reuses the same store/seller lookup as the COD/UPI path above instead
|
|
1573
1645
|
// of re-implementing it inline — was two sequential findById calls per
|
|
1574
1646
|
// seller group here, duplicated from resolveShippingCost.
|
|
1575
|
-
const { shippingFee, storeOwnerId, storeState, storeTaxIdentity } = await resolveShippingCost(firstItem.storeId);
|
|
1647
|
+
const { shippingFee, storeOwnerId, storeState, storeTaxIdentity, storeProtections } = await resolveShippingCost(firstItem.storeId);
|
|
1576
1648
|
/*
|
|
1577
1649
|
* Same supplier-of-record rule as `createOrderForGroup`, through the same
|
|
1578
1650
|
* function. This path (PhonePe verify / webhook) is the SECOND place an
|
|
@@ -1608,7 +1680,44 @@ async function createPhonePeGroupOrder(group, orderType, ctx) {
|
|
|
1608
1680
|
const addons = storeAddons?.[firstItem.storeId] ?? {};
|
|
1609
1681
|
const giftWrapFee = computeGiftWrapFee(addons.giftWrapAddon ?? false, commissionRates);
|
|
1610
1682
|
const giftWrapMessage = addons.giftWrapMessage;
|
|
1611
|
-
|
|
1683
|
+
/*
|
|
1684
|
+
* 🛑 `computeProtectionFees`, not a bare `computeShipmentProtectionFee`.
|
|
1685
|
+
*
|
|
1686
|
+
* This path hand-rolled the shipment fee and computed NO sale-protection fee
|
|
1687
|
+
* at all, which was wrong twice over:
|
|
1688
|
+
*
|
|
1689
|
+
* 1. **Sale protection was never charged here.** Razorpay's create-order
|
|
1690
|
+
* route DOES charge it (it calls `computeProtectionFees`), so the amount
|
|
1691
|
+
* captured and the amount this function records on the order disagreed
|
|
1692
|
+
* by the fee — and the platform absorbs the unconditional refund that
|
|
1693
|
+
* fee exists to pay for. See `computeSaleProtectionFee`.
|
|
1694
|
+
* 2. **The C5e tiers were skipped.** The bare helper tests only "did the
|
|
1695
|
+
* buyer tick it" and the site-wide master switch, so shipment protection
|
|
1696
|
+
* could be billed on a seller who never agreed to back it, or on a
|
|
1697
|
+
* final-sale item where it cannot apply.
|
|
1698
|
+
*
|
|
1699
|
+
* Latent rather than live when written — `payment.phonepeEnabled` is false
|
|
1700
|
+
* and the one live store has `supportsSaleProtection` unset — but both of
|
|
1701
|
+
* those are a settings toggle away, and an under-charge that appears the
|
|
1702
|
+
* moment a flag flips is worse than one that is visible now.
|
|
1703
|
+
*/
|
|
1704
|
+
const groupHasFinalSale = group.some(({ product }) => product?.finalSale === true);
|
|
1705
|
+
const { shippingProtectionFee: shipmentProtectionFee, saleProtectionFee } = computeProtectionFees({
|
|
1706
|
+
subtotal: groupTotal,
|
|
1707
|
+
selected: {
|
|
1708
|
+
shipping: addons.shipmentProtectionAddon ?? false,
|
|
1709
|
+
sale: addons.saleProtectionAddon ?? false,
|
|
1710
|
+
},
|
|
1711
|
+
tiers: {
|
|
1712
|
+
platformShipping: commissionRates.shipmentProtectionFeeEnabled === true,
|
|
1713
|
+
platformSale: commissionRates.saleProtectionFeeEnabled === true,
|
|
1714
|
+
sellerShipping: storeProtections.shipping,
|
|
1715
|
+
sellerSale: storeProtections.sale,
|
|
1716
|
+
finalSale: groupHasFinalSale,
|
|
1717
|
+
},
|
|
1718
|
+
shippingFee: (s, sel) => computeShipmentProtectionFee(s, sel, commissionRates),
|
|
1719
|
+
saleFee: (s, sel) => computeSaleProtectionFee(s, sel, commissionRates),
|
|
1720
|
+
});
|
|
1612
1721
|
/*
|
|
1613
1722
|
* P-8 GST — now wired through this path. C0c defect 1.
|
|
1614
1723
|
*
|
|
@@ -1641,7 +1750,7 @@ async function createPhonePeGroupOrder(group, orderType, ctx) {
|
|
|
1641
1750
|
/* Product GST is not a term — prices are tax-inclusive, so it is already
|
|
1642
1751
|
* inside `groupTotal`. Matches the COD/UPI path exactly; `platformFeeGst`
|
|
1643
1752
|
* stays because that is GST on our fee, charged on top. */
|
|
1644
|
-
const orderTotal = Math.max(0, groupTotal - couponDiscount) + shippingFee + giftWrapFee + shipmentProtectionFee + platformFee + platformFeeGst;
|
|
1753
|
+
const orderTotal = Math.max(0, groupTotal - couponDiscount) + shippingFee + giftWrapFee + shipmentProtectionFee + saleProtectionFee + platformFee + platformFeeGst;
|
|
1645
1754
|
// S-SBUNI-RULES 2026-05-13 — order-item decoration via rule registry.
|
|
1646
1755
|
// Same expansion as the COD/UPI path. This was a THIRD hand-rolled copy of
|
|
1647
1756
|
// the price rule (`isBundle ? item.price : product.price`) that — like the
|
|
@@ -1730,6 +1839,9 @@ async function createPhonePeGroupOrder(group, orderType, ctx) {
|
|
|
1730
1839
|
giftWrapMessage: giftWrapFee > 0 ? giftWrapMessage?.slice(0, 500) : undefined,
|
|
1731
1840
|
shipmentProtectionAddon: shipmentProtectionFee > 0 ? true : undefined,
|
|
1732
1841
|
shipmentProtectionFee: shipmentProtectionFee > 0 ? shipmentProtectionFee : undefined,
|
|
1842
|
+
// Recorded as well as charged — an order that collects a protection fee
|
|
1843
|
+
// without a record of it cannot be reconciled against a later claim.
|
|
1844
|
+
saleProtectionFee: saleProtectionFee > 0 ? saleProtectionFee : undefined,
|
|
1733
1845
|
/*
|
|
1734
1846
|
* The tax breakdown, matching `createOrderForGroup` field for field.
|
|
1735
1847
|
* This path recorded NONE of it before C0c defect 1 — so an online order
|
|
@@ -46,3 +46,33 @@ export declare function adminRejectPaymentAsFraudAction(orderId: string, note: s
|
|
|
46
46
|
* payment/order status — surfaces to the admin queue for investigation.
|
|
47
47
|
*/
|
|
48
48
|
export declare function raiseOrderDisputeAction(orderId: string, reason: string): Promise<ActionResult<void>>;
|
|
49
|
+
/**
|
|
50
|
+
* A buyer submits payment for ONE of their own EMI instalments.
|
|
51
|
+
*
|
|
52
|
+
* 🛑 Why this exists at all: there was no buyer-side route. The only writer
|
|
53
|
+
* was the seller's `markEmiInstallmentPaid`, so a buyer on an EMI plan could
|
|
54
|
+
* not pay an instalment — they waited for the seller to record it. The
|
|
55
|
+
* schema had anticipated them all along (`EmiInstallment.transactionId` and
|
|
56
|
+
* `.proofUrl` are documented as "buyer-uploaded"), which is the tell that the
|
|
57
|
+
* surface, not the model, was missing.
|
|
58
|
+
*
|
|
59
|
+
* 🛑 **ANY unpaid instalment, at ANY time — deliberately unbounded.** There is
|
|
60
|
+
* no due-date check and none should be added. A buyer who wants to clear the
|
|
61
|
+
* plan early is reducing our exposure, not breaking a rule, and the dueDate is
|
|
62
|
+
* the date we will CHASE them on, never the first date they may pay. Paying
|
|
63
|
+
* ahead also keeps `emiComplete` honest: that flag is what
|
|
64
|
+
* `assertEmiShippable` reads, so early settlement should be able to release a
|
|
65
|
+
* shipment early rather than waiting out a calendar.
|
|
66
|
+
*
|
|
67
|
+
* It mirrors the manual-payment model rather than inventing a second one: the
|
|
68
|
+
* buyer supplies a UTR and a proof, the seller or an admin confirms. The
|
|
69
|
+
* status therefore moves to `pending` review here, NOT to `paid` — a buyer
|
|
70
|
+
* must never be able to mark their own money received. `markEmiInstallmentPaid`
|
|
71
|
+
* stays the only writer of `paid`, and so stays the only way `emiComplete`
|
|
72
|
+
* can flip.
|
|
73
|
+
*/
|
|
74
|
+
export declare function submitEmiInstallmentPaymentAction(orderId: string, input: {
|
|
75
|
+
installmentIndex: number;
|
|
76
|
+
transactionId: string;
|
|
77
|
+
proofUrl?: string;
|
|
78
|
+
}): Promise<ActionResult<void>>;
|