@mohasinac/appkit 4.62.1 → 4.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.build-stamp.json +3 -3
- package/dist/_internal/client/features/layout/DashboardLayoutClient.js +8 -29
- package/dist/_internal/client/features/layout/filterNavItems.d.ts +28 -0
- package/dist/_internal/client/features/layout/filterNavItems.js +35 -0
- package/dist/_internal/server/features/ads/booking.d.ts +118 -0
- package/dist/_internal/server/features/ads/booking.js +326 -0
- package/dist/_internal/server/features/checkout/actions.d.ts +51 -11
- package/dist/_internal/server/features/checkout/actions.js +335 -80
- package/dist/_internal/server/features/checkout/expand-order-items.d.ts +1 -2
- package/dist/_internal/server/features/checkout/expand-order-items.js +32 -0
- package/dist/_internal/server/features/checkout/freeze-parts.d.ts +50 -0
- package/dist/_internal/server/features/checkout/freeze-parts.js +94 -0
- package/dist/_internal/server/features/checkout/value-otp-gate.d.ts +55 -0
- package/dist/_internal/server/features/checkout/value-otp-gate.js +60 -0
- package/dist/_internal/server/features/dashboard/attention.d.ts +128 -0
- package/dist/_internal/server/features/dashboard/attention.js +183 -0
- package/dist/_internal/server/features/dashboard/data.d.ts +47 -0
- package/dist/_internal/server/features/dashboard/data.js +82 -0
- package/dist/_internal/server/features/inventory/reservations.d.ts +99 -0
- package/dist/_internal/server/features/inventory/reservations.js +273 -0
- package/dist/_internal/server/features/offers/destinations.d.ts +40 -0
- package/dist/_internal/server/features/offers/destinations.js +65 -0
- package/dist/_internal/server/features/orders/adapters.js +21 -0
- package/dist/_internal/server/features/orders/amend-delivery.d.ts +56 -0
- package/dist/_internal/server/features/orders/amend-delivery.js +124 -0
- package/dist/_internal/server/features/orders/balance.d.ts +89 -0
- package/dist/_internal/server/features/orders/balance.js +256 -0
- package/dist/_internal/server/features/pricing/actions.d.ts +58 -0
- package/dist/_internal/server/features/pricing/actions.js +144 -0
- package/dist/_internal/server/features/shipping/book-split.d.ts +86 -0
- package/dist/_internal/server/features/shipping/book-split.js +130 -0
- package/dist/_internal/server/features/shipping/book.d.ts +61 -0
- package/dist/_internal/server/features/shipping/book.js +176 -0
- package/dist/_internal/server/features/shipping/courier-block.js +26 -1
- package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
- package/dist/_internal/server/features/site-settings/adapters.js +14 -0
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +14 -0
- package/dist/_internal/server/functions/scheduled.d.ts +2 -1
- package/dist/_internal/server/functions/scheduled.js +57 -1
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +2 -0
- package/dist/_internal/server/jobs/core/jobRunners.js +45 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +4 -1
- package/dist/_internal/server/jobs/core/reservationExpiry.d.ts +17 -0
- package/dist/_internal/server/jobs/core/reservationExpiry.js +40 -0
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +15 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
- package/dist/_internal/server/jobs/handlers/index.js +1 -0
- package/dist/_internal/server/jobs/handlers/reservationExpiry.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/reservationExpiry.js +2 -0
- package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
- package/dist/_internal/shared/actions/action-registry.js +268 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +31 -2
- package/dist/_internal/shared/checkout/order-math.js +22 -36
- package/dist/_internal/shared/checkout/rules/index.d.ts +1 -1
- package/dist/_internal/shared/checkout/rules/index.js +1 -1
- package/dist/_internal/shared/checkout/rules/payment-gate.d.ts +18 -0
- package/dist/_internal/shared/checkout/rules/payment-gate.js +25 -0
- package/dist/_internal/shared/checkout/split.d.ts +0 -12
- package/dist/_internal/shared/checkout/split.js +22 -2
- package/dist/_internal/shared/content/disclaimers.js +7 -7
- package/dist/_internal/shared/content/published-values.d.ts +6 -1
- package/dist/_internal/shared/content/published-values.js +8 -0
- package/dist/_internal/shared/features/admin/integrations.d.ts +32 -2
- package/dist/_internal/shared/features/admin/integrations.js +19 -9
- package/dist/_internal/shared/features/dashboard/hub-sections.d.ts +72 -0
- package/dist/_internal/shared/features/dashboard/hub-sections.js +75 -0
- package/dist/_internal/shared/features/emi/choice.d.ts +98 -0
- package/dist/_internal/shared/features/emi/choice.js +69 -0
- package/dist/_internal/shared/features/emi/group.d.ts +84 -0
- package/dist/_internal/shared/features/emi/group.js +126 -0
- package/dist/_internal/shared/features/ledger/posting.d.ts +107 -0
- package/dist/_internal/shared/features/ledger/posting.js +165 -0
- package/dist/_internal/shared/features/ledger/reports.d.ts +76 -0
- package/dist/_internal/shared/features/ledger/reports.js +127 -0
- package/dist/_internal/shared/features/ledger/rules.d.ts +170 -0
- package/dist/_internal/shared/features/ledger/rules.js +221 -0
- package/dist/_internal/shared/features/offers/coordination.d.ts +122 -0
- package/dist/_internal/shared/features/offers/coordination.js +80 -0
- package/dist/_internal/shared/features/orders/delivery-snapshot.d.ts +114 -0
- package/dist/_internal/shared/features/orders/delivery-snapshot.js +116 -0
- package/dist/_internal/shared/features/payouts/pay-period.d.ts +91 -0
- package/dist/_internal/shared/features/payouts/pay-period.js +104 -0
- package/dist/_internal/shared/features/points/accepts.d.ts +1 -8
- package/dist/_internal/shared/features/points/accepts.js +12 -6
- package/dist/_internal/shared/features/pricing/rate-registry.d.ts +150 -0
- package/dist/_internal/shared/features/pricing/rate-registry.js +246 -0
- package/dist/_internal/shared/features/shipping/parcel.d.ts +123 -0
- package/dist/_internal/shared/features/shipping/parcel.js +139 -0
- package/dist/_internal/shared/features/site-settings/fees.d.ts +16 -8
- package/dist/_internal/shared/features/site-settings/fees.js +12 -8
- package/dist/_internal/shared/fees/explain.d.ts +81 -0
- package/dist/_internal/shared/fees/explain.js +101 -0
- package/dist/_internal/shared/history/index.d.ts +1 -1
- package/dist/_internal/shared/history/index.js +1 -1
- package/dist/_internal/shared/history/types.d.ts +7 -1
- package/dist/_internal/shared/history/types.js +25 -0
- package/dist/client.d.ts +11 -1
- package/dist/client.js +13 -0
- package/dist/constants/api-endpoints.d.ts +72 -0
- package/dist/constants/api-endpoints.js +41 -0
- package/dist/constants/field-names.d.ts +41 -0
- package/dist/constants/field-names.js +47 -0
- package/dist/features/account/components/UserOrdersView.js +28 -2
- package/dist/features/admin/components/AdminIntegrationsView.d.ts +55 -0
- package/dist/features/admin/components/AdminIntegrationsView.js +74 -0
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +16 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +72 -3
- package/dist/features/admin/components/AdminOrdersView.js +20 -1
- package/dist/features/admin/schemas/firestore.d.ts +32 -0
- package/dist/features/ads/schemas/booking-write.d.ts +120 -0
- package/dist/features/ads/schemas/booking-write.js +75 -0
- package/dist/features/ads/schemas/creative-form.d.ts +18 -0
- package/dist/features/ads/schemas/creative-form.js +51 -0
- package/dist/features/ads/schemas/firestore.d.ts +140 -0
- package/dist/features/ads/schemas/firestore.js +109 -0
- package/dist/features/auctions/repository/bid.repository.js +3 -1
- package/dist/features/audit-log/schemas/firestore.d.ts +110 -0
- package/dist/features/audit-log/schemas/firestore.js +74 -0
- package/dist/features/auth/actions/profile-actions.d.ts +69 -1
- package/dist/features/auth/actions/profile-actions.js +62 -1
- package/dist/features/auth/schemas/firestore.d.ts +16 -2
- package/dist/features/auth/schemas/firestore.js +2 -1
- package/dist/features/blog/actions/blog-actions.d.ts +6 -0
- package/dist/features/blog/actions/blog-actions.js +17 -2
- package/dist/features/blog/api/route.js +4 -1
- package/dist/features/blog/columns/index.js +5 -0
- package/dist/features/blog/components/BlogIndexPageView.js +2 -1
- package/dist/features/blog/repository/blog.repository.d.ts +4 -0
- package/dist/features/blog/repository/blog.repository.js +5 -0
- package/dist/features/blog/schemas/blog-form.d.ts +3 -0
- package/dist/features/blog/schemas/blog-form.js +1 -0
- package/dist/features/blog/schemas/firestore.d.ts +38 -1
- package/dist/features/blog/schemas/firestore.js +23 -0
- package/dist/features/blog/schemas/index.d.ts +6 -0
- package/dist/features/blog/schemas/index.js +2 -0
- package/dist/features/blog/types/index.d.ts +2 -0
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +12 -1
- package/dist/features/cart/components/CartPriceBreakdown.js +24 -5
- package/dist/features/cart/repository/cart.repository.d.ts +14 -0
- package/dist/features/cart/repository/cart.repository.js +20 -0
- package/dist/features/cart/schemas/firestore.d.ts +24 -0
- package/dist/features/dashboard/components/DashboardHubView.d.ts +83 -0
- package/dist/features/dashboard/components/DashboardHubView.js +56 -0
- package/dist/features/dashboard/components/DashboardSectionsClient.d.ts +23 -0
- package/dist/features/dashboard/components/DashboardSectionsClient.js +66 -0
- package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.d.ts +55 -0
- package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.js +175 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +24 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +28 -0
- package/dist/features/dispute-reasons/schemas/firestore.d.ts +3 -5
- package/dist/features/events/components/EventSaleBanner.js +1 -1
- package/dist/features/faq/components/FAQPageContent.js +2 -2
- package/dist/features/gateway-events/schemas/index.d.ts +2 -2
- package/dist/features/homepage/schemas/index.d.ts +8 -8
- package/dist/features/inventory/schemas/stock-reservation.d.ts +114 -0
- package/dist/features/inventory/schemas/stock-reservation.js +103 -0
- package/dist/features/layout/AppLayoutShell.d.ts +9 -1
- package/dist/features/layout/AppLayoutShell.js +12 -4
- package/dist/features/layout/BottomActions.js +2 -1
- package/dist/features/layout/BottomNavbar.d.ts +10 -2
- package/dist/features/layout/SideActionDock.js +52 -2
- package/dist/features/layout/TitleBarLayout.js +13 -4
- package/dist/features/layout/TitleBarLayout.style.css +59 -0
- package/dist/features/layout/useCtaReveal.d.ts +16 -1
- package/dist/features/layout/useCtaReveal.js +17 -26
- package/dist/features/ledger/repository/journal.repository.d.ts +110 -0
- package/dist/features/ledger/repository/journal.repository.js +312 -0
- package/dist/features/ledger/schemas/expenses.d.ts +87 -0
- package/dist/features/ledger/schemas/expenses.js +27 -0
- package/dist/features/ledger/schemas/firestore.d.ts +183 -0
- package/dist/features/ledger/schemas/firestore.js +69 -0
- package/dist/features/orders/components/BalancePaymentPanel.d.ts +12 -0
- package/dist/features/orders/components/BalancePaymentPanel.js +94 -0
- package/dist/features/orders/components/RaiseDisputeForm.js +19 -11
- package/dist/features/orders/repository/orders.repository.d.ts +28 -1
- package/dist/features/orders/repository/orders.repository.js +60 -6
- package/dist/features/orders/schemas/balance.d.ts +40 -0
- package/dist/features/orders/schemas/balance.js +47 -0
- package/dist/features/orders/schemas/firestore.d.ts +202 -0
- package/dist/features/orders/schemas/firestore.js +43 -0
- package/dist/features/orders/schemas/index.d.ts +2 -2
- package/dist/features/orders/types/index.d.ts +21 -0
- package/dist/features/payments/repository/payout.repository.js +3 -2
- package/dist/features/payments/schemas/firestore.d.ts +92 -1
- package/dist/features/payments/schemas/firestore.js +23 -0
- package/dist/features/pricing/components/AdminPricingView.d.ts +13 -0
- package/dist/features/pricing/components/AdminPricingView.js +157 -0
- package/dist/features/products/schemas/firestore.d.ts +26 -1
- package/dist/features/products/schemas/firestore.js +2 -0
- package/dist/features/products/schemas/index.d.ts +2 -2
- package/dist/features/reviews/repository/reviews.repository.d.ts +14 -0
- package/dist/features/reviews/repository/reviews.repository.js +16 -0
- package/dist/features/scams/repository/scammer.repository.js +2 -1
- package/dist/features/scams/schemas/firestore.d.ts +2 -1
- package/dist/features/scams/schemas/firestore.js +2 -1
- package/dist/features/seller/actions/offer-actions.js +56 -0
- package/dist/features/seller/actions/seller-actions.d.ts +1 -1
- package/dist/features/seller/actions/seller-actions.js +4 -0
- package/dist/features/seller/components/DispatchForm.d.ts +11 -0
- package/dist/features/seller/components/DispatchForm.js +190 -0
- package/dist/features/seller/components/SellerOrdersView.js +94 -2
- package/dist/features/seller/components/index.d.ts +1 -0
- package/dist/features/seller/components/index.js +1 -0
- package/dist/features/seller/repository/offer.repository.js +4 -3
- package/dist/features/seller/schemas/firestore.d.ts +23 -2
- package/dist/features/seller/schemas/firestore.js +3 -2
- package/dist/features/seller/schemas/index.d.ts +4 -4
- package/dist/features/seller/schemas/order-forms.d.ts +61 -4
- package/dist/features/seller/schemas/order-forms.js +41 -8
- package/dist/features/shipments/schemas/firestore.d.ts +25 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +39 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +55 -0
- package/dist/features/shipping-boxes/schemas/firestore.d.ts +2 -4
- package/dist/features/stores/repository/store.repository.d.ts +18 -0
- package/dist/features/stores/repository/store.repository.js +27 -1
- package/dist/features/stores/schemas/firestore.d.ts +41 -0
- package/dist/features/stores/schemas/firestore.js +20 -0
- package/dist/features/stores/schemas/index.d.ts +6 -6
- package/dist/features/support/repository/support.repository.js +72 -6
- package/dist/features/support/schemas/firestore.d.ts +17 -0
- package/dist/features/support/schemas/index.d.ts +2 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +19 -0
- package/dist/next/routing/route-map.d.ts +92 -0
- package/dist/next/routing/route-map.js +53 -0
- package/dist/react/contexts/SessionContext.d.ts +17 -1
- package/dist/schemas/registry.d.ts +22 -16
- package/dist/security/pii-schemas.d.ts +1 -1
- package/dist/security/pii-schemas.js +45 -0
- package/dist/server.d.ts +27 -1
- package/dist/server.js +144 -0
- package/dist/styles.css +349 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Button.d.ts +10 -1
- package/dist/ui/components/Button.js +25 -3
- package/dist/ui/components/Chart.d.ts +63 -0
- package/dist/ui/components/Chart.js +169 -0
- package/dist/ui/components/Chart.style.css +214 -0
- package/dist/ui/components/Disclaimer.js +10 -1
- package/dist/ui/components/OverflowRow.js +54 -2
- package/dist/ui/components/OverflowRow.style.css +38 -0
- package/dist/ui/components/RichTextEditor.js +99 -8
- package/dist/ui/components/chart-spec.d.ts +68 -0
- package/dist/ui/components/chart-spec.js +140 -0
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/components/surface-tokens.d.ts +1 -1
- package/dist/ui/components/surface-tokens.js +9 -0
- package/dist/ui/components/toolbar-collapse.d.ts +13 -0
- package/dist/ui/components/toolbar-collapse.js +10 -1
- package/dist/ui/index.d.ts +6 -0
- package/dist/ui/index.js +3 -0
- package/dist/ui/rich-text/RichText.d.ts +10 -1
- package/dist/ui/rich-text/RichText.js +33 -2
- package/dist/ui/rich-text/RichText.style.css +35 -0
- package/dist/ui/rich-text/RichTextRenderer.d.ts +30 -1
- package/dist/ui/rich-text/RichTextRenderer.js +34 -2
- package/dist/ui/rich-text/chart-blocks.d.ts +62 -0
- package/dist/ui/rich-text/chart-blocks.js +87 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +3 -0
|
@@ -48,6 +48,49 @@ export const RefundStatusValues = {
|
|
|
48
48
|
COMPLETED: "completed",
|
|
49
49
|
REJECTED: "rejected",
|
|
50
50
|
};
|
|
51
|
+
/** Firestore storage shape for an order line item â€" distinct from the API display model OrderItem */
|
|
52
|
+
/**
|
|
53
|
+
* The PII inside one amendment.
|
|
54
|
+
*
|
|
55
|
+
* 🛑 BARE keys, and `ORDER_PII_FIELDS` uses the `deliveryAmendments[].…`
|
|
56
|
+
* form for the same five fields. Two notations, two scopes, both correct:
|
|
57
|
+
* the `[]` form is for the whole ORDER document, where the hook has to walk
|
|
58
|
+
* into the array; these bare names are for ONE amendment object, where they
|
|
59
|
+
* are genuinely top-level.
|
|
60
|
+
*
|
|
61
|
+
* 🛑 This list was first justified as "`encryptPiiFields` CANNOT cover an
|
|
62
|
+
* array" — CLAUDE.md's wording, and FALSE. `encryptInArray` has always
|
|
63
|
+
* handled `a[].b`, in both directions. The real rule is that the encryptor
|
|
64
|
+
* does not infer: a field is covered only where it is NAMED, in the notation
|
|
65
|
+
* matching the object the list is applied to.
|
|
66
|
+
*/
|
|
67
|
+
export const AMENDMENT_PII_FIELDS = [
|
|
68
|
+
"fullName",
|
|
69
|
+
"phone",
|
|
70
|
+
"addressLine1",
|
|
71
|
+
"addressLine2",
|
|
72
|
+
/* Encrypted on the snapshot as `deliveryLandmark`; the two must agree or the
|
|
73
|
+
* same datum is protected before a correction and exposed after it. */
|
|
74
|
+
"landmark",
|
|
75
|
+
];
|
|
76
|
+
/**
|
|
77
|
+
* How much seller prose a single order line carries.
|
|
78
|
+
*
|
|
79
|
+
* 🛑 A Firestore document caps at 1 MiB and `items[]` is inline on the order.
|
|
80
|
+
* 2,000 characters × 20 lines is ~40 KB of description — comfortable. The
|
|
81
|
+
* same field unbounded is a rejected write at checkout, and the checkout is
|
|
82
|
+
* the one operation in the system that must never fail for a reason the buyer
|
|
83
|
+
* cannot act on.
|
|
84
|
+
*/
|
|
85
|
+
export const ORDER_ITEM_DESCRIPTION_MAX = 2000;
|
|
86
|
+
/**
|
|
87
|
+
* How many spec rows one order line keeps.
|
|
88
|
+
*
|
|
89
|
+
* Capped by COUNT rather than characters: a spec table is rows, and half a
|
|
90
|
+
* row is worse than a missing one — "Weight: 4" with the unit cut off reads
|
|
91
|
+
* as a fact rather than as truncation.
|
|
92
|
+
*/
|
|
93
|
+
export const ORDER_ITEM_SPECIFICATIONS_MAX = 30;
|
|
51
94
|
export const ORDER_COLLECTION = "orders";
|
|
52
95
|
/**
|
|
53
96
|
* At-most-once gate for refunds. Document id = `sha256(idempotencyKey)`,
|
|
@@ -684,6 +684,7 @@ export declare const orderFirestoreSchema: z.ZodObject<{
|
|
|
684
684
|
prizeRevealStatus?: "closed" | "open" | "pending" | "revealed" | undefined;
|
|
685
685
|
}[] | undefined;
|
|
686
686
|
bundleId?: string | undefined;
|
|
687
|
+
orderType?: string | undefined;
|
|
687
688
|
prizeWon?: {
|
|
688
689
|
title: string;
|
|
689
690
|
images: string[];
|
|
@@ -700,7 +701,6 @@ export declare const orderFirestoreSchema: z.ZodObject<{
|
|
|
700
701
|
bin: string;
|
|
701
702
|
} | undefined;
|
|
702
703
|
offerId?: string | undefined;
|
|
703
|
-
orderType?: string | undefined;
|
|
704
704
|
isNonRefundable?: boolean | undefined;
|
|
705
705
|
imageUrls?: string[] | undefined;
|
|
706
706
|
refundFeeDeducted?: number | undefined;
|
|
@@ -843,6 +843,7 @@ export declare const orderFirestoreSchema: z.ZodObject<{
|
|
|
843
843
|
prizeRevealStatus?: "closed" | "open" | "pending" | "revealed" | undefined;
|
|
844
844
|
}[] | undefined;
|
|
845
845
|
bundleId?: string | undefined;
|
|
846
|
+
orderType?: string | undefined;
|
|
846
847
|
prizeWon?: {
|
|
847
848
|
title: string;
|
|
848
849
|
images: string[];
|
|
@@ -859,7 +860,6 @@ export declare const orderFirestoreSchema: z.ZodObject<{
|
|
|
859
860
|
bin: string;
|
|
860
861
|
} | undefined;
|
|
861
862
|
offerId?: string | undefined;
|
|
862
|
-
orderType?: string | undefined;
|
|
863
863
|
isNonRefundable?: boolean | undefined;
|
|
864
864
|
imageUrls?: string[] | undefined;
|
|
865
865
|
refundFeeDeducted?: number | undefined;
|
|
@@ -287,6 +287,27 @@ export interface Order {
|
|
|
287
287
|
timelineTruncated?: number;
|
|
288
288
|
/** How the buyer acquired the right to buy. Written once, at creation. */
|
|
289
289
|
sourceContext?: OrderSourceContext;
|
|
290
|
+
/**
|
|
291
|
+
* D55 — the pre-order balance, once the seller has OPENED it for collection.
|
|
292
|
+
*
|
|
293
|
+
* 🛑 Absent until then, and that is the difference between "a balance
|
|
294
|
+
* exists" (`sourceContext.balanceDue > 0`, true from the moment the order
|
|
295
|
+
* was placed) and "a balance is payable". The buyer's panel gates on this
|
|
296
|
+
* field, so forgetting it in `orderDocumentToOrder` would render the
|
|
297
|
+
* "nothing to pay" branch on every order forever — Root Cause #57, which
|
|
298
|
+
* is what made the whole manual-payment flow dead for weeks.
|
|
299
|
+
*/
|
|
300
|
+
balance?: {
|
|
301
|
+
amount: number;
|
|
302
|
+
method: "manual";
|
|
303
|
+
status: "awaiting" | "submitted" | "paid" | "rejected";
|
|
304
|
+
dueBy?: string;
|
|
305
|
+
proofUrl?: string;
|
|
306
|
+
transactionId?: string;
|
|
307
|
+
submittedAt?: string;
|
|
308
|
+
reviewedAt?: string;
|
|
309
|
+
reviewNote?: string;
|
|
310
|
+
};
|
|
290
311
|
physicalLocation?: {
|
|
291
312
|
zone: string;
|
|
292
313
|
shelf: string;
|
|
@@ -220,8 +220,9 @@ export class PayoutRepository extends BaseRepository {
|
|
|
220
220
|
trigger: ctx?.trigger ?? defaultTrigger,
|
|
221
221
|
reason: ctx?.reason,
|
|
222
222
|
note: ctx?.note,
|
|
223
|
-
// `sellerEmail` / `upiId` — `encryptPiiFields`
|
|
224
|
-
//
|
|
223
|
+
// `sellerEmail` / `upiId` — `encryptPiiFields` covers only NAMED paths,
|
|
224
|
+
// and nothing declares them under `statusHistory[].changes`, so either
|
|
225
|
+
// one reaching `changes` would persist in PLAINTEXT.
|
|
225
226
|
piiFields: PAYOUT_PII_FIELDS,
|
|
226
227
|
});
|
|
227
228
|
return withEntry ?? patch;
|
|
@@ -5,6 +5,7 @@ import { type GeneratePayoutIdInput } from "../../../utils/id-generators";
|
|
|
5
5
|
import type { BaseDocument } from "../../../_internal/shared/types/base-document";
|
|
6
6
|
import type { StatusChangeEntry } from "../../../_internal/shared/history/types";
|
|
7
7
|
import type { Encrypted, Plaintext } from "../../../security/encrypted";
|
|
8
|
+
import type { PayCadence } from "../../../_internal/shared/features/payouts/pay-period";
|
|
8
9
|
export interface PayoutBankAccount {
|
|
9
10
|
accountHolderName: string;
|
|
10
11
|
accountNumberMasked: string;
|
|
@@ -45,7 +46,29 @@ export type PayoutPaymentMethod = "bank_transfer" | "upi";
|
|
|
45
46
|
* asterisks". The distinction is which named method you called.
|
|
46
47
|
*/
|
|
47
48
|
export type PayoutPlain = Plaintext<PayoutDocument>;
|
|
48
|
-
|
|
49
|
+
/**
|
|
50
|
+
* 🛑 **A seller payout settles a LIABILITY. An employee payout is an EXPENSE.**
|
|
51
|
+
*
|
|
52
|
+
* One undiscriminated shape misstates both statements at once — wages vanish
|
|
53
|
+
* from the P&L and reappear as settled seller liabilities — so `payeeType` is
|
|
54
|
+
* a discriminated union rather than an optional field.
|
|
55
|
+
*
|
|
56
|
+
* ## Why `PayoutDocument` still names the STORE variant
|
|
57
|
+
*
|
|
58
|
+
* Every one of the ~30 existing readers means a seller payout, and every one
|
|
59
|
+
* of them is correct. Renaming the type they import would turn a safety change
|
|
60
|
+
* into a mechanical sweep, and the sweep is where the real mistakes go in.
|
|
61
|
+
* `PayoutDocument = StorePayoutDocument` keeps them compiling and correct;
|
|
62
|
+
* anything that must handle BOTH takes `AnyPayoutDocument` and is forced by
|
|
63
|
+
* `tsc` to narrow.
|
|
64
|
+
*
|
|
65
|
+
* 🛑 `payeeType` is REQUIRED here, not optional-with-a-default. That breaks
|
|
66
|
+
* the five sites that CONSTRUCT a payout, which is the entire point — a row
|
|
67
|
+
* written without it is a row the books cannot classify, and an optional
|
|
68
|
+
* discriminant defers that discovery to a reader rather than a writer.
|
|
69
|
+
*/
|
|
70
|
+
export interface StorePayoutDocument extends BaseDocument {
|
|
71
|
+
payeeType: "store";
|
|
49
72
|
storeId: string;
|
|
50
73
|
sellerName: string;
|
|
51
74
|
sellerEmail: Encrypted<string>;
|
|
@@ -92,6 +115,74 @@ export interface PayoutDocument extends BaseDocument {
|
|
|
92
115
|
statusHistory?: StatusChangeEntry[];
|
|
93
116
|
statusHistoryTruncated?: number;
|
|
94
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Wages for a period, earned by resolving support tickets.
|
|
120
|
+
*
|
|
121
|
+
* ## 🛑 `platformFee` is ABSENT, not zero
|
|
122
|
+
*
|
|
123
|
+
* A `0` would read as "we took no commission on these wages", which is a
|
|
124
|
+
* statement about a transaction that did not happen. Commission is not a
|
|
125
|
+
* concept between a platform and its own staff, and the books must be able to
|
|
126
|
+
* tell "not applicable" from "nil" — a nil commission is a seller payout where
|
|
127
|
+
* the rate happened to resolve to zero, and those post differently.
|
|
128
|
+
*
|
|
129
|
+
* The same argument removes `storeId`, `sellerName`, `sellerEmail`,
|
|
130
|
+
* `grossAmount`, `platformFeeRate`, `gatewayFee*`, `orderIds` and
|
|
131
|
+
* `refundDeductions`. A field that cannot apply is omitted so `tsc` refuses
|
|
132
|
+
* the read, rather than present-and-empty so a reader quietly gets a falsy
|
|
133
|
+
* value.
|
|
134
|
+
*/
|
|
135
|
+
export interface EmployeePayoutDocument extends BaseDocument {
|
|
136
|
+
payeeType: "employee";
|
|
137
|
+
employeeUid: string;
|
|
138
|
+
employeeName: string;
|
|
139
|
+
/** PII, same treatment as a seller's. */
|
|
140
|
+
employeeEmail: Encrypted<string>;
|
|
141
|
+
amount: number;
|
|
142
|
+
currency: string;
|
|
143
|
+
status: PayoutStatus;
|
|
144
|
+
paymentMethod: PayoutPaymentMethod;
|
|
145
|
+
bankAccount?: PayoutBankAccount;
|
|
146
|
+
upiId?: Encrypted<string>;
|
|
147
|
+
/**
|
|
148
|
+
* The tickets this payslip is for.
|
|
149
|
+
*
|
|
150
|
+
* 🛑 Tickets **still resolved at period end**, not tickets closed during it.
|
|
151
|
+
* Paying per ticket closed rewards closing rather than resolving, so a
|
|
152
|
+
* reopened ticket does not count — see `resolvedTicketsForPeriod`.
|
|
153
|
+
*/
|
|
154
|
+
ticketIds: string[];
|
|
155
|
+
/** Resolved by `payPeriodFor`, never computed at a call site. */
|
|
156
|
+
periodStart: Date;
|
|
157
|
+
periodEnd: Date;
|
|
158
|
+
payCadence: PayCadence;
|
|
159
|
+
notes?: string;
|
|
160
|
+
adminNote?: string;
|
|
161
|
+
requestedAt: Date;
|
|
162
|
+
processedAt?: Date;
|
|
163
|
+
transactionId?: string;
|
|
164
|
+
statusHistory?: StatusChangeEntry[];
|
|
165
|
+
statusHistoryTruncated?: number;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* What ~30 existing readers mean, and what they should keep meaning.
|
|
169
|
+
*
|
|
170
|
+
* Deliberately an alias rather than a rename: see the header on
|
|
171
|
+
* `StorePayoutDocument`.
|
|
172
|
+
*/
|
|
173
|
+
export type PayoutDocument = StorePayoutDocument;
|
|
174
|
+
/** Use this ONLY where both kinds genuinely arrive together. */
|
|
175
|
+
export type AnyPayoutDocument = StorePayoutDocument | EmployeePayoutDocument;
|
|
176
|
+
/**
|
|
177
|
+
* 🛑 Narrow on the DISCRIMINANT, never on the presence of `storeId`.
|
|
178
|
+
*
|
|
179
|
+
* A `"storeId" in payout` test reads as equivalent and is not: it answers a
|
|
180
|
+
* question about the object in hand rather than about what the row IS, so a
|
|
181
|
+
* partially-populated or hand-built object answers it wrongly, silently, and
|
|
182
|
+
* in the direction that posts wages as a settled seller liability.
|
|
183
|
+
*/
|
|
184
|
+
export declare function isStorePayout(p: AnyPayoutDocument): p is StorePayoutDocument;
|
|
185
|
+
export declare function isEmployeePayout(p: AnyPayoutDocument): p is EmployeePayoutDocument;
|
|
95
186
|
/**
|
|
96
187
|
* The fields whose changes earn a timeline entry.
|
|
97
188
|
*
|
|
@@ -8,6 +8,20 @@ export const PayoutStatusValues = {
|
|
|
8
8
|
PAID: "paid",
|
|
9
9
|
FAILED: "failed",
|
|
10
10
|
};
|
|
11
|
+
/**
|
|
12
|
+
* 🛑 Narrow on the DISCRIMINANT, never on the presence of `storeId`.
|
|
13
|
+
*
|
|
14
|
+
* A `"storeId" in payout` test reads as equivalent and is not: it answers a
|
|
15
|
+
* question about the object in hand rather than about what the row IS, so a
|
|
16
|
+
* partially-populated or hand-built object answers it wrongly, silently, and
|
|
17
|
+
* in the direction that posts wages as a settled seller liability.
|
|
18
|
+
*/
|
|
19
|
+
export function isStorePayout(p) {
|
|
20
|
+
return p.payeeType === "store";
|
|
21
|
+
}
|
|
22
|
+
export function isEmployeePayout(p) {
|
|
23
|
+
return p.payeeType === "employee";
|
|
24
|
+
}
|
|
11
25
|
/**
|
|
12
26
|
* The fields whose changes earn a timeline entry.
|
|
13
27
|
*
|
|
@@ -36,6 +50,15 @@ export const PAYOUT_INDEXED_FIELDS = [
|
|
|
36
50
|
];
|
|
37
51
|
export const DEFAULT_PLATFORM_FEE_RATE = 0.05;
|
|
38
52
|
export const DEFAULT_PAYOUT_DATA = {
|
|
53
|
+
/*
|
|
54
|
+
* 🛑 Here as a BACKSTOP, not as the reason the field is set. Every caller
|
|
55
|
+
* passes `payeeType` explicitly and must keep doing so — a default that is
|
|
56
|
+
* the only writer makes the discriminant optional in practice, which is the
|
|
57
|
+
* exact failure the required field exists to prevent. This catches a row
|
|
58
|
+
* built by some path nobody enumerated and lands it on the correct side of
|
|
59
|
+
* the books rather than on neither.
|
|
60
|
+
*/
|
|
61
|
+
payeeType: "store",
|
|
39
62
|
status: "pending",
|
|
40
63
|
platformFeeRate: DEFAULT_PLATFORM_FEE_RATE,
|
|
41
64
|
orderIds: [],
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface AdminPricingViewProps {
|
|
3
|
+
/**
|
|
4
|
+
* The rate endpoint, supplied by the consumer.
|
|
5
|
+
*
|
|
6
|
+
* 🛑 REQUIRED, with no default. appkit cannot import `API_ROUTES` (that is
|
|
7
|
+
* consumer-side), so a default would be a path literal living in the
|
|
8
|
+
* library — a second place the route is written, and the one nobody updates
|
|
9
|
+
* when it moves. `audit-hardcoded-api-routes` blocks it, correctly.
|
|
10
|
+
*/
|
|
11
|
+
endpoint: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function AdminPricingView({ endpoint }: AdminPricingViewProps): React.JSX.Element;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { Badge, Button, Heading, Row, Stack, Text } from "../../../ui/index.js";
|
|
5
|
+
import { Modal } from "../../../ui/components/Modal.js";
|
|
6
|
+
import { useToast } from "../../../ui/components/Toast.js";
|
|
7
|
+
import { Form } from "../../../ui/components/Form.js";
|
|
8
|
+
import { FieldInput } from "../../../ui/forms/FieldInput.js";
|
|
9
|
+
import { FieldTextarea } from "../../../ui/forms/FieldTextarea.js";
|
|
10
|
+
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary.js";
|
|
11
|
+
import { apiClient } from "../../../http/ApiClient.js";
|
|
12
|
+
import { normalizeError } from "../../../errors/normalize.js";
|
|
13
|
+
import { toUserMessage } from "../../../errors/error-display-map.js";
|
|
14
|
+
import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
|
|
15
|
+
import { RATE_TIER_ORDER, } from "../../../_internal/shared/features/pricing/rate-registry.js";
|
|
16
|
+
const TIER_LABEL = {
|
|
17
|
+
statutory: "Statutory",
|
|
18
|
+
policy: "Policy ceiling",
|
|
19
|
+
operational: "Operational",
|
|
20
|
+
};
|
|
21
|
+
const TIER_BLURB = {
|
|
22
|
+
statutory: "Set by law, not by us. Sourced per HSN code in Tax codes — they deliberately do not appear below.",
|
|
23
|
+
policy: "A ceiling the owner decides. Administrator only, and a reason is required.",
|
|
24
|
+
operational: "Tuned, audited and reversible by back-office staff.",
|
|
25
|
+
};
|
|
26
|
+
const TIER_TONE = {
|
|
27
|
+
statutory: "warning",
|
|
28
|
+
policy: "info",
|
|
29
|
+
operational: "default",
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* 🛑 Format by the descriptor's UNIT, never by guessing from the value.
|
|
33
|
+
*
|
|
34
|
+
* `2` is 2% as a commission, ₹2 as a floor and 2 days as a hold, and the three
|
|
35
|
+
* are side by side in one table. A bare number would make the COD ceiling and
|
|
36
|
+
* the COD percentage read as the same kind of thing.
|
|
37
|
+
*/
|
|
38
|
+
function formatRate(value, unit) {
|
|
39
|
+
if (value === null)
|
|
40
|
+
return "—";
|
|
41
|
+
switch (unit) {
|
|
42
|
+
case "percent":
|
|
43
|
+
return `${value}%`;
|
|
44
|
+
case "rupees":
|
|
45
|
+
return `₹${value.toLocaleString("en-IN")}`;
|
|
46
|
+
case "days":
|
|
47
|
+
return value === 1 ? "1 day" : `${value} days`;
|
|
48
|
+
case "minutes":
|
|
49
|
+
return value === 1 ? "1 minute" : `${value} minutes`;
|
|
50
|
+
case "count":
|
|
51
|
+
return String(value);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function formatWhen(iso) {
|
|
55
|
+
const d = new Date(iso);
|
|
56
|
+
return Number.isNaN(d.getTime()) ? "—" : d.toLocaleDateString("en-IN", { dateStyle: "medium" });
|
|
57
|
+
}
|
|
58
|
+
export function AdminPricingView({ endpoint }) {
|
|
59
|
+
const { showToast } = useToast();
|
|
60
|
+
const [rates, setRates] = useState(null);
|
|
61
|
+
const [loadError, setLoadError] = useState(null);
|
|
62
|
+
const [editing, setEditing] = useState(null);
|
|
63
|
+
const [draftValue, setDraftValue] = useState("");
|
|
64
|
+
const [draftReason, setDraftReason] = useState("");
|
|
65
|
+
const [saving, setSaving] = useState(false);
|
|
66
|
+
const load = useCallback(async () => {
|
|
67
|
+
setLoadError(null);
|
|
68
|
+
try {
|
|
69
|
+
const payload = await apiClient.get(endpoint);
|
|
70
|
+
setRates(payload?.rates ?? []);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
/*
|
|
74
|
+
* Surfaced, never swallowed into an empty table. An empty rate list and
|
|
75
|
+
* a failed read look identical, and the first reads as "we charge
|
|
76
|
+
* nothing" (Root Cause #59/#72 — undefined must never render as zero).
|
|
77
|
+
*
|
|
78
|
+
* 🛑 A FIXED string, never `normalized.message`. The server's own text
|
|
79
|
+
* is how a Node `MODULE_NOT_FOUND` with a require stack ended up inside
|
|
80
|
+
* "Place your bid" (Root Cause #86) — and `mapToHttpError` scrubs 5xx
|
|
81
|
+
* messages in production anyway, so the raw value is either useless or
|
|
82
|
+
* dangerous. `normalizeError` still runs: it is what keeps `unknown`
|
|
83
|
+
* out of the catch body.
|
|
84
|
+
*/
|
|
85
|
+
void normalizeError(err);
|
|
86
|
+
setLoadError("Could not load the rate table. Try again.");
|
|
87
|
+
setRates([]);
|
|
88
|
+
}
|
|
89
|
+
}, [endpoint]);
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
void load();
|
|
92
|
+
}, [load]);
|
|
93
|
+
const byTier = useMemo(() => {
|
|
94
|
+
const groups = new Map();
|
|
95
|
+
for (const tier of RATE_TIER_ORDER)
|
|
96
|
+
groups.set(tier, []);
|
|
97
|
+
for (const rate of rates ?? [])
|
|
98
|
+
groups.get(rate.tier)?.push(rate);
|
|
99
|
+
return groups;
|
|
100
|
+
}, [rates]);
|
|
101
|
+
const openEditor = useCallback((rate) => {
|
|
102
|
+
setEditing(rate);
|
|
103
|
+
setDraftValue(rate.value === null ? "" : String(rate.value));
|
|
104
|
+
setDraftReason("");
|
|
105
|
+
}, []);
|
|
106
|
+
const submit = useCallback(async (setFieldError) => {
|
|
107
|
+
if (!editing)
|
|
108
|
+
return;
|
|
109
|
+
const parsed = Number(draftValue);
|
|
110
|
+
if (!Number.isFinite(parsed)) {
|
|
111
|
+
setFieldError("newValue", "Enter a number.");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
setSaving(true);
|
|
115
|
+
try {
|
|
116
|
+
await apiClient.post(endpoint, {
|
|
117
|
+
path: editing.path,
|
|
118
|
+
newValue: parsed,
|
|
119
|
+
...(draftReason.trim() ? { reason: draftReason.trim() } : {}),
|
|
120
|
+
});
|
|
121
|
+
showToast(`${editing.label} updated.`, "success");
|
|
122
|
+
setEditing(null);
|
|
123
|
+
await load();
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
void normalizeError(err);
|
|
127
|
+
/*
|
|
128
|
+
* 🛑 `toUserMessage`, never the server's own message — and no
|
|
129
|
+
* `?? normalized.message` fallback either. That fallback IS the bug:
|
|
130
|
+
* `toUserMessage` already terminates at `errors.codes.UNKNOWN`, so
|
|
131
|
+
* adding a raw-text branch for the unmapped case reintroduces exactly
|
|
132
|
+
* what Root Cause #86 describes — a require stack rendered inside a
|
|
133
|
+
* modal. The first draft of this file had it.
|
|
134
|
+
*/
|
|
135
|
+
const code = err?.code;
|
|
136
|
+
setFieldError("newValue", toUserMessage(code ?? "UNKNOWN"));
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
setSaving(false);
|
|
140
|
+
}
|
|
141
|
+
}, [draftReason, draftValue, editing, endpoint, load, showToast]);
|
|
142
|
+
return (_jsxs(Stack, { gap: "loose", children: [_jsxs(Stack, { gap: "dense", children: [_jsx(Heading, { level: 1, children: "Pricing" }), _jsx(Text, { color: "muted", children: "Every rate the platform charges, what kind of number it is, and when it last moved. Changes take effect from the moment they are saved and never restate past orders." })] }), loadError ? (_jsxs(Stack, { surface: "danger-surface", padding: "md", rounded: "lg", gap: "dense", children: [_jsx(Text, { weight: "semibold", children: "The rate table could not be loaded." }), _jsx(Text, { size: "sm", children: loadError }), _jsx(Row, { children: _jsx(Button, { variant: "secondary", onClick: () => void load(), children: "Try again" }) })] })) : null, RATE_TIER_ORDER.map((tier) => {
|
|
143
|
+
const rows = byTier.get(tier) ?? [];
|
|
144
|
+
/*
|
|
145
|
+
* The statutory section renders with NO rows and that is the point —
|
|
146
|
+
* it tells a reader the tier exists and where it is set. Hiding an
|
|
147
|
+
* empty section would make statutory rates look like something this
|
|
148
|
+
* platform does not have.
|
|
149
|
+
*/
|
|
150
|
+
if (rows.length === 0 && tier !== "statutory")
|
|
151
|
+
return null;
|
|
152
|
+
return (_jsxs(Stack, { gap: "comfortable", children: [_jsxs(Stack, { gap: "dense", children: [_jsxs(Row, { gap: "dense", align: "center", children: [_jsx(Heading, { level: 2, children: TIER_LABEL[tier] }), _jsx(Badge, { variant: TIER_TONE[tier], children: rows.length || "set elsewhere" })] }), _jsx(Text, { size: "sm", color: "muted", children: TIER_BLURB[tier] })] }), rows.map((rate) => (_jsxs(Row, { surface: "card", padding: "card", rounded: "lg", gap: "comfortable", align: "start", justify: "between", children: [_jsxs(Stack, { gap: "dense", children: [_jsxs(Row, { gap: "dense", align: "center", children: [_jsx(Text, { weight: "semibold", children: rate.label }), rate.sellerOnly ? _jsx(Badge, { variant: "default", children: "Seller-facing" }) : null] }), _jsx(Text, { size: "sm", color: "muted", children: rate.description }), rate.lastChange ? (_jsxs(Text, { size: "xs", color: "muted", children: [formatRate(rate.lastChange.previousValue, rate.unit), " \u2192", " ", formatRate(rate.lastChange.newValue, rate.unit), " on", " ", formatWhen(rate.lastChange.effectiveFrom), rate.lastChange.changedByName ? ` by ${rate.lastChange.changedByName}` : "", rate.lastChange.reason ? ` — ${rate.lastChange.reason}` : ""] })) : (_jsx(Text, { size: "xs", color: "muted", children: "No recorded change \u2014 this is the value it was configured with." }))] }), _jsxs(Row, { gap: "comfortable", align: "center", children: [_jsx(Text, { weight: "bold", numeric: true, children: formatRate(rate.value, rate.unit) }), _jsx(Button, { variant: "secondary", size: "sm", disabled: !rate.editable, onClick: () => openEditor(rate), children: "Change" })] })] }, rate.path)))] }, tier));
|
|
153
|
+
}), _jsx(Modal, { isOpen: editing !== null, onClose: () => setEditing(null), title: editing ? `Change ${editing.label}` : "Change rate", children: editing ? (_jsx(Form, { onSubmit: (e) => e.preventDefault(), children: ({ setFieldError, clearErrors }) => (_jsxs(Stack, { gap: "comfortable", children: [_jsx(FormErrorSummary, {}), _jsx(Text, { size: "sm", color: "muted", children: editing.description }), _jsx(FieldInput, { name: "newValue", label: `New value (${editing.unit})`, required: true, value: draftValue, onChange: setDraftValue }), _jsx(FieldTextarea, { name: "reason", label: editing.tier === "policy" ? "Reason (required)" : "Reason (optional)", value: draftReason, onChange: setDraftReason }), _jsx(Text, { size: "xs", color: "muted", children: "This takes effect now. Orders, invoices and payouts already recorded keep the rate they were charged at \u2014 nothing is restated." }), _jsxs(Row, { gap: "dense", justify: "end", children: [_jsx(Button, { variant: "ghost", onClick: () => setEditing(null), children: "Cancel" }), _jsx(Button, { action: ACTIONS.ADMIN["update-pricing"], isLoading: saving, onClick: () => {
|
|
154
|
+
clearErrors();
|
|
155
|
+
void submit(setFieldError);
|
|
156
|
+
} })] })] })) })) : null })] }));
|
|
157
|
+
}
|
|
@@ -603,6 +603,31 @@ export interface ProductDocument extends BaseDocument {
|
|
|
603
603
|
howToUse?: string[];
|
|
604
604
|
allowOffers?: boolean;
|
|
605
605
|
minOfferPercent?: number;
|
|
606
|
+
/**
|
|
607
|
+
* The seller permits a COORDINATED purchase on this listing.
|
|
608
|
+
*
|
|
609
|
+
* ## 🛑 A coordinated purchase is a SPECIALISED OFFER, not a listing type
|
|
610
|
+
*
|
|
611
|
+
* Owner, 2026-10-22: *"it is not a variant, it is similar to an offer, which
|
|
612
|
+
* the seller enables on the listing, then the customer can coordinate on it,
|
|
613
|
+
* it is a specialized offer."*
|
|
614
|
+
*
|
|
615
|
+
* That framing REMOVES work rather than adding it, and the removals are the
|
|
616
|
+
* part worth knowing — otherwise somebody rebuilds them:
|
|
617
|
+
*
|
|
618
|
+
* | Looks like it needs | Actually |
|
|
619
|
+
* |---|---|
|
|
620
|
+
* | a 7th `OrderSourceContext` variant | **nothing** — it is `offer-accepted`, and the provenance card already renders that branch |
|
|
621
|
+
* | an `orderType` tab | **nothing** — the Offer-wins tab already covers it |
|
|
622
|
+
* | a commit/settlement path | the offer lifecycle, with its chain fields, `checkoutDeadline` and lapse sweep |
|
|
623
|
+
* | a new money term | **none** — `unitPriceFor` already honours `lockedPrice`, so tax, payout base, refund ceiling and the invoice all follow |
|
|
624
|
+
*
|
|
625
|
+
* So this flag is deliberately shaped exactly like `allowOffers` above: same
|
|
626
|
+
* optional boolean, same seller control, same projection list. A listing
|
|
627
|
+
* with `allowOffers: false` and this `true` is coherent — the seller takes
|
|
628
|
+
* no solo offers but will consider a group — and nothing needs to forbid it.
|
|
629
|
+
*/
|
|
630
|
+
allowCoordination?: boolean;
|
|
606
631
|
customFields?: CustomField[];
|
|
607
632
|
customSections?: CustomSection[];
|
|
608
633
|
sublistingCategoryId?: string;
|
|
@@ -837,7 +862,7 @@ export declare const ProductStatusValues: {
|
|
|
837
862
|
export declare const PRODUCT_COLLECTION: "products";
|
|
838
863
|
export declare const PRODUCT_INDEXED_FIELDS: readonly ["storeId", "status", "category", "categoryIds", "featured", "listingType", "isPromoted", "isOnSale", "isSold", "searchTxt", "createdAt", "barcodeId", "physicalLocation.zone"];
|
|
839
864
|
export declare const DEFAULT_PRODUCT_DATA: Partial<ProductDocument>;
|
|
840
|
-
export declare const PRODUCT_PUBLIC_FIELDS: readonly ["allowOffers", "minOfferPercent", "id", "title", "description", "category", "categoryIds", "categoryIdNames", "subcategory", "brand", "price", "originalPrice", "currency", "stockQuantity", "availableQuantity", "images", "status", "storeName", "featured", "tags", "specifications", "features", "shippingInfo", "returnPolicy", "finalSale", "isLot", "lotSize", "unitLabel", "partGlossaryId", "colourway", "mouldVariant", "assembledFrom", "views7d", "views30d", "unitLabel", "partGlossaryId", "colourway", "mouldVariant", "assembledFrom", "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", "madeToOrder", "sourceStickerSheetId", "sourceStickerPartOrdinal", "slug", "seoTitle", "seoDescription", "seoKeywords", "viewCount", "customFields", "customSections", "sublistingCategoryId", "groupId", "isGroupParent", "groupParentSlug", "groupChildSlugs", "groupTitle", "groupMinMembers", "groupMaxMembers", "groupMaxUnitsPerMember", "groupRequiredMemberIds", "groupDiscountPercent", "createdAt"];
|
|
865
|
+
export declare const PRODUCT_PUBLIC_FIELDS: readonly ["allowOffers", "minOfferPercent", "allowCoordination", "id", "title", "description", "category", "categoryIds", "categoryIdNames", "subcategory", "brand", "price", "originalPrice", "currency", "stockQuantity", "availableQuantity", "images", "status", "storeName", "featured", "tags", "specifications", "features", "shippingInfo", "returnPolicy", "finalSale", "isLot", "lotSize", "unitLabel", "partGlossaryId", "colourway", "mouldVariant", "assembledFrom", "views7d", "views30d", "unitLabel", "partGlossaryId", "colourway", "mouldVariant", "assembledFrom", "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", "madeToOrder", "sourceStickerSheetId", "sourceStickerPartOrdinal", "slug", "seoTitle", "seoDescription", "seoKeywords", "viewCount", "customFields", "customSections", "sublistingCategoryId", "groupId", "isGroupParent", "groupParentSlug", "groupChildSlugs", "groupTitle", "groupMinMembers", "groupMaxMembers", "groupMaxUnitsPerMember", "groupRequiredMemberIds", "groupDiscountPercent", "createdAt"];
|
|
841
866
|
export declare const PRODUCT_UPDATABLE_FIELDS: readonly ["title", "description", "category", "categoryIds", "subcategory", "brand", "price", "originalPrice", "stockQuantity", "images", "status", "tags", "specifications", "features", "shippingInfo", "returnPolicy", "finalSale", "isLot", "lotSize", "physicalLocation", "sku", "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", "madeToOrder", "sourceStickerSheetId", "sourceStickerPartOrdinal", "allowShipBeforeEmiComplete", "gstRate", "hsnCode", "authenticity", "printMeta", "seoTitle", "seoDescription", "seoKeywords", "customFields", "customSections", "sublistingCategoryId", "groupId", "isGroupParent", "groupParentSlug", "groupChildSlugs", "groupTitle", "groupMinMembers", "groupMaxMembers", "groupMaxUnitsPerMember", "groupRequiredMemberIds", "groupDiscountPercent", "barcodeId"];
|
|
842
867
|
export type ProductCreateInput = Omit<ProductDocument, "id" | "createdAt" | "updatedAt" | "availableQuantity" | "bidCount" | "currentBid" | "auctionOriginalEndDate">;
|
|
843
868
|
export type ProductUpdateInput = Partial<Pick<ProductDocument, (typeof PRODUCT_UPDATABLE_FIELDS)[number]>>;
|
|
@@ -142,6 +142,8 @@ export const PRODUCT_PUBLIC_FIELDS = [
|
|
|
142
142
|
// the moment something starts projecting through it.
|
|
143
143
|
"allowOffers",
|
|
144
144
|
"minOfferPercent",
|
|
145
|
+
/* Same reasoning as `allowOffers`: the card renders a pill from it. */
|
|
146
|
+
"allowCoordination",
|
|
145
147
|
"id",
|
|
146
148
|
"title",
|
|
147
149
|
"description",
|
|
@@ -449,10 +449,10 @@ export declare const productItemSchema: z.ZodObject<{
|
|
|
449
449
|
wonByOrderId?: string | undefined;
|
|
450
450
|
}[] | undefined;
|
|
451
451
|
prizeGithubFileUrl?: string | undefined;
|
|
452
|
+
attributes?: Record<string, string> | undefined;
|
|
452
453
|
inStock?: boolean | undefined;
|
|
453
454
|
sellerAvatar?: string | undefined;
|
|
454
455
|
stockCount?: number | undefined;
|
|
455
|
-
attributes?: Record<string, string> | undefined;
|
|
456
456
|
}, {
|
|
457
457
|
status: "published" | "draft" | "in_review" | "archived";
|
|
458
458
|
id: string;
|
|
@@ -589,10 +589,10 @@ export declare const productItemSchema: z.ZodObject<{
|
|
|
589
589
|
wonByOrderId?: string | undefined;
|
|
590
590
|
}[] | undefined;
|
|
591
591
|
prizeGithubFileUrl?: string | undefined;
|
|
592
|
+
attributes?: Record<string, string> | undefined;
|
|
592
593
|
inStock?: boolean | undefined;
|
|
593
594
|
sellerAvatar?: string | undefined;
|
|
594
595
|
stockCount?: number | undefined;
|
|
595
|
-
attributes?: Record<string, string> | undefined;
|
|
596
596
|
}>;
|
|
597
597
|
/** Base Zod schema for list-query parameters. */
|
|
598
598
|
export declare const productListParamsSchema: z.ZodObject<{
|
|
@@ -52,6 +52,20 @@ declare class ReviewRepository extends BaseRepository<ReviewDocument> {
|
|
|
52
52
|
findApprovedByStore(storeId: string, limit?: number): Promise<ReviewDocument[]>;
|
|
53
53
|
findByUser(userId: string): Promise<ReviewDocument[]>;
|
|
54
54
|
findApprovedByUser(userId: string): Promise<ReviewDocument[]>;
|
|
55
|
+
/**
|
|
56
|
+
* How many reviews this account has WRITTEN.
|
|
57
|
+
*
|
|
58
|
+
* Deliberately every status and both reviewer roles — it counts the same set
|
|
59
|
+
* `GET /api/user/reviews` lists by default (`findByUser`), so the number in
|
|
60
|
+
* the sidebar and the rows on the page it links to agree by construction. A
|
|
61
|
+
* count that quietly excluded pending reviews would read as the author's
|
|
62
|
+
* work having vanished.
|
|
63
|
+
*
|
|
64
|
+
* An aggregation, not `findByUser(...).length`: that sibling downloads every
|
|
65
|
+
* review the account has ever written to learn how many there are, and the
|
|
66
|
+
* caller here runs on every session profile fetch.
|
|
67
|
+
*/
|
|
68
|
+
countByUser(userId: string): Promise<number>;
|
|
55
69
|
findPending(): Promise<ReviewDocument[]>;
|
|
56
70
|
findByStatus(status: ReviewStatus): Promise<ReviewDocument[]>;
|
|
57
71
|
/**
|
|
@@ -175,6 +175,22 @@ class ReviewRepository extends BaseRepository {
|
|
|
175
175
|
.get();
|
|
176
176
|
return snapshot.docs.map((doc) => this.mapDoc(doc));
|
|
177
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* How many reviews this account has WRITTEN.
|
|
180
|
+
*
|
|
181
|
+
* Deliberately every status and both reviewer roles — it counts the same set
|
|
182
|
+
* `GET /api/user/reviews` lists by default (`findByUser`), so the number in
|
|
183
|
+
* the sidebar and the rows on the page it links to agree by construction. A
|
|
184
|
+
* count that quietly excluded pending reviews would read as the author's
|
|
185
|
+
* work having vanished.
|
|
186
|
+
*
|
|
187
|
+
* An aggregation, not `findByUser(...).length`: that sibling downloads every
|
|
188
|
+
* review the account has ever written to learn how many there are, and the
|
|
189
|
+
* caller here runs on every session profile fetch.
|
|
190
|
+
*/
|
|
191
|
+
async countByUser(userId) {
|
|
192
|
+
return getFirestoreCount(this.getCollection().where(REVIEW_FIELDS.USER_ID, "==", userId));
|
|
193
|
+
}
|
|
178
194
|
async findPending() {
|
|
179
195
|
return this.findBy(REVIEW_FIELDS.STATUS, "pending");
|
|
180
196
|
}
|
|
@@ -142,7 +142,8 @@ class ScammerRepository extends BaseRepository {
|
|
|
142
142
|
reason: ctx?.reason,
|
|
143
143
|
note: ctx?.note,
|
|
144
144
|
// A scammer profile is built entirely from identifying details, and
|
|
145
|
-
// `encryptPiiFields`
|
|
145
|
+
// `encryptPiiFields` covers a field only where it is NAMED. It CAN
|
|
146
|
+
// reach into an array via `array[].key`, but a bare key does not.
|
|
146
147
|
piiFields: SCAMMER_HISTORY_PII_FIELDS,
|
|
147
148
|
});
|
|
148
149
|
const data = prepareForFirestore(withEntry ?? { ...input, updatedAt: new Date() });
|
|
@@ -403,6 +403,7 @@ export declare const SCAMMER_TRACKED_FIELDS: readonly ["status", "verifiedBy", "
|
|
|
403
403
|
/**
|
|
404
404
|
* PII on this document, for `withHistory`'s scrub. A scammer profile is built
|
|
405
405
|
* ENTIRELY out of identifying details, so this list is long on purpose —
|
|
406
|
-
* `encryptPiiFields`
|
|
406
|
+
* `encryptPiiFields` covers only NAMED paths, and nobody is going to declare
|
|
407
|
+
* `statusHistory[].changes.<key>` for every field that might land there.
|
|
407
408
|
*/
|
|
408
409
|
export declare const SCAMMER_HISTORY_PII_FIELDS: readonly ["name", "displayName", "email", "phone", "phoneNumber", "upiId", "bankAccount", "accountNumber", "reporterEmail", "reporterName"];
|
|
@@ -248,7 +248,8 @@ export const SCAMMER_TRACKED_FIELDS = [
|
|
|
248
248
|
/**
|
|
249
249
|
* PII on this document, for `withHistory`'s scrub. A scammer profile is built
|
|
250
250
|
* ENTIRELY out of identifying details, so this list is long on purpose —
|
|
251
|
-
* `encryptPiiFields`
|
|
251
|
+
* `encryptPiiFields` covers only NAMED paths, and nobody is going to declare
|
|
252
|
+
* `statusHistory[].changes.<key>` for every field that might land there.
|
|
252
253
|
*/
|
|
253
254
|
export const SCAMMER_HISTORY_PII_FIELDS = [
|
|
254
255
|
"name",
|