@mohasinac/appkit 4.14.0 → 4.16.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/auctions/actions.js +2 -2
- package/dist/_internal/server/features/cart/service.d.ts +3 -3
- package/dist/_internal/server/features/checkout/actions.js +19 -0
- package/dist/_internal/server/features/checkout/source-context.d.ts +5 -0
- package/dist/_internal/server/features/checkout/source-context.js +157 -0
- package/dist/_internal/server/features/offers/adapters.d.ts +103 -0
- package/dist/_internal/server/features/offers/adapters.js +88 -0
- package/dist/_internal/server/features/offers/index.d.ts +2 -0
- package/dist/_internal/server/features/offers/index.js +1 -0
- package/dist/_internal/server/features/orders/adapters.js +24 -0
- package/dist/_internal/server/jobs/core/offerExpiry.js +10 -10
- package/dist/_internal/shared/actions/action-registry.d.ts +9 -2
- package/dist/_internal/shared/actions/action-registry.js +458 -7
- package/dist/_internal/shared/features/auctions/schema.d.ts +14 -2
- package/dist/_internal/shared/features/auctions/schema.js +13 -1
- package/dist/_internal/shared/features/blog/schema.d.ts +4 -4
- package/dist/_internal/shared/features/cart/schema.d.ts +25 -20
- package/dist/_internal/shared/features/cart/schema.js +15 -0
- package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +22 -22
- package/dist/_internal/shared/features/orders/schema.d.ts +4 -4
- package/dist/_internal/shared/features/products/schema.d.ts +27 -27
- package/dist/_internal/shared/features/promotions/schema.d.ts +4 -4
- package/dist/_internal/shared/features/reviews/schema.d.ts +2 -2
- package/dist/_internal/shared/history/history.d.ts +22 -0
- package/dist/_internal/shared/history/history.js +41 -1
- package/dist/client.d.ts +13 -2
- package/dist/client.js +20 -2
- package/dist/constants/field-names.d.ts +8 -6
- package/dist/constants/field-names.js +48 -6
- package/dist/contracts/extend.d.ts +29 -0
- package/dist/contracts/extend.js +20 -0
- package/dist/contracts/index.d.ts +2 -1
- package/dist/contracts/index.js +2 -0
- package/dist/features/account/components/UserBidsView.js +12 -4
- package/dist/features/account/components/UserReturnsView.d.ts +3 -0
- package/dist/features/account/components/UserReturnsView.js +17 -1
- package/dist/features/admin/components/AdminAuditLogView.js +1 -0
- package/dist/features/admin/components/AdminBidsView.js +15 -2
- package/dist/features/admin/components/AdminBlogEditorView.js +65 -33
- package/dist/features/admin/components/AdminFeatureEditorView.js +35 -6
- package/dist/features/admin/components/AdminNavEditorView.js +15 -5
- package/dist/features/admin/components/AdminNewsletterView.js +18 -2
- package/dist/features/admin/components/AdminOffersView.js +62 -18
- package/dist/features/admin/components/AdminPaymentMethodsView.js +24 -4
- package/dist/features/admin/components/AdminScammerEditorView.d.ts +2 -1
- package/dist/features/admin/components/AdminScammerEditorView.js +3 -13
- package/dist/features/admin/components/AdminScammersView.js +3 -8
- package/dist/features/admin/components/AdminSectionsView.js +32 -23
- package/dist/features/admin/components/AdminSessionsView.js +17 -2
- package/dist/features/admin/components/AdminViewCards.js +24 -17
- package/dist/features/admin/components/DataListingView.js +15 -1
- package/dist/features/admin/components/DataTable.js +7 -17
- package/dist/features/admin/components/ViewAuditLogEntryModal.js +1 -0
- package/dist/features/admin/components/sections/adminSectionsTypes.d.ts +4 -2
- package/dist/features/admin/components/sections/adminSectionsTypes.js +45 -27
- package/dist/features/admin/constants/filter-tabs.d.ts +3 -0
- package/dist/features/admin/constants/filter-tabs.js +1 -0
- package/dist/features/admin/schemas/homepage-section-form.d.ts +22 -0
- package/dist/features/admin/schemas/homepage-section-form.js +68 -0
- package/dist/features/admin/schemas/nav-item-form.d.ts +74 -0
- package/dist/features/admin/schemas/nav-item-form.js +72 -0
- package/dist/features/auctions/components/AuctionBidsTable.d.ts +10 -1
- package/dist/features/auctions/components/AuctionBidsTable.js +22 -4
- package/dist/features/auctions/components/bid-detail-fields.d.ts +29 -0
- package/dist/features/auctions/components/bid-detail-fields.js +103 -0
- package/dist/features/auctions/schemas/index.d.ts +14 -14
- package/dist/features/audit-log/schemas/firestore.d.ts +14 -0
- package/dist/features/audit-log/schemas/firestore.js +7 -0
- package/dist/features/auth/schemas/index.d.ts +2 -2
- package/dist/features/blog/actions/blog-actions.d.ts +7 -7
- package/dist/features/blog/schemas/blog-form.d.ts +80 -0
- package/dist/features/blog/schemas/blog-form.js +100 -0
- package/dist/features/blog/schemas/index.d.ts +10 -10
- package/dist/features/cart/schemas/index.d.ts +3 -3
- package/dist/features/catalogue/schemas/validation.d.ts +12 -12
- package/dist/features/categories/schemas/index.d.ts +16 -16
- package/dist/features/collections/schemas/index.d.ts +8 -8
- package/dist/features/events/components/AdminEventEditorView.js +32 -54
- package/dist/features/events/components/AdminEventEntriesView.js +22 -3
- package/dist/features/events/schemas/event-form.d.ts +113 -0
- package/dist/features/events/schemas/event-form.js +82 -0
- package/dist/features/events/schemas/index.d.ts +2 -2
- package/dist/features/faq/actions/faq-actions.d.ts +6 -6
- package/dist/features/faq/schemas/index.d.ts +6 -6
- package/dist/features/grouped/schemas/grouped-listing-form.d.ts +101 -0
- package/dist/features/grouped/schemas/grouped-listing-form.js +89 -0
- package/dist/features/grouped/schemas/index.d.ts +4 -4
- package/dist/features/homepage/actions/homepage-section-actions.d.ts +11 -3
- package/dist/features/homepage/actions/homepage-section-actions.js +33 -1
- package/dist/features/homepage/schemas/index.d.ts +71 -40
- package/dist/features/homepage/schemas/index.js +24 -0
- package/dist/features/messages/schemas/index.d.ts +2 -2
- package/dist/features/orders/components/OrderStatusTimeline.d.ts +5 -1
- package/dist/features/orders/components/OrderStatusTimeline.js +39 -20
- package/dist/features/orders/repository/orders.repository.js +5 -0
- package/dist/features/orders/schemas/firestore.d.ts +36 -1
- package/dist/features/orders/schemas/index.d.ts +24 -24
- package/dist/features/orders/types/index.d.ts +35 -7
- package/dist/features/pre-orders/schemas/index.d.ts +4 -4
- package/dist/features/products/components/GroupSettingsPanel.js +5 -1
- package/dist/features/products/schemas/index.d.ts +38 -38
- package/dist/features/products/schemas/product-features.validators.d.ts +75 -2
- package/dist/features/products/schemas/product-features.validators.js +63 -0
- package/dist/features/promotions/schemas/index.d.ts +14 -14
- package/dist/features/reviews/schemas/index.d.ts +14 -14
- package/dist/features/scams/constants/scammer-status-display.d.ts +28 -0
- package/dist/features/scams/constants/scammer-status-display.js +79 -0
- package/dist/features/scams/schemas/index.d.ts +2 -2
- package/dist/features/search/schemas/index.d.ts +2 -2
- package/dist/features/seller/actions/offer-actions.js +32 -2
- package/dist/features/seller/components/OfferPhaseTimeline.d.ts +59 -0
- package/dist/features/seller/components/OfferPhaseTimeline.js +107 -0
- package/dist/features/seller/components/SellerBidsView.js +11 -4
- package/dist/features/seller/components/SellerCouponEditorView.js +17 -17
- package/dist/features/seller/components/SellerOffersView.js +2 -1
- package/dist/features/seller/components/SellerPayoutSettingsView.js +38 -14
- package/dist/features/seller/repository/offer.repository.d.ts +69 -8
- package/dist/features/seller/repository/offer.repository.js +127 -23
- package/dist/features/seller/schemas/coupon-form.d.ts +72 -0
- package/dist/features/seller/schemas/coupon-form.js +116 -0
- package/dist/features/seller/schemas/firestore.d.ts +56 -2
- package/dist/features/seller/schemas/firestore.js +27 -0
- package/dist/features/seller/schemas/index.d.ts +2 -2
- package/dist/features/seller/schemas/offer-forms.d.ts +21 -0
- package/dist/features/seller/schemas/offer-forms.js +21 -0
- package/dist/features/shell/build-sections.d.ts +63 -0
- package/dist/features/shell/build-sections.js +207 -0
- package/dist/features/shell/field-ui-meta.d.ts +115 -0
- package/dist/features/shell/field-ui-meta.js +324 -0
- package/dist/features/shell/index.d.ts +4 -0
- package/dist/features/shell/index.js +2 -0
- package/dist/features/shipments/schemas/validation.d.ts +22 -22
- package/dist/features/status-history/components/StatusTimeline.d.ts +74 -0
- package/dist/features/status-history/components/StatusTimeline.js +46 -0
- package/dist/features/status-history/index.d.ts +11 -0
- package/dist/features/status-history/index.js +10 -0
- package/dist/features/store-extensions/components/ReviewDecisionModal.d.ts +27 -0
- package/dist/features/store-extensions/components/ReviewDecisionModal.js +82 -0
- package/dist/features/store-extensions/index.d.ts +12 -0
- package/dist/features/store-extensions/index.js +12 -0
- package/dist/features/store-extensions/schemas/admin-notification-form.d.ts +36 -0
- package/dist/features/store-extensions/schemas/admin-notification-form.js +114 -0
- package/dist/features/store-extensions/schemas/analytics-forms.d.ts +57 -0
- package/dist/features/store-extensions/schemas/analytics-forms.js +158 -0
- package/dist/features/store-extensions/schemas/custom-role-form.d.ts +85 -0
- package/dist/features/store-extensions/schemas/custom-role-form.js +118 -0
- package/dist/features/store-extensions/schemas/firestore.d.ts +21 -1
- package/dist/features/store-extensions/schemas/firestore.js +60 -0
- package/dist/features/store-extensions/schemas/google-config-form.d.ts +53 -0
- package/dist/features/store-extensions/schemas/google-config-form.js +105 -0
- package/dist/features/store-extensions/schemas/item-request-create-form.d.ts +28 -0
- package/dist/features/store-extensions/schemas/item-request-create-form.js +80 -0
- package/dist/features/store-extensions/schemas/listing-template-form.d.ts +78 -0
- package/dist/features/store-extensions/schemas/listing-template-form.js +87 -0
- package/dist/features/store-extensions/schemas/moderation-review-form.d.ts +49 -0
- package/dist/features/store-extensions/schemas/moderation-review-form.js +101 -0
- package/dist/features/store-extensions/schemas/payout-method-form.d.ts +153 -0
- package/dist/features/store-extensions/schemas/payout-method-form.js +155 -0
- package/dist/features/store-extensions/schemas/report-create-form.d.ts +25 -0
- package/dist/features/store-extensions/schemas/report-create-form.js +79 -0
- package/dist/features/store-extensions/schemas/report-review-form.d.ts +47 -0
- package/dist/features/store-extensions/schemas/report-review-form.js +111 -0
- package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +148 -0
- package/dist/features/store-extensions/schemas/shipping-config-form.js +104 -0
- package/dist/features/store-extensions/schemas/store-category-form.d.ts +54 -0
- package/dist/features/store-extensions/schemas/store-category-form.js +73 -0
- package/dist/features/stores/schemas/index.d.ts +4 -4
- package/dist/features/support/schemas/index.d.ts +4 -4
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +350 -0
- package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +1 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.js +12 -2
- package/dist/next/routing/route-map.d.ts +16 -2
- package/dist/next/routing/route-map.js +9 -2
- package/dist/schemas/firestore-value.d.ts +8 -0
- package/dist/schemas/firestore-value.js +44 -0
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.js +1 -1
- package/dist/schemas/registry.d.ts +734 -101
- package/dist/schemas/registry.js +64 -1
- package/dist/seed/bids-seed-data.js +30 -0
- package/dist/seed/offers-seed-data.js +212 -7
- package/dist/seed/orders-seed-data.js +336 -1
- package/dist/seed/products-auctions-seed-data.js +41 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +5 -0
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/DataTable.js +4 -2
- package/dist/ui/columns/build-columns.d.ts +6 -1
- package/dist/ui/columns/build-columns.js +53 -0
- package/dist/ui/columns/cell-renderers.d.ts +68 -0
- package/dist/ui/columns/cell-renderers.js +111 -0
- package/dist/ui/columns/column-renderers.d.ts +6 -1
- package/dist/ui/columns/column-renderers.js +6 -1
- package/dist/ui/columns/index.d.ts +2 -1
- package/dist/ui/columns/index.js +5 -1
- package/dist/ui/components/Button.js +27 -2
- package/dist/ui/components/RecordDetailModal.d.ts +10 -1
- package/dist/ui/components/RecordDetailModal.js +2 -2
- package/dist/ui/icons/icon-registry.d.ts +65 -0
- package/dist/ui/icons/icon-registry.js +115 -0
- package/package.json +2 -1
- package/scripts/node-esm-loader.mjs +65 -0
- package/scripts/seed-cli-loader.mjs +6 -49
- package/scripts/seed-cli.mjs +1 -1
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
import { wrapAction } from "@mohasinac/appkit/server";
|
|
3
3
|
import { bidRepository, productRepository, userRepository, siteSettingsRepository } from "../../../../repositories";
|
|
4
4
|
import { requireRoleUser } from "../../../../providers/auth-firebase/helpers";
|
|
5
|
-
import {
|
|
5
|
+
import { placeBidActionSchema } from "../../../shared/features/auctions/schema";
|
|
6
6
|
import { assertAuctionActive, assertBidAmount, assertNotAuctionOwner, shouldAutoExtend, computeExtendedEndDate, } from "./service";
|
|
7
7
|
import { ValidationError } from "../../../shared/errors/index";
|
|
8
8
|
export async function placeBidAction(input) {
|
|
9
9
|
return wrapAction(async () => {
|
|
10
10
|
const user = await requireRoleUser(["buyer", "seller", "admin"]);
|
|
11
|
-
const parsed =
|
|
11
|
+
const parsed = placeBidActionSchema.safeParse(input);
|
|
12
12
|
if (!parsed.success)
|
|
13
13
|
throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid bid input");
|
|
14
14
|
const { auctionId, amount } = parsed.data;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { z } from "zod";
|
|
2
2
|
import type { addToCartSchema } from "../../../shared/features/cart/schema";
|
|
3
|
-
type
|
|
3
|
+
type AddToCartActionInput = z.infer<typeof addToCartSchema>;
|
|
4
4
|
export declare function assertCartCapacity(userId: string, addingQty: number): Promise<void>;
|
|
5
5
|
export declare function assertValidQuantity(quantity: number): void;
|
|
6
|
-
export declare function upsertCartItem(userId: string, item:
|
|
7
|
-
export declare function mergeGuestItems(userId: string, guestItems:
|
|
6
|
+
export declare function upsertCartItem(userId: string, item: AddToCartActionInput): Promise<void>;
|
|
7
|
+
export declare function mergeGuestItems(userId: string, guestItems: AddToCartActionInput[]): Promise<void>;
|
|
8
8
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeError } from "../../../../errors/normalize";
|
|
2
|
+
import { buildOrderSourceContext } from "./source-context";
|
|
2
3
|
import { roundRupees } from "../../../../utils/number.formatter";
|
|
3
4
|
/**
|
|
4
5
|
* Checkout server actions (appkit).
|
|
@@ -488,6 +489,10 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
488
489
|
const displayedUpiId = hasPaymentWindow
|
|
489
490
|
? await resolveDisplayedUpiId(firstItem.storeId, siteContactUpiVpa)
|
|
490
491
|
: undefined;
|
|
492
|
+
// Both creation paths call the SAME builder — two hand-written copies is the
|
|
493
|
+
// create-vs-create axis of Root Cause #39, and these two functions have
|
|
494
|
+
// already drifted once before (add-on fees, Root Cause #65).
|
|
495
|
+
const sourceContext = await buildOrderSourceContext(firstItem, firstProduct, orderType);
|
|
491
496
|
const order = await unitOfWork.orders.create({
|
|
492
497
|
productId: firstItem.productId,
|
|
493
498
|
productTitle: firstItem.productTitle,
|
|
@@ -502,6 +507,11 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
502
507
|
storeName: firstItem.storeName || undefined,
|
|
503
508
|
items: orderItems,
|
|
504
509
|
orderType,
|
|
510
|
+
// How the buyer earned the right to buy at this price. `orderType` cannot
|
|
511
|
+
// answer that — it has five values, and both a settled win and a buyout
|
|
512
|
+
// are "auction". Written ONCE here and never recomputed: the auction and
|
|
513
|
+
// offer documents it summarises are mutable and eventually archived.
|
|
514
|
+
sourceContext,
|
|
505
515
|
offerId: firstItem.offerId ?? undefined,
|
|
506
516
|
// Distinguishes a Buy Now purchase from a settled auction win. Both are
|
|
507
517
|
// orderType "auction"; only this one gets the 24h admin-review window.
|
|
@@ -1204,6 +1214,10 @@ async function createRazorpayGroupOrder(group, orderType, ctx) {
|
|
|
1204
1214
|
const extraOrderFields = {
|
|
1205
1215
|
...groupRuleRzp.decorateOrderDoc(group[0].item, group[0].product),
|
|
1206
1216
|
};
|
|
1217
|
+
// Both creation paths call the SAME builder — two hand-written copies is the
|
|
1218
|
+
// create-vs-create axis of Root Cause #39, and these two functions have
|
|
1219
|
+
// already drifted once before (add-on fees, Root Cause #65).
|
|
1220
|
+
const sourceContext = await buildOrderSourceContext(firstItem, group[0].product, orderType);
|
|
1207
1221
|
const order = await unitOfWork.orders.create({
|
|
1208
1222
|
productId: firstItem.productId,
|
|
1209
1223
|
productTitle: firstItem.productTitle,
|
|
@@ -1218,6 +1232,11 @@ async function createRazorpayGroupOrder(group, orderType, ctx) {
|
|
|
1218
1232
|
storeName: firstItem.storeName || undefined,
|
|
1219
1233
|
items: orderItems,
|
|
1220
1234
|
orderType,
|
|
1235
|
+
// How the buyer earned the right to buy at this price. `orderType` cannot
|
|
1236
|
+
// answer that — it has five values, and both a settled win and a buyout
|
|
1237
|
+
// are "auction". Written ONCE here and never recomputed: the auction and
|
|
1238
|
+
// offer documents it summarises are mutable and eventually archived.
|
|
1239
|
+
sourceContext,
|
|
1221
1240
|
offerId: firstItem.offerId ?? undefined,
|
|
1222
1241
|
// Distinguishes a Buy Now purchase from a settled auction win. Both are
|
|
1223
1242
|
// orderType "auction"; only this one gets the 24h admin-review window.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { CartItemDocument } from "../../../../features/cart/schemas/firestore";
|
|
2
|
+
import type { ProductDocument } from "../../../../features/products/schemas/firestore";
|
|
3
|
+
import type { OrderSourceContext } from "../../../../features/orders/schemas/firestore";
|
|
4
|
+
import type { OrderType } from "../../../../features/orders/utils/order-splitter";
|
|
5
|
+
export declare function buildOrderSourceContext(item: CartItemDocument, product: ProductDocument, orderType: OrderType): Promise<OrderSourceContext | undefined>;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: An order records WHAT was bought and WHAT it cost, and nothing about how
|
|
3
|
+
* the buyer earned the right to buy it at that price. `orderType` has five
|
|
4
|
+
* values and cannot even separate "won at ₹4,200 against 11 bids" from
|
|
5
|
+
* "bought out at ₹6,000 while the standing bid was ₹3,800" — both land as
|
|
6
|
+
* `orderType: "auction"`. Everything else was recoverable only by reading
|
|
7
|
+
* the auction or offer document, which is mutable and eventually archived.
|
|
8
|
+
* WHAT: `buildOrderSourceContext()` — the ONE builder for `OrderSourceContext`,
|
|
9
|
+
* called by both order-creation paths (manual/COD and Razorpay).
|
|
10
|
+
*
|
|
11
|
+
* ## Why one shared function and not two inline blocks
|
|
12
|
+
*
|
|
13
|
+
* `createOrderForGroup` and `createRazorpayGroupOrder` are near-identical
|
|
14
|
+
* ~200-line twins that already drifted once on add-on fees (Root Cause #65).
|
|
15
|
+
* Two hand-written copies of this would be the create-vs-create axis of Root
|
|
16
|
+
* Cause #39: one lane records provenance, the other silently doesn't, and the
|
|
17
|
+
* difference is invisible until someone compares two orders months later.
|
|
18
|
+
*
|
|
19
|
+
* ## Written once, never recomputed
|
|
20
|
+
*
|
|
21
|
+
* These numbers describe a moment. `product.price` moves, `currentBid` is a
|
|
22
|
+
* mirror that a pending buyout deliberately never writes to, and a settled
|
|
23
|
+
* auction is eventually archived. Recomputing later would silently rewrite
|
|
24
|
+
* history, so nothing outside order creation may write this field.
|
|
25
|
+
*
|
|
26
|
+
* ## Read cost
|
|
27
|
+
*
|
|
28
|
+
* Standard, pre-order and prize-draw cost **zero** extra reads — everything
|
|
29
|
+
* comes from the product already in hand. Only the auction and offer lanes pay:
|
|
30
|
+
* one `findByProductSorted` for auctions (bid count, winner, runner-up, the
|
|
31
|
+
* standing bid — all four from one query) and one `findById` for offers. Since
|
|
32
|
+
* a checkout is single-lane by construction, a standard cart never pays either.
|
|
33
|
+
*
|
|
34
|
+
* ## Failure is non-fatal, deliberately
|
|
35
|
+
*
|
|
36
|
+
* A missing bid or offer document returns `undefined` rather than throwing. The
|
|
37
|
+
* money path must not fail because a provenance record could not be assembled —
|
|
38
|
+
* an order with no `sourceContext` is a thinner record; a checkout that 500s
|
|
39
|
+
* after payment authorisation is a support ticket.
|
|
40
|
+
*
|
|
41
|
+
* EXPORTS:
|
|
42
|
+
* buildOrderSourceContext
|
|
43
|
+
*
|
|
44
|
+
* @tag domain:checkout,orders
|
|
45
|
+
* @tag layer:server-feature
|
|
46
|
+
* @tag pattern:none
|
|
47
|
+
* @tag access:server
|
|
48
|
+
* @tag consumers:checkout/actions.ts
|
|
49
|
+
* @tag sideEffects:firestore-read
|
|
50
|
+
*/
|
|
51
|
+
import { bidRepository } from "../../../../features/auctions/repository/bid.repository";
|
|
52
|
+
import { offerRepository } from "../../../../features/seller/repository/offer.repository";
|
|
53
|
+
import { normalizeError } from "../../../../errors/normalize";
|
|
54
|
+
import { serverLogger } from "../../../../monitoring";
|
|
55
|
+
/** Bids that count toward "against N bidders" — a buyout is excluded. */
|
|
56
|
+
function competitiveBids(bids) {
|
|
57
|
+
return bids.filter((b) => !b.isBuyout).sort((a, b) => b.bidAmount - a.bidAmount);
|
|
58
|
+
}
|
|
59
|
+
export async function buildOrderSourceContext(item, product, orderType) {
|
|
60
|
+
try {
|
|
61
|
+
// The offer lane is checked BEFORE orderType, because an accepted offer on
|
|
62
|
+
// a standard listing arrives as orderType "offer" but an accepted offer on
|
|
63
|
+
// a classified does not — the cart line's own `offerId` is the reliable
|
|
64
|
+
// discriminator, exactly as `laneOf()` treats it.
|
|
65
|
+
if (item.offerId) {
|
|
66
|
+
const offer = await offerRepository.findById(item.offerId);
|
|
67
|
+
if (!offer)
|
|
68
|
+
return undefined;
|
|
69
|
+
return {
|
|
70
|
+
path: "offer-accepted",
|
|
71
|
+
offerId: offer.id,
|
|
72
|
+
offeredAmount: offer.offerAmount,
|
|
73
|
+
listPriceAtOffer: offer.listedPrice,
|
|
74
|
+
counterAmount: offer.counterAmount,
|
|
75
|
+
// Persisted since W2. Before the chain fields existed this number was
|
|
76
|
+
// genuinely unknowable — each counter created an unlinked document.
|
|
77
|
+
counterRounds: offer.counterRound ?? 1,
|
|
78
|
+
acceptedBy: offer.storeId,
|
|
79
|
+
acceptedAt: offer.acceptedAt ?? offer.respondedAt ?? new Date(),
|
|
80
|
+
checkoutDeadline: offer.checkoutDeadline,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (item.bidId) {
|
|
84
|
+
const bids = await bidRepository.findByProductSorted(item.productId);
|
|
85
|
+
const rivals = competitiveBids(bids);
|
|
86
|
+
const auctionId = item.auctionId ?? item.productId;
|
|
87
|
+
if (item.isBuyout) {
|
|
88
|
+
return {
|
|
89
|
+
path: "auction-buy-now",
|
|
90
|
+
auctionId,
|
|
91
|
+
bidId: item.bidId,
|
|
92
|
+
buyNowPrice: item.lockedPrice ?? product.buyNowPrice ?? item.price,
|
|
93
|
+
listPrice: product.price,
|
|
94
|
+
// From the bids, not `product.currentBid` — a pending buyout never
|
|
95
|
+
// writes that mirror, so reading it would understate the auction.
|
|
96
|
+
standingBidAtBuyout: rivals[0]?.bidAmount ?? 0,
|
|
97
|
+
bidCount: rivals.length,
|
|
98
|
+
boughtAt: new Date(),
|
|
99
|
+
checkoutDeadline: item.checkoutDeadline ?? new Date(),
|
|
100
|
+
auctionEndDate: product.auctionEndDate,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const winning = bids.find((b) => b.id === item.bidId);
|
|
104
|
+
const winningAmount = winning?.bidAmount ?? item.lockedPrice ?? item.price;
|
|
105
|
+
const reserve = product.reservePrice;
|
|
106
|
+
return {
|
|
107
|
+
path: "auction-won",
|
|
108
|
+
auctionId,
|
|
109
|
+
bidId: item.bidId,
|
|
110
|
+
winningBidAmount: winningAmount,
|
|
111
|
+
bidCount: rivals.length,
|
|
112
|
+
startingBid: product.startingBid ?? 0,
|
|
113
|
+
reservePrice: reserve,
|
|
114
|
+
// No reserve set means the reserve cannot fail, so it is met.
|
|
115
|
+
reserveMet: reserve == null ? true : winningAmount >= reserve,
|
|
116
|
+
auctionEndedAt: product.auctionEndDate ?? new Date(),
|
|
117
|
+
runnerUpAmount: rivals.find((b) => b.id !== item.bidId)?.bidAmount,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (orderType === "preorder") {
|
|
121
|
+
const deposit = product.preOrderDepositAmount ??
|
|
122
|
+
(product.preOrderDepositPercent != null
|
|
123
|
+
? (product.price * product.preOrderDepositPercent) / 100
|
|
124
|
+
: 0);
|
|
125
|
+
return {
|
|
126
|
+
path: "pre-order",
|
|
127
|
+
depositAmount: deposit,
|
|
128
|
+
balanceDue: Math.max(0, product.price - deposit),
|
|
129
|
+
expectedDeliveryDate: product.preOrderDeliveryDate,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (orderType === "prize-draw") {
|
|
133
|
+
return {
|
|
134
|
+
path: "prize-draw",
|
|
135
|
+
prizeDrawProductId: item.productId,
|
|
136
|
+
pricePerEntry: product.pricePerEntry ?? product.price,
|
|
137
|
+
entryCount: item.quantity,
|
|
138
|
+
// `prizeDrawMode` is reveal-vs-lottery, a different axis. The reveal
|
|
139
|
+
// TIMING is what this records: a draw with a reveal window is
|
|
140
|
+
// scheduled, one without reveals as soon as it sells out.
|
|
141
|
+
revealMode: product.prizeRevealWindowEnd ? "scheduled" : "instant",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return { path: "standard", listPrice: product.price };
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
const normalized = normalizeError(err);
|
|
148
|
+
// Non-fatal by design — see the header. Logged loudly so a systematic
|
|
149
|
+
// failure shows up as a pattern rather than as quietly thinner orders.
|
|
150
|
+
serverLogger.warn("[checkout] could not build sourceContext", {
|
|
151
|
+
productId: item.productId,
|
|
152
|
+
orderType,
|
|
153
|
+
error: normalized.message,
|
|
154
|
+
});
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offer document → API shape.
|
|
3
|
+
*
|
|
4
|
+
* Offers had **no adapter at all** before this: all three routes
|
|
5
|
+
* (`/api/user/offers`, `/api/store/offers`, `/api/admin/offers`) returned raw
|
|
6
|
+
* `OfferDocument`s straight from the repository, carrying live `Date`
|
|
7
|
+
* objects. `UserOffersPanel.relativeTime` still has a defensive
|
|
8
|
+
* `date instanceof Date ? … : new Date(…)` branch precisely because the shape
|
|
9
|
+
* it receives depends on whether the value survived JSON serialisation.
|
|
10
|
+
*
|
|
11
|
+
* Mirrors `orders/adapters.ts`, including its `toIsoOrUndefined` helper.
|
|
12
|
+
*/
|
|
13
|
+
import type { OfferDocument } from "../../../../features/seller/schemas";
|
|
14
|
+
import type { FieldChange } from "../../../shared/history/index";
|
|
15
|
+
/** One entry on an offer's timeline — dates as ISO, ready for JSON. */
|
|
16
|
+
export interface OfferTimelineEntry {
|
|
17
|
+
at: string;
|
|
18
|
+
actorUid?: string;
|
|
19
|
+
actorRole: "buyer" | "seller" | "admin" | "system";
|
|
20
|
+
changes: Record<string, FieldChange>;
|
|
21
|
+
reason?: string;
|
|
22
|
+
note?: string;
|
|
23
|
+
trigger: string;
|
|
24
|
+
}
|
|
25
|
+
/** One round of a negotiation, as rendered in the chain rail. */
|
|
26
|
+
export interface OfferChainRound {
|
|
27
|
+
id: string;
|
|
28
|
+
counterRound: number;
|
|
29
|
+
status: OfferDocument["status"];
|
|
30
|
+
offerAmount: number;
|
|
31
|
+
counterAmount?: number;
|
|
32
|
+
/**
|
|
33
|
+
* `withdrawn` + a `supersededByOfferId` is NOT a walk-away — the buyer
|
|
34
|
+
* countered. The timeline renders it neutral ("Superseded") rather than
|
|
35
|
+
* negative ("Withdrawn"), which is why this flag is carried explicitly
|
|
36
|
+
* instead of being re-derived in the component.
|
|
37
|
+
*/
|
|
38
|
+
superseded: boolean;
|
|
39
|
+
createdAt: string;
|
|
40
|
+
respondedAt?: string;
|
|
41
|
+
sellerNote?: string;
|
|
42
|
+
buyerNote?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface Offer {
|
|
45
|
+
id: string;
|
|
46
|
+
productId: string;
|
|
47
|
+
productTitle: string;
|
|
48
|
+
productSlug?: string;
|
|
49
|
+
productImageUrl?: string;
|
|
50
|
+
storeId: string;
|
|
51
|
+
storeName: string;
|
|
52
|
+
offerAmount: number;
|
|
53
|
+
listedPrice: number;
|
|
54
|
+
counterAmount?: number;
|
|
55
|
+
lockedPrice?: number;
|
|
56
|
+
currency: string;
|
|
57
|
+
status: OfferDocument["status"];
|
|
58
|
+
buyerNote?: string;
|
|
59
|
+
sellerNote?: string;
|
|
60
|
+
expiresAt: string;
|
|
61
|
+
acceptedAt?: string;
|
|
62
|
+
checkoutDeadline?: string;
|
|
63
|
+
respondedAt?: string;
|
|
64
|
+
paidOrderId?: string;
|
|
65
|
+
paidAt?: string;
|
|
66
|
+
createdAt: string;
|
|
67
|
+
updatedAt: string;
|
|
68
|
+
timeline?: OfferTimelineEntry[];
|
|
69
|
+
timelineTruncated?: number;
|
|
70
|
+
previousOfferId?: string;
|
|
71
|
+
supersededByOfferId?: string;
|
|
72
|
+
chainRootOfferId?: string;
|
|
73
|
+
counterRound?: number;
|
|
74
|
+
/** True when this round was superseded by the buyer's own counter. */
|
|
75
|
+
superseded: boolean;
|
|
76
|
+
cancelledByAdminUid?: string;
|
|
77
|
+
cancelReason?: string;
|
|
78
|
+
buyerUid?: string;
|
|
79
|
+
buyerName?: string;
|
|
80
|
+
buyerEmail?: string;
|
|
81
|
+
}
|
|
82
|
+
export interface OfferAdapterOptions {
|
|
83
|
+
/**
|
|
84
|
+
* Include `buyerUid`/`buyerName`/`buyerEmail`.
|
|
85
|
+
*
|
|
86
|
+
* Default **false**, deliberately. The seller-facing route masks buyer
|
|
87
|
+
* identity, and a route that forgot to call `maskOfferForSeller` would
|
|
88
|
+
* previously have shipped it verbatim — there was no adapter to stop it.
|
|
89
|
+
* Making the omission the default means forgetting the flag under-shares
|
|
90
|
+
* rather than leaks.
|
|
91
|
+
*/
|
|
92
|
+
includeBuyerIdentity?: boolean;
|
|
93
|
+
}
|
|
94
|
+
export declare function offerDocumentToOffer(doc: OfferDocument, opts?: OfferAdapterOptions): Offer;
|
|
95
|
+
/**
|
|
96
|
+
* A chain round — the compact shape the timeline needs, without re-sending
|
|
97
|
+
* the whole document for every round.
|
|
98
|
+
*
|
|
99
|
+
* Buyer identity is never included: the chain is rendered on all three
|
|
100
|
+
* portals from one component, and the seller's view must not gain identity
|
|
101
|
+
* it is masked from elsewhere.
|
|
102
|
+
*/
|
|
103
|
+
export declare function offerToChainRound(doc: OfferDocument): OfferChainRound;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offer document → API shape.
|
|
3
|
+
*
|
|
4
|
+
* Offers had **no adapter at all** before this: all three routes
|
|
5
|
+
* (`/api/user/offers`, `/api/store/offers`, `/api/admin/offers`) returned raw
|
|
6
|
+
* `OfferDocument`s straight from the repository, carrying live `Date`
|
|
7
|
+
* objects. `UserOffersPanel.relativeTime` still has a defensive
|
|
8
|
+
* `date instanceof Date ? … : new Date(…)` branch precisely because the shape
|
|
9
|
+
* it receives depends on whether the value survived JSON serialisation.
|
|
10
|
+
*
|
|
11
|
+
* Mirrors `orders/adapters.ts`, including its `toIsoOrUndefined` helper.
|
|
12
|
+
*/
|
|
13
|
+
function toIsoOrUndefined(value) {
|
|
14
|
+
return value instanceof Date ? value.toISOString() : undefined;
|
|
15
|
+
}
|
|
16
|
+
function toIso(value) {
|
|
17
|
+
return value instanceof Date ? value.toISOString() : String(value);
|
|
18
|
+
}
|
|
19
|
+
export function offerDocumentToOffer(doc, opts = {}) {
|
|
20
|
+
const superseded = doc.status === "withdrawn" && Boolean(doc.supersededByOfferId);
|
|
21
|
+
return {
|
|
22
|
+
id: doc.id,
|
|
23
|
+
productId: doc.productId,
|
|
24
|
+
productTitle: doc.productTitle,
|
|
25
|
+
productSlug: doc.productSlug,
|
|
26
|
+
productImageUrl: doc.productImageUrl,
|
|
27
|
+
storeId: doc.storeId,
|
|
28
|
+
storeName: doc.storeName,
|
|
29
|
+
offerAmount: doc.offerAmount,
|
|
30
|
+
listedPrice: doc.listedPrice,
|
|
31
|
+
counterAmount: doc.counterAmount,
|
|
32
|
+
lockedPrice: doc.lockedPrice,
|
|
33
|
+
currency: doc.currency,
|
|
34
|
+
status: doc.status,
|
|
35
|
+
buyerNote: doc.buyerNote,
|
|
36
|
+
sellerNote: doc.sellerNote,
|
|
37
|
+
expiresAt: toIso(doc.expiresAt),
|
|
38
|
+
acceptedAt: toIsoOrUndefined(doc.acceptedAt),
|
|
39
|
+
checkoutDeadline: toIsoOrUndefined(doc.checkoutDeadline),
|
|
40
|
+
respondedAt: toIsoOrUndefined(doc.respondedAt),
|
|
41
|
+
paidOrderId: doc.paidOrderId,
|
|
42
|
+
paidAt: toIsoOrUndefined(doc.paidAt),
|
|
43
|
+
createdAt: toIso(doc.createdAt),
|
|
44
|
+
updatedAt: toIso(doc.updatedAt),
|
|
45
|
+
timeline: doc.statusHistory?.map((entry) => ({
|
|
46
|
+
at: entry.at instanceof Date ? entry.at.toISOString() : String(entry.at),
|
|
47
|
+
actorUid: entry.actorUid,
|
|
48
|
+
actorRole: entry.actorRole,
|
|
49
|
+
changes: entry.changes,
|
|
50
|
+
reason: entry.reason,
|
|
51
|
+
note: entry.note,
|
|
52
|
+
trigger: entry.trigger,
|
|
53
|
+
})),
|
|
54
|
+
timelineTruncated: doc.statusHistoryTruncated,
|
|
55
|
+
previousOfferId: doc.previousOfferId,
|
|
56
|
+
supersededByOfferId: doc.supersededByOfferId,
|
|
57
|
+
chainRootOfferId: doc.chainRootOfferId,
|
|
58
|
+
counterRound: doc.counterRound,
|
|
59
|
+
superseded,
|
|
60
|
+
cancelledByAdminUid: doc.cancelledByAdminUid,
|
|
61
|
+
cancelReason: doc.cancelReason,
|
|
62
|
+
...(opts.includeBuyerIdentity
|
|
63
|
+
? { buyerUid: doc.buyerUid, buyerName: doc.buyerName, buyerEmail: doc.buyerEmail }
|
|
64
|
+
: {}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A chain round — the compact shape the timeline needs, without re-sending
|
|
69
|
+
* the whole document for every round.
|
|
70
|
+
*
|
|
71
|
+
* Buyer identity is never included: the chain is rendered on all three
|
|
72
|
+
* portals from one component, and the seller's view must not gain identity
|
|
73
|
+
* it is masked from elsewhere.
|
|
74
|
+
*/
|
|
75
|
+
export function offerToChainRound(doc) {
|
|
76
|
+
return {
|
|
77
|
+
id: doc.id,
|
|
78
|
+
counterRound: doc.counterRound ?? 1,
|
|
79
|
+
status: doc.status,
|
|
80
|
+
offerAmount: doc.offerAmount,
|
|
81
|
+
counterAmount: doc.counterAmount,
|
|
82
|
+
superseded: doc.status === "withdrawn" && Boolean(doc.supersededByOfferId),
|
|
83
|
+
createdAt: toIso(doc.createdAt),
|
|
84
|
+
respondedAt: toIsoOrUndefined(doc.respondedAt),
|
|
85
|
+
sellerNote: doc.sellerNote,
|
|
86
|
+
buyerNote: doc.buyerNote,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { offerDocumentToOffer, offerToChainRound, } from "./adapters";
|
|
@@ -104,6 +104,30 @@ export function orderDocumentToOrder(doc) {
|
|
|
104
104
|
paymentReviewOutcome: doc.paymentReviewOutcome,
|
|
105
105
|
paymentReviewNote: doc.paymentReviewNote,
|
|
106
106
|
cancellationReason: doc.cancellationReason,
|
|
107
|
+
// ── W2: history + provenance ──────────────────────────────────────────
|
|
108
|
+
//
|
|
109
|
+
// `Order.timeline` has existed, been exported, and had ZERO writers since
|
|
110
|
+
// it was declared — `OrderStatusTimeline` synthesised its steps from four
|
|
111
|
+
// scalar dates instead, so a status change with no dedicated date field
|
|
112
|
+
// (payment reviewed, refund posted) was simply invisible.
|
|
113
|
+
//
|
|
114
|
+
// `statusHistory` is that field's real source. Dates are serialised to
|
|
115
|
+
// ISO here for the same reason every other date on this shape is: the
|
|
116
|
+
// client receives JSON, and a raw `Date` arrives as a string that no
|
|
117
|
+
// longer satisfies the type.
|
|
118
|
+
timeline: doc.statusHistory?.map((entry) => ({
|
|
119
|
+
at: entry.at instanceof Date ? entry.at.toISOString() : String(entry.at),
|
|
120
|
+
actorUid: entry.actorUid,
|
|
121
|
+
actorRole: entry.actorRole,
|
|
122
|
+
changes: entry.changes,
|
|
123
|
+
reason: entry.reason,
|
|
124
|
+
note: entry.note,
|
|
125
|
+
trigger: entry.trigger,
|
|
126
|
+
})),
|
|
127
|
+
/** Entries trimmed off the front — so the UI never implies "this is all of it". */
|
|
128
|
+
timelineTruncated: doc.statusHistoryTruncated,
|
|
129
|
+
/** How the buyer acquired the right to buy. Written once, at creation. */
|
|
130
|
+
sourceContext: doc.sourceContext,
|
|
107
131
|
createdAt: doc.createdAt instanceof Date
|
|
108
132
|
? doc.createdAt.toISOString()
|
|
109
133
|
: String(doc.createdAt),
|
|
@@ -21,10 +21,10 @@ export async function runOfferExpiry(ctx) {
|
|
|
21
21
|
else {
|
|
22
22
|
ctx.logger.info(`Found ${expiredOffers.length} expired offer(s) to process`);
|
|
23
23
|
}
|
|
24
|
-
const
|
|
24
|
+
const expired = [];
|
|
25
25
|
for (const offer of expiredOffers) {
|
|
26
26
|
try {
|
|
27
|
-
|
|
27
|
+
expired.push(offer);
|
|
28
28
|
await sendNotification({
|
|
29
29
|
userId: offer.buyerUid,
|
|
30
30
|
type: "offer_expired",
|
|
@@ -42,9 +42,9 @@ export async function runOfferExpiry(ctx) {
|
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
-
if (
|
|
45
|
+
if (expired.length > 0) {
|
|
46
46
|
try {
|
|
47
|
-
await offerRepository.expireMany(
|
|
47
|
+
await offerRepository.expireMany(expired, { trigger: "runOfferExpiry:active" });
|
|
48
48
|
}
|
|
49
49
|
catch (err) {
|
|
50
50
|
ctx.logger.error("Failed to batch-expire offers", err);
|
|
@@ -54,8 +54,8 @@ export async function runOfferExpiry(ctx) {
|
|
|
54
54
|
await expireAcceptedPastCheckoutDeadline(ctx);
|
|
55
55
|
await lapseUnpaidAuctionWins(ctx);
|
|
56
56
|
ctx.logger.info("Offer expiry complete", {
|
|
57
|
-
processed:
|
|
58
|
-
skipped: expiredOffers.length -
|
|
57
|
+
processed: expired.length,
|
|
58
|
+
skipped: expiredOffers.length - expired.length,
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
61
|
/**
|
|
@@ -81,14 +81,14 @@ async function expireAcceptedPastCheckoutDeadline(ctx) {
|
|
|
81
81
|
if (stale.length === 0)
|
|
82
82
|
return;
|
|
83
83
|
ctx.logger.info(`Found ${stale.length} accepted offer(s) past their checkout deadline`);
|
|
84
|
-
const
|
|
84
|
+
const lapsed = [];
|
|
85
85
|
for (const offer of stale) {
|
|
86
86
|
try {
|
|
87
87
|
// Drop the locked cart line first — leaving it behind would keep the
|
|
88
88
|
// buyer's offer lane non-empty and block their standard checkout on an
|
|
89
89
|
// offer they can no longer act on.
|
|
90
90
|
await cartRepository.removeItemsByOfferId(offer.buyerUid, offer.id);
|
|
91
|
-
|
|
91
|
+
lapsed.push(offer);
|
|
92
92
|
await sendNotification({
|
|
93
93
|
userId: offer.buyerUid,
|
|
94
94
|
type: "offer_expired",
|
|
@@ -106,9 +106,9 @@ async function expireAcceptedPastCheckoutDeadline(ctx) {
|
|
|
106
106
|
});
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
-
if (
|
|
109
|
+
if (lapsed.length > 0) {
|
|
110
110
|
try {
|
|
111
|
-
await offerRepository.expireMany(
|
|
111
|
+
await offerRepository.expireMany(lapsed, { trigger: "runOfferExpiry:acceptedLapsed" });
|
|
112
112
|
}
|
|
113
113
|
catch (err) {
|
|
114
114
|
void normalizeError(err);
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import type { UserRole } from "../../../features/auth/types";
|
|
26
26
|
import type { ListingType } from "../../../features/products/types/index";
|
|
27
27
|
import type { CategoryType } from "../../../features/categories/types/index";
|
|
28
|
+
import type { IconKey } from "../../../ui/icons/icon-registry";
|
|
28
29
|
/** What KIND of action — drives default UI affordance (link vs button vs danger). */
|
|
29
30
|
export type ActionKind = "primary" | "secondary" | "danger" | "ghost" | "link" | "chip";
|
|
30
31
|
/** Resource bucket — keyed top-level in the ACTIONS tree. */
|
|
@@ -63,8 +64,14 @@ export interface ActionDef {
|
|
|
63
64
|
listingTypeScope?: readonly ListingType[];
|
|
64
65
|
/** Category-type scope — same but for category discriminators. */
|
|
65
66
|
categoryTypeScope?: readonly CategoryType[];
|
|
66
|
-
/**
|
|
67
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Icon for this action, resolved through `ICONS` in `ui/icons/icon-registry`.
|
|
69
|
+
*
|
|
70
|
+
* Typed `IconKey`, not `string`: as a bare string this field was set on 2 of
|
|
71
|
+
* 220 actions and rendered by nothing for months, and a typo was a silent
|
|
72
|
+
* no-op. A key outside the registry is now a compile error.
|
|
73
|
+
*/
|
|
74
|
+
iconKey?: IconKey;
|
|
68
75
|
/** Optional confirmation modal config. */
|
|
69
76
|
confirmation?: ActionConfirmation;
|
|
70
77
|
}
|